feat: initialize Kurdistan SDK - independent fork of Polkadot SDK
This commit is contained in:
@@ -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(¬ification_with_session_index);
|
||||
Scheduler::initializer_on_new_session(¬ification_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);
|
||||
}
|
||||
@@ -0,0 +1,946 @@
|
||||
// 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 crate::{
|
||||
configuration, inclusion, initializer, paras,
|
||||
paras::ParaKind,
|
||||
paras_inherent,
|
||||
scheduler::{
|
||||
self,
|
||||
common::{Assignment, AssignmentProvider},
|
||||
},
|
||||
session_info, shared,
|
||||
};
|
||||
use alloc::{
|
||||
collections::{btree_map::BTreeMap, btree_set::BTreeSet, vec_deque::VecDeque},
|
||||
vec,
|
||||
vec::Vec,
|
||||
};
|
||||
use bitvec::{order::Lsb0 as BitOrderLsb0, vec::BitVec};
|
||||
use frame_support::pallet_prelude::*;
|
||||
use frame_system::pallet_prelude::*;
|
||||
use pezkuwi_primitives::{
|
||||
ApprovedPeerId, AvailabilityBitfield, BackedCandidate, CandidateCommitments,
|
||||
CandidateDescriptorV2, CandidateHash, ClaimQueueOffset,
|
||||
CommittedCandidateReceiptV2 as CommittedCandidateReceipt, CompactStatement, CoreIndex,
|
||||
CoreSelector, DisputeStatement, DisputeStatementSet, GroupIndex, HeadData, Id as ParaId,
|
||||
IndexedVec, InherentData as TeyrchainsInherentData, InvalidDisputeStatementKind,
|
||||
PersistedValidationData, SessionIndex, SigningContext, UMPSignal, UncheckedSigned,
|
||||
ValidDisputeStatementKind, ValidationCode, ValidatorId, ValidatorIndex, ValidityAttestation,
|
||||
UMP_SEPARATOR,
|
||||
};
|
||||
use sp_core::H256;
|
||||
use sp_runtime::{
|
||||
traits::{Header as HeaderT, One, TrailingZeroInput, Zero},
|
||||
RuntimeAppPublic,
|
||||
};
|
||||
|
||||
fn mock_validation_code() -> ValidationCode {
|
||||
ValidationCode(vec![1, 2, 3])
|
||||
}
|
||||
|
||||
/// Grab an account, seeded by a name and index.
|
||||
///
|
||||
/// This is directly from frame-benchmarking. Copy/pasted so we can use it when not compiling with
|
||||
/// "features = runtime-benchmarks".
|
||||
fn account<AccountId: Decode>(name: &'static str, index: u32, seed: u32) -> AccountId {
|
||||
let entropy = (name, index, seed).using_encoded(sp_io::hashing::blake2_256);
|
||||
AccountId::decode(&mut TrailingZeroInput::new(&entropy[..]))
|
||||
.expect("infinite input; no invalid input; qed")
|
||||
}
|
||||
|
||||
pub fn generate_validator_pairs<T: frame_system::Config>(
|
||||
validator_count: u32,
|
||||
) -> Vec<(T::AccountId, ValidatorId)> {
|
||||
(0..validator_count)
|
||||
.map(|i| {
|
||||
let public = ValidatorId::generate_pair(None);
|
||||
|
||||
// The account Id is not actually used anywhere, just necessary to fulfill the
|
||||
// expected type of the `validators` param of `test_trigger_on_new_session`.
|
||||
let account: T::AccountId = account("validator", i, i);
|
||||
(account, public)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Create a 32 byte slice based on the given number.
|
||||
fn byte32_slice_from(n: u32) -> [u8; 32] {
|
||||
let mut slice = [0u8; 32];
|
||||
slice[31] = (n % (1 << 8)) as u8;
|
||||
slice[30] = ((n >> 8) % (1 << 8)) as u8;
|
||||
slice[29] = ((n >> 16) % (1 << 8)) as u8;
|
||||
slice[28] = ((n >> 24) % (1 << 8)) as u8;
|
||||
|
||||
slice
|
||||
}
|
||||
|
||||
/// Paras inherent `enter` benchmark scenario builder.
|
||||
pub(crate) struct BenchBuilder<T: paras_inherent::Config> {
|
||||
/// Active validators. Validators should be declared prior to all other setup.
|
||||
validators: Option<IndexedVec<ValidatorIndex, ValidatorId>>,
|
||||
/// Starting block number; we expect it to get incremented on session setup.
|
||||
block_number: BlockNumberFor<T>,
|
||||
/// Starting session; we expect it to get incremented on session setup.
|
||||
session: SessionIndex,
|
||||
/// Session we want the scenario to take place in. We will roll to this session.
|
||||
target_session: u32,
|
||||
/// Optionally set the max validators per core; otherwise uses the configuration value.
|
||||
max_validators_per_core: Option<u32>,
|
||||
/// Optionally set the max validators; otherwise uses the configuration value.
|
||||
max_validators: Option<u32>,
|
||||
/// Optionally set the number of dispute statements for each candidate.
|
||||
dispute_statements: BTreeMap<u32, u32>,
|
||||
/// Session index of for each dispute. Index of slice corresponds to a core,
|
||||
/// which is offset by the number of entries for `backed_and_concluding_paras`. I.E. if
|
||||
/// `backed_and_concluding_paras` has 3 entries, the first index of `dispute_sessions`
|
||||
/// will correspond to core index 3. There must be one entry for each core with a dispute
|
||||
/// statement set.
|
||||
dispute_sessions: Vec<u32>,
|
||||
/// Paras here will both be backed in the inherent data and already occupying a core (which is
|
||||
/// freed via bitfields).
|
||||
///
|
||||
/// Map from para id to number of validity votes. Core indices are generated based on
|
||||
/// `elastic_paras` configuration. Each para id in `elastic_paras` gets the
|
||||
/// specified amount of consecutive cores assigned to it. If a para id is not present
|
||||
/// in `elastic_paras` it get assigned to a single core.
|
||||
backed_and_concluding_paras: BTreeMap<u32, u32>,
|
||||
|
||||
/// Paras which don't yet occupy a core, but will after the inherent has been processed.
|
||||
backed_in_inherent_paras: BTreeMap<u32, u32>,
|
||||
/// Map from para id (seed) to number of chained candidates.
|
||||
elastic_paras: BTreeMap<u32, u8>,
|
||||
/// Make every candidate include a code upgrade by setting this to `Some` where the interior
|
||||
/// value is the byte length of the new code.
|
||||
code_upgrade: Option<u32>,
|
||||
/// Cores which should not be available when being populated with pending candidates.
|
||||
unavailable_cores: Vec<u32>,
|
||||
/// Use v2 candidate descriptor.
|
||||
candidate_descriptor_v2: bool,
|
||||
/// Send an approved peer ump signal. Only useful for v2 descriptors
|
||||
approved_peer_signal: Option<ApprovedPeerId>,
|
||||
/// Apply custom changes to generated candidates
|
||||
candidate_modifier: Option<CandidateModifier<T::Hash>>,
|
||||
_phantom: core::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
pub type CandidateModifier<Hash> =
|
||||
fn(CommittedCandidateReceipt<Hash>) -> CommittedCandidateReceipt<Hash>;
|
||||
|
||||
/// Paras inherent `enter` benchmark scenario.
|
||||
#[cfg(any(feature = "runtime-benchmarks", test))]
|
||||
pub(crate) struct Bench<T: paras_inherent::Config> {
|
||||
pub(crate) data: TeyrchainsInherentData<HeaderFor<T>>,
|
||||
pub(crate) _session: u32,
|
||||
pub(crate) _block_number: BlockNumberFor<T>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl<T: paras_inherent::Config> BenchBuilder<T> {
|
||||
/// Create a new `BenchBuilder` with some opinionated values that should work with the rest
|
||||
/// of the functions in this implementation.
|
||||
pub(crate) fn new() -> Self {
|
||||
BenchBuilder {
|
||||
validators: None,
|
||||
block_number: Zero::zero(),
|
||||
session: SessionIndex::from(0u32),
|
||||
target_session: 2u32,
|
||||
max_validators_per_core: None,
|
||||
max_validators: None,
|
||||
dispute_statements: BTreeMap::new(),
|
||||
dispute_sessions: Default::default(),
|
||||
backed_and_concluding_paras: Default::default(),
|
||||
backed_in_inherent_paras: Default::default(),
|
||||
elastic_paras: Default::default(),
|
||||
code_upgrade: None,
|
||||
unavailable_cores: vec![],
|
||||
candidate_descriptor_v2: false,
|
||||
approved_peer_signal: None,
|
||||
candidate_modifier: None,
|
||||
_phantom: core::marker::PhantomData::<T>,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the session index for each dispute statement set (in other words, set the session the
|
||||
/// the dispute statement set's relay chain block is from). Indexes of `dispute_sessions`
|
||||
/// correspond to a core, which is offset by the number of entries for
|
||||
/// `backed_and_concluding_paras`. I.E. if `backed_and_concluding_paras` cores has 3 entries,
|
||||
/// the first index of `dispute_sessions` will correspond to core index 3.
|
||||
///
|
||||
/// Note that there must be an entry for each core with a dispute statement set.
|
||||
pub(crate) fn set_dispute_sessions(mut self, dispute_sessions: impl AsRef<[u32]>) -> Self {
|
||||
self.dispute_sessions = dispute_sessions.as_ref().to_vec();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the cores which should not be available when being populated with pending candidates.
|
||||
pub(crate) fn set_unavailable_cores(mut self, unavailable_cores: Vec<u32>) -> Self {
|
||||
self.unavailable_cores = unavailable_cores;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a map from para id seed to number of validity votes.
|
||||
pub(crate) fn set_backed_and_concluding_paras(
|
||||
mut self,
|
||||
backed_and_concluding_paras: BTreeMap<u32, u32>,
|
||||
) -> Self {
|
||||
self.backed_and_concluding_paras = backed_and_concluding_paras;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a map from para id seed to number of validity votes for votes in inherent data.
|
||||
pub(crate) fn set_backed_in_inherent_paras(mut self, backed: BTreeMap<u32, u32>) -> Self {
|
||||
self.backed_in_inherent_paras = backed;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a map from para id seed to number of cores assigned to it.
|
||||
pub(crate) fn set_elastic_paras(mut self, elastic_paras: BTreeMap<u32, u8>) -> Self {
|
||||
self.elastic_paras = elastic_paras;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set to include a code upgrade for all backed candidates. The value will be the byte length
|
||||
/// of the code.
|
||||
pub(crate) fn set_code_upgrade(mut self, code_upgrade: impl Into<Option<u32>>) -> Self {
|
||||
self.code_upgrade = code_upgrade.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Mock header.
|
||||
pub(crate) fn header(block_number: BlockNumberFor<T>) -> HeaderFor<T> {
|
||||
HeaderFor::<T>::new(
|
||||
block_number, // `block_number`,
|
||||
Default::default(), // `extrinsics_root`,
|
||||
Default::default(), // `storage_root`,
|
||||
Default::default(), // `parent_hash`,
|
||||
Default::default(), // digest,
|
||||
)
|
||||
}
|
||||
|
||||
/// Number of the relay parent block.
|
||||
fn relay_parent_number(&self) -> u32 {
|
||||
(self.block_number - One::one())
|
||||
.try_into()
|
||||
.map_err(|_| ())
|
||||
.expect("self.block_number is u32")
|
||||
}
|
||||
|
||||
/// Fallback for the maximum number of validators participating in teyrchains consensus (a.k.a.
|
||||
/// active validators).
|
||||
pub(crate) fn fallback_max_validators() -> u32 {
|
||||
configuration::ActiveConfig::<T>::get().max_validators.unwrap_or(1024)
|
||||
}
|
||||
|
||||
/// Maximum number of validators participating in teyrchains consensus (a.k.a. active
|
||||
/// validators).
|
||||
fn max_validators(&self) -> u32 {
|
||||
self.max_validators.unwrap_or(Self::fallback_max_validators())
|
||||
}
|
||||
|
||||
/// Set the maximum number of active validators.
|
||||
#[cfg(not(feature = "runtime-benchmarks"))]
|
||||
pub(crate) fn set_max_validators(mut self, n: u32) -> Self {
|
||||
self.max_validators = Some(n);
|
||||
self
|
||||
}
|
||||
|
||||
/// Maximum number of validators per core (a.k.a. max validators per group). This value is used
|
||||
/// if none is explicitly set on the builder.
|
||||
pub(crate) fn fallback_max_validators_per_core() -> u32 {
|
||||
configuration::ActiveConfig::<T>::get()
|
||||
.scheduler_params
|
||||
.max_validators_per_core
|
||||
.unwrap_or(5)
|
||||
}
|
||||
|
||||
/// Specify a mapping of core index/ para id to the number of dispute statements for the
|
||||
/// corresponding dispute statement set. Note that if the number of disputes is not specified
|
||||
/// it fallbacks to having a dispute per every validator. Additionally, an entry is not
|
||||
/// guaranteed to have a dispute - it must line up with the cores marked as disputed as defined
|
||||
/// in `Self::Build`.
|
||||
#[cfg(not(feature = "runtime-benchmarks"))]
|
||||
pub(crate) fn set_dispute_statements(mut self, m: BTreeMap<u32, u32>) -> Self {
|
||||
self.dispute_statements = m;
|
||||
self
|
||||
}
|
||||
|
||||
/// Toggle usage of v2 candidate descriptors.
|
||||
pub(crate) fn set_candidate_descriptor_v2(mut self, enable: bool) -> Self {
|
||||
self.candidate_descriptor_v2 = enable;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set an approved peer to be sent as a UMP signal. Only used for v2 descriptors
|
||||
pub(crate) fn set_approved_peer_signal(mut self, peer_id: ApprovedPeerId) -> Self {
|
||||
self.approved_peer_signal = Some(peer_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the candidate modifier.
|
||||
pub(crate) fn set_candidate_modifier(
|
||||
mut self,
|
||||
modifier: Option<CandidateModifier<T::Hash>>,
|
||||
) -> Self {
|
||||
self.candidate_modifier = modifier;
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the maximum number of validators per core.
|
||||
fn max_validators_per_core(&self) -> u32 {
|
||||
self.max_validators_per_core.unwrap_or(Self::fallback_max_validators_per_core())
|
||||
}
|
||||
|
||||
/// Set maximum number of validators per core.
|
||||
#[cfg(not(feature = "runtime-benchmarks"))]
|
||||
pub(crate) fn set_max_validators_per_core(mut self, n: u32) -> Self {
|
||||
self.max_validators_per_core = Some(n);
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the maximum number of cores we expect from this configuration.
|
||||
pub(crate) fn max_cores(&self) -> u32 {
|
||||
self.max_validators() / self.max_validators_per_core()
|
||||
}
|
||||
|
||||
/// Get the minimum number of validity votes in order for a backed candidate to be included.
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
pub(crate) fn fallback_min_backing_votes() -> u32 {
|
||||
2
|
||||
}
|
||||
|
||||
fn mock_head_data() -> HeadData {
|
||||
let max_head_size = configuration::ActiveConfig::<T>::get().max_head_data_size;
|
||||
HeadData(vec![0xFF; max_head_size as usize])
|
||||
}
|
||||
|
||||
fn candidate_descriptor_mock(para_id: ParaId) -> CandidateDescriptorV2<T::Hash> {
|
||||
CandidateDescriptorV2::new(
|
||||
para_id,
|
||||
Default::default(),
|
||||
CoreIndex(200),
|
||||
2,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
mock_validation_code().hash(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a mock of `CandidatePendingAvailability`.
|
||||
fn candidate_availability_mock(
|
||||
para_id: ParaId,
|
||||
group_idx: GroupIndex,
|
||||
core_idx: CoreIndex,
|
||||
candidate_hash: CandidateHash,
|
||||
availability_votes: BitVec<u8, BitOrderLsb0>,
|
||||
commitments: CandidateCommitments,
|
||||
) -> inclusion::CandidatePendingAvailability<T::Hash, BlockNumberFor<T>> {
|
||||
inclusion::CandidatePendingAvailability::<T::Hash, BlockNumberFor<T>>::new(
|
||||
core_idx, // core
|
||||
candidate_hash, // hash
|
||||
Self::candidate_descriptor_mock(para_id), /* candidate descriptor */
|
||||
commitments, // commitments
|
||||
availability_votes, /* availability
|
||||
* votes */
|
||||
Default::default(), // backers
|
||||
Zero::zero(), // relay parent
|
||||
One::one(), /* relay chain block this
|
||||
* was backed in */
|
||||
group_idx, // backing group
|
||||
)
|
||||
}
|
||||
|
||||
/// Add `CandidatePendingAvailability` and `CandidateCommitments` to the relevant storage items.
|
||||
///
|
||||
/// NOTE: the default `CandidateCommitments` used does not include any data that would lead to
|
||||
/// heavy code paths in `enact_candidate`. But enact_candidates does return a weight which will
|
||||
/// get taken into account.
|
||||
fn add_availability(
|
||||
para_id: ParaId,
|
||||
core_idx: CoreIndex,
|
||||
group_idx: GroupIndex,
|
||||
availability_votes: BitVec<u8, BitOrderLsb0>,
|
||||
candidate_hash: CandidateHash,
|
||||
) {
|
||||
let commitments = CandidateCommitments::<u32> {
|
||||
upward_messages: Default::default(),
|
||||
horizontal_messages: Default::default(),
|
||||
new_validation_code: None,
|
||||
head_data: Self::mock_head_data(),
|
||||
processed_downward_messages: 0,
|
||||
hrmp_watermark: 0u32.into(),
|
||||
};
|
||||
let candidate_availability = Self::candidate_availability_mock(
|
||||
para_id,
|
||||
group_idx,
|
||||
core_idx,
|
||||
candidate_hash,
|
||||
availability_votes,
|
||||
commitments,
|
||||
);
|
||||
inclusion::PendingAvailability::<T>::mutate(para_id, |maybe_candidates| {
|
||||
if let Some(candidates) = maybe_candidates {
|
||||
candidates.push_back(candidate_availability);
|
||||
} else {
|
||||
*maybe_candidates =
|
||||
Some([candidate_availability].into_iter().collect::<VecDeque<_>>());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Create an `AvailabilityBitfield` where `concluding` is a map where each key is a core index
|
||||
/// that is concluding and `cores` is the total number of cores in the system.
|
||||
fn availability_bitvec(concluding_cores: &BTreeSet<u32>, cores: usize) -> AvailabilityBitfield {
|
||||
let mut bitfields = bitvec::bitvec![u8, bitvec::order::Lsb0; 0; 0];
|
||||
for i in 0..cores {
|
||||
if concluding_cores.contains(&(i as u32)) {
|
||||
bitfields.push(true);
|
||||
} else {
|
||||
bitfields.push(false)
|
||||
}
|
||||
}
|
||||
|
||||
bitfields.into()
|
||||
}
|
||||
|
||||
/// Run to block number `to`, calling `initializer` `on_initialize` and `on_finalize` along the
|
||||
/// way.
|
||||
fn run_to_block(to: u32) {
|
||||
let to = to.into();
|
||||
while frame_system::Pallet::<T>::block_number() < to {
|
||||
let b = frame_system::Pallet::<T>::block_number();
|
||||
initializer::Pallet::<T>::on_finalize(b);
|
||||
|
||||
let b = b + One::one();
|
||||
frame_system::Pallet::<T>::set_block_number(b);
|
||||
initializer::Pallet::<T>::on_initialize(b);
|
||||
}
|
||||
}
|
||||
|
||||
/// Register `n_paras` count of teyrchains.
|
||||
///
|
||||
/// Note that this must be called at least 2 sessions before the target session as there is a
|
||||
/// n+2 session delay for the scheduled actions to take effect.
|
||||
fn setup_para_ids(n_paras: usize) {
|
||||
// make sure teyrchains exist prior to session change.
|
||||
for i in 0..n_paras {
|
||||
let para_id = ParaId::from(i as u32);
|
||||
let validation_code = mock_validation_code();
|
||||
|
||||
paras::Pallet::<T>::schedule_para_initialize(
|
||||
para_id,
|
||||
paras::ParaGenesisArgs {
|
||||
genesis_head: Self::mock_head_data(),
|
||||
validation_code: validation_code.clone(),
|
||||
para_kind: ParaKind::Teyrchain,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
paras::Pallet::<T>::add_trusted_validation_code(
|
||||
frame_system::Origin::<T>::Root.into(),
|
||||
validation_code,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn signing_context(&self) -> SigningContext<T::Hash> {
|
||||
SigningContext {
|
||||
parent_hash: Self::header(self.block_number).hash(),
|
||||
session_index: self.session,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a bitvec of `validators` length with all yes votes.
|
||||
fn validator_availability_votes_yes(validators: usize) -> BitVec<u8, bitvec::order::Lsb0> {
|
||||
// every validator confirms availability.
|
||||
bitvec::bitvec![u8, bitvec::order::Lsb0; 1; validators as usize]
|
||||
}
|
||||
|
||||
/// Setup session 1 and create `self.validators_map` and `self.validators`.
|
||||
fn setup_session(
|
||||
mut self,
|
||||
target_session: SessionIndex,
|
||||
validators: Vec<(T::AccountId, ValidatorId)>,
|
||||
// Total cores used in the scenario
|
||||
total_cores: usize,
|
||||
// Additional cores for elastic teyrchains
|
||||
extra_cores: usize,
|
||||
) -> Self {
|
||||
let mut block = 1;
|
||||
for session in 0..target_session {
|
||||
initializer::Pallet::<T>::test_trigger_on_new_session(
|
||||
false,
|
||||
session,
|
||||
validators.iter().map(|(a, v)| (a, v.clone())),
|
||||
None,
|
||||
);
|
||||
block += 1;
|
||||
Self::run_to_block(block);
|
||||
}
|
||||
|
||||
initializer::Pallet::<T>::test_trigger_on_new_session(
|
||||
false,
|
||||
block - 1,
|
||||
validators.iter().map(|(a, v)| (a, v.clone())),
|
||||
None,
|
||||
);
|
||||
initializer::Pallet::<T>::on_finalize(block.into());
|
||||
let block_number = BlockNumberFor::<T>::from(block + 1);
|
||||
let header = Self::header(block_number);
|
||||
|
||||
frame_system::Pallet::<T>::reset_events();
|
||||
frame_system::Pallet::<T>::initialize(&header.number(), &header.hash(), header.digest());
|
||||
initializer::Pallet::<T>::on_initialize(*header.number());
|
||||
|
||||
assert_eq!(shared::CurrentSessionIndex::<T>::get(), target_session);
|
||||
|
||||
// We need to refetch validators since they have been shuffled.
|
||||
let validators_shuffled =
|
||||
session_info::Sessions::<T>::get(target_session).unwrap().validators.clone();
|
||||
|
||||
self.validators = Some(validators_shuffled);
|
||||
self.block_number = block_number;
|
||||
self.session = target_session;
|
||||
assert_eq!(paras::Teyrchains::<T>::get().len(), total_cores - extra_cores);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Create a `UncheckedSigned<AvailabilityBitfield> for each validator where each core in
|
||||
/// `concluding_cores` is fully available. Additionally set up storage such that each
|
||||
/// `concluding_cores`is pending becoming fully available so the generated bitfields will be
|
||||
/// to the cores successfully being freed from the candidates being marked as available.
|
||||
fn create_availability_bitfields(
|
||||
&self,
|
||||
concluding_paras: &BTreeMap<u32, u32>,
|
||||
elastic_paras: &BTreeMap<u32, u8>,
|
||||
total_cores: usize,
|
||||
) -> Vec<UncheckedSigned<AvailabilityBitfield>> {
|
||||
let validators =
|
||||
self.validators.as_ref().expect("must have some validators prior to calling");
|
||||
|
||||
let mut current_core_idx = 0u32;
|
||||
let mut concluding_cores = BTreeSet::new();
|
||||
|
||||
for (seed, _) in concluding_paras.iter() {
|
||||
// make sure the candidates that will be concluding are marked as pending availability.
|
||||
let para_id = ParaId::from(*seed);
|
||||
|
||||
for _chain_idx in 0..elastic_paras.get(&seed).cloned().unwrap_or(1) {
|
||||
let core_idx = CoreIndex::from(current_core_idx);
|
||||
let group_idx =
|
||||
scheduler::Pallet::<T>::group_assigned_to_core(core_idx, self.block_number)
|
||||
.unwrap();
|
||||
|
||||
Self::add_availability(
|
||||
para_id,
|
||||
core_idx,
|
||||
group_idx,
|
||||
// No validators have made this candidate available yet.
|
||||
bitvec::bitvec![u8, bitvec::order::Lsb0; 0; validators.len()],
|
||||
CandidateHash(H256::from(byte32_slice_from(current_core_idx))),
|
||||
);
|
||||
if !self.unavailable_cores.contains(¤t_core_idx) {
|
||||
concluding_cores.insert(current_core_idx);
|
||||
}
|
||||
current_core_idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let availability_bitvec = Self::availability_bitvec(&concluding_cores, total_cores);
|
||||
|
||||
let bitfields: Vec<UncheckedSigned<AvailabilityBitfield>> = validators
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, public)| {
|
||||
let unchecked_signed = UncheckedSigned::<AvailabilityBitfield>::benchmark_sign(
|
||||
public,
|
||||
availability_bitvec.clone(),
|
||||
&self.signing_context(),
|
||||
ValidatorIndex(i as u32),
|
||||
);
|
||||
|
||||
unchecked_signed
|
||||
})
|
||||
.collect();
|
||||
|
||||
bitfields
|
||||
}
|
||||
|
||||
/// Create backed candidates for `cores_with_backed_candidates`. You need these cores to be
|
||||
/// scheduled _within_ paras inherent, which requires marking the available bitfields as fully
|
||||
/// available.
|
||||
/// - `cores_with_backed_candidates` Mapping of `para_id` seed to number of
|
||||
/// validity votes.
|
||||
fn create_backed_candidates(
|
||||
&self,
|
||||
paras_with_backed_candidates: &BTreeMap<u32, u32>,
|
||||
elastic_paras: &BTreeMap<u32, u8>,
|
||||
includes_code_upgrade: Option<u32>,
|
||||
) -> Vec<BackedCandidate<T::Hash>> {
|
||||
let validators =
|
||||
self.validators.as_ref().expect("must have some validators prior to calling");
|
||||
let config = configuration::ActiveConfig::<T>::get();
|
||||
|
||||
let mut current_core_idx = 0u32;
|
||||
paras_with_backed_candidates
|
||||
.iter()
|
||||
.flat_map(|(seed, num_votes)| {
|
||||
assert!(*num_votes <= validators.len() as u32);
|
||||
|
||||
let para_id = ParaId::from(*seed);
|
||||
let mut prev_head = None;
|
||||
// How many chained candidates we want to build ?
|
||||
(0..elastic_paras.get(&seed).cloned().unwrap_or(1))
|
||||
.map(|chain_idx| {
|
||||
let core_idx = CoreIndex::from(current_core_idx);
|
||||
// Advance core index.
|
||||
current_core_idx += 1;
|
||||
let group_idx = scheduler::Pallet::<T>::group_assigned_to_core(
|
||||
core_idx,
|
||||
self.block_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// This generates a pair and adds it to the keystore, returning just the
|
||||
// public.
|
||||
let header = Self::header(self.block_number);
|
||||
let relay_parent = header.hash();
|
||||
|
||||
// Set the head data so it can be used while validating the signatures on
|
||||
// the candidate receipt.
|
||||
let mut head_data = Self::mock_head_data();
|
||||
|
||||
if chain_idx == 0 {
|
||||
// Only first parahead of the chain needs to be set in storage.
|
||||
paras::Pallet::<T>::heads_insert(¶_id, head_data.clone());
|
||||
} else {
|
||||
// Make each candidate head data unique to avoid cycles.
|
||||
head_data.0[0] = chain_idx;
|
||||
}
|
||||
|
||||
let persisted_validation_data_hash = PersistedValidationData::<H256> {
|
||||
// To form a chain we set parent head to previous block if any, or
|
||||
// default to what is in storage already setup.
|
||||
parent_head: prev_head.take().unwrap_or(head_data.clone()),
|
||||
relay_parent_number: self.relay_parent_number(),
|
||||
relay_parent_storage_root: Default::default(),
|
||||
max_pov_size: config.max_pov_size,
|
||||
}
|
||||
.hash();
|
||||
|
||||
prev_head = Some(head_data.clone());
|
||||
|
||||
let pov_hash = Default::default();
|
||||
let validation_code_hash = mock_validation_code().hash();
|
||||
|
||||
let mut past_code_meta =
|
||||
paras::ParaPastCodeMeta::<BlockNumberFor<T>>::default();
|
||||
past_code_meta.note_replacement(0u32.into(), 0u32.into());
|
||||
|
||||
let group_validators =
|
||||
scheduler::Pallet::<T>::group_validators(group_idx).unwrap();
|
||||
|
||||
let descriptor = CandidateDescriptorV2::new(
|
||||
para_id,
|
||||
relay_parent,
|
||||
core_idx,
|
||||
self.target_session,
|
||||
persisted_validation_data_hash,
|
||||
pov_hash,
|
||||
Default::default(),
|
||||
head_data.hash(),
|
||||
validation_code_hash,
|
||||
);
|
||||
|
||||
let mut candidate = CommittedCandidateReceipt::<T::Hash> {
|
||||
descriptor,
|
||||
commitments: CandidateCommitments::<u32> {
|
||||
upward_messages: Default::default(),
|
||||
horizontal_messages: Default::default(),
|
||||
new_validation_code: includes_code_upgrade
|
||||
.map(|v| ValidationCode(vec![42u8; v as usize])),
|
||||
head_data,
|
||||
processed_downward_messages: 0,
|
||||
hrmp_watermark: self.relay_parent_number(),
|
||||
},
|
||||
};
|
||||
|
||||
if self.candidate_descriptor_v2 {
|
||||
// `UMPSignal` separator.
|
||||
candidate.commitments.upward_messages.force_push(UMP_SEPARATOR);
|
||||
|
||||
// `SelectCore` commitment.
|
||||
// Claim queue offset must be `0` so this candidate is for the very
|
||||
// next block.
|
||||
candidate.commitments.upward_messages.force_push(
|
||||
UMPSignal::SelectCore(
|
||||
CoreSelector(chain_idx as u8),
|
||||
ClaimQueueOffset(0),
|
||||
)
|
||||
.encode(),
|
||||
);
|
||||
|
||||
if let Some(approved_peer_signal) = &self.approved_peer_signal {
|
||||
candidate.commitments.upward_messages.force_push(
|
||||
UMPSignal::ApprovedPeer(approved_peer_signal.clone()).encode(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Maybe apply the candidate modifier
|
||||
if let Some(modifier) = self.candidate_modifier {
|
||||
candidate = modifier(candidate);
|
||||
}
|
||||
|
||||
let candidate_hash = candidate.hash();
|
||||
|
||||
let validity_votes: Vec<_> = group_validators
|
||||
.iter()
|
||||
.take(*num_votes as usize)
|
||||
.map(|val_idx| {
|
||||
let public = validators.get(*val_idx).unwrap();
|
||||
let sig = UncheckedSigned::<CompactStatement>::benchmark_sign(
|
||||
public,
|
||||
CompactStatement::Valid(candidate_hash),
|
||||
&self.signing_context(),
|
||||
*val_idx,
|
||||
)
|
||||
.benchmark_signature();
|
||||
|
||||
ValidityAttestation::Explicit(sig.clone())
|
||||
})
|
||||
.collect();
|
||||
|
||||
BackedCandidate::<T::Hash>::new(
|
||||
candidate,
|
||||
validity_votes,
|
||||
bitvec::bitvec![u8, bitvec::order::Lsb0; 1; group_validators.len()],
|
||||
core_idx,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Fill cores `start..last` with dispute statement sets. The statement sets will have 3/4th of
|
||||
/// votes be valid, and 1/4th of votes be invalid.
|
||||
fn create_disputes(
|
||||
&self,
|
||||
start: u32,
|
||||
last: u32,
|
||||
dispute_sessions: impl AsRef<[u32]>,
|
||||
) -> Vec<DisputeStatementSet> {
|
||||
let validators =
|
||||
self.validators.as_ref().expect("must have some validators prior to calling");
|
||||
|
||||
let dispute_sessions = dispute_sessions.as_ref();
|
||||
let mut current_core_idx = start;
|
||||
|
||||
(start..last)
|
||||
.map(|seed| {
|
||||
let dispute_session_idx = (seed - start) as usize;
|
||||
let session = dispute_sessions
|
||||
.get(dispute_session_idx)
|
||||
.cloned()
|
||||
.unwrap_or(self.target_session);
|
||||
|
||||
let para_id = ParaId::from(seed);
|
||||
let core_idx = CoreIndex::from(current_core_idx);
|
||||
current_core_idx +=1;
|
||||
|
||||
let group_idx =
|
||||
scheduler::Pallet::<T>::group_assigned_to_core(core_idx, self.block_number)
|
||||
.unwrap();
|
||||
|
||||
let candidate_hash = CandidateHash(H256::from(byte32_slice_from(seed)));
|
||||
let relay_parent = H256::from(byte32_slice_from(seed));
|
||||
|
||||
Self::add_availability(
|
||||
para_id,
|
||||
core_idx,
|
||||
group_idx,
|
||||
Self::validator_availability_votes_yes(validators.len()),
|
||||
candidate_hash,
|
||||
);
|
||||
|
||||
let statements_len =
|
||||
self.dispute_statements.get(&seed).cloned().unwrap_or(validators.len() as u32);
|
||||
let statements = (0..statements_len)
|
||||
.map(|validator_index| {
|
||||
let validator_public = &validators.get(ValidatorIndex::from(validator_index)).expect("Test case is not borked. `ValidatorIndex` out of bounds of `ValidatorId`s.");
|
||||
|
||||
// We need dispute statements on each side. And we don't want a revert log
|
||||
// so we make sure that we have a super majority with valid statements.
|
||||
let dispute_statement = if validator_index % 4 == 0 {
|
||||
DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit)
|
||||
} else if validator_index < 3 {
|
||||
// Set two votes as backing for the dispute set to be accepted
|
||||
DisputeStatement::Valid(
|
||||
ValidDisputeStatementKind::BackingValid(relay_parent)
|
||||
)
|
||||
} else {
|
||||
DisputeStatement::Valid(ValidDisputeStatementKind::Explicit)
|
||||
};
|
||||
let data = dispute_statement.payload_data(candidate_hash, session).unwrap();
|
||||
let statement_sig = validator_public.sign(&data).unwrap();
|
||||
|
||||
(dispute_statement, ValidatorIndex(validator_index), statement_sig)
|
||||
})
|
||||
.collect();
|
||||
|
||||
DisputeStatementSet { candidate_hash, session, statements }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Build a scenario for testing or benchmarks.
|
||||
///
|
||||
/// Note that this API only allows building scenarios where the `backed_and_concluding_paras`
|
||||
/// are mutually exclusive with the cores for disputes. So
|
||||
/// `backed_and_concluding_paras.len() + dispute_sessions.len() + backed_in_inherent_paras` must
|
||||
/// be less than the max number of cores.
|
||||
pub(crate) fn build(self) -> Bench<T> {
|
||||
// Make sure relevant storage is cleared. This is just to get the asserts to work when
|
||||
// running tests because it seems the storage is not cleared in between.
|
||||
#[allow(deprecated)]
|
||||
inclusion::PendingAvailability::<T>::remove_all(None);
|
||||
|
||||
// We don't allow a core to have both disputes and be marked fully available at this block.
|
||||
let max_cores = self.max_cores() as usize;
|
||||
|
||||
let extra_cores = self
|
||||
.elastic_paras
|
||||
.values()
|
||||
.map(|count| *count as usize)
|
||||
.sum::<usize>()
|
||||
.saturating_sub(self.elastic_paras.len() as usize);
|
||||
|
||||
let used_cores = self.dispute_sessions.len() +
|
||||
self.backed_and_concluding_paras.len() +
|
||||
self.backed_in_inherent_paras.len() +
|
||||
extra_cores;
|
||||
|
||||
assert!(used_cores <= max_cores);
|
||||
|
||||
// NOTE: there is an n+2 session delay for these actions to take effect.
|
||||
// We are currently in Session 0, so these changes will take effect in Session 2.
|
||||
Self::setup_para_ids(used_cores - extra_cores);
|
||||
configuration::Pallet::<T>::set_coretime_cores_unchecked(used_cores as u32).unwrap();
|
||||
|
||||
let validator_ids = generate_validator_pairs::<T>(self.max_validators());
|
||||
let target_session = SessionIndex::from(self.target_session);
|
||||
let builder = self.setup_session(target_session, validator_ids, used_cores, extra_cores);
|
||||
|
||||
let bitfields = builder.create_availability_bitfields(
|
||||
&builder.backed_and_concluding_paras,
|
||||
&builder.elastic_paras,
|
||||
scheduler::Pallet::<T>::num_availability_cores(),
|
||||
);
|
||||
|
||||
let mut backed_in_inherent = BTreeMap::new();
|
||||
backed_in_inherent.append(&mut builder.backed_and_concluding_paras.clone());
|
||||
backed_in_inherent.append(&mut builder.backed_in_inherent_paras.clone());
|
||||
let backed_candidates = builder.create_backed_candidates(
|
||||
&backed_in_inherent,
|
||||
&builder.elastic_paras,
|
||||
builder.code_upgrade,
|
||||
);
|
||||
|
||||
let disputes = builder.create_disputes(
|
||||
builder.backed_and_concluding_paras.len() as u32,
|
||||
(used_cores - extra_cores) as u32,
|
||||
builder.dispute_sessions.as_slice(),
|
||||
);
|
||||
let mut disputed_cores = (builder.backed_and_concluding_paras.len() as u32..
|
||||
((used_cores - extra_cores) as u32))
|
||||
.into_iter()
|
||||
.map(|idx| (idx, 0))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
let mut all_cores = builder.backed_and_concluding_paras.clone();
|
||||
all_cores.append(&mut disputed_cores);
|
||||
|
||||
assert_eq!(inclusion::PendingAvailability::<T>::iter().count(), used_cores - extra_cores);
|
||||
|
||||
// Sanity check that the occupied cores reported by the inclusion module are what we expect
|
||||
// to be.
|
||||
let mut core_idx = 0u32;
|
||||
let elastic_paras = &builder.elastic_paras;
|
||||
|
||||
let mut occupied_cores = inclusion::Pallet::<T>::get_occupied_cores()
|
||||
.map(|(core, candidate)| (core, candidate.candidate_descriptor().para_id()))
|
||||
.collect::<Vec<_>>();
|
||||
occupied_cores.sort_by(|(core_a, _), (core_b, _)| core_a.0.cmp(&core_b.0));
|
||||
|
||||
let mut expected_cores = all_cores
|
||||
.iter()
|
||||
.flat_map(|(para_id, _)| {
|
||||
(0..elastic_paras.get(¶_id).cloned().unwrap_or(1))
|
||||
.map(|_para_local_core_idx| {
|
||||
let old_core_idx = core_idx;
|
||||
core_idx += 1;
|
||||
(CoreIndex(old_core_idx), ParaId::from(*para_id))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
expected_cores.sort_by(|(core_a, _), (core_b, _)| core_a.0.cmp(&core_b.0));
|
||||
|
||||
assert_eq!(expected_cores, occupied_cores);
|
||||
|
||||
// We need entries in the claim queue for those:
|
||||
all_cores.append(&mut builder.backed_in_inherent_paras.clone());
|
||||
|
||||
let mut core_idx = 0u32;
|
||||
let cores = all_cores
|
||||
.keys()
|
||||
.flat_map(|para_id| {
|
||||
(0..elastic_paras.get(¶_id).cloned().unwrap_or(1))
|
||||
.map(|_para_local_core_idx| {
|
||||
// Load an assignment into provider so that one is present to pop
|
||||
let assignment =
|
||||
<T as scheduler::Config>::AssignmentProvider::get_mock_assignment(
|
||||
CoreIndex(core_idx),
|
||||
ParaId::from(*para_id),
|
||||
);
|
||||
|
||||
core_idx += 1;
|
||||
(CoreIndex(core_idx - 1), [assignment].into())
|
||||
})
|
||||
.collect::<Vec<(CoreIndex, VecDeque<Assignment>)>>()
|
||||
})
|
||||
.collect::<BTreeMap<CoreIndex, VecDeque<Assignment>>>();
|
||||
|
||||
scheduler::ClaimQueue::<T>::set(cores);
|
||||
|
||||
Bench::<T> {
|
||||
data: TeyrchainsInherentData {
|
||||
bitfields,
|
||||
backed_candidates,
|
||||
disputes,
|
||||
parent_header: Self::header(builder.block_number),
|
||||
},
|
||||
_session: target_session,
|
||||
_block_number: builder.block_number,
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,107 @@
|
||||
// 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/>.
|
||||
#![cfg(feature = "runtime-benchmarks")]
|
||||
|
||||
use crate::configuration::*;
|
||||
use frame_benchmarking::v2::*;
|
||||
use frame_system::RawOrigin;
|
||||
use pezkuwi_primitives::{ExecutorParam, ExecutorParams, PvfExecKind, PvfPrepKind};
|
||||
use sp_runtime::traits::One;
|
||||
|
||||
#[benchmarks]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
#[benchmark]
|
||||
fn set_config_with_block_number() {
|
||||
#[extrinsic_call]
|
||||
set_code_retention_period(RawOrigin::Root, One::one());
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn set_config_with_u32() {
|
||||
#[extrinsic_call]
|
||||
set_max_code_size(RawOrigin::Root, 100);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn set_config_with_option_u32() {
|
||||
#[extrinsic_call]
|
||||
set_max_validators(RawOrigin::Root, Some(10));
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn set_hrmp_open_request_ttl() -> Result<(), BenchmarkError> {
|
||||
#[block]
|
||||
{
|
||||
Err(BenchmarkError::Override(BenchmarkResult::from_weight(
|
||||
T::BlockWeights::get().max_block,
|
||||
)))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn set_config_with_balance() {
|
||||
#[extrinsic_call]
|
||||
set_hrmp_sender_deposit(RawOrigin::Root, 100_000_000_000);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn set_config_with_executor_params() {
|
||||
#[extrinsic_call]
|
||||
set_executor_params(
|
||||
RawOrigin::Root,
|
||||
ExecutorParams::from(
|
||||
&[
|
||||
ExecutorParam::MaxMemoryPages(2080),
|
||||
ExecutorParam::StackLogicalMax(65536),
|
||||
ExecutorParam::StackNativeMax(256 * 1024 * 1024),
|
||||
ExecutorParam::WasmExtBulkMemory,
|
||||
ExecutorParam::PrecheckingMaxMemory(2 * 1024 * 1024 * 1024),
|
||||
ExecutorParam::PvfPrepTimeout(PvfPrepKind::Precheck, 60_000),
|
||||
ExecutorParam::PvfPrepTimeout(PvfPrepKind::Prepare, 360_000),
|
||||
ExecutorParam::PvfExecTimeout(PvfExecKind::Backing, 2_000),
|
||||
ExecutorParam::PvfExecTimeout(PvfExecKind::Approval, 12_000),
|
||||
][..],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn set_config_with_perbill() {
|
||||
#[extrinsic_call]
|
||||
set_on_demand_fee_variability(RawOrigin::Root, Perbill::from_percent(100));
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn set_node_feature() {
|
||||
#[extrinsic_call]
|
||||
set_node_feature(RawOrigin::Root, 255, true);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn set_config_with_scheduler_params() {
|
||||
#[extrinsic_call]
|
||||
set_scheduler_params(RawOrigin::Root, SchedulerParams::default());
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(
|
||||
Pallet,
|
||||
crate::mock::new_test_ext(Default::default()),
|
||||
crate::mock::Test
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// 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/>.
|
||||
|
||||
//! A module that is responsible for migration of storage.
|
||||
|
||||
pub mod v10;
|
||||
pub mod v11;
|
||||
pub mod v12;
|
||||
pub mod v6;
|
||||
pub mod v7;
|
||||
pub mod v8;
|
||||
pub mod v9;
|
||||
@@ -0,0 +1,384 @@
|
||||
// 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/>.
|
||||
|
||||
//! A module that is responsible for migration of storage.
|
||||
|
||||
use crate::configuration::{Config, Pallet};
|
||||
use alloc::vec::Vec;
|
||||
use frame_support::{
|
||||
pallet_prelude::*,
|
||||
traits::{Defensive, UncheckedOnRuntimeUpgrade},
|
||||
weights::Weight,
|
||||
};
|
||||
use frame_system::pallet_prelude::BlockNumberFor;
|
||||
use pezkuwi_primitives::{
|
||||
AsyncBackingParams, Balance, ExecutorParams, NodeFeatures, SessionIndex,
|
||||
LEGACY_MIN_BACKING_VOTES, ON_DEMAND_DEFAULT_QUEUE_MAX_SIZE,
|
||||
};
|
||||
use sp_runtime::Perbill;
|
||||
|
||||
use super::v9::V9HostConfiguration;
|
||||
// All configuration of the runtime with respect to paras.
|
||||
#[derive(Clone, Encode, PartialEq, Decode, Debug)]
|
||||
pub struct V10HostConfiguration<BlockNumber> {
|
||||
pub max_code_size: u32,
|
||||
pub max_head_data_size: u32,
|
||||
pub max_upward_queue_count: u32,
|
||||
pub max_upward_queue_size: u32,
|
||||
pub max_upward_message_size: u32,
|
||||
pub max_upward_message_num_per_candidate: u32,
|
||||
pub hrmp_max_message_num_per_candidate: u32,
|
||||
pub validation_upgrade_cooldown: BlockNumber,
|
||||
pub validation_upgrade_delay: BlockNumber,
|
||||
pub async_backing_params: AsyncBackingParams,
|
||||
pub max_pov_size: u32,
|
||||
pub max_downward_message_size: u32,
|
||||
pub hrmp_max_teyrchain_outbound_channels: u32,
|
||||
pub hrmp_sender_deposit: Balance,
|
||||
pub hrmp_recipient_deposit: Balance,
|
||||
pub hrmp_channel_max_capacity: u32,
|
||||
pub hrmp_channel_max_total_size: u32,
|
||||
pub hrmp_max_teyrchain_inbound_channels: u32,
|
||||
pub hrmp_channel_max_message_size: u32,
|
||||
pub executor_params: ExecutorParams,
|
||||
pub code_retention_period: BlockNumber,
|
||||
pub on_demand_cores: u32,
|
||||
pub on_demand_retries: u32,
|
||||
pub on_demand_queue_max_size: u32,
|
||||
pub on_demand_target_queue_utilization: Perbill,
|
||||
pub on_demand_fee_variability: Perbill,
|
||||
pub on_demand_base_fee: Balance,
|
||||
pub on_demand_ttl: BlockNumber,
|
||||
pub group_rotation_frequency: BlockNumber,
|
||||
pub paras_availability_period: BlockNumber,
|
||||
pub scheduling_lookahead: u32,
|
||||
pub max_validators_per_core: Option<u32>,
|
||||
pub max_validators: Option<u32>,
|
||||
pub dispute_period: SessionIndex,
|
||||
pub dispute_post_conclusion_acceptance_period: BlockNumber,
|
||||
pub no_show_slots: u32,
|
||||
pub n_delay_tranches: u32,
|
||||
pub zeroth_delay_tranche_width: u32,
|
||||
pub needed_approvals: u32,
|
||||
pub relay_vrf_modulo_samples: u32,
|
||||
pub pvf_voting_ttl: SessionIndex,
|
||||
pub minimum_validation_upgrade_delay: BlockNumber,
|
||||
pub minimum_backing_votes: u32,
|
||||
pub node_features: NodeFeatures,
|
||||
}
|
||||
|
||||
impl<BlockNumber: Default + From<u32>> Default for V10HostConfiguration<BlockNumber> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
async_backing_params: AsyncBackingParams {
|
||||
max_candidate_depth: 0,
|
||||
allowed_ancestry_len: 0,
|
||||
},
|
||||
group_rotation_frequency: 1u32.into(),
|
||||
paras_availability_period: 1u32.into(),
|
||||
no_show_slots: 1u32.into(),
|
||||
validation_upgrade_cooldown: Default::default(),
|
||||
validation_upgrade_delay: 2u32.into(),
|
||||
code_retention_period: Default::default(),
|
||||
max_code_size: Default::default(),
|
||||
max_pov_size: Default::default(),
|
||||
max_head_data_size: Default::default(),
|
||||
on_demand_cores: Default::default(),
|
||||
on_demand_retries: Default::default(),
|
||||
scheduling_lookahead: 1,
|
||||
max_validators_per_core: Default::default(),
|
||||
max_validators: None,
|
||||
dispute_period: 6,
|
||||
dispute_post_conclusion_acceptance_period: 100.into(),
|
||||
n_delay_tranches: Default::default(),
|
||||
zeroth_delay_tranche_width: Default::default(),
|
||||
needed_approvals: Default::default(),
|
||||
relay_vrf_modulo_samples: Default::default(),
|
||||
max_upward_queue_count: Default::default(),
|
||||
max_upward_queue_size: Default::default(),
|
||||
max_downward_message_size: Default::default(),
|
||||
max_upward_message_size: Default::default(),
|
||||
max_upward_message_num_per_candidate: Default::default(),
|
||||
hrmp_sender_deposit: Default::default(),
|
||||
hrmp_recipient_deposit: Default::default(),
|
||||
hrmp_channel_max_capacity: Default::default(),
|
||||
hrmp_channel_max_total_size: Default::default(),
|
||||
hrmp_max_teyrchain_inbound_channels: Default::default(),
|
||||
hrmp_channel_max_message_size: Default::default(),
|
||||
hrmp_max_teyrchain_outbound_channels: Default::default(),
|
||||
hrmp_max_message_num_per_candidate: Default::default(),
|
||||
pvf_voting_ttl: 2u32.into(),
|
||||
minimum_validation_upgrade_delay: 2.into(),
|
||||
executor_params: Default::default(),
|
||||
on_demand_queue_max_size: ON_DEMAND_DEFAULT_QUEUE_MAX_SIZE,
|
||||
on_demand_base_fee: 10_000_000u128,
|
||||
on_demand_fee_variability: Perbill::from_percent(3),
|
||||
on_demand_target_queue_utilization: Perbill::from_percent(25),
|
||||
on_demand_ttl: 5u32.into(),
|
||||
minimum_backing_votes: LEGACY_MIN_BACKING_VOTES,
|
||||
node_features: NodeFeatures::EMPTY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod v9 {
|
||||
use super::*;
|
||||
|
||||
#[frame_support::storage_alias]
|
||||
pub(crate) type ActiveConfig<T: Config> =
|
||||
StorageValue<Pallet<T>, V9HostConfiguration<BlockNumberFor<T>>, OptionQuery>;
|
||||
|
||||
#[frame_support::storage_alias]
|
||||
pub(crate) type PendingConfigs<T: Config> = StorageValue<
|
||||
Pallet<T>,
|
||||
Vec<(SessionIndex, V9HostConfiguration<BlockNumberFor<T>>)>,
|
||||
OptionQuery,
|
||||
>;
|
||||
}
|
||||
|
||||
mod v10 {
|
||||
use super::*;
|
||||
|
||||
#[frame_support::storage_alias]
|
||||
pub(crate) type ActiveConfig<T: Config> =
|
||||
StorageValue<Pallet<T>, V10HostConfiguration<BlockNumberFor<T>>, OptionQuery>;
|
||||
|
||||
#[frame_support::storage_alias]
|
||||
pub(crate) type PendingConfigs<T: Config> = StorageValue<
|
||||
Pallet<T>,
|
||||
Vec<(SessionIndex, V10HostConfiguration<BlockNumberFor<T>>)>,
|
||||
OptionQuery,
|
||||
>;
|
||||
}
|
||||
|
||||
pub struct VersionUncheckedMigrateToV10<T>(core::marker::PhantomData<T>);
|
||||
impl<T: Config> UncheckedOnRuntimeUpgrade for VersionUncheckedMigrateToV10<T> {
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::TryRuntimeError> {
|
||||
log::trace!(target: crate::configuration::LOG_TARGET, "Running pre_upgrade() for HostConfiguration MigrateToV10");
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
migrate_to_v10::<T>()
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(_state: Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
|
||||
log::trace!(target: crate::configuration::LOG_TARGET, "Running post_upgrade() for HostConfiguration MigrateToV10");
|
||||
ensure!(
|
||||
Pallet::<T>::on_chain_storage_version() >= StorageVersion::new(10),
|
||||
"Storage version should be >= 10 after the migration"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub type MigrateToV10<T> = frame_support::migrations::VersionedMigration<
|
||||
9,
|
||||
10,
|
||||
VersionUncheckedMigrateToV10<T>,
|
||||
Pallet<T>,
|
||||
<T as frame_system::Config>::DbWeight,
|
||||
>;
|
||||
|
||||
// Unusual formatting is justified:
|
||||
// - make it easier to verify that fields assign what they supposed to assign.
|
||||
// - this code is transient and will be removed after all migrations are done.
|
||||
// - this code is important enough to optimize for legibility sacrificing consistency.
|
||||
#[rustfmt::skip]
|
||||
fn translate<T: Config>(pre: V9HostConfiguration<BlockNumberFor<T>>) -> V10HostConfiguration<BlockNumberFor<T>> {
|
||||
V10HostConfiguration {
|
||||
max_code_size : pre.max_code_size,
|
||||
max_head_data_size : pre.max_head_data_size,
|
||||
max_upward_queue_count : pre.max_upward_queue_count,
|
||||
max_upward_queue_size : pre.max_upward_queue_size,
|
||||
max_upward_message_size : pre.max_upward_message_size,
|
||||
max_upward_message_num_per_candidate : pre.max_upward_message_num_per_candidate,
|
||||
hrmp_max_message_num_per_candidate : pre.hrmp_max_message_num_per_candidate,
|
||||
validation_upgrade_cooldown : pre.validation_upgrade_cooldown,
|
||||
validation_upgrade_delay : pre.validation_upgrade_delay,
|
||||
max_pov_size : pre.max_pov_size,
|
||||
max_downward_message_size : pre.max_downward_message_size,
|
||||
hrmp_sender_deposit : pre.hrmp_sender_deposit,
|
||||
hrmp_recipient_deposit : pre.hrmp_recipient_deposit,
|
||||
hrmp_channel_max_capacity : pre.hrmp_channel_max_capacity,
|
||||
hrmp_channel_max_total_size : pre.hrmp_channel_max_total_size,
|
||||
hrmp_max_teyrchain_inbound_channels : pre.hrmp_max_teyrchain_inbound_channels,
|
||||
hrmp_max_teyrchain_outbound_channels : pre.hrmp_max_teyrchain_outbound_channels,
|
||||
hrmp_channel_max_message_size : pre.hrmp_channel_max_message_size,
|
||||
code_retention_period : pre.code_retention_period,
|
||||
on_demand_cores : pre.on_demand_cores,
|
||||
on_demand_retries : pre.on_demand_retries,
|
||||
group_rotation_frequency : pre.group_rotation_frequency,
|
||||
paras_availability_period : pre.paras_availability_period,
|
||||
scheduling_lookahead : pre.scheduling_lookahead,
|
||||
max_validators_per_core : pre.max_validators_per_core,
|
||||
max_validators : pre.max_validators,
|
||||
dispute_period : pre.dispute_period,
|
||||
dispute_post_conclusion_acceptance_period: pre.dispute_post_conclusion_acceptance_period,
|
||||
no_show_slots : pre.no_show_slots,
|
||||
n_delay_tranches : pre.n_delay_tranches,
|
||||
zeroth_delay_tranche_width : pre.zeroth_delay_tranche_width,
|
||||
needed_approvals : pre.needed_approvals,
|
||||
relay_vrf_modulo_samples : pre.relay_vrf_modulo_samples,
|
||||
pvf_voting_ttl : pre.pvf_voting_ttl,
|
||||
minimum_validation_upgrade_delay : pre.minimum_validation_upgrade_delay,
|
||||
async_backing_params : pre.async_backing_params,
|
||||
executor_params : pre.executor_params,
|
||||
on_demand_queue_max_size : pre.on_demand_queue_max_size,
|
||||
on_demand_base_fee : pre.on_demand_base_fee,
|
||||
on_demand_fee_variability : pre.on_demand_fee_variability,
|
||||
on_demand_target_queue_utilization : pre.on_demand_target_queue_utilization,
|
||||
on_demand_ttl : pre.on_demand_ttl,
|
||||
minimum_backing_votes : pre.minimum_backing_votes,
|
||||
node_features : NodeFeatures::EMPTY
|
||||
}
|
||||
}
|
||||
|
||||
fn migrate_to_v10<T: Config>() -> Weight {
|
||||
let v9 = v9::ActiveConfig::<T>::get()
|
||||
.defensive_proof("Could not decode old config")
|
||||
.unwrap_or_default();
|
||||
let v10 = translate::<T>(v9);
|
||||
v10::ActiveConfig::<T>::set(Some(v10));
|
||||
|
||||
// Allowed to be empty.
|
||||
let pending_v9 = v9::PendingConfigs::<T>::get().unwrap_or_default();
|
||||
let mut pending_v10 = Vec::new();
|
||||
|
||||
for (session, v9) in pending_v9.into_iter() {
|
||||
let v10 = translate::<T>(v9);
|
||||
pending_v10.push((session, v10));
|
||||
}
|
||||
v10::PendingConfigs::<T>::set(Some(pending_v10.clone()));
|
||||
|
||||
let num_configs = (pending_v10.len() + 1) as u64;
|
||||
T::DbWeight::get().reads_writes(num_configs, num_configs)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mock::{new_test_ext, Test};
|
||||
use pezkuwi_primitives::LEGACY_MIN_BACKING_VOTES;
|
||||
|
||||
#[test]
|
||||
fn v10_deserialized_from_actual_data() {
|
||||
// Example how to get new `raw_config`:
|
||||
// We'll obtain the raw_config at a specified a block
|
||||
// Steps:
|
||||
// 1. Go to Pezkuwi.js -> Developer -> Chain state -> Storage: https://pezkuwichain.io/#/chainstate
|
||||
// 2. Set these parameters:
|
||||
// 2.1. selected state query: configuration; activeConfig():
|
||||
// PezkuwiRuntimeTeyrchainsConfigurationHostConfiguration
|
||||
// 2.2. blockhash to query at:
|
||||
// 0xf89d3ab5312c5f70d396dc59612f0aa65806c798346f9db4b35278baed2e0e53 (the hash of
|
||||
// the block)
|
||||
// 2.3. Note the value of encoded storage key ->
|
||||
// 0x06de3d8a54d27e44a9d5ce189618f22db4b49d95320d9021994c850f25b8e385 for the
|
||||
// referenced block.
|
||||
// 2.4. You'll also need the decoded values to update the test.
|
||||
// 3. Go to Pezkuwi.js -> Developer -> Chain state -> Raw storage
|
||||
// 3.1 Enter the encoded storage key and you get the raw config.
|
||||
|
||||
// This exceeds the maximal line width length, but that's fine, since this is not code and
|
||||
// doesn't need to be read and also leaving it as one line allows to easily copy it.
|
||||
let raw_config =
|
||||
hex_literal::hex!["
|
||||
0000300000800000080000000000100000c8000005000000050000000200000002000000000000000000000000005000000010000400000000000000000000000000000000000000000000000000000000000000000000000800000000200000040000000000100000b004000000000000000000001027000080b2e60e80c3c90180969800000000000000000000000000050000001400000004000000010000000101000000000600000064000000020000001900000000000000020000000200000002000000050000000200000000"
|
||||
];
|
||||
|
||||
let v10 =
|
||||
V10HostConfiguration::<pezkuwi_primitives::BlockNumber>::decode(&mut &raw_config[..])
|
||||
.unwrap();
|
||||
|
||||
// We check only a sample of the values here. If we missed any fields or messed up data
|
||||
// types that would skew all the fields coming after.
|
||||
assert_eq!(v10.max_code_size, 3_145_728);
|
||||
assert_eq!(v10.validation_upgrade_cooldown, 2);
|
||||
assert_eq!(v10.max_pov_size, 5_242_880);
|
||||
assert_eq!(v10.hrmp_channel_max_message_size, 1_048_576);
|
||||
assert_eq!(v10.n_delay_tranches, 25);
|
||||
assert_eq!(v10.minimum_validation_upgrade_delay, 5);
|
||||
assert_eq!(v10.group_rotation_frequency, 20);
|
||||
assert_eq!(v10.on_demand_cores, 0);
|
||||
assert_eq!(v10.on_demand_base_fee, 10_000_000);
|
||||
assert_eq!(v10.minimum_backing_votes, LEGACY_MIN_BACKING_VOTES);
|
||||
assert_eq!(v10.node_features, NodeFeatures::EMPTY);
|
||||
}
|
||||
|
||||
// Test that `migrate_to_v10`` correctly applies the `translate` function to current and pending
|
||||
// configs.
|
||||
#[test]
|
||||
fn test_migrate_to_v10() {
|
||||
// Host configuration has lots of fields. However, in this migration we only add one
|
||||
// field. The most important part to check are a couple of the last fields. We also pick
|
||||
// extra fields to check arbitrarily, e.g. depending on their position (i.e. the middle) and
|
||||
// also their type.
|
||||
//
|
||||
// We specify only the picked fields and the rest should be provided by the `Default`
|
||||
// implementation. That implementation is copied over between the two types and should work
|
||||
// fine.
|
||||
let v9 = V9HostConfiguration::<pezkuwi_primitives::BlockNumber> {
|
||||
needed_approvals: 69,
|
||||
paras_availability_period: 55,
|
||||
hrmp_recipient_deposit: 1337,
|
||||
max_pov_size: 1111,
|
||||
minimum_validation_upgrade_delay: 20,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut pending_configs = Vec::new();
|
||||
pending_configs.push((100, v9.clone()));
|
||||
pending_configs.push((300, v9.clone()));
|
||||
|
||||
new_test_ext(Default::default()).execute_with(|| {
|
||||
// Implant the v9 version in the state.
|
||||
v9::ActiveConfig::<Test>::set(Some(v9.clone()));
|
||||
v9::PendingConfigs::<Test>::set(Some(pending_configs));
|
||||
|
||||
migrate_to_v10::<Test>();
|
||||
|
||||
let v10 = translate::<Test>(v9);
|
||||
let mut configs_to_check = v10::PendingConfigs::<Test>::get().unwrap();
|
||||
configs_to_check.push((0, v10::ActiveConfig::<Test>::get().unwrap()));
|
||||
|
||||
for (_, config) in configs_to_check {
|
||||
assert_eq!(config, v10);
|
||||
assert_eq!(config.node_features, NodeFeatures::EMPTY);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Test that migration doesn't panic in case there're no pending configurations upgrades in
|
||||
// pallet's storage.
|
||||
#[test]
|
||||
fn test_migrate_to_v10_no_pending() {
|
||||
let v9 = V9HostConfiguration::<pezkuwi_primitives::BlockNumber>::default();
|
||||
|
||||
new_test_ext(Default::default()).execute_with(|| {
|
||||
// Implant the v9 version in the state.
|
||||
v9::ActiveConfig::<Test>::set(Some(v9));
|
||||
// Ensure there're no pending configs.
|
||||
v9::PendingConfigs::<Test>::set(None);
|
||||
|
||||
// Shouldn't fail.
|
||||
migrate_to_v10::<Test>();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
// 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/>.
|
||||
|
||||
//! A module that is responsible for migration of storage.
|
||||
|
||||
use crate::configuration::{self, Config, Pallet};
|
||||
use alloc::vec::Vec;
|
||||
use frame_support::{
|
||||
migrations::VersionedMigration,
|
||||
pallet_prelude::*,
|
||||
traits::{Defensive, UncheckedOnRuntimeUpgrade},
|
||||
weights::Weight,
|
||||
};
|
||||
use frame_system::pallet_prelude::BlockNumberFor;
|
||||
use pezkuwi_primitives::{
|
||||
ApprovalVotingParams, AsyncBackingParams, ExecutorParams, NodeFeatures, SessionIndex,
|
||||
LEGACY_MIN_BACKING_VOTES, ON_DEMAND_DEFAULT_QUEUE_MAX_SIZE,
|
||||
};
|
||||
|
||||
use pezkuwi_core_primitives::Balance;
|
||||
use sp_arithmetic::Perbill;
|
||||
|
||||
use super::v10::V10HostConfiguration;
|
||||
|
||||
#[derive(Clone, Encode, PartialEq, Decode, Debug)]
|
||||
pub struct V11HostConfiguration<BlockNumber> {
|
||||
pub max_code_size: u32,
|
||||
pub max_head_data_size: u32,
|
||||
pub max_upward_queue_count: u32,
|
||||
pub max_upward_queue_size: u32,
|
||||
pub max_upward_message_size: u32,
|
||||
pub max_upward_message_num_per_candidate: u32,
|
||||
pub hrmp_max_message_num_per_candidate: u32,
|
||||
pub validation_upgrade_cooldown: BlockNumber,
|
||||
pub validation_upgrade_delay: BlockNumber,
|
||||
pub async_backing_params: AsyncBackingParams,
|
||||
pub max_pov_size: u32,
|
||||
pub max_downward_message_size: u32,
|
||||
pub hrmp_max_teyrchain_outbound_channels: u32,
|
||||
pub hrmp_sender_deposit: Balance,
|
||||
pub hrmp_recipient_deposit: Balance,
|
||||
pub hrmp_channel_max_capacity: u32,
|
||||
pub hrmp_channel_max_total_size: u32,
|
||||
pub hrmp_max_teyrchain_inbound_channels: u32,
|
||||
pub hrmp_channel_max_message_size: u32,
|
||||
pub executor_params: ExecutorParams,
|
||||
pub code_retention_period: BlockNumber,
|
||||
pub coretime_cores: u32,
|
||||
pub on_demand_retries: u32,
|
||||
pub on_demand_queue_max_size: u32,
|
||||
pub on_demand_target_queue_utilization: Perbill,
|
||||
pub on_demand_fee_variability: Perbill,
|
||||
pub on_demand_base_fee: Balance,
|
||||
pub on_demand_ttl: BlockNumber,
|
||||
pub group_rotation_frequency: BlockNumber,
|
||||
pub paras_availability_period: BlockNumber,
|
||||
pub scheduling_lookahead: u32,
|
||||
pub max_validators_per_core: Option<u32>,
|
||||
pub max_validators: Option<u32>,
|
||||
pub dispute_period: SessionIndex,
|
||||
pub dispute_post_conclusion_acceptance_period: BlockNumber,
|
||||
pub no_show_slots: u32,
|
||||
pub n_delay_tranches: u32,
|
||||
pub zeroth_delay_tranche_width: u32,
|
||||
pub needed_approvals: u32,
|
||||
pub relay_vrf_modulo_samples: u32,
|
||||
pub pvf_voting_ttl: SessionIndex,
|
||||
pub minimum_validation_upgrade_delay: BlockNumber,
|
||||
pub minimum_backing_votes: u32,
|
||||
pub node_features: NodeFeatures,
|
||||
pub approval_voting_params: ApprovalVotingParams,
|
||||
}
|
||||
|
||||
impl<BlockNumber: Default + From<u32>> Default for V11HostConfiguration<BlockNumber> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
async_backing_params: AsyncBackingParams {
|
||||
max_candidate_depth: 0,
|
||||
allowed_ancestry_len: 0,
|
||||
},
|
||||
group_rotation_frequency: 1u32.into(),
|
||||
paras_availability_period: 1u32.into(),
|
||||
no_show_slots: 1u32.into(),
|
||||
validation_upgrade_cooldown: Default::default(),
|
||||
validation_upgrade_delay: 2u32.into(),
|
||||
code_retention_period: Default::default(),
|
||||
max_code_size: Default::default(),
|
||||
max_pov_size: Default::default(),
|
||||
max_head_data_size: Default::default(),
|
||||
coretime_cores: Default::default(),
|
||||
on_demand_retries: Default::default(),
|
||||
scheduling_lookahead: 1,
|
||||
max_validators_per_core: Default::default(),
|
||||
max_validators: None,
|
||||
dispute_period: 6,
|
||||
dispute_post_conclusion_acceptance_period: 100.into(),
|
||||
n_delay_tranches: Default::default(),
|
||||
zeroth_delay_tranche_width: Default::default(),
|
||||
needed_approvals: Default::default(),
|
||||
relay_vrf_modulo_samples: Default::default(),
|
||||
max_upward_queue_count: Default::default(),
|
||||
max_upward_queue_size: Default::default(),
|
||||
max_downward_message_size: Default::default(),
|
||||
max_upward_message_size: Default::default(),
|
||||
max_upward_message_num_per_candidate: Default::default(),
|
||||
hrmp_sender_deposit: Default::default(),
|
||||
hrmp_recipient_deposit: Default::default(),
|
||||
hrmp_channel_max_capacity: Default::default(),
|
||||
hrmp_channel_max_total_size: Default::default(),
|
||||
hrmp_max_teyrchain_inbound_channels: Default::default(),
|
||||
hrmp_channel_max_message_size: Default::default(),
|
||||
hrmp_max_teyrchain_outbound_channels: Default::default(),
|
||||
hrmp_max_message_num_per_candidate: Default::default(),
|
||||
pvf_voting_ttl: 2u32.into(),
|
||||
minimum_validation_upgrade_delay: 2.into(),
|
||||
executor_params: Default::default(),
|
||||
approval_voting_params: ApprovalVotingParams { max_approval_coalesce_count: 1 },
|
||||
on_demand_queue_max_size: ON_DEMAND_DEFAULT_QUEUE_MAX_SIZE,
|
||||
on_demand_base_fee: 10_000_000u128,
|
||||
on_demand_fee_variability: Perbill::from_percent(3),
|
||||
on_demand_target_queue_utilization: Perbill::from_percent(25),
|
||||
on_demand_ttl: 5u32.into(),
|
||||
minimum_backing_votes: LEGACY_MIN_BACKING_VOTES,
|
||||
node_features: NodeFeatures::EMPTY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod v10 {
|
||||
use super::*;
|
||||
|
||||
#[frame_support::storage_alias]
|
||||
pub(crate) type ActiveConfig<T: Config> =
|
||||
StorageValue<Pallet<T>, V10HostConfiguration<BlockNumberFor<T>>, OptionQuery>;
|
||||
|
||||
#[frame_support::storage_alias]
|
||||
pub(crate) type PendingConfigs<T: Config> = StorageValue<
|
||||
Pallet<T>,
|
||||
Vec<(SessionIndex, V10HostConfiguration<BlockNumberFor<T>>)>,
|
||||
OptionQuery,
|
||||
>;
|
||||
}
|
||||
|
||||
mod v11 {
|
||||
use super::*;
|
||||
|
||||
#[frame_support::storage_alias]
|
||||
pub(crate) type ActiveConfig<T: Config> =
|
||||
StorageValue<Pallet<T>, V11HostConfiguration<BlockNumberFor<T>>, OptionQuery>;
|
||||
|
||||
#[frame_support::storage_alias]
|
||||
pub(crate) type PendingConfigs<T: Config> = StorageValue<
|
||||
Pallet<T>,
|
||||
Vec<(SessionIndex, V11HostConfiguration<BlockNumberFor<T>>)>,
|
||||
OptionQuery,
|
||||
>;
|
||||
}
|
||||
|
||||
pub type MigrateToV11<T> = VersionedMigration<
|
||||
10,
|
||||
11,
|
||||
UncheckedMigrateToV11<T>,
|
||||
Pallet<T>,
|
||||
<T as frame_system::Config>::DbWeight,
|
||||
>;
|
||||
|
||||
pub struct UncheckedMigrateToV11<T>(core::marker::PhantomData<T>);
|
||||
impl<T: Config> UncheckedOnRuntimeUpgrade for UncheckedMigrateToV11<T> {
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::TryRuntimeError> {
|
||||
log::trace!(target: crate::configuration::LOG_TARGET, "Running pre_upgrade() for HostConfiguration MigrateToV11");
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
log::info!(target: configuration::LOG_TARGET, "HostConfiguration MigrateToV11 started");
|
||||
let weight_consumed = migrate_to_v11::<T>();
|
||||
|
||||
log::info!(target: configuration::LOG_TARGET, "HostConfiguration MigrateToV11 executed successfully");
|
||||
|
||||
weight_consumed
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(_state: Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
|
||||
log::trace!(target: crate::configuration::LOG_TARGET, "Running post_upgrade() for HostConfiguration MigrateToV11");
|
||||
ensure!(
|
||||
StorageVersion::get::<Pallet<T>>() >= 11,
|
||||
"Storage version should be >= 11 after the migration"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn migrate_to_v11<T: Config>() -> Weight {
|
||||
// Unusual formatting is justified:
|
||||
// - make it easier to verify that fields assign what they supposed to assign.
|
||||
// - this code is transient and will be removed after all migrations are done.
|
||||
// - this code is important enough to optimize for legibility sacrificing consistency.
|
||||
#[rustfmt::skip]
|
||||
let translate =
|
||||
|pre: V10HostConfiguration<BlockNumberFor<T>>| ->
|
||||
V11HostConfiguration<BlockNumberFor<T>>
|
||||
{
|
||||
V11HostConfiguration {
|
||||
max_code_size : pre.max_code_size,
|
||||
max_head_data_size : pre.max_head_data_size,
|
||||
max_upward_queue_count : pre.max_upward_queue_count,
|
||||
max_upward_queue_size : pre.max_upward_queue_size,
|
||||
max_upward_message_size : pre.max_upward_message_size,
|
||||
max_upward_message_num_per_candidate : pre.max_upward_message_num_per_candidate,
|
||||
hrmp_max_message_num_per_candidate : pre.hrmp_max_message_num_per_candidate,
|
||||
validation_upgrade_cooldown : pre.validation_upgrade_cooldown,
|
||||
validation_upgrade_delay : pre.validation_upgrade_delay,
|
||||
max_pov_size : pre.max_pov_size,
|
||||
max_downward_message_size : pre.max_downward_message_size,
|
||||
hrmp_sender_deposit : pre.hrmp_sender_deposit,
|
||||
hrmp_recipient_deposit : pre.hrmp_recipient_deposit,
|
||||
hrmp_channel_max_capacity : pre.hrmp_channel_max_capacity,
|
||||
hrmp_channel_max_total_size : pre.hrmp_channel_max_total_size,
|
||||
hrmp_max_teyrchain_inbound_channels : pre.hrmp_max_teyrchain_inbound_channels,
|
||||
hrmp_max_teyrchain_outbound_channels : pre.hrmp_max_teyrchain_outbound_channels,
|
||||
hrmp_channel_max_message_size : pre.hrmp_channel_max_message_size,
|
||||
code_retention_period : pre.code_retention_period,
|
||||
coretime_cores : pre.on_demand_cores,
|
||||
on_demand_retries : pre.on_demand_retries,
|
||||
group_rotation_frequency : pre.group_rotation_frequency,
|
||||
paras_availability_period : pre.paras_availability_period,
|
||||
scheduling_lookahead : pre.scheduling_lookahead,
|
||||
max_validators_per_core : pre.max_validators_per_core,
|
||||
max_validators : pre.max_validators,
|
||||
dispute_period : pre.dispute_period,
|
||||
dispute_post_conclusion_acceptance_period: pre.dispute_post_conclusion_acceptance_period,
|
||||
no_show_slots : pre.no_show_slots,
|
||||
n_delay_tranches : pre.n_delay_tranches,
|
||||
zeroth_delay_tranche_width : pre.zeroth_delay_tranche_width,
|
||||
needed_approvals : pre.needed_approvals,
|
||||
relay_vrf_modulo_samples : pre.relay_vrf_modulo_samples,
|
||||
pvf_voting_ttl : pre.pvf_voting_ttl,
|
||||
minimum_validation_upgrade_delay : pre.minimum_validation_upgrade_delay,
|
||||
async_backing_params : pre.async_backing_params,
|
||||
executor_params : pre.executor_params,
|
||||
on_demand_queue_max_size : pre.on_demand_queue_max_size,
|
||||
on_demand_base_fee : pre.on_demand_base_fee,
|
||||
on_demand_fee_variability : pre.on_demand_fee_variability,
|
||||
on_demand_target_queue_utilization : pre.on_demand_target_queue_utilization,
|
||||
on_demand_ttl : pre.on_demand_ttl,
|
||||
minimum_backing_votes : pre.minimum_backing_votes,
|
||||
node_features : pre.node_features,
|
||||
approval_voting_params : ApprovalVotingParams {
|
||||
max_approval_coalesce_count: 1,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let v10 = v10::ActiveConfig::<T>::get()
|
||||
.defensive_proof("Could not decode old config")
|
||||
.unwrap_or_default();
|
||||
let v11 = translate(v10);
|
||||
v11::ActiveConfig::<T>::set(Some(v11));
|
||||
|
||||
// Allowed to be empty.
|
||||
let pending_v9 = v10::PendingConfigs::<T>::get().unwrap_or_default();
|
||||
let mut pending_v10 = Vec::new();
|
||||
|
||||
for (session, v10) in pending_v9.into_iter() {
|
||||
let v11 = translate(v10);
|
||||
pending_v10.push((session, v11));
|
||||
}
|
||||
v11::PendingConfigs::<T>::set(Some(pending_v10.clone()));
|
||||
|
||||
let num_configs = (pending_v10.len() + 1) as u64;
|
||||
T::DbWeight::get().reads_writes(num_configs, num_configs)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pezkuwi_primitives::LEGACY_MIN_BACKING_VOTES;
|
||||
|
||||
use super::*;
|
||||
use crate::mock::{new_test_ext, Test};
|
||||
|
||||
#[test]
|
||||
fn v11_deserialized_from_actual_data() {
|
||||
// Example how to get new `raw_config`:
|
||||
// We'll obtain the raw_config at a specified a block
|
||||
// Steps:
|
||||
// 1. Go to Pezkuwi.js -> Developer -> Chain state -> Storage: https://pezkuwichain.io/#/chainstate
|
||||
// 2. Set these parameters:
|
||||
// 2.1. selected state query: configuration; activeConfig():
|
||||
// PezkuwiRuntimeTeyrchainsConfigurationHostConfiguration
|
||||
// 2.2. blockhash to query at:
|
||||
// 0xf89d3ab5312c5f70d396dc59612f0aa65806c798346f9db4b35278baed2e0e53 (the hash of
|
||||
// the block)
|
||||
// 2.3. Note the value of encoded storage key ->
|
||||
// 0x06de3d8a54d27e44a9d5ce189618f22db4b49d95320d9021994c850f25b8e385 for the
|
||||
// referenced block.
|
||||
// 2.4. You'll also need the decoded values to update the test.
|
||||
// 3. Go to Pezkuwi.js -> Developer -> Chain state -> Raw storage
|
||||
// 3.1 Enter the encoded storage key and you get the raw config.
|
||||
|
||||
// This exceeds the maximal line width length, but that's fine, since this is not code and
|
||||
// doesn't need to be read and also leaving it as one line allows to easily copy it.
|
||||
let raw_config =
|
||||
hex_literal::hex!["
|
||||
0000300000800000080000000000100000c8000005000000050000000200000002000000000000000000000000005000000010000400000000000000000000000000000000000000000000000000000000000000000000000800000000200000040000000000100000b004000000000000000000001027000080b2e60e80c3c9018096980000000000000000000000000005000000140000000400000001000000010100000000060000006400000002000000190000000000000002000000020000000200000005000000020000000001000000"
|
||||
];
|
||||
|
||||
let v11 =
|
||||
V11HostConfiguration::<pezkuwi_primitives::BlockNumber>::decode(&mut &raw_config[..])
|
||||
.unwrap();
|
||||
|
||||
// We check only a sample of the values here. If we missed any fields or messed up data
|
||||
// types that would skew all the fields coming after.
|
||||
assert_eq!(v11.max_code_size, 3_145_728);
|
||||
assert_eq!(v11.validation_upgrade_cooldown, 2);
|
||||
assert_eq!(v11.max_pov_size, 5_242_880);
|
||||
assert_eq!(v11.hrmp_channel_max_message_size, 1_048_576);
|
||||
assert_eq!(v11.n_delay_tranches, 25);
|
||||
assert_eq!(v11.minimum_validation_upgrade_delay, 5);
|
||||
assert_eq!(v11.group_rotation_frequency, 20);
|
||||
assert_eq!(v11.coretime_cores, 0);
|
||||
assert_eq!(v11.on_demand_base_fee, 10_000_000);
|
||||
assert_eq!(v11.minimum_backing_votes, LEGACY_MIN_BACKING_VOTES);
|
||||
assert_eq!(v11.approval_voting_params.max_approval_coalesce_count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_migrate_to_v11() {
|
||||
// Host configuration has lots of fields. However, in this migration we only add one
|
||||
// field. The most important part to check are a couple of the last fields. We also pick
|
||||
// extra fields to check arbitrarily, e.g. depending on their position (i.e. the middle) and
|
||||
// also their type.
|
||||
//
|
||||
// We specify only the picked fields and the rest should be provided by the `Default`
|
||||
// implementation. That implementation is copied over between the two types and should work
|
||||
// fine.
|
||||
let v10 = V10HostConfiguration::<pezkuwi_primitives::BlockNumber> {
|
||||
needed_approvals: 69,
|
||||
paras_availability_period: 55,
|
||||
hrmp_recipient_deposit: 1337,
|
||||
max_pov_size: 1111,
|
||||
minimum_validation_upgrade_delay: 20,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut pending_configs = Vec::new();
|
||||
pending_configs.push((100, v10.clone()));
|
||||
pending_configs.push((300, v10.clone()));
|
||||
|
||||
new_test_ext(Default::default()).execute_with(|| {
|
||||
// Implant the v10 version in the state.
|
||||
v10::ActiveConfig::<Test>::set(Some(v10.clone()));
|
||||
v10::PendingConfigs::<Test>::set(Some(pending_configs));
|
||||
|
||||
migrate_to_v11::<Test>();
|
||||
|
||||
let v11 = v11::ActiveConfig::<Test>::get().unwrap();
|
||||
assert_eq!(v11.approval_voting_params.max_approval_coalesce_count, 1);
|
||||
|
||||
let mut configs_to_check = v11::PendingConfigs::<Test>::get().unwrap();
|
||||
configs_to_check.push((0, v11.clone()));
|
||||
|
||||
for (_, v11) in configs_to_check {
|
||||
#[rustfmt::skip]
|
||||
{
|
||||
assert_eq!(v10.max_code_size , v11.max_code_size);
|
||||
assert_eq!(v10.max_head_data_size , v11.max_head_data_size);
|
||||
assert_eq!(v10.max_upward_queue_count , v11.max_upward_queue_count);
|
||||
assert_eq!(v10.max_upward_queue_size , v11.max_upward_queue_size);
|
||||
assert_eq!(v10.max_upward_message_size , v11.max_upward_message_size);
|
||||
assert_eq!(v10.max_upward_message_num_per_candidate , v11.max_upward_message_num_per_candidate);
|
||||
assert_eq!(v10.hrmp_max_message_num_per_candidate , v11.hrmp_max_message_num_per_candidate);
|
||||
assert_eq!(v10.validation_upgrade_cooldown , v11.validation_upgrade_cooldown);
|
||||
assert_eq!(v10.validation_upgrade_delay , v11.validation_upgrade_delay);
|
||||
assert_eq!(v10.max_pov_size , v11.max_pov_size);
|
||||
assert_eq!(v10.max_downward_message_size , v11.max_downward_message_size);
|
||||
assert_eq!(v10.hrmp_max_teyrchain_outbound_channels , v11.hrmp_max_teyrchain_outbound_channels);
|
||||
assert_eq!(v10.hrmp_sender_deposit , v11.hrmp_sender_deposit);
|
||||
assert_eq!(v10.hrmp_recipient_deposit , v11.hrmp_recipient_deposit);
|
||||
assert_eq!(v10.hrmp_channel_max_capacity , v11.hrmp_channel_max_capacity);
|
||||
assert_eq!(v10.hrmp_channel_max_total_size , v11.hrmp_channel_max_total_size);
|
||||
assert_eq!(v10.hrmp_max_teyrchain_inbound_channels , v11.hrmp_max_teyrchain_inbound_channels);
|
||||
assert_eq!(v10.hrmp_channel_max_message_size , v11.hrmp_channel_max_message_size);
|
||||
assert_eq!(v10.code_retention_period , v11.code_retention_period);
|
||||
assert_eq!(v10.on_demand_cores , v11.coretime_cores);
|
||||
assert_eq!(v10.on_demand_retries , v11.on_demand_retries);
|
||||
assert_eq!(v10.group_rotation_frequency , v11.group_rotation_frequency);
|
||||
assert_eq!(v10.paras_availability_period , v11.paras_availability_period);
|
||||
assert_eq!(v10.scheduling_lookahead , v11.scheduling_lookahead);
|
||||
assert_eq!(v10.max_validators_per_core , v11.max_validators_per_core);
|
||||
assert_eq!(v10.max_validators , v11.max_validators);
|
||||
assert_eq!(v10.dispute_period , v11.dispute_period);
|
||||
assert_eq!(v10.no_show_slots , v11.no_show_slots);
|
||||
assert_eq!(v10.n_delay_tranches , v11.n_delay_tranches);
|
||||
assert_eq!(v10.zeroth_delay_tranche_width , v11.zeroth_delay_tranche_width);
|
||||
assert_eq!(v10.needed_approvals , v11.needed_approvals);
|
||||
assert_eq!(v10.relay_vrf_modulo_samples , v11.relay_vrf_modulo_samples);
|
||||
assert_eq!(v10.pvf_voting_ttl , v11.pvf_voting_ttl);
|
||||
assert_eq!(v10.minimum_validation_upgrade_delay , v11.minimum_validation_upgrade_delay);
|
||||
assert_eq!(v10.async_backing_params.allowed_ancestry_len, v11.async_backing_params.allowed_ancestry_len);
|
||||
assert_eq!(v10.async_backing_params.max_candidate_depth , v11.async_backing_params.max_candidate_depth);
|
||||
assert_eq!(v10.executor_params , v11.executor_params);
|
||||
assert_eq!(v10.minimum_backing_votes , v11.minimum_backing_votes);
|
||||
}; // ; makes this a statement. `rustfmt::skip` cannot be put on an expression.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Test that migration doesn't panic in case there're no pending configurations upgrades in
|
||||
// pallet's storage.
|
||||
#[test]
|
||||
fn test_migrate_to_v11_no_pending() {
|
||||
let v10 = V10HostConfiguration::<pezkuwi_primitives::BlockNumber>::default();
|
||||
|
||||
new_test_ext(Default::default()).execute_with(|| {
|
||||
// Implant the v10 version in the state.
|
||||
v10::ActiveConfig::<Test>::set(Some(v10));
|
||||
// Ensure there're no pending configs.
|
||||
v11::PendingConfigs::<Test>::set(None);
|
||||
|
||||
// Shouldn't fail.
|
||||
migrate_to_v11::<Test>();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
// 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/>.
|
||||
|
||||
//! A module that is responsible for migration of storage.
|
||||
|
||||
use crate::configuration::{self, migration::v11::V11HostConfiguration, Config, Pallet};
|
||||
use alloc::vec::Vec;
|
||||
use frame_support::{
|
||||
migrations::VersionedMigration,
|
||||
pallet_prelude::*,
|
||||
traits::{Defensive, UncheckedOnRuntimeUpgrade},
|
||||
};
|
||||
use frame_system::pallet_prelude::BlockNumberFor;
|
||||
use pezkuwi_primitives::SchedulerParams;
|
||||
use sp_core::Get;
|
||||
use sp_staking::SessionIndex;
|
||||
|
||||
type V12HostConfiguration<BlockNumber> = configuration::HostConfiguration<BlockNumber>;
|
||||
|
||||
mod v11 {
|
||||
use super::*;
|
||||
|
||||
#[frame_support::storage_alias]
|
||||
pub(crate) type ActiveConfig<T: Config> =
|
||||
StorageValue<Pallet<T>, V11HostConfiguration<BlockNumberFor<T>>, OptionQuery>;
|
||||
|
||||
#[frame_support::storage_alias]
|
||||
pub(crate) type PendingConfigs<T: Config> = StorageValue<
|
||||
Pallet<T>,
|
||||
Vec<(SessionIndex, V11HostConfiguration<BlockNumberFor<T>>)>,
|
||||
OptionQuery,
|
||||
>;
|
||||
}
|
||||
|
||||
mod v12 {
|
||||
use super::*;
|
||||
|
||||
#[frame_support::storage_alias]
|
||||
pub(crate) type ActiveConfig<T: Config> =
|
||||
StorageValue<Pallet<T>, V12HostConfiguration<BlockNumberFor<T>>, OptionQuery>;
|
||||
|
||||
#[frame_support::storage_alias]
|
||||
pub(crate) type PendingConfigs<T: Config> = StorageValue<
|
||||
Pallet<T>,
|
||||
Vec<(SessionIndex, V12HostConfiguration<BlockNumberFor<T>>)>,
|
||||
OptionQuery,
|
||||
>;
|
||||
}
|
||||
|
||||
pub type MigrateToV12<T> = VersionedMigration<
|
||||
11,
|
||||
12,
|
||||
UncheckedMigrateToV12<T>,
|
||||
Pallet<T>,
|
||||
<T as frame_system::Config>::DbWeight,
|
||||
>;
|
||||
|
||||
pub struct UncheckedMigrateToV12<T>(core::marker::PhantomData<T>);
|
||||
|
||||
impl<T: Config> UncheckedOnRuntimeUpgrade for UncheckedMigrateToV12<T> {
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::TryRuntimeError> {
|
||||
log::trace!(target: crate::configuration::LOG_TARGET, "Running pre_upgrade() for HostConfiguration MigrateToV12");
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
log::info!(target: configuration::LOG_TARGET, "HostConfiguration MigrateToV12 started");
|
||||
let weight_consumed = migrate_to_v12::<T>();
|
||||
|
||||
log::info!(target: configuration::LOG_TARGET, "HostConfiguration MigrateToV12 executed successfully");
|
||||
|
||||
weight_consumed
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(_state: Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
|
||||
log::trace!(target: crate::configuration::LOG_TARGET, "Running post_upgrade() for HostConfiguration MigrateToV12");
|
||||
ensure!(
|
||||
StorageVersion::get::<Pallet<T>>() >= 12,
|
||||
"Storage version should be >= 12 after the migration"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn migrate_to_v12<T: Config>() -> Weight {
|
||||
// Unusual formatting is justified:
|
||||
// - make it easier to verify that fields assign what they supposed to assign.
|
||||
// - this code is transient and will be removed after all migrations are done.
|
||||
// - this code is important enough to optimize for legibility sacrificing consistency.
|
||||
#[rustfmt::skip]
|
||||
let translate =
|
||||
|pre: V11HostConfiguration<BlockNumberFor<T>>| ->
|
||||
V12HostConfiguration<BlockNumberFor<T>>
|
||||
{
|
||||
V12HostConfiguration {
|
||||
max_code_size : pre.max_code_size,
|
||||
max_head_data_size : pre.max_head_data_size,
|
||||
max_upward_queue_count : pre.max_upward_queue_count,
|
||||
max_upward_queue_size : pre.max_upward_queue_size,
|
||||
max_upward_message_size : pre.max_upward_message_size,
|
||||
max_upward_message_num_per_candidate : pre.max_upward_message_num_per_candidate,
|
||||
hrmp_max_message_num_per_candidate : pre.hrmp_max_message_num_per_candidate,
|
||||
validation_upgrade_cooldown : pre.validation_upgrade_cooldown,
|
||||
validation_upgrade_delay : pre.validation_upgrade_delay,
|
||||
max_pov_size : pre.max_pov_size,
|
||||
max_downward_message_size : pre.max_downward_message_size,
|
||||
hrmp_sender_deposit : pre.hrmp_sender_deposit,
|
||||
hrmp_recipient_deposit : pre.hrmp_recipient_deposit,
|
||||
hrmp_channel_max_capacity : pre.hrmp_channel_max_capacity,
|
||||
hrmp_channel_max_total_size : pre.hrmp_channel_max_total_size,
|
||||
hrmp_max_teyrchain_inbound_channels : pre.hrmp_max_teyrchain_inbound_channels,
|
||||
hrmp_max_teyrchain_outbound_channels : pre.hrmp_max_teyrchain_outbound_channels,
|
||||
hrmp_channel_max_message_size : pre.hrmp_channel_max_message_size,
|
||||
code_retention_period : pre.code_retention_period,
|
||||
max_validators : pre.max_validators,
|
||||
dispute_period : pre.dispute_period,
|
||||
dispute_post_conclusion_acceptance_period: pre.dispute_post_conclusion_acceptance_period,
|
||||
no_show_slots : pre.no_show_slots,
|
||||
n_delay_tranches : pre.n_delay_tranches,
|
||||
zeroth_delay_tranche_width : pre.zeroth_delay_tranche_width,
|
||||
needed_approvals : pre.needed_approvals,
|
||||
relay_vrf_modulo_samples : pre.relay_vrf_modulo_samples,
|
||||
pvf_voting_ttl : pre.pvf_voting_ttl,
|
||||
minimum_validation_upgrade_delay : pre.minimum_validation_upgrade_delay,
|
||||
async_backing_params : pre.async_backing_params,
|
||||
executor_params : pre.executor_params,
|
||||
minimum_backing_votes : pre.minimum_backing_votes,
|
||||
node_features : pre.node_features,
|
||||
approval_voting_params : pre.approval_voting_params,
|
||||
#[allow(deprecated)]
|
||||
scheduler_params: SchedulerParams {
|
||||
group_rotation_frequency : pre.group_rotation_frequency,
|
||||
paras_availability_period : pre.paras_availability_period,
|
||||
max_validators_per_core : pre.max_validators_per_core,
|
||||
lookahead : pre.scheduling_lookahead,
|
||||
num_cores : pre.coretime_cores,
|
||||
max_availability_timeouts : pre.on_demand_retries,
|
||||
on_demand_queue_max_size : pre.on_demand_queue_max_size,
|
||||
on_demand_target_queue_utilization : pre.on_demand_target_queue_utilization,
|
||||
on_demand_fee_variability : pre.on_demand_fee_variability,
|
||||
on_demand_base_fee : pre.on_demand_base_fee,
|
||||
ttl : pre.on_demand_ttl,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let v11 = v11::ActiveConfig::<T>::get()
|
||||
.defensive_proof("Could not decode old config")
|
||||
.unwrap_or_default();
|
||||
let v12 = translate(v11);
|
||||
v12::ActiveConfig::<T>::set(Some(v12));
|
||||
|
||||
// Allowed to be empty.
|
||||
let pending_v11 = v11::PendingConfigs::<T>::get().unwrap_or_default();
|
||||
let mut pending_v12 = Vec::new();
|
||||
|
||||
for (session, v11) in pending_v11.into_iter() {
|
||||
let v12 = translate(v11);
|
||||
pending_v12.push((session, v12));
|
||||
}
|
||||
v12::PendingConfigs::<T>::set(Some(pending_v12.clone()));
|
||||
|
||||
let num_configs = (pending_v12.len() + 1) as u64;
|
||||
T::DbWeight::get().reads_writes(num_configs, num_configs)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pezkuwi_primitives::LEGACY_MIN_BACKING_VOTES;
|
||||
use sp_arithmetic::Perbill;
|
||||
|
||||
use super::*;
|
||||
use crate::mock::{new_test_ext, Test};
|
||||
|
||||
#[test]
|
||||
fn v12_deserialized_from_actual_data() {
|
||||
// Example how to get new `raw_config`:
|
||||
// We'll obtain the raw_config at a specified a block
|
||||
// Steps:
|
||||
// 1. Go to Pezkuwi.js -> Developer -> Chain state -> Storage: https://pezkuwichain.io/#/chainstate
|
||||
// 2. Set these parameters:
|
||||
// 2.1. selected state query: configuration; activeConfig():
|
||||
// PezkuwiRuntimeTeyrchainsConfigurationHostConfiguration
|
||||
// 2.2. blockhash to query at:
|
||||
// 0xf89d3ab5312c5f70d396dc59612f0aa65806c798346f9db4b35278baed2e0e53 (the hash of
|
||||
// the block)
|
||||
// 2.3. Note the value of encoded storage key ->
|
||||
// 0x06de3d8a54d27e44a9d5ce189618f22db4b49d95320d9021994c850f25b8e385 for the
|
||||
// referenced block.
|
||||
// 2.4. You'll also need the decoded values to update the test.
|
||||
// 3. Go to Pezkuwi.js -> Developer -> Chain state -> Raw storage
|
||||
// 3.1 Enter the encoded storage key and you get the raw config.
|
||||
|
||||
// This exceeds the maximal line width length, but that's fine, since this is not code and
|
||||
// doesn't need to be read and also leaving it as one line allows to easily copy it.
|
||||
let raw_config =
|
||||
hex_literal::hex![
|
||||
"0000300000800000080000000000100000c8000005000000050000000200000002000000000000000000000000005000000010000400000000000000000000000000000000000000000000000000000000000000000000000800000000200000040000000000100000b004000000060000006400000002000000190000000000000002000000020000000200000005000000020000000001000000140000000400000001010000000100000001000000000000001027000080b2e60e80c3c9018096980000000000000000000000000005000000"
|
||||
];
|
||||
|
||||
let v12 =
|
||||
V12HostConfiguration::<pezkuwi_primitives::BlockNumber>::decode(&mut &raw_config[..])
|
||||
.unwrap();
|
||||
|
||||
// We check only a sample of the values here. If we missed any fields or messed up data
|
||||
// types that would skew all the fields coming after.
|
||||
assert_eq!(v12.max_code_size, 3_145_728);
|
||||
assert_eq!(v12.validation_upgrade_cooldown, 2);
|
||||
assert_eq!(v12.max_pov_size, 5_242_880);
|
||||
assert_eq!(v12.hrmp_channel_max_message_size, 1_048_576);
|
||||
assert_eq!(v12.n_delay_tranches, 25);
|
||||
assert_eq!(v12.minimum_validation_upgrade_delay, 5);
|
||||
assert_eq!(v12.minimum_backing_votes, LEGACY_MIN_BACKING_VOTES);
|
||||
assert_eq!(v12.approval_voting_params.max_approval_coalesce_count, 1);
|
||||
assert_eq!(v12.scheduler_params.group_rotation_frequency, 20);
|
||||
assert_eq!(v12.scheduler_params.paras_availability_period, 4);
|
||||
assert_eq!(v12.scheduler_params.lookahead, 1);
|
||||
assert_eq!(v12.scheduler_params.num_cores, 1);
|
||||
#[allow(deprecated)]
|
||||
{
|
||||
assert_eq!(v12.scheduler_params.max_availability_timeouts, 0);
|
||||
}
|
||||
assert_eq!(v12.scheduler_params.on_demand_queue_max_size, 10_000);
|
||||
assert_eq!(
|
||||
v12.scheduler_params.on_demand_target_queue_utilization,
|
||||
Perbill::from_percent(25)
|
||||
);
|
||||
assert_eq!(v12.scheduler_params.on_demand_fee_variability, Perbill::from_percent(3));
|
||||
assert_eq!(v12.scheduler_params.on_demand_base_fee, 10_000_000);
|
||||
#[allow(deprecated)]
|
||||
{
|
||||
assert_eq!(v12.scheduler_params.ttl, 5);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_migrate_to_v12() {
|
||||
// Host configuration has lots of fields. However, in this migration we only add one
|
||||
// field. The most important part to check are a couple of the last fields. We also pick
|
||||
// extra fields to check arbitrarily, e.g. depending on their position (i.e. the middle) and
|
||||
// also their type.
|
||||
//
|
||||
// We specify only the picked fields and the rest should be provided by the `Default`
|
||||
// implementation. That implementation is copied over between the two types and should work
|
||||
// fine.
|
||||
let v11 = V11HostConfiguration::<pezkuwi_primitives::BlockNumber> {
|
||||
needed_approvals: 69,
|
||||
paras_availability_period: 55,
|
||||
hrmp_recipient_deposit: 1337,
|
||||
max_pov_size: 1111,
|
||||
minimum_validation_upgrade_delay: 20,
|
||||
on_demand_ttl: 3,
|
||||
on_demand_retries: 10,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut pending_configs = Vec::new();
|
||||
pending_configs.push((100, v11.clone()));
|
||||
pending_configs.push((300, v11.clone()));
|
||||
|
||||
new_test_ext(Default::default()).execute_with(|| {
|
||||
// Implant the v10 version in the state.
|
||||
v11::ActiveConfig::<Test>::set(Some(v11.clone()));
|
||||
v11::PendingConfigs::<Test>::set(Some(pending_configs));
|
||||
|
||||
migrate_to_v12::<Test>();
|
||||
|
||||
let v12 = v12::ActiveConfig::<Test>::get().unwrap();
|
||||
assert_eq!(v12.approval_voting_params.max_approval_coalesce_count, 1);
|
||||
|
||||
let mut configs_to_check = v12::PendingConfigs::<Test>::get().unwrap();
|
||||
configs_to_check.push((0, v12.clone()));
|
||||
|
||||
for (_, v12) in configs_to_check {
|
||||
#[rustfmt::skip]
|
||||
#[allow(deprecated)]
|
||||
{
|
||||
assert_eq!(v11.max_code_size , v12.max_code_size);
|
||||
assert_eq!(v11.max_head_data_size , v12.max_head_data_size);
|
||||
assert_eq!(v11.max_upward_queue_count , v12.max_upward_queue_count);
|
||||
assert_eq!(v11.max_upward_queue_size , v12.max_upward_queue_size);
|
||||
assert_eq!(v11.max_upward_message_size , v12.max_upward_message_size);
|
||||
assert_eq!(v11.max_upward_message_num_per_candidate , v12.max_upward_message_num_per_candidate);
|
||||
assert_eq!(v11.hrmp_max_message_num_per_candidate , v12.hrmp_max_message_num_per_candidate);
|
||||
assert_eq!(v11.validation_upgrade_cooldown , v12.validation_upgrade_cooldown);
|
||||
assert_eq!(v11.validation_upgrade_delay , v12.validation_upgrade_delay);
|
||||
assert_eq!(v11.max_pov_size , v12.max_pov_size);
|
||||
assert_eq!(v11.max_downward_message_size , v12.max_downward_message_size);
|
||||
assert_eq!(v11.hrmp_max_teyrchain_outbound_channels , v12.hrmp_max_teyrchain_outbound_channels);
|
||||
assert_eq!(v11.hrmp_sender_deposit , v12.hrmp_sender_deposit);
|
||||
assert_eq!(v11.hrmp_recipient_deposit , v12.hrmp_recipient_deposit);
|
||||
assert_eq!(v11.hrmp_channel_max_capacity , v12.hrmp_channel_max_capacity);
|
||||
assert_eq!(v11.hrmp_channel_max_total_size , v12.hrmp_channel_max_total_size);
|
||||
assert_eq!(v11.hrmp_max_teyrchain_inbound_channels , v12.hrmp_max_teyrchain_inbound_channels);
|
||||
assert_eq!(v11.hrmp_channel_max_message_size , v12.hrmp_channel_max_message_size);
|
||||
assert_eq!(v11.code_retention_period , v12.code_retention_period);
|
||||
assert_eq!(v11.max_validators , v12.max_validators);
|
||||
assert_eq!(v11.dispute_period , v12.dispute_period);
|
||||
assert_eq!(v11.no_show_slots , v12.no_show_slots);
|
||||
assert_eq!(v11.n_delay_tranches , v12.n_delay_tranches);
|
||||
assert_eq!(v11.zeroth_delay_tranche_width , v12.zeroth_delay_tranche_width);
|
||||
assert_eq!(v11.needed_approvals , v12.needed_approvals);
|
||||
assert_eq!(v11.relay_vrf_modulo_samples , v12.relay_vrf_modulo_samples);
|
||||
assert_eq!(v11.pvf_voting_ttl , v12.pvf_voting_ttl);
|
||||
assert_eq!(v11.minimum_validation_upgrade_delay , v12.minimum_validation_upgrade_delay);
|
||||
assert_eq!(v11.async_backing_params.allowed_ancestry_len, v12.async_backing_params.allowed_ancestry_len);
|
||||
assert_eq!(v11.async_backing_params.max_candidate_depth , v12.async_backing_params.max_candidate_depth);
|
||||
assert_eq!(v11.executor_params , v12.executor_params);
|
||||
assert_eq!(v11.minimum_backing_votes , v12.minimum_backing_votes);
|
||||
assert_eq!(v11.group_rotation_frequency , v12.scheduler_params.group_rotation_frequency);
|
||||
assert_eq!(v11.paras_availability_period , v12.scheduler_params.paras_availability_period);
|
||||
assert_eq!(v11.max_validators_per_core , v12.scheduler_params.max_validators_per_core);
|
||||
assert_eq!(v11.scheduling_lookahead , v12.scheduler_params.lookahead);
|
||||
assert_eq!(v11.coretime_cores , v12.scheduler_params.num_cores);
|
||||
assert_eq!(v11.on_demand_retries , v12.scheduler_params.max_availability_timeouts);
|
||||
assert_eq!(v11.on_demand_queue_max_size , v12.scheduler_params.on_demand_queue_max_size);
|
||||
assert_eq!(v11.on_demand_target_queue_utilization , v12.scheduler_params.on_demand_target_queue_utilization);
|
||||
assert_eq!(v11.on_demand_fee_variability , v12.scheduler_params.on_demand_fee_variability);
|
||||
assert_eq!(v11.on_demand_base_fee , v12.scheduler_params.on_demand_base_fee);
|
||||
assert_eq!(v11.on_demand_ttl , v12.scheduler_params.ttl);
|
||||
}; // ; makes this a statement. `rustfmt::skip` cannot be put on an expression.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Test that migration doesn't panic in case there are no pending configurations upgrades in
|
||||
// pallet's storage.
|
||||
#[test]
|
||||
fn test_migrate_to_v12_no_pending() {
|
||||
let v11 = V11HostConfiguration::<pezkuwi_primitives::BlockNumber>::default();
|
||||
|
||||
new_test_ext(Default::default()).execute_with(|| {
|
||||
// Implant the v10 version in the state.
|
||||
v11::ActiveConfig::<Test>::set(Some(v11));
|
||||
// Ensure there are no pending configs.
|
||||
v12::PendingConfigs::<Test>::set(None);
|
||||
|
||||
// Shouldn't fail.
|
||||
migrate_to_v12::<Test>();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
// 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/>.
|
||||
|
||||
//! Contains the V6 storage definition of the host configuration.
|
||||
|
||||
use crate::configuration::{Config, Pallet};
|
||||
use alloc::vec::Vec;
|
||||
use frame_support::pallet_prelude::*;
|
||||
use frame_system::pallet_prelude::BlockNumberFor;
|
||||
|
||||
use pezkuwi_primitives::{AsyncBackingParams, Balance, ExecutorParams, SessionIndex};
|
||||
|
||||
#[derive(codec::Encode, codec::Decode, Debug, Clone)]
|
||||
pub struct V6HostConfiguration<BlockNumber> {
|
||||
pub max_code_size: u32,
|
||||
pub max_head_data_size: u32,
|
||||
pub max_upward_queue_count: u32,
|
||||
pub max_upward_queue_size: u32,
|
||||
pub max_upward_message_size: u32,
|
||||
pub max_upward_message_num_per_candidate: u32,
|
||||
pub hrmp_max_message_num_per_candidate: u32,
|
||||
pub validation_upgrade_cooldown: BlockNumber,
|
||||
pub validation_upgrade_delay: BlockNumber,
|
||||
pub async_backing_params: AsyncBackingParams,
|
||||
pub max_pov_size: u32,
|
||||
pub max_downward_message_size: u32,
|
||||
pub hrmp_max_teyrchain_outbound_channels: u32,
|
||||
pub hrmp_max_parathread_outbound_channels: u32,
|
||||
pub hrmp_sender_deposit: Balance,
|
||||
pub hrmp_recipient_deposit: Balance,
|
||||
pub hrmp_channel_max_capacity: u32,
|
||||
pub hrmp_channel_max_total_size: u32,
|
||||
pub hrmp_max_teyrchain_inbound_channels: u32,
|
||||
pub hrmp_max_parathread_inbound_channels: u32,
|
||||
pub hrmp_channel_max_message_size: u32,
|
||||
pub executor_params: ExecutorParams,
|
||||
pub code_retention_period: BlockNumber,
|
||||
pub parathread_cores: u32,
|
||||
pub parathread_retries: u32,
|
||||
pub group_rotation_frequency: BlockNumber,
|
||||
pub chain_availability_period: BlockNumber,
|
||||
pub thread_availability_period: BlockNumber,
|
||||
pub scheduling_lookahead: u32,
|
||||
pub max_validators_per_core: Option<u32>,
|
||||
pub max_validators: Option<u32>,
|
||||
pub dispute_period: SessionIndex,
|
||||
pub dispute_post_conclusion_acceptance_period: BlockNumber,
|
||||
pub no_show_slots: u32,
|
||||
pub n_delay_tranches: u32,
|
||||
pub zeroth_delay_tranche_width: u32,
|
||||
pub needed_approvals: u32,
|
||||
pub relay_vrf_modulo_samples: u32,
|
||||
pub pvf_checking_enabled: bool,
|
||||
pub pvf_voting_ttl: SessionIndex,
|
||||
pub minimum_validation_upgrade_delay: BlockNumber,
|
||||
}
|
||||
|
||||
impl<BlockNumber: Default + From<u32>> Default for V6HostConfiguration<BlockNumber> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
async_backing_params: AsyncBackingParams {
|
||||
max_candidate_depth: 0,
|
||||
allowed_ancestry_len: 0,
|
||||
},
|
||||
group_rotation_frequency: 1u32.into(),
|
||||
chain_availability_period: 1u32.into(),
|
||||
thread_availability_period: 1u32.into(),
|
||||
no_show_slots: 1u32.into(),
|
||||
validation_upgrade_cooldown: Default::default(),
|
||||
validation_upgrade_delay: 2u32.into(),
|
||||
code_retention_period: Default::default(),
|
||||
max_code_size: Default::default(),
|
||||
max_pov_size: Default::default(),
|
||||
max_head_data_size: Default::default(),
|
||||
parathread_cores: Default::default(),
|
||||
parathread_retries: Default::default(),
|
||||
scheduling_lookahead: Default::default(),
|
||||
max_validators_per_core: Default::default(),
|
||||
max_validators: None,
|
||||
dispute_period: 6,
|
||||
dispute_post_conclusion_acceptance_period: 100.into(),
|
||||
n_delay_tranches: Default::default(),
|
||||
zeroth_delay_tranche_width: Default::default(),
|
||||
needed_approvals: Default::default(),
|
||||
relay_vrf_modulo_samples: Default::default(),
|
||||
max_upward_queue_count: Default::default(),
|
||||
max_upward_queue_size: Default::default(),
|
||||
max_downward_message_size: Default::default(),
|
||||
max_upward_message_size: Default::default(),
|
||||
max_upward_message_num_per_candidate: Default::default(),
|
||||
hrmp_sender_deposit: Default::default(),
|
||||
hrmp_recipient_deposit: Default::default(),
|
||||
hrmp_channel_max_capacity: Default::default(),
|
||||
hrmp_channel_max_total_size: Default::default(),
|
||||
hrmp_max_teyrchain_inbound_channels: Default::default(),
|
||||
hrmp_max_parathread_inbound_channels: Default::default(),
|
||||
hrmp_channel_max_message_size: Default::default(),
|
||||
hrmp_max_teyrchain_outbound_channels: Default::default(),
|
||||
hrmp_max_parathread_outbound_channels: Default::default(),
|
||||
hrmp_max_message_num_per_candidate: Default::default(),
|
||||
pvf_checking_enabled: false,
|
||||
pvf_voting_ttl: 2u32.into(),
|
||||
minimum_validation_upgrade_delay: 2.into(),
|
||||
executor_params: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod v6 {
|
||||
use super::*;
|
||||
|
||||
#[frame_support::storage_alias]
|
||||
pub(crate) type ActiveConfig<T: Config> =
|
||||
StorageValue<Pallet<T>, V6HostConfiguration<BlockNumberFor<T>>, OptionQuery>;
|
||||
|
||||
#[frame_support::storage_alias]
|
||||
pub(crate) type PendingConfigs<T: Config> = StorageValue<
|
||||
Pallet<T>,
|
||||
Vec<(SessionIndex, V6HostConfiguration<BlockNumberFor<T>>)>,
|
||||
OptionQuery,
|
||||
>;
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
// 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/>.
|
||||
|
||||
//! A module that is responsible for migration of storage.
|
||||
|
||||
use crate::configuration::{self, Config, Pallet};
|
||||
use alloc::vec::Vec;
|
||||
use frame_support::{
|
||||
pallet_prelude::*,
|
||||
traits::{Defensive, StorageVersion},
|
||||
weights::Weight,
|
||||
};
|
||||
use frame_system::pallet_prelude::BlockNumberFor;
|
||||
use pezkuwi_primitives::{AsyncBackingParams, Balance, ExecutorParams, SessionIndex};
|
||||
|
||||
use frame_support::traits::OnRuntimeUpgrade;
|
||||
|
||||
use super::v6::V6HostConfiguration;
|
||||
|
||||
#[derive(codec::Encode, codec::Decode, Debug, Clone)]
|
||||
pub struct V7HostConfiguration<BlockNumber> {
|
||||
pub max_code_size: u32,
|
||||
pub max_head_data_size: u32,
|
||||
pub max_upward_queue_count: u32,
|
||||
pub max_upward_queue_size: u32,
|
||||
pub max_upward_message_size: u32,
|
||||
pub max_upward_message_num_per_candidate: u32,
|
||||
pub hrmp_max_message_num_per_candidate: u32,
|
||||
pub validation_upgrade_cooldown: BlockNumber,
|
||||
pub validation_upgrade_delay: BlockNumber,
|
||||
pub async_backing_params: AsyncBackingParams,
|
||||
pub max_pov_size: u32,
|
||||
pub max_downward_message_size: u32,
|
||||
pub hrmp_max_teyrchain_outbound_channels: u32,
|
||||
pub hrmp_max_parathread_outbound_channels: u32,
|
||||
pub hrmp_sender_deposit: Balance,
|
||||
pub hrmp_recipient_deposit: Balance,
|
||||
pub hrmp_channel_max_capacity: u32,
|
||||
pub hrmp_channel_max_total_size: u32,
|
||||
pub hrmp_max_teyrchain_inbound_channels: u32,
|
||||
pub hrmp_max_parathread_inbound_channels: u32,
|
||||
pub hrmp_channel_max_message_size: u32,
|
||||
pub executor_params: ExecutorParams,
|
||||
pub code_retention_period: BlockNumber,
|
||||
pub parathread_cores: u32,
|
||||
pub parathread_retries: u32,
|
||||
pub group_rotation_frequency: BlockNumber,
|
||||
pub chain_availability_period: BlockNumber,
|
||||
pub thread_availability_period: BlockNumber,
|
||||
pub scheduling_lookahead: u32,
|
||||
pub max_validators_per_core: Option<u32>,
|
||||
pub max_validators: Option<u32>,
|
||||
pub dispute_period: SessionIndex,
|
||||
pub dispute_post_conclusion_acceptance_period: BlockNumber,
|
||||
pub no_show_slots: u32,
|
||||
pub n_delay_tranches: u32,
|
||||
pub zeroth_delay_tranche_width: u32,
|
||||
pub needed_approvals: u32,
|
||||
pub relay_vrf_modulo_samples: u32,
|
||||
pub pvf_voting_ttl: SessionIndex,
|
||||
pub minimum_validation_upgrade_delay: BlockNumber,
|
||||
}
|
||||
|
||||
impl<BlockNumber: Default + From<u32>> Default for V7HostConfiguration<BlockNumber> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
async_backing_params: AsyncBackingParams {
|
||||
max_candidate_depth: 0,
|
||||
allowed_ancestry_len: 0,
|
||||
},
|
||||
group_rotation_frequency: 1u32.into(),
|
||||
chain_availability_period: 1u32.into(),
|
||||
thread_availability_period: 1u32.into(),
|
||||
no_show_slots: 1u32.into(),
|
||||
validation_upgrade_cooldown: Default::default(),
|
||||
validation_upgrade_delay: 2u32.into(),
|
||||
code_retention_period: Default::default(),
|
||||
max_code_size: Default::default(),
|
||||
max_pov_size: Default::default(),
|
||||
max_head_data_size: Default::default(),
|
||||
parathread_cores: Default::default(),
|
||||
parathread_retries: Default::default(),
|
||||
scheduling_lookahead: Default::default(),
|
||||
max_validators_per_core: Default::default(),
|
||||
max_validators: None,
|
||||
dispute_period: 6,
|
||||
dispute_post_conclusion_acceptance_period: 100.into(),
|
||||
n_delay_tranches: Default::default(),
|
||||
zeroth_delay_tranche_width: Default::default(),
|
||||
needed_approvals: Default::default(),
|
||||
relay_vrf_modulo_samples: Default::default(),
|
||||
max_upward_queue_count: Default::default(),
|
||||
max_upward_queue_size: Default::default(),
|
||||
max_downward_message_size: Default::default(),
|
||||
max_upward_message_size: Default::default(),
|
||||
max_upward_message_num_per_candidate: Default::default(),
|
||||
hrmp_sender_deposit: Default::default(),
|
||||
hrmp_recipient_deposit: Default::default(),
|
||||
hrmp_channel_max_capacity: Default::default(),
|
||||
hrmp_channel_max_total_size: Default::default(),
|
||||
hrmp_max_teyrchain_inbound_channels: Default::default(),
|
||||
hrmp_max_parathread_inbound_channels: Default::default(),
|
||||
hrmp_channel_max_message_size: Default::default(),
|
||||
hrmp_max_teyrchain_outbound_channels: Default::default(),
|
||||
hrmp_max_parathread_outbound_channels: Default::default(),
|
||||
hrmp_max_message_num_per_candidate: Default::default(),
|
||||
pvf_voting_ttl: 2u32.into(),
|
||||
minimum_validation_upgrade_delay: 2.into(),
|
||||
executor_params: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod v6 {
|
||||
use super::*;
|
||||
|
||||
#[frame_support::storage_alias]
|
||||
pub(crate) type ActiveConfig<T: Config> =
|
||||
StorageValue<Pallet<T>, V6HostConfiguration<BlockNumberFor<T>>, OptionQuery>;
|
||||
|
||||
#[frame_support::storage_alias]
|
||||
pub(crate) type PendingConfigs<T: Config> = StorageValue<
|
||||
Pallet<T>,
|
||||
Vec<(SessionIndex, V6HostConfiguration<BlockNumberFor<T>>)>,
|
||||
OptionQuery,
|
||||
>;
|
||||
}
|
||||
|
||||
mod v7 {
|
||||
use super::*;
|
||||
|
||||
#[frame_support::storage_alias]
|
||||
pub(crate) type ActiveConfig<T: Config> =
|
||||
StorageValue<Pallet<T>, V7HostConfiguration<BlockNumberFor<T>>, OptionQuery>;
|
||||
|
||||
#[frame_support::storage_alias]
|
||||
pub(crate) type PendingConfigs<T: Config> = StorageValue<
|
||||
Pallet<T>,
|
||||
Vec<(SessionIndex, V7HostConfiguration<BlockNumberFor<T>>)>,
|
||||
OptionQuery,
|
||||
>;
|
||||
}
|
||||
|
||||
pub struct MigrateToV7<T>(core::marker::PhantomData<T>);
|
||||
impl<T: Config> OnRuntimeUpgrade for MigrateToV7<T> {
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::TryRuntimeError> {
|
||||
log::trace!(target: crate::configuration::LOG_TARGET, "Running pre_upgrade()");
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
log::info!(target: configuration::LOG_TARGET, "MigrateToV7 started");
|
||||
if StorageVersion::get::<Pallet<T>>() == 6 {
|
||||
let weight_consumed = migrate_to_v7::<T>();
|
||||
|
||||
log::info!(target: configuration::LOG_TARGET, "MigrateToV7 executed successfully");
|
||||
StorageVersion::new(7).put::<Pallet<T>>();
|
||||
|
||||
weight_consumed
|
||||
} else {
|
||||
log::warn!(target: configuration::LOG_TARGET, "MigrateToV7 should be removed.");
|
||||
T::DbWeight::get().reads(1)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(_state: Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
|
||||
log::trace!(target: crate::configuration::LOG_TARGET, "Running post_upgrade()");
|
||||
ensure!(
|
||||
StorageVersion::get::<Pallet<T>>() >= 7,
|
||||
"Storage version should be >= 7 after the migration"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn migrate_to_v7<T: Config>() -> Weight {
|
||||
// Unusual formatting is justified:
|
||||
// - make it easier to verify that fields assign what they supposed to assign.
|
||||
// - this code is transient and will be removed after all migrations are done.
|
||||
// - this code is important enough to optimize for legibility sacrificing consistency.
|
||||
#[rustfmt::skip]
|
||||
let translate =
|
||||
|pre: V6HostConfiguration<BlockNumberFor<T>>| ->
|
||||
V7HostConfiguration<BlockNumberFor<T>>
|
||||
{
|
||||
V7HostConfiguration {
|
||||
max_code_size : pre.max_code_size,
|
||||
max_head_data_size : pre.max_head_data_size,
|
||||
max_upward_queue_count : pre.max_upward_queue_count,
|
||||
max_upward_queue_size : pre.max_upward_queue_size,
|
||||
max_upward_message_size : pre.max_upward_message_size,
|
||||
max_upward_message_num_per_candidate : pre.max_upward_message_num_per_candidate,
|
||||
hrmp_max_message_num_per_candidate : pre.hrmp_max_message_num_per_candidate,
|
||||
validation_upgrade_cooldown : pre.validation_upgrade_cooldown,
|
||||
validation_upgrade_delay : pre.validation_upgrade_delay,
|
||||
max_pov_size : pre.max_pov_size,
|
||||
max_downward_message_size : pre.max_downward_message_size,
|
||||
hrmp_max_teyrchain_outbound_channels : pre.hrmp_max_teyrchain_outbound_channels,
|
||||
hrmp_max_parathread_outbound_channels : pre.hrmp_max_parathread_outbound_channels,
|
||||
hrmp_sender_deposit : pre.hrmp_sender_deposit,
|
||||
hrmp_recipient_deposit : pre.hrmp_recipient_deposit,
|
||||
hrmp_channel_max_capacity : pre.hrmp_channel_max_capacity,
|
||||
hrmp_channel_max_total_size : pre.hrmp_channel_max_total_size,
|
||||
hrmp_max_teyrchain_inbound_channels : pre.hrmp_max_teyrchain_inbound_channels,
|
||||
hrmp_max_parathread_inbound_channels : pre.hrmp_max_parathread_inbound_channels,
|
||||
hrmp_channel_max_message_size : pre.hrmp_channel_max_message_size,
|
||||
code_retention_period : pre.code_retention_period,
|
||||
parathread_cores : pre.parathread_cores,
|
||||
parathread_retries : pre.parathread_retries,
|
||||
group_rotation_frequency : pre.group_rotation_frequency,
|
||||
chain_availability_period : pre.chain_availability_period,
|
||||
thread_availability_period : pre.thread_availability_period,
|
||||
scheduling_lookahead : pre.scheduling_lookahead,
|
||||
max_validators_per_core : pre.max_validators_per_core,
|
||||
max_validators : pre.max_validators,
|
||||
dispute_period : pre.dispute_period,
|
||||
dispute_post_conclusion_acceptance_period: pre.dispute_post_conclusion_acceptance_period,
|
||||
no_show_slots : pre.no_show_slots,
|
||||
n_delay_tranches : pre.n_delay_tranches,
|
||||
zeroth_delay_tranche_width : pre.zeroth_delay_tranche_width,
|
||||
needed_approvals : pre.needed_approvals,
|
||||
relay_vrf_modulo_samples : pre.relay_vrf_modulo_samples,
|
||||
pvf_voting_ttl : pre.pvf_voting_ttl,
|
||||
minimum_validation_upgrade_delay : pre.minimum_validation_upgrade_delay,
|
||||
async_backing_params : pre.async_backing_params,
|
||||
executor_params : pre.executor_params,
|
||||
}
|
||||
};
|
||||
|
||||
let v6 = v6::ActiveConfig::<T>::get()
|
||||
.defensive_proof("Could not decode old config")
|
||||
.unwrap_or_default();
|
||||
let v7 = translate(v6);
|
||||
v7::ActiveConfig::<T>::set(Some(v7));
|
||||
|
||||
// Allowed to be empty.
|
||||
let pending_v6 = v6::PendingConfigs::<T>::get().unwrap_or_default();
|
||||
let mut pending_v7 = Vec::new();
|
||||
|
||||
for (session, v6) in pending_v6.into_iter() {
|
||||
let v7 = translate(v6);
|
||||
pending_v7.push((session, v7));
|
||||
}
|
||||
v7::PendingConfigs::<T>::set(Some(pending_v7.clone()));
|
||||
|
||||
let num_configs = (pending_v7.len() + 1) as u64;
|
||||
T::DbWeight::get().reads_writes(num_configs, num_configs)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mock::{new_test_ext, Test};
|
||||
|
||||
#[test]
|
||||
fn v6_deserialized_from_actual_data() {
|
||||
// Example how to get new `raw_config`:
|
||||
// We'll obtain the raw_config at a specified a block
|
||||
// Steps:
|
||||
// 1. Go to Pezkuwi.js -> Developer -> Chain state -> Storage: https://pezkuwichain.io/#/chainstate
|
||||
// 2. Set these parameters:
|
||||
// 2.1. selected state query: configuration; activeConfig():
|
||||
// PezkuwiRuntimeTeyrchainsConfigurationHostConfiguration 2.2. blockhash to query at:
|
||||
// 0xf89d3ab5312c5f70d396dc59612f0aa65806c798346f9db4b35278baed2e0e53 (the hash of the
|
||||
// block) 2.3. Note the value of encoded storage key ->
|
||||
// 0x06de3d8a54d27e44a9d5ce189618f22db4b49d95320d9021994c850f25b8e385 for the referenced
|
||||
// block. 2.4. You'll also need the decoded values to update the test.
|
||||
// 3. Go to Pezkuwi.js -> Developer -> Chain state -> Raw storage
|
||||
// 3.1 Enter the encoded storage key and you get the raw config.
|
||||
|
||||
// This exceeds the maximal line width length, but that's fine, since this is not code and
|
||||
// doesn't need to be read and also leaving it as one line allows to easily copy it.
|
||||
let raw_config = hex_literal::hex!["00003000005000005555150000008000fbff0100000200000a000000c80000006400000000000000000000000000500000c800000a0000000000000000c0220fca950300000000000000000000c0220fca9503000000000000000000e8030000009001000a0000000000000000900100008070000000000000000000000a000000050000000500000001000000010500000001c80000000600000058020000020000002800000000000000020000000100000001020000000f000000"];
|
||||
|
||||
let v6 =
|
||||
V6HostConfiguration::<pezkuwi_primitives::BlockNumber>::decode(&mut &raw_config[..])
|
||||
.unwrap();
|
||||
|
||||
// We check only a sample of the values here. If we missed any fields or messed up data
|
||||
// types that would skew all the fields coming after.
|
||||
assert_eq!(v6.max_code_size, 3_145_728);
|
||||
assert_eq!(v6.validation_upgrade_cooldown, 200);
|
||||
assert_eq!(v6.max_pov_size, 5_242_880);
|
||||
assert_eq!(v6.hrmp_channel_max_message_size, 102_400);
|
||||
assert_eq!(v6.n_delay_tranches, 40);
|
||||
assert_eq!(v6.minimum_validation_upgrade_delay, 15);
|
||||
assert_eq!(v6.group_rotation_frequency, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_migrate_to_v7() {
|
||||
// Host configuration has lots of fields. However, in this migration we only remove one
|
||||
// field. The most important part to check are a couple of the last fields. We also pick
|
||||
// extra fields to check arbitrarily, e.g. depending on their position (i.e. the middle) and
|
||||
// also their type.
|
||||
//
|
||||
// We specify only the picked fields and the rest should be provided by the `Default`
|
||||
// implementation. That implementation is copied over between the two types and should work
|
||||
// fine.
|
||||
let v6 = V6HostConfiguration::<pezkuwi_primitives::BlockNumber> {
|
||||
needed_approvals: 69,
|
||||
thread_availability_period: 55,
|
||||
hrmp_recipient_deposit: 1337,
|
||||
max_pov_size: 1111,
|
||||
chain_availability_period: 33,
|
||||
minimum_validation_upgrade_delay: 20,
|
||||
pvf_checking_enabled: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut pending_configs = Vec::new();
|
||||
pending_configs.push((100, v6.clone()));
|
||||
pending_configs.push((300, v6.clone()));
|
||||
|
||||
new_test_ext(Default::default()).execute_with(|| {
|
||||
// Implant the v6 version in the state.
|
||||
v6::ActiveConfig::<Test>::set(Some(v6));
|
||||
v6::PendingConfigs::<Test>::set(Some(pending_configs));
|
||||
|
||||
migrate_to_v7::<Test>();
|
||||
|
||||
let v7 = v7::ActiveConfig::<Test>::get().unwrap();
|
||||
let mut configs_to_check = v7::PendingConfigs::<Test>::get().unwrap();
|
||||
configs_to_check.push((0, v7.clone()));
|
||||
|
||||
for (_, v6) in configs_to_check {
|
||||
#[rustfmt::skip]
|
||||
{
|
||||
assert_eq!(v6.max_code_size , v7.max_code_size);
|
||||
assert_eq!(v6.max_head_data_size , v7.max_head_data_size);
|
||||
assert_eq!(v6.max_upward_queue_count , v7.max_upward_queue_count);
|
||||
assert_eq!(v6.max_upward_queue_size , v7.max_upward_queue_size);
|
||||
assert_eq!(v6.max_upward_message_size , v7.max_upward_message_size);
|
||||
assert_eq!(v6.max_upward_message_num_per_candidate , v7.max_upward_message_num_per_candidate);
|
||||
assert_eq!(v6.hrmp_max_message_num_per_candidate , v7.hrmp_max_message_num_per_candidate);
|
||||
assert_eq!(v6.validation_upgrade_cooldown , v7.validation_upgrade_cooldown);
|
||||
assert_eq!(v6.validation_upgrade_delay , v7.validation_upgrade_delay);
|
||||
assert_eq!(v6.max_pov_size , v7.max_pov_size);
|
||||
assert_eq!(v6.max_downward_message_size , v7.max_downward_message_size);
|
||||
assert_eq!(v6.hrmp_max_teyrchain_outbound_channels , v7.hrmp_max_teyrchain_outbound_channels);
|
||||
assert_eq!(v6.hrmp_max_parathread_outbound_channels , v7.hrmp_max_parathread_outbound_channels);
|
||||
assert_eq!(v6.hrmp_sender_deposit , v7.hrmp_sender_deposit);
|
||||
assert_eq!(v6.hrmp_recipient_deposit , v7.hrmp_recipient_deposit);
|
||||
assert_eq!(v6.hrmp_channel_max_capacity , v7.hrmp_channel_max_capacity);
|
||||
assert_eq!(v6.hrmp_channel_max_total_size , v7.hrmp_channel_max_total_size);
|
||||
assert_eq!(v6.hrmp_max_teyrchain_inbound_channels , v7.hrmp_max_teyrchain_inbound_channels);
|
||||
assert_eq!(v6.hrmp_max_parathread_inbound_channels , v7.hrmp_max_parathread_inbound_channels);
|
||||
assert_eq!(v6.hrmp_channel_max_message_size , v7.hrmp_channel_max_message_size);
|
||||
assert_eq!(v6.code_retention_period , v7.code_retention_period);
|
||||
assert_eq!(v6.parathread_cores , v7.parathread_cores);
|
||||
assert_eq!(v6.parathread_retries , v7.parathread_retries);
|
||||
assert_eq!(v6.group_rotation_frequency , v7.group_rotation_frequency);
|
||||
assert_eq!(v6.chain_availability_period , v7.chain_availability_period);
|
||||
assert_eq!(v6.thread_availability_period , v7.thread_availability_period);
|
||||
assert_eq!(v6.scheduling_lookahead , v7.scheduling_lookahead);
|
||||
assert_eq!(v6.max_validators_per_core , v7.max_validators_per_core);
|
||||
assert_eq!(v6.max_validators , v7.max_validators);
|
||||
assert_eq!(v6.dispute_period , v7.dispute_period);
|
||||
assert_eq!(v6.no_show_slots , v7.no_show_slots);
|
||||
assert_eq!(v6.n_delay_tranches , v7.n_delay_tranches);
|
||||
assert_eq!(v6.zeroth_delay_tranche_width , v7.zeroth_delay_tranche_width);
|
||||
assert_eq!(v6.needed_approvals , v7.needed_approvals);
|
||||
assert_eq!(v6.relay_vrf_modulo_samples , v7.relay_vrf_modulo_samples);
|
||||
assert_eq!(v6.pvf_voting_ttl , v7.pvf_voting_ttl);
|
||||
assert_eq!(v6.minimum_validation_upgrade_delay , v7.minimum_validation_upgrade_delay);
|
||||
assert_eq!(v6.async_backing_params.allowed_ancestry_len, v7.async_backing_params.allowed_ancestry_len);
|
||||
assert_eq!(v6.async_backing_params.max_candidate_depth , v7.async_backing_params.max_candidate_depth);
|
||||
assert_eq!(v6.executor_params , v7.executor_params);
|
||||
}; // ; makes this a statement. `rustfmt::skip` cannot be put on an expression.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Test that migration doesn't panic in case there're no pending configurations upgrades in
|
||||
// pallet's storage.
|
||||
#[test]
|
||||
fn test_migrate_to_v7_no_pending() {
|
||||
let v6 = V6HostConfiguration::<pezkuwi_primitives::BlockNumber>::default();
|
||||
|
||||
new_test_ext(Default::default()).execute_with(|| {
|
||||
// Implant the v6 version in the state.
|
||||
v6::ActiveConfig::<Test>::set(Some(v6));
|
||||
// Ensure there're no pending configs.
|
||||
v6::PendingConfigs::<Test>::set(None);
|
||||
|
||||
// Shouldn't fail.
|
||||
migrate_to_v7::<Test>();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
// 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/>.
|
||||
|
||||
//! A module that is responsible for migration of storage.
|
||||
|
||||
use crate::configuration::{self, Config, Pallet};
|
||||
use alloc::vec::Vec;
|
||||
use frame_support::{
|
||||
pallet_prelude::*,
|
||||
traits::{Defensive, StorageVersion},
|
||||
weights::Weight,
|
||||
};
|
||||
use frame_system::pallet_prelude::BlockNumberFor;
|
||||
use pezkuwi_primitives::{
|
||||
AsyncBackingParams, Balance, ExecutorParams, SessionIndex, ON_DEMAND_DEFAULT_QUEUE_MAX_SIZE,
|
||||
};
|
||||
use sp_runtime::Perbill;
|
||||
|
||||
use frame_support::traits::OnRuntimeUpgrade;
|
||||
|
||||
use super::v7::V7HostConfiguration;
|
||||
/// All configuration of the runtime with respect to paras.
|
||||
#[derive(Clone, Encode, Decode, Debug)]
|
||||
pub struct V8HostConfiguration<BlockNumber> {
|
||||
pub max_code_size: u32,
|
||||
pub max_head_data_size: u32,
|
||||
pub max_upward_queue_count: u32,
|
||||
pub max_upward_queue_size: u32,
|
||||
pub max_upward_message_size: u32,
|
||||
pub max_upward_message_num_per_candidate: u32,
|
||||
pub hrmp_max_message_num_per_candidate: u32,
|
||||
pub validation_upgrade_cooldown: BlockNumber,
|
||||
pub validation_upgrade_delay: BlockNumber,
|
||||
pub async_backing_params: AsyncBackingParams,
|
||||
pub max_pov_size: u32,
|
||||
pub max_downward_message_size: u32,
|
||||
pub hrmp_max_teyrchain_outbound_channels: u32,
|
||||
pub hrmp_sender_deposit: Balance,
|
||||
pub hrmp_recipient_deposit: Balance,
|
||||
pub hrmp_channel_max_capacity: u32,
|
||||
pub hrmp_channel_max_total_size: u32,
|
||||
pub hrmp_max_teyrchain_inbound_channels: u32,
|
||||
pub hrmp_channel_max_message_size: u32,
|
||||
pub executor_params: ExecutorParams,
|
||||
pub code_retention_period: BlockNumber,
|
||||
pub on_demand_cores: u32,
|
||||
pub on_demand_retries: u32,
|
||||
pub on_demand_queue_max_size: u32,
|
||||
pub on_demand_target_queue_utilization: Perbill,
|
||||
pub on_demand_fee_variability: Perbill,
|
||||
pub on_demand_base_fee: Balance,
|
||||
pub on_demand_ttl: BlockNumber,
|
||||
pub group_rotation_frequency: BlockNumber,
|
||||
pub paras_availability_period: BlockNumber,
|
||||
pub scheduling_lookahead: u32,
|
||||
pub max_validators_per_core: Option<u32>,
|
||||
pub max_validators: Option<u32>,
|
||||
pub dispute_period: SessionIndex,
|
||||
pub dispute_post_conclusion_acceptance_period: BlockNumber,
|
||||
pub no_show_slots: u32,
|
||||
pub n_delay_tranches: u32,
|
||||
pub zeroth_delay_tranche_width: u32,
|
||||
pub needed_approvals: u32,
|
||||
pub relay_vrf_modulo_samples: u32,
|
||||
pub pvf_voting_ttl: SessionIndex,
|
||||
pub minimum_validation_upgrade_delay: BlockNumber,
|
||||
}
|
||||
|
||||
impl<BlockNumber: Default + From<u32>> Default for V8HostConfiguration<BlockNumber> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
async_backing_params: AsyncBackingParams {
|
||||
max_candidate_depth: 0,
|
||||
allowed_ancestry_len: 0,
|
||||
},
|
||||
group_rotation_frequency: 1u32.into(),
|
||||
paras_availability_period: 1u32.into(),
|
||||
no_show_slots: 1u32.into(),
|
||||
validation_upgrade_cooldown: Default::default(),
|
||||
validation_upgrade_delay: 2u32.into(),
|
||||
code_retention_period: Default::default(),
|
||||
max_code_size: Default::default(),
|
||||
max_pov_size: Default::default(),
|
||||
max_head_data_size: Default::default(),
|
||||
on_demand_cores: Default::default(),
|
||||
on_demand_retries: Default::default(),
|
||||
scheduling_lookahead: 1,
|
||||
max_validators_per_core: Default::default(),
|
||||
max_validators: None,
|
||||
dispute_period: 6,
|
||||
dispute_post_conclusion_acceptance_period: 100.into(),
|
||||
n_delay_tranches: Default::default(),
|
||||
zeroth_delay_tranche_width: Default::default(),
|
||||
needed_approvals: Default::default(),
|
||||
relay_vrf_modulo_samples: Default::default(),
|
||||
max_upward_queue_count: Default::default(),
|
||||
max_upward_queue_size: Default::default(),
|
||||
max_downward_message_size: Default::default(),
|
||||
max_upward_message_size: Default::default(),
|
||||
max_upward_message_num_per_candidate: Default::default(),
|
||||
hrmp_sender_deposit: Default::default(),
|
||||
hrmp_recipient_deposit: Default::default(),
|
||||
hrmp_channel_max_capacity: Default::default(),
|
||||
hrmp_channel_max_total_size: Default::default(),
|
||||
hrmp_max_teyrchain_inbound_channels: Default::default(),
|
||||
hrmp_channel_max_message_size: Default::default(),
|
||||
hrmp_max_teyrchain_outbound_channels: Default::default(),
|
||||
hrmp_max_message_num_per_candidate: Default::default(),
|
||||
pvf_voting_ttl: 2u32.into(),
|
||||
minimum_validation_upgrade_delay: 2.into(),
|
||||
executor_params: Default::default(),
|
||||
on_demand_queue_max_size: ON_DEMAND_DEFAULT_QUEUE_MAX_SIZE,
|
||||
on_demand_base_fee: 10_000_000u128,
|
||||
on_demand_fee_variability: Perbill::from_percent(3),
|
||||
on_demand_target_queue_utilization: Perbill::from_percent(25),
|
||||
on_demand_ttl: 5u32.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod v7 {
|
||||
use super::*;
|
||||
|
||||
#[frame_support::storage_alias]
|
||||
pub(crate) type ActiveConfig<T: Config> =
|
||||
StorageValue<Pallet<T>, V7HostConfiguration<BlockNumberFor<T>>, OptionQuery>;
|
||||
|
||||
#[frame_support::storage_alias]
|
||||
pub(crate) type PendingConfigs<T: Config> = StorageValue<
|
||||
Pallet<T>,
|
||||
Vec<(SessionIndex, V7HostConfiguration<BlockNumberFor<T>>)>,
|
||||
OptionQuery,
|
||||
>;
|
||||
}
|
||||
|
||||
mod v8 {
|
||||
use super::*;
|
||||
|
||||
#[frame_support::storage_alias]
|
||||
pub(crate) type ActiveConfig<T: Config> =
|
||||
StorageValue<Pallet<T>, V8HostConfiguration<BlockNumberFor<T>>, OptionQuery>;
|
||||
|
||||
#[frame_support::storage_alias]
|
||||
pub(crate) type PendingConfigs<T: Config> = StorageValue<
|
||||
Pallet<T>,
|
||||
Vec<(SessionIndex, V8HostConfiguration<BlockNumberFor<T>>)>,
|
||||
OptionQuery,
|
||||
>;
|
||||
}
|
||||
|
||||
pub struct MigrateToV8<T>(core::marker::PhantomData<T>);
|
||||
impl<T: Config> OnRuntimeUpgrade for MigrateToV8<T> {
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::TryRuntimeError> {
|
||||
log::trace!(target: crate::configuration::LOG_TARGET, "Running pre_upgrade() for HostConfiguration MigrateToV8");
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
log::info!(target: configuration::LOG_TARGET, "HostConfiguration MigrateToV8 started");
|
||||
if StorageVersion::get::<Pallet<T>>() == 7 {
|
||||
let weight_consumed = migrate_to_v8::<T>();
|
||||
|
||||
log::info!(target: configuration::LOG_TARGET, "HostConfiguration MigrateToV8 executed successfully");
|
||||
StorageVersion::new(8).put::<Pallet<T>>();
|
||||
|
||||
weight_consumed
|
||||
} else {
|
||||
log::warn!(target: configuration::LOG_TARGET, "HostConfiguration MigrateToV8 should be removed.");
|
||||
T::DbWeight::get().reads(1)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(_state: Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
|
||||
log::trace!(target: crate::configuration::LOG_TARGET, "Running post_upgrade() for HostConfiguration MigrateToV8");
|
||||
ensure!(
|
||||
StorageVersion::get::<Pallet<T>>() >= 8,
|
||||
"Storage version should be >= 8 after the migration"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn migrate_to_v8<T: Config>() -> Weight {
|
||||
// Unusual formatting is justified:
|
||||
// - make it easier to verify that fields assign what they supposed to assign.
|
||||
// - this code is transient and will be removed after all migrations are done.
|
||||
// - this code is important enough to optimize for legibility sacrificing consistency.
|
||||
#[rustfmt::skip]
|
||||
let translate =
|
||||
|pre: V7HostConfiguration<BlockNumberFor<T>>| ->
|
||||
V8HostConfiguration<BlockNumberFor<T>>
|
||||
{
|
||||
V8HostConfiguration {
|
||||
max_code_size : pre.max_code_size,
|
||||
max_head_data_size : pre.max_head_data_size,
|
||||
max_upward_queue_count : pre.max_upward_queue_count,
|
||||
max_upward_queue_size : pre.max_upward_queue_size,
|
||||
max_upward_message_size : pre.max_upward_message_size,
|
||||
max_upward_message_num_per_candidate : pre.max_upward_message_num_per_candidate,
|
||||
hrmp_max_message_num_per_candidate : pre.hrmp_max_message_num_per_candidate,
|
||||
validation_upgrade_cooldown : pre.validation_upgrade_cooldown,
|
||||
validation_upgrade_delay : pre.validation_upgrade_delay,
|
||||
max_pov_size : pre.max_pov_size,
|
||||
max_downward_message_size : pre.max_downward_message_size,
|
||||
hrmp_sender_deposit : pre.hrmp_sender_deposit,
|
||||
hrmp_recipient_deposit : pre.hrmp_recipient_deposit,
|
||||
hrmp_channel_max_capacity : pre.hrmp_channel_max_capacity,
|
||||
hrmp_channel_max_total_size : pre.hrmp_channel_max_total_size,
|
||||
hrmp_max_teyrchain_inbound_channels : pre.hrmp_max_teyrchain_inbound_channels,
|
||||
hrmp_max_teyrchain_outbound_channels : pre.hrmp_max_teyrchain_outbound_channels,
|
||||
hrmp_channel_max_message_size : pre.hrmp_channel_max_message_size,
|
||||
code_retention_period : pre.code_retention_period,
|
||||
on_demand_cores : pre.parathread_cores,
|
||||
on_demand_retries : pre.parathread_retries,
|
||||
group_rotation_frequency : pre.group_rotation_frequency,
|
||||
paras_availability_period : pre.chain_availability_period,
|
||||
scheduling_lookahead : pre.scheduling_lookahead,
|
||||
max_validators_per_core : pre.max_validators_per_core,
|
||||
max_validators : pre.max_validators,
|
||||
dispute_period : pre.dispute_period,
|
||||
dispute_post_conclusion_acceptance_period: pre.dispute_post_conclusion_acceptance_period,
|
||||
no_show_slots : pre.no_show_slots,
|
||||
n_delay_tranches : pre.n_delay_tranches,
|
||||
zeroth_delay_tranche_width : pre.zeroth_delay_tranche_width,
|
||||
needed_approvals : pre.needed_approvals,
|
||||
relay_vrf_modulo_samples : pre.relay_vrf_modulo_samples,
|
||||
pvf_voting_ttl : pre.pvf_voting_ttl,
|
||||
minimum_validation_upgrade_delay : pre.minimum_validation_upgrade_delay,
|
||||
async_backing_params : pre.async_backing_params,
|
||||
executor_params : pre.executor_params,
|
||||
on_demand_queue_max_size : 10_000u32,
|
||||
on_demand_base_fee : 10_000_000u128,
|
||||
on_demand_fee_variability : Perbill::from_percent(3),
|
||||
on_demand_target_queue_utilization : Perbill::from_percent(25),
|
||||
on_demand_ttl : 5u32.into(),
|
||||
}
|
||||
};
|
||||
|
||||
let v7 = v7::ActiveConfig::<T>::get()
|
||||
.defensive_proof("Could not decode old config")
|
||||
.unwrap_or_default();
|
||||
let v8 = translate(v7);
|
||||
v8::ActiveConfig::<T>::set(Some(v8));
|
||||
|
||||
// Allowed to be empty.
|
||||
let pending_v7 = v7::PendingConfigs::<T>::get().unwrap_or_default();
|
||||
let mut pending_v8 = Vec::new();
|
||||
|
||||
for (session, v7) in pending_v7.into_iter() {
|
||||
let v8 = translate(v7);
|
||||
pending_v8.push((session, v8));
|
||||
}
|
||||
v8::PendingConfigs::<T>::set(Some(pending_v8.clone()));
|
||||
|
||||
let num_configs = (pending_v8.len() + 1) as u64;
|
||||
T::DbWeight::get().reads_writes(num_configs, num_configs)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mock::{new_test_ext, Test};
|
||||
|
||||
#[test]
|
||||
fn v8_deserialized_from_actual_data() {
|
||||
// Example how to get new `raw_config`:
|
||||
// We'll obtain the raw_config at a specified a block
|
||||
// Steps:
|
||||
// 1. Go to Pezkuwi.js -> Developer -> Chain state -> Storage: https://pezkuwichain.io/#/chainstate
|
||||
// 2. Set these parameters:
|
||||
// 2.1. selected state query: configuration; activeConfig():
|
||||
// PezkuwiRuntimeTeyrchainsConfigurationHostConfiguration
|
||||
// 2.2. blockhash to query at:
|
||||
// 0xf89d3ab5312c5f70d396dc59612f0aa65806c798346f9db4b35278baed2e0e53 (the hash of
|
||||
// the block)
|
||||
// 2.3. Note the value of encoded storage key ->
|
||||
// 0x06de3d8a54d27e44a9d5ce189618f22db4b49d95320d9021994c850f25b8e385 for the
|
||||
// referenced block.
|
||||
// 2.4. You'll also need the decoded values to update the test.
|
||||
// 3. Go to Pezkuwi.js -> Developer -> Chain state -> Raw storage
|
||||
// 3.1 Enter the encoded storage key and you get the raw config.
|
||||
|
||||
// This exceeds the maximal line width length, but that's fine, since this is not code and
|
||||
// doesn't need to be read and also leaving it as one line allows to easily copy it.
|
||||
let raw_config =
|
||||
hex_literal::hex!["
|
||||
0000300000800000080000000000100000c8000005000000050000000200000002000000000000000000000000005000000010000400000000000000000000000000000000000000000000000000000000000000000000000800000000200000040000000000100000b004000000000000000000001027000080b2e60e80c3c9018096980000000000000000000000000005000000140000000400000001000000010100000000060000006400000002000000190000000000000002000000020000000200000005000000"
|
||||
];
|
||||
|
||||
let v8 =
|
||||
V8HostConfiguration::<pezkuwi_primitives::BlockNumber>::decode(&mut &raw_config[..])
|
||||
.unwrap();
|
||||
|
||||
// We check only a sample of the values here. If we missed any fields or messed up data
|
||||
// types that would skew all the fields coming after.
|
||||
assert_eq!(v8.max_code_size, 3_145_728);
|
||||
assert_eq!(v8.validation_upgrade_cooldown, 2);
|
||||
assert_eq!(v8.max_pov_size, 5_242_880);
|
||||
assert_eq!(v8.hrmp_channel_max_message_size, 1_048_576);
|
||||
assert_eq!(v8.n_delay_tranches, 25);
|
||||
assert_eq!(v8.minimum_validation_upgrade_delay, 5);
|
||||
assert_eq!(v8.group_rotation_frequency, 20);
|
||||
assert_eq!(v8.on_demand_cores, 0);
|
||||
assert_eq!(v8.on_demand_base_fee, 10_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_migrate_to_v8() {
|
||||
// Host configuration has lots of fields. However, in this migration we only remove one
|
||||
// field. The most important part to check are a couple of the last fields. We also pick
|
||||
// extra fields to check arbitrarily, e.g. depending on their position (i.e. the middle) and
|
||||
// also their type.
|
||||
//
|
||||
// We specify only the picked fields and the rest should be provided by the `Default`
|
||||
// implementation. That implementation is copied over between the two types and should work
|
||||
// fine.
|
||||
let v7 = V7HostConfiguration::<pezkuwi_primitives::BlockNumber> {
|
||||
needed_approvals: 69,
|
||||
thread_availability_period: 55,
|
||||
hrmp_recipient_deposit: 1337,
|
||||
max_pov_size: 1111,
|
||||
chain_availability_period: 33,
|
||||
minimum_validation_upgrade_delay: 20,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut pending_configs = Vec::new();
|
||||
pending_configs.push((100, v7.clone()));
|
||||
pending_configs.push((300, v7.clone()));
|
||||
|
||||
new_test_ext(Default::default()).execute_with(|| {
|
||||
// Implant the v6 version in the state.
|
||||
v7::ActiveConfig::<Test>::set(Some(v7));
|
||||
v7::PendingConfigs::<Test>::set(Some(pending_configs));
|
||||
|
||||
migrate_to_v8::<Test>();
|
||||
|
||||
let v8 = v8::ActiveConfig::<Test>::get().unwrap();
|
||||
let mut configs_to_check = v8::PendingConfigs::<Test>::get().unwrap();
|
||||
configs_to_check.push((0, v8.clone()));
|
||||
|
||||
for (_, v7) in configs_to_check {
|
||||
#[rustfmt::skip]
|
||||
{
|
||||
assert_eq!(v7.max_code_size , v8.max_code_size);
|
||||
assert_eq!(v7.max_head_data_size , v8.max_head_data_size);
|
||||
assert_eq!(v7.max_upward_queue_count , v8.max_upward_queue_count);
|
||||
assert_eq!(v7.max_upward_queue_size , v8.max_upward_queue_size);
|
||||
assert_eq!(v7.max_upward_message_size , v8.max_upward_message_size);
|
||||
assert_eq!(v7.max_upward_message_num_per_candidate , v8.max_upward_message_num_per_candidate);
|
||||
assert_eq!(v7.hrmp_max_message_num_per_candidate , v8.hrmp_max_message_num_per_candidate);
|
||||
assert_eq!(v7.validation_upgrade_cooldown , v8.validation_upgrade_cooldown);
|
||||
assert_eq!(v7.validation_upgrade_delay , v8.validation_upgrade_delay);
|
||||
assert_eq!(v7.max_pov_size , v8.max_pov_size);
|
||||
assert_eq!(v7.max_downward_message_size , v8.max_downward_message_size);
|
||||
assert_eq!(v7.hrmp_max_teyrchain_outbound_channels , v8.hrmp_max_teyrchain_outbound_channels);
|
||||
assert_eq!(v7.hrmp_sender_deposit , v8.hrmp_sender_deposit);
|
||||
assert_eq!(v7.hrmp_recipient_deposit , v8.hrmp_recipient_deposit);
|
||||
assert_eq!(v7.hrmp_channel_max_capacity , v8.hrmp_channel_max_capacity);
|
||||
assert_eq!(v7.hrmp_channel_max_total_size , v8.hrmp_channel_max_total_size);
|
||||
assert_eq!(v7.hrmp_max_teyrchain_inbound_channels , v8.hrmp_max_teyrchain_inbound_channels);
|
||||
assert_eq!(v7.hrmp_channel_max_message_size , v8.hrmp_channel_max_message_size);
|
||||
assert_eq!(v7.code_retention_period , v8.code_retention_period);
|
||||
assert_eq!(v7.on_demand_cores , v8.on_demand_cores);
|
||||
assert_eq!(v7.on_demand_retries , v8.on_demand_retries);
|
||||
assert_eq!(v7.group_rotation_frequency , v8.group_rotation_frequency);
|
||||
assert_eq!(v7.paras_availability_period , v8.paras_availability_period);
|
||||
assert_eq!(v7.scheduling_lookahead , v8.scheduling_lookahead);
|
||||
assert_eq!(v7.max_validators_per_core , v8.max_validators_per_core);
|
||||
assert_eq!(v7.max_validators , v8.max_validators);
|
||||
assert_eq!(v7.dispute_period , v8.dispute_period);
|
||||
assert_eq!(v7.no_show_slots , v8.no_show_slots);
|
||||
assert_eq!(v7.n_delay_tranches , v8.n_delay_tranches);
|
||||
assert_eq!(v7.zeroth_delay_tranche_width , v8.zeroth_delay_tranche_width);
|
||||
assert_eq!(v7.needed_approvals , v8.needed_approvals);
|
||||
assert_eq!(v7.relay_vrf_modulo_samples , v8.relay_vrf_modulo_samples);
|
||||
assert_eq!(v7.pvf_voting_ttl , v8.pvf_voting_ttl);
|
||||
assert_eq!(v7.minimum_validation_upgrade_delay , v8.minimum_validation_upgrade_delay);
|
||||
assert_eq!(v7.async_backing_params.allowed_ancestry_len, v8.async_backing_params.allowed_ancestry_len);
|
||||
assert_eq!(v7.async_backing_params.max_candidate_depth , v8.async_backing_params.max_candidate_depth);
|
||||
assert_eq!(v7.executor_params , v8.executor_params);
|
||||
}; // ; makes this a statement. `rustfmt::skip` cannot be put on an expression.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Test that migration doesn't panic in case there're no pending configurations upgrades in
|
||||
// pallet's storage.
|
||||
#[test]
|
||||
fn test_migrate_to_v8_no_pending() {
|
||||
let v7 = V7HostConfiguration::<pezkuwi_primitives::BlockNumber>::default();
|
||||
|
||||
new_test_ext(Default::default()).execute_with(|| {
|
||||
// Implant the v6 version in the state.
|
||||
v7::ActiveConfig::<Test>::set(Some(v7));
|
||||
// Ensure there're no pending configs.
|
||||
v7::PendingConfigs::<Test>::set(None);
|
||||
|
||||
// Shouldn't fail.
|
||||
migrate_to_v8::<Test>();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
// 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/>.
|
||||
|
||||
//! A module that is responsible for migration of storage.
|
||||
|
||||
use crate::configuration::{self, Config, Pallet};
|
||||
use alloc::vec::Vec;
|
||||
use frame_support::{
|
||||
pallet_prelude::*,
|
||||
traits::{Defensive, StorageVersion},
|
||||
weights::Weight,
|
||||
};
|
||||
use frame_system::pallet_prelude::BlockNumberFor;
|
||||
use pezkuwi_primitives::{
|
||||
AsyncBackingParams, Balance, ExecutorParams, SessionIndex, LEGACY_MIN_BACKING_VOTES,
|
||||
ON_DEMAND_DEFAULT_QUEUE_MAX_SIZE,
|
||||
};
|
||||
use sp_runtime::Perbill;
|
||||
|
||||
use frame_support::traits::OnRuntimeUpgrade;
|
||||
|
||||
use super::v8::V8HostConfiguration;
|
||||
/// All configuration of the runtime with respect to paras.
|
||||
#[derive(Clone, Encode, Decode, Debug)]
|
||||
pub struct V9HostConfiguration<BlockNumber> {
|
||||
pub max_code_size: u32,
|
||||
pub max_head_data_size: u32,
|
||||
pub max_upward_queue_count: u32,
|
||||
pub max_upward_queue_size: u32,
|
||||
pub max_upward_message_size: u32,
|
||||
pub max_upward_message_num_per_candidate: u32,
|
||||
pub hrmp_max_message_num_per_candidate: u32,
|
||||
pub validation_upgrade_cooldown: BlockNumber,
|
||||
pub validation_upgrade_delay: BlockNumber,
|
||||
pub async_backing_params: AsyncBackingParams,
|
||||
pub max_pov_size: u32,
|
||||
pub max_downward_message_size: u32,
|
||||
pub hrmp_max_teyrchain_outbound_channels: u32,
|
||||
pub hrmp_sender_deposit: Balance,
|
||||
pub hrmp_recipient_deposit: Balance,
|
||||
pub hrmp_channel_max_capacity: u32,
|
||||
pub hrmp_channel_max_total_size: u32,
|
||||
pub hrmp_max_teyrchain_inbound_channels: u32,
|
||||
pub hrmp_channel_max_message_size: u32,
|
||||
pub executor_params: ExecutorParams,
|
||||
pub code_retention_period: BlockNumber,
|
||||
pub on_demand_cores: u32,
|
||||
pub on_demand_retries: u32,
|
||||
pub on_demand_queue_max_size: u32,
|
||||
pub on_demand_target_queue_utilization: Perbill,
|
||||
pub on_demand_fee_variability: Perbill,
|
||||
pub on_demand_base_fee: Balance,
|
||||
pub on_demand_ttl: BlockNumber,
|
||||
pub group_rotation_frequency: BlockNumber,
|
||||
pub paras_availability_period: BlockNumber,
|
||||
pub scheduling_lookahead: u32,
|
||||
pub max_validators_per_core: Option<u32>,
|
||||
pub max_validators: Option<u32>,
|
||||
pub dispute_period: SessionIndex,
|
||||
pub dispute_post_conclusion_acceptance_period: BlockNumber,
|
||||
pub no_show_slots: u32,
|
||||
pub n_delay_tranches: u32,
|
||||
pub zeroth_delay_tranche_width: u32,
|
||||
pub needed_approvals: u32,
|
||||
pub relay_vrf_modulo_samples: u32,
|
||||
pub pvf_voting_ttl: SessionIndex,
|
||||
pub minimum_validation_upgrade_delay: BlockNumber,
|
||||
pub minimum_backing_votes: u32,
|
||||
}
|
||||
|
||||
impl<BlockNumber: Default + From<u32>> Default for V9HostConfiguration<BlockNumber> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
async_backing_params: AsyncBackingParams {
|
||||
max_candidate_depth: 0,
|
||||
allowed_ancestry_len: 0,
|
||||
},
|
||||
group_rotation_frequency: 1u32.into(),
|
||||
paras_availability_period: 1u32.into(),
|
||||
no_show_slots: 1u32.into(),
|
||||
validation_upgrade_cooldown: Default::default(),
|
||||
validation_upgrade_delay: 2u32.into(),
|
||||
code_retention_period: Default::default(),
|
||||
max_code_size: Default::default(),
|
||||
max_pov_size: Default::default(),
|
||||
max_head_data_size: Default::default(),
|
||||
on_demand_cores: Default::default(),
|
||||
on_demand_retries: Default::default(),
|
||||
scheduling_lookahead: 1,
|
||||
max_validators_per_core: Default::default(),
|
||||
max_validators: None,
|
||||
dispute_period: 6,
|
||||
dispute_post_conclusion_acceptance_period: 100.into(),
|
||||
n_delay_tranches: Default::default(),
|
||||
zeroth_delay_tranche_width: Default::default(),
|
||||
needed_approvals: Default::default(),
|
||||
relay_vrf_modulo_samples: Default::default(),
|
||||
max_upward_queue_count: Default::default(),
|
||||
max_upward_queue_size: Default::default(),
|
||||
max_downward_message_size: Default::default(),
|
||||
max_upward_message_size: Default::default(),
|
||||
max_upward_message_num_per_candidate: Default::default(),
|
||||
hrmp_sender_deposit: Default::default(),
|
||||
hrmp_recipient_deposit: Default::default(),
|
||||
hrmp_channel_max_capacity: Default::default(),
|
||||
hrmp_channel_max_total_size: Default::default(),
|
||||
hrmp_max_teyrchain_inbound_channels: Default::default(),
|
||||
hrmp_channel_max_message_size: Default::default(),
|
||||
hrmp_max_teyrchain_outbound_channels: Default::default(),
|
||||
hrmp_max_message_num_per_candidate: Default::default(),
|
||||
pvf_voting_ttl: 2u32.into(),
|
||||
minimum_validation_upgrade_delay: 2.into(),
|
||||
executor_params: Default::default(),
|
||||
on_demand_queue_max_size: ON_DEMAND_DEFAULT_QUEUE_MAX_SIZE,
|
||||
on_demand_base_fee: 10_000_000u128,
|
||||
on_demand_fee_variability: Perbill::from_percent(3),
|
||||
on_demand_target_queue_utilization: Perbill::from_percent(25),
|
||||
on_demand_ttl: 5u32.into(),
|
||||
minimum_backing_votes: LEGACY_MIN_BACKING_VOTES,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod v8 {
|
||||
use super::*;
|
||||
|
||||
#[frame_support::storage_alias]
|
||||
pub(crate) type ActiveConfig<T: Config> =
|
||||
StorageValue<Pallet<T>, V8HostConfiguration<BlockNumberFor<T>>, OptionQuery>;
|
||||
|
||||
#[frame_support::storage_alias]
|
||||
pub(crate) type PendingConfigs<T: Config> = StorageValue<
|
||||
Pallet<T>,
|
||||
Vec<(SessionIndex, V8HostConfiguration<BlockNumberFor<T>>)>,
|
||||
OptionQuery,
|
||||
>;
|
||||
}
|
||||
|
||||
mod v9 {
|
||||
use super::*;
|
||||
|
||||
#[frame_support::storage_alias]
|
||||
pub(crate) type ActiveConfig<T: Config> =
|
||||
StorageValue<Pallet<T>, V9HostConfiguration<BlockNumberFor<T>>, OptionQuery>;
|
||||
|
||||
#[frame_support::storage_alias]
|
||||
pub(crate) type PendingConfigs<T: Config> = StorageValue<
|
||||
Pallet<T>,
|
||||
Vec<(SessionIndex, V9HostConfiguration<BlockNumberFor<T>>)>,
|
||||
OptionQuery,
|
||||
>;
|
||||
}
|
||||
|
||||
pub struct MigrateToV9<T>(core::marker::PhantomData<T>);
|
||||
impl<T: Config> OnRuntimeUpgrade for MigrateToV9<T> {
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::TryRuntimeError> {
|
||||
log::trace!(target: crate::configuration::LOG_TARGET, "Running pre_upgrade() for HostConfiguration MigrateToV9");
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
log::info!(target: configuration::LOG_TARGET, "HostConfiguration MigrateToV9 started");
|
||||
if StorageVersion::get::<Pallet<T>>() == 8 {
|
||||
let weight_consumed = migrate_to_v9::<T>();
|
||||
|
||||
log::info!(target: configuration::LOG_TARGET, "HostConfiguration MigrateToV9 executed successfully");
|
||||
StorageVersion::new(9).put::<Pallet<T>>();
|
||||
|
||||
weight_consumed
|
||||
} else {
|
||||
log::warn!(target: configuration::LOG_TARGET, "HostConfiguration MigrateToV9 should be removed.");
|
||||
T::DbWeight::get().reads(1)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(_state: Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
|
||||
log::trace!(target: crate::configuration::LOG_TARGET, "Running post_upgrade() for HostConfiguration MigrateToV9");
|
||||
ensure!(
|
||||
StorageVersion::get::<Pallet<T>>() >= 9,
|
||||
"Storage version should be >= 9 after the migration"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn migrate_to_v9<T: Config>() -> Weight {
|
||||
// Unusual formatting is justified:
|
||||
// - make it easier to verify that fields assign what they supposed to assign.
|
||||
// - this code is transient and will be removed after all migrations are done.
|
||||
// - this code is important enough to optimize for legibility sacrificing consistency.
|
||||
#[rustfmt::skip]
|
||||
let translate =
|
||||
|pre: V8HostConfiguration<BlockNumberFor<T>>| ->
|
||||
V9HostConfiguration<BlockNumberFor<T>>
|
||||
{
|
||||
V9HostConfiguration {
|
||||
max_code_size : pre.max_code_size,
|
||||
max_head_data_size : pre.max_head_data_size,
|
||||
max_upward_queue_count : pre.max_upward_queue_count,
|
||||
max_upward_queue_size : pre.max_upward_queue_size,
|
||||
max_upward_message_size : pre.max_upward_message_size,
|
||||
max_upward_message_num_per_candidate : pre.max_upward_message_num_per_candidate,
|
||||
hrmp_max_message_num_per_candidate : pre.hrmp_max_message_num_per_candidate,
|
||||
validation_upgrade_cooldown : pre.validation_upgrade_cooldown,
|
||||
validation_upgrade_delay : pre.validation_upgrade_delay,
|
||||
max_pov_size : pre.max_pov_size,
|
||||
max_downward_message_size : pre.max_downward_message_size,
|
||||
hrmp_sender_deposit : pre.hrmp_sender_deposit,
|
||||
hrmp_recipient_deposit : pre.hrmp_recipient_deposit,
|
||||
hrmp_channel_max_capacity : pre.hrmp_channel_max_capacity,
|
||||
hrmp_channel_max_total_size : pre.hrmp_channel_max_total_size,
|
||||
hrmp_max_teyrchain_inbound_channels : pre.hrmp_max_teyrchain_inbound_channels,
|
||||
hrmp_max_teyrchain_outbound_channels : pre.hrmp_max_teyrchain_outbound_channels,
|
||||
hrmp_channel_max_message_size : pre.hrmp_channel_max_message_size,
|
||||
code_retention_period : pre.code_retention_period,
|
||||
on_demand_cores : pre.on_demand_cores,
|
||||
on_demand_retries : pre.on_demand_retries,
|
||||
group_rotation_frequency : pre.group_rotation_frequency,
|
||||
paras_availability_period : pre.paras_availability_period,
|
||||
scheduling_lookahead : pre.scheduling_lookahead,
|
||||
max_validators_per_core : pre.max_validators_per_core,
|
||||
max_validators : pre.max_validators,
|
||||
dispute_period : pre.dispute_period,
|
||||
dispute_post_conclusion_acceptance_period: pre.dispute_post_conclusion_acceptance_period,
|
||||
no_show_slots : pre.no_show_slots,
|
||||
n_delay_tranches : pre.n_delay_tranches,
|
||||
zeroth_delay_tranche_width : pre.zeroth_delay_tranche_width,
|
||||
needed_approvals : pre.needed_approvals,
|
||||
relay_vrf_modulo_samples : pre.relay_vrf_modulo_samples,
|
||||
pvf_voting_ttl : pre.pvf_voting_ttl,
|
||||
minimum_validation_upgrade_delay : pre.minimum_validation_upgrade_delay,
|
||||
async_backing_params : pre.async_backing_params,
|
||||
executor_params : pre.executor_params,
|
||||
on_demand_queue_max_size : pre.on_demand_queue_max_size,
|
||||
on_demand_base_fee : pre.on_demand_base_fee,
|
||||
on_demand_fee_variability : pre.on_demand_fee_variability,
|
||||
on_demand_target_queue_utilization : pre.on_demand_target_queue_utilization,
|
||||
on_demand_ttl : pre.on_demand_ttl,
|
||||
minimum_backing_votes : LEGACY_MIN_BACKING_VOTES
|
||||
}
|
||||
};
|
||||
|
||||
let v8 = v8::ActiveConfig::<T>::get()
|
||||
.defensive_proof("Could not decode old config")
|
||||
.unwrap_or_default();
|
||||
let v9 = translate(v8);
|
||||
v9::ActiveConfig::<T>::set(Some(v9));
|
||||
|
||||
// Allowed to be empty.
|
||||
let pending_v8 = v8::PendingConfigs::<T>::get().unwrap_or_default();
|
||||
let mut pending_v9 = Vec::new();
|
||||
|
||||
for (session, v8) in pending_v8.into_iter() {
|
||||
let v9 = translate(v8);
|
||||
pending_v9.push((session, v9));
|
||||
}
|
||||
v9::PendingConfigs::<T>::set(Some(pending_v9.clone()));
|
||||
|
||||
let num_configs = (pending_v9.len() + 1) as u64;
|
||||
T::DbWeight::get().reads_writes(num_configs, num_configs)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mock::{new_test_ext, Test};
|
||||
|
||||
#[test]
|
||||
fn v9_deserialized_from_actual_data() {
|
||||
// Example how to get new `raw_config`:
|
||||
// We'll obtain the raw_config at a specified a block
|
||||
// Steps:
|
||||
// 1. Go to Pezkuwi.js -> Developer -> Chain state -> Storage: https://pezkuwichain.io/#/chainstate
|
||||
// 2. Set these parameters:
|
||||
// 2.1. selected state query: configuration; activeConfig():
|
||||
// PezkuwiRuntimeTeyrchainsConfigurationHostConfiguration
|
||||
// 2.2. blockhash to query at:
|
||||
// 0xf89d3ab5312c5f70d396dc59612f0aa65806c798346f9db4b35278baed2e0e53 (the hash of
|
||||
// the block)
|
||||
// 2.3. Note the value of encoded storage key ->
|
||||
// 0x06de3d8a54d27e44a9d5ce189618f22db4b49d95320d9021994c850f25b8e385 for the
|
||||
// referenced block.
|
||||
// 2.4. You'll also need the decoded values to update the test.
|
||||
// 3. Go to Pezkuwi.js -> Developer -> Chain state -> Raw storage
|
||||
// 3.1 Enter the encoded storage key and you get the raw config.
|
||||
|
||||
// This exceeds the maximal line width length, but that's fine, since this is not code and
|
||||
// doesn't need to be read and also leaving it as one line allows to easily copy it.
|
||||
let raw_config =
|
||||
hex_literal::hex!["
|
||||
0000300000800000080000000000100000c8000005000000050000000200000002000000000000000000000000005000000010000400000000000000000000000000000000000000000000000000000000000000000000000800000000200000040000000000100000b004000000000000000000001027000080b2e60e80c3c901809698000000000000000000000000000500000014000000040000000100000001010000000006000000640000000200000019000000000000000300000002000000020000000500000002000000"
|
||||
];
|
||||
|
||||
let v9 =
|
||||
V9HostConfiguration::<pezkuwi_primitives::BlockNumber>::decode(&mut &raw_config[..])
|
||||
.unwrap();
|
||||
|
||||
// We check only a sample of the values here. If we missed any fields or messed up data
|
||||
// types that would skew all the fields coming after.
|
||||
assert_eq!(v9.max_code_size, 3_145_728);
|
||||
assert_eq!(v9.validation_upgrade_cooldown, 2);
|
||||
assert_eq!(v9.max_pov_size, 5_242_880);
|
||||
assert_eq!(v9.hrmp_channel_max_message_size, 1_048_576);
|
||||
assert_eq!(v9.n_delay_tranches, 25);
|
||||
assert_eq!(v9.minimum_validation_upgrade_delay, 5);
|
||||
assert_eq!(v9.group_rotation_frequency, 20);
|
||||
assert_eq!(v9.on_demand_cores, 0);
|
||||
assert_eq!(v9.on_demand_base_fee, 10_000_000);
|
||||
assert_eq!(v9.minimum_backing_votes, LEGACY_MIN_BACKING_VOTES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_migrate_to_v9() {
|
||||
// Host configuration has lots of fields. However, in this migration we only add one
|
||||
// field. The most important part to check are a couple of the last fields. We also pick
|
||||
// extra fields to check arbitrarily, e.g. depending on their position (i.e. the middle) and
|
||||
// also their type.
|
||||
//
|
||||
// We specify only the picked fields and the rest should be provided by the `Default`
|
||||
// implementation. That implementation is copied over between the two types and should work
|
||||
// fine.
|
||||
let v8 = V8HostConfiguration::<pezkuwi_primitives::BlockNumber> {
|
||||
needed_approvals: 69,
|
||||
paras_availability_period: 55,
|
||||
hrmp_recipient_deposit: 1337,
|
||||
max_pov_size: 1111,
|
||||
minimum_validation_upgrade_delay: 20,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut pending_configs = Vec::new();
|
||||
pending_configs.push((100, v8.clone()));
|
||||
pending_configs.push((300, v8.clone()));
|
||||
|
||||
new_test_ext(Default::default()).execute_with(|| {
|
||||
// Implant the v8 version in the state.
|
||||
v8::ActiveConfig::<Test>::set(Some(v8));
|
||||
v8::PendingConfigs::<Test>::set(Some(pending_configs));
|
||||
|
||||
migrate_to_v9::<Test>();
|
||||
|
||||
let v9 = v9::ActiveConfig::<Test>::get().unwrap();
|
||||
let mut configs_to_check = v9::PendingConfigs::<Test>::get().unwrap();
|
||||
configs_to_check.push((0, v9.clone()));
|
||||
|
||||
for (_, v8) in configs_to_check {
|
||||
#[rustfmt::skip]
|
||||
{
|
||||
assert_eq!(v8.max_code_size , v9.max_code_size);
|
||||
assert_eq!(v8.max_head_data_size , v9.max_head_data_size);
|
||||
assert_eq!(v8.max_upward_queue_count , v9.max_upward_queue_count);
|
||||
assert_eq!(v8.max_upward_queue_size , v9.max_upward_queue_size);
|
||||
assert_eq!(v8.max_upward_message_size , v9.max_upward_message_size);
|
||||
assert_eq!(v8.max_upward_message_num_per_candidate , v9.max_upward_message_num_per_candidate);
|
||||
assert_eq!(v8.hrmp_max_message_num_per_candidate , v9.hrmp_max_message_num_per_candidate);
|
||||
assert_eq!(v8.validation_upgrade_cooldown , v9.validation_upgrade_cooldown);
|
||||
assert_eq!(v8.validation_upgrade_delay , v9.validation_upgrade_delay);
|
||||
assert_eq!(v8.max_pov_size , v9.max_pov_size);
|
||||
assert_eq!(v8.max_downward_message_size , v9.max_downward_message_size);
|
||||
assert_eq!(v8.hrmp_max_teyrchain_outbound_channels , v9.hrmp_max_teyrchain_outbound_channels);
|
||||
assert_eq!(v8.hrmp_sender_deposit , v9.hrmp_sender_deposit);
|
||||
assert_eq!(v8.hrmp_recipient_deposit , v9.hrmp_recipient_deposit);
|
||||
assert_eq!(v8.hrmp_channel_max_capacity , v9.hrmp_channel_max_capacity);
|
||||
assert_eq!(v8.hrmp_channel_max_total_size , v9.hrmp_channel_max_total_size);
|
||||
assert_eq!(v8.hrmp_max_teyrchain_inbound_channels , v9.hrmp_max_teyrchain_inbound_channels);
|
||||
assert_eq!(v8.hrmp_channel_max_message_size , v9.hrmp_channel_max_message_size);
|
||||
assert_eq!(v8.code_retention_period , v9.code_retention_period);
|
||||
assert_eq!(v8.on_demand_cores , v9.on_demand_cores);
|
||||
assert_eq!(v8.on_demand_retries , v9.on_demand_retries);
|
||||
assert_eq!(v8.group_rotation_frequency , v9.group_rotation_frequency);
|
||||
assert_eq!(v8.paras_availability_period , v9.paras_availability_period);
|
||||
assert_eq!(v8.scheduling_lookahead , v9.scheduling_lookahead);
|
||||
assert_eq!(v8.max_validators_per_core , v9.max_validators_per_core);
|
||||
assert_eq!(v8.max_validators , v9.max_validators);
|
||||
assert_eq!(v8.dispute_period , v9.dispute_period);
|
||||
assert_eq!(v8.no_show_slots , v9.no_show_slots);
|
||||
assert_eq!(v8.n_delay_tranches , v9.n_delay_tranches);
|
||||
assert_eq!(v8.zeroth_delay_tranche_width , v9.zeroth_delay_tranche_width);
|
||||
assert_eq!(v8.needed_approvals , v9.needed_approvals);
|
||||
assert_eq!(v8.relay_vrf_modulo_samples , v9.relay_vrf_modulo_samples);
|
||||
assert_eq!(v8.pvf_voting_ttl , v9.pvf_voting_ttl);
|
||||
assert_eq!(v8.minimum_validation_upgrade_delay , v9.minimum_validation_upgrade_delay);
|
||||
assert_eq!(v8.async_backing_params.allowed_ancestry_len, v9.async_backing_params.allowed_ancestry_len);
|
||||
assert_eq!(v8.async_backing_params.max_candidate_depth , v9.async_backing_params.max_candidate_depth);
|
||||
assert_eq!(v8.executor_params , v9.executor_params);
|
||||
assert_eq!(v8.minimum_backing_votes , v9.minimum_backing_votes);
|
||||
}; // ; makes this a statement. `rustfmt::skip` cannot be put on an expression.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Test that migration doesn't panic in case there're no pending configurations upgrades in
|
||||
// pallet's storage.
|
||||
#[test]
|
||||
fn test_migrate_to_v9_no_pending() {
|
||||
let v8 = V8HostConfiguration::<pezkuwi_primitives::BlockNumber>::default();
|
||||
|
||||
new_test_ext(Default::default()).execute_with(|| {
|
||||
// Implant the v8 version in the state.
|
||||
v8::ActiveConfig::<Test>::set(Some(v8));
|
||||
// Ensure there're no pending configs.
|
||||
v8::PendingConfigs::<Test>::set(None);
|
||||
|
||||
// Shouldn't fail.
|
||||
migrate_to_v9::<Test>();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,586 @@
|
||||
// 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::{
|
||||
configuration,
|
||||
mock::{new_test_ext, Configuration, MockGenesisConfig, ParasShared, RuntimeOrigin, Test},
|
||||
};
|
||||
use bitvec::{bitvec, prelude::Lsb0};
|
||||
use frame_support::{assert_err, assert_ok};
|
||||
|
||||
fn on_new_session(session_index: SessionIndex) -> (HostConfiguration<u32>, HostConfiguration<u32>) {
|
||||
ParasShared::set_session_index(session_index);
|
||||
let SessionChangeOutcome { prev_config, new_config } =
|
||||
Configuration::initializer_on_new_session(&session_index);
|
||||
let new_config = new_config.unwrap_or_else(|| prev_config.clone());
|
||||
(prev_config, new_config)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_is_consistent() {
|
||||
new_test_ext(Default::default()).execute_with(|| {
|
||||
configuration::ActiveConfig::<Test>::get().panic_if_not_consistent();
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scheduled_session_is_two_sessions_from_now() {
|
||||
new_test_ext(Default::default()).execute_with(|| {
|
||||
// The logic here is really tested only with scheduled_session = 2. It should work
|
||||
// with other values, but that should receive a more rigorous testing.
|
||||
on_new_session(1);
|
||||
assert_eq!(Configuration::scheduled_session(), 3);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initializer_on_new_session() {
|
||||
new_test_ext(Default::default()).execute_with(|| {
|
||||
let (prev_config, new_config) = on_new_session(1);
|
||||
assert_eq!(prev_config, new_config);
|
||||
assert_ok!(Configuration::set_validation_upgrade_delay(RuntimeOrigin::root(), 100));
|
||||
|
||||
let (prev_config, new_config) = on_new_session(2);
|
||||
assert_eq!(prev_config, new_config);
|
||||
|
||||
let (prev_config, new_config) = on_new_session(3);
|
||||
assert_eq!(prev_config, HostConfiguration::default());
|
||||
assert_eq!(new_config, HostConfiguration { validation_upgrade_delay: 100, ..prev_config });
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_changes_after_2_session_boundary() {
|
||||
new_test_ext(Default::default()).execute_with(|| {
|
||||
let old_config = configuration::ActiveConfig::<Test>::get();
|
||||
let mut config = old_config.clone();
|
||||
config.validation_upgrade_delay = 100;
|
||||
assert!(old_config != config);
|
||||
|
||||
assert_ok!(Configuration::set_validation_upgrade_delay(RuntimeOrigin::root(), 100));
|
||||
|
||||
// Verify that the current configuration has not changed and that there is a scheduled
|
||||
// change for the SESSION_DELAY sessions in advance.
|
||||
assert_eq!(configuration::ActiveConfig::<Test>::get(), old_config);
|
||||
assert_eq!(PendingConfigs::<Test>::get(), vec![(2, config.clone())]);
|
||||
|
||||
on_new_session(1);
|
||||
|
||||
// One session has passed, we should be still waiting for the pending configuration.
|
||||
assert_eq!(configuration::ActiveConfig::<Test>::get(), old_config);
|
||||
assert_eq!(PendingConfigs::<Test>::get(), vec![(2, config.clone())]);
|
||||
|
||||
on_new_session(2);
|
||||
|
||||
assert_eq!(configuration::ActiveConfig::<Test>::get(), config);
|
||||
assert_eq!(PendingConfigs::<Test>::get(), vec![]);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consecutive_changes_within_one_session() {
|
||||
new_test_ext(Default::default()).execute_with(|| {
|
||||
let old_config = configuration::ActiveConfig::<Test>::get();
|
||||
let mut config = old_config.clone();
|
||||
config.validation_upgrade_delay = 100;
|
||||
config.validation_upgrade_cooldown = 100;
|
||||
assert!(old_config != config);
|
||||
|
||||
assert_ok!(Configuration::set_validation_upgrade_delay(RuntimeOrigin::root(), 100));
|
||||
assert_ok!(Configuration::set_validation_upgrade_cooldown(RuntimeOrigin::root(), 100));
|
||||
assert_eq!(configuration::ActiveConfig::<Test>::get(), old_config);
|
||||
assert_eq!(PendingConfigs::<Test>::get(), vec![(2, config.clone())]);
|
||||
|
||||
on_new_session(1);
|
||||
|
||||
assert_eq!(configuration::ActiveConfig::<Test>::get(), old_config);
|
||||
assert_eq!(PendingConfigs::<Test>::get(), vec![(2, config.clone())]);
|
||||
|
||||
on_new_session(2);
|
||||
|
||||
assert_eq!(configuration::ActiveConfig::<Test>::get(), config);
|
||||
assert_eq!(PendingConfigs::<Test>::get(), vec![]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pending_next_session_but_we_upgrade_once_more() {
|
||||
new_test_ext(Default::default()).execute_with(|| {
|
||||
let initial_config = configuration::ActiveConfig::<Test>::get();
|
||||
let intermediate_config =
|
||||
HostConfiguration { validation_upgrade_delay: 100, ..initial_config.clone() };
|
||||
let final_config = HostConfiguration {
|
||||
validation_upgrade_delay: 100,
|
||||
validation_upgrade_cooldown: 99,
|
||||
..initial_config.clone()
|
||||
};
|
||||
|
||||
assert_ok!(Configuration::set_validation_upgrade_delay(RuntimeOrigin::root(), 100));
|
||||
assert_eq!(configuration::ActiveConfig::<Test>::get(), initial_config);
|
||||
assert_eq!(PendingConfigs::<Test>::get(), vec![(2, intermediate_config.clone())]);
|
||||
|
||||
on_new_session(1);
|
||||
|
||||
// We are still waiting until the pending configuration is applied and we add another
|
||||
// update.
|
||||
assert_ok!(Configuration::set_validation_upgrade_cooldown(RuntimeOrigin::root(), 99));
|
||||
|
||||
// This should result in yet another configuration change scheduled.
|
||||
assert_eq!(configuration::ActiveConfig::<Test>::get(), initial_config);
|
||||
assert_eq!(
|
||||
PendingConfigs::<Test>::get(),
|
||||
vec![(2, intermediate_config.clone()), (3, final_config.clone())]
|
||||
);
|
||||
|
||||
on_new_session(2);
|
||||
|
||||
assert_eq!(configuration::ActiveConfig::<Test>::get(), intermediate_config);
|
||||
assert_eq!(PendingConfigs::<Test>::get(), vec![(3, final_config.clone())]);
|
||||
|
||||
on_new_session(3);
|
||||
|
||||
assert_eq!(configuration::ActiveConfig::<Test>::get(), final_config);
|
||||
assert_eq!(PendingConfigs::<Test>::get(), vec![]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scheduled_session_config_update_while_next_session_pending() {
|
||||
new_test_ext(Default::default()).execute_with(|| {
|
||||
let initial_config = configuration::ActiveConfig::<Test>::get();
|
||||
let intermediate_config =
|
||||
HostConfiguration { validation_upgrade_delay: 100, ..initial_config.clone() };
|
||||
let final_config = HostConfiguration {
|
||||
validation_upgrade_delay: 100,
|
||||
validation_upgrade_cooldown: 99,
|
||||
code_retention_period: 98,
|
||||
..initial_config.clone()
|
||||
};
|
||||
|
||||
assert_ok!(Configuration::set_validation_upgrade_delay(RuntimeOrigin::root(), 100));
|
||||
assert_eq!(configuration::ActiveConfig::<Test>::get(), initial_config);
|
||||
assert_eq!(PendingConfigs::<Test>::get(), vec![(2, intermediate_config.clone())]);
|
||||
|
||||
on_new_session(1);
|
||||
|
||||
// The second call should fall into the case where we already have a pending config
|
||||
// update for the scheduled_session, but we want to update it once more.
|
||||
assert_ok!(Configuration::set_validation_upgrade_cooldown(RuntimeOrigin::root(), 99));
|
||||
assert_ok!(Configuration::set_code_retention_period(RuntimeOrigin::root(), 98));
|
||||
|
||||
// This should result in yet another configuration change scheduled.
|
||||
assert_eq!(configuration::ActiveConfig::<Test>::get(), initial_config);
|
||||
assert_eq!(
|
||||
PendingConfigs::<Test>::get(),
|
||||
vec![(2, intermediate_config.clone()), (3, final_config.clone())]
|
||||
);
|
||||
|
||||
on_new_session(2);
|
||||
|
||||
assert_eq!(configuration::ActiveConfig::<Test>::get(), intermediate_config);
|
||||
assert_eq!(PendingConfigs::<Test>::get(), vec![(3, final_config.clone())]);
|
||||
|
||||
on_new_session(3);
|
||||
|
||||
assert_eq!(configuration::ActiveConfig::<Test>::get(), final_config);
|
||||
assert_eq!(PendingConfigs::<Test>::get(), vec![]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invariants() {
|
||||
new_test_ext(Default::default()).execute_with(|| {
|
||||
assert_err!(
|
||||
Configuration::set_max_code_size(RuntimeOrigin::root(), MAX_CODE_SIZE + 1),
|
||||
Error::<Test>::InvalidNewValue
|
||||
);
|
||||
|
||||
assert_err!(
|
||||
Configuration::set_max_pov_size(RuntimeOrigin::root(), POV_SIZE_HARD_LIMIT + 1),
|
||||
Error::<Test>::InvalidNewValue
|
||||
);
|
||||
|
||||
assert_err!(
|
||||
Configuration::set_max_head_data_size(RuntimeOrigin::root(), MAX_HEAD_DATA_SIZE + 1),
|
||||
Error::<Test>::InvalidNewValue
|
||||
);
|
||||
|
||||
assert_err!(
|
||||
Configuration::set_paras_availability_period(RuntimeOrigin::root(), 0),
|
||||
Error::<Test>::InvalidNewValue
|
||||
);
|
||||
assert_err!(
|
||||
Configuration::set_no_show_slots(RuntimeOrigin::root(), 0),
|
||||
Error::<Test>::InvalidNewValue
|
||||
);
|
||||
|
||||
ActiveConfig::<Test>::put(HostConfiguration {
|
||||
minimum_validation_upgrade_delay: 11,
|
||||
scheduler_params: SchedulerParams {
|
||||
paras_availability_period: 10,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
assert_err!(
|
||||
Configuration::set_paras_availability_period(RuntimeOrigin::root(), 12),
|
||||
Error::<Test>::InvalidNewValue
|
||||
);
|
||||
assert_err!(
|
||||
Configuration::set_minimum_validation_upgrade_delay(RuntimeOrigin::root(), 9),
|
||||
Error::<Test>::InvalidNewValue
|
||||
);
|
||||
|
||||
assert_err!(
|
||||
Configuration::set_validation_upgrade_delay(RuntimeOrigin::root(), 0),
|
||||
Error::<Test>::InvalidNewValue
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consistency_bypass_works() {
|
||||
new_test_ext(Default::default()).execute_with(|| {
|
||||
assert_err!(
|
||||
Configuration::set_max_code_size(RuntimeOrigin::root(), MAX_CODE_SIZE + 1),
|
||||
Error::<Test>::InvalidNewValue
|
||||
);
|
||||
|
||||
assert_ok!(Configuration::set_bypass_consistency_check(RuntimeOrigin::root(), true));
|
||||
assert_ok!(Configuration::set_max_code_size(RuntimeOrigin::root(), MAX_CODE_SIZE + 1));
|
||||
|
||||
assert_eq!(
|
||||
configuration::ActiveConfig::<Test>::get().max_code_size,
|
||||
HostConfiguration::<u32>::default().max_code_size
|
||||
);
|
||||
|
||||
on_new_session(1);
|
||||
on_new_session(2);
|
||||
|
||||
assert_eq!(configuration::ActiveConfig::<Test>::get().max_code_size, MAX_CODE_SIZE + 1);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn setting_pending_config_members() {
|
||||
new_test_ext(Default::default()).execute_with(|| {
|
||||
let new_config = HostConfiguration {
|
||||
async_backing_params: AsyncBackingParams {
|
||||
allowed_ancestry_len: 0,
|
||||
max_candidate_depth: 0,
|
||||
},
|
||||
validation_upgrade_cooldown: 100,
|
||||
validation_upgrade_delay: 10,
|
||||
code_retention_period: 5,
|
||||
max_code_size: 100_000,
|
||||
max_pov_size: 1024,
|
||||
max_head_data_size: 1_000,
|
||||
max_validators: None,
|
||||
dispute_period: 239,
|
||||
dispute_post_conclusion_acceptance_period: 10,
|
||||
no_show_slots: 240,
|
||||
n_delay_tranches: 241,
|
||||
zeroth_delay_tranche_width: 242,
|
||||
needed_approvals: 242,
|
||||
relay_vrf_modulo_samples: 243,
|
||||
max_upward_queue_count: 1337,
|
||||
max_upward_queue_size: 228,
|
||||
max_downward_message_size: 2048,
|
||||
max_upward_message_size: 448,
|
||||
max_upward_message_num_per_candidate: 5,
|
||||
hrmp_sender_deposit: 22,
|
||||
hrmp_recipient_deposit: 4905,
|
||||
hrmp_channel_max_capacity: 3921,
|
||||
hrmp_channel_max_total_size: 7687,
|
||||
hrmp_max_teyrchain_inbound_channels: 37,
|
||||
hrmp_channel_max_message_size: 8192,
|
||||
hrmp_max_teyrchain_outbound_channels: 10,
|
||||
hrmp_max_message_num_per_candidate: 20,
|
||||
pvf_voting_ttl: 3,
|
||||
minimum_validation_upgrade_delay: 20,
|
||||
executor_params: Default::default(),
|
||||
approval_voting_params: ApprovalVotingParams { max_approval_coalesce_count: 1 },
|
||||
minimum_backing_votes: 5,
|
||||
node_features: bitvec![u8, Lsb0; 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1],
|
||||
#[allow(deprecated)]
|
||||
scheduler_params: SchedulerParams {
|
||||
group_rotation_frequency: 20,
|
||||
paras_availability_period: 10,
|
||||
max_validators_per_core: None,
|
||||
lookahead: 3,
|
||||
num_cores: 2,
|
||||
max_availability_timeouts: 0,
|
||||
on_demand_queue_max_size: 10_000u32,
|
||||
on_demand_base_fee: 10_000_000u128,
|
||||
on_demand_fee_variability: Perbill::from_percent(3),
|
||||
on_demand_target_queue_utilization: Perbill::from_percent(25),
|
||||
ttl: 5u32,
|
||||
},
|
||||
};
|
||||
|
||||
Configuration::set_validation_upgrade_cooldown(
|
||||
RuntimeOrigin::root(),
|
||||
new_config.validation_upgrade_cooldown,
|
||||
)
|
||||
.unwrap();
|
||||
Configuration::set_validation_upgrade_delay(
|
||||
RuntimeOrigin::root(),
|
||||
new_config.validation_upgrade_delay,
|
||||
)
|
||||
.unwrap();
|
||||
Configuration::set_code_retention_period(
|
||||
RuntimeOrigin::root(),
|
||||
new_config.code_retention_period,
|
||||
)
|
||||
.unwrap();
|
||||
Configuration::set_max_code_size(RuntimeOrigin::root(), new_config.max_code_size).unwrap();
|
||||
Configuration::set_max_pov_size(RuntimeOrigin::root(), new_config.max_pov_size).unwrap();
|
||||
Configuration::set_max_head_data_size(RuntimeOrigin::root(), new_config.max_head_data_size)
|
||||
.unwrap();
|
||||
Configuration::set_coretime_cores(
|
||||
RuntimeOrigin::root(),
|
||||
new_config.scheduler_params.num_cores,
|
||||
)
|
||||
.unwrap();
|
||||
Configuration::set_group_rotation_frequency(
|
||||
RuntimeOrigin::root(),
|
||||
new_config.scheduler_params.group_rotation_frequency,
|
||||
)
|
||||
.unwrap();
|
||||
// This comes out of order to satisfy the validity criteria for the chain and thread
|
||||
// availability periods.
|
||||
Configuration::set_minimum_validation_upgrade_delay(
|
||||
RuntimeOrigin::root(),
|
||||
new_config.minimum_validation_upgrade_delay,
|
||||
)
|
||||
.unwrap();
|
||||
Configuration::set_paras_availability_period(
|
||||
RuntimeOrigin::root(),
|
||||
new_config.scheduler_params.paras_availability_period,
|
||||
)
|
||||
.unwrap();
|
||||
Configuration::set_scheduling_lookahead(
|
||||
RuntimeOrigin::root(),
|
||||
new_config.scheduler_params.lookahead,
|
||||
)
|
||||
.unwrap();
|
||||
Configuration::set_max_validators_per_core(
|
||||
RuntimeOrigin::root(),
|
||||
new_config.scheduler_params.max_validators_per_core,
|
||||
)
|
||||
.unwrap();
|
||||
Configuration::set_max_validators(RuntimeOrigin::root(), new_config.max_validators)
|
||||
.unwrap();
|
||||
Configuration::set_dispute_period(RuntimeOrigin::root(), new_config.dispute_period)
|
||||
.unwrap();
|
||||
Configuration::set_dispute_post_conclusion_acceptance_period(
|
||||
RuntimeOrigin::root(),
|
||||
new_config.dispute_post_conclusion_acceptance_period,
|
||||
)
|
||||
.unwrap();
|
||||
Configuration::set_no_show_slots(RuntimeOrigin::root(), new_config.no_show_slots).unwrap();
|
||||
Configuration::set_n_delay_tranches(RuntimeOrigin::root(), new_config.n_delay_tranches)
|
||||
.unwrap();
|
||||
Configuration::set_zeroth_delay_tranche_width(
|
||||
RuntimeOrigin::root(),
|
||||
new_config.zeroth_delay_tranche_width,
|
||||
)
|
||||
.unwrap();
|
||||
Configuration::set_needed_approvals(RuntimeOrigin::root(), new_config.needed_approvals)
|
||||
.unwrap();
|
||||
Configuration::set_relay_vrf_modulo_samples(
|
||||
RuntimeOrigin::root(),
|
||||
new_config.relay_vrf_modulo_samples,
|
||||
)
|
||||
.unwrap();
|
||||
Configuration::set_max_upward_queue_count(
|
||||
RuntimeOrigin::root(),
|
||||
new_config.max_upward_queue_count,
|
||||
)
|
||||
.unwrap();
|
||||
Configuration::set_max_upward_queue_size(
|
||||
RuntimeOrigin::root(),
|
||||
new_config.max_upward_queue_size,
|
||||
)
|
||||
.unwrap();
|
||||
Configuration::set_max_downward_message_size(
|
||||
RuntimeOrigin::root(),
|
||||
new_config.max_downward_message_size,
|
||||
)
|
||||
.unwrap();
|
||||
Configuration::set_max_upward_message_size(
|
||||
RuntimeOrigin::root(),
|
||||
new_config.max_upward_message_size,
|
||||
)
|
||||
.unwrap();
|
||||
Configuration::set_max_upward_message_num_per_candidate(
|
||||
RuntimeOrigin::root(),
|
||||
new_config.max_upward_message_num_per_candidate,
|
||||
)
|
||||
.unwrap();
|
||||
Configuration::set_hrmp_sender_deposit(
|
||||
RuntimeOrigin::root(),
|
||||
new_config.hrmp_sender_deposit,
|
||||
)
|
||||
.unwrap();
|
||||
Configuration::set_hrmp_recipient_deposit(
|
||||
RuntimeOrigin::root(),
|
||||
new_config.hrmp_recipient_deposit,
|
||||
)
|
||||
.unwrap();
|
||||
Configuration::set_hrmp_channel_max_capacity(
|
||||
RuntimeOrigin::root(),
|
||||
new_config.hrmp_channel_max_capacity,
|
||||
)
|
||||
.unwrap();
|
||||
Configuration::set_hrmp_channel_max_total_size(
|
||||
RuntimeOrigin::root(),
|
||||
new_config.hrmp_channel_max_total_size,
|
||||
)
|
||||
.unwrap();
|
||||
Configuration::set_hrmp_max_teyrchain_inbound_channels(
|
||||
RuntimeOrigin::root(),
|
||||
new_config.hrmp_max_teyrchain_inbound_channels,
|
||||
)
|
||||
.unwrap();
|
||||
Configuration::set_hrmp_channel_max_message_size(
|
||||
RuntimeOrigin::root(),
|
||||
new_config.hrmp_channel_max_message_size,
|
||||
)
|
||||
.unwrap();
|
||||
Configuration::set_hrmp_max_teyrchain_outbound_channels(
|
||||
RuntimeOrigin::root(),
|
||||
new_config.hrmp_max_teyrchain_outbound_channels,
|
||||
)
|
||||
.unwrap();
|
||||
Configuration::set_hrmp_max_message_num_per_candidate(
|
||||
RuntimeOrigin::root(),
|
||||
new_config.hrmp_max_message_num_per_candidate,
|
||||
)
|
||||
.unwrap();
|
||||
Configuration::set_pvf_voting_ttl(RuntimeOrigin::root(), new_config.pvf_voting_ttl)
|
||||
.unwrap();
|
||||
Configuration::set_minimum_backing_votes(
|
||||
RuntimeOrigin::root(),
|
||||
new_config.minimum_backing_votes,
|
||||
)
|
||||
.unwrap();
|
||||
Configuration::set_node_feature(RuntimeOrigin::root(), 1, true).unwrap();
|
||||
Configuration::set_node_feature(RuntimeOrigin::root(), 1, true).unwrap();
|
||||
Configuration::set_node_feature(RuntimeOrigin::root(), 3, true).unwrap();
|
||||
Configuration::set_node_feature(RuntimeOrigin::root(), 10, true).unwrap();
|
||||
Configuration::set_node_feature(RuntimeOrigin::root(), 10, false).unwrap();
|
||||
Configuration::set_node_feature(RuntimeOrigin::root(), 11, true).unwrap();
|
||||
|
||||
assert_eq!(PendingConfigs::<Test>::get(), vec![(shared::SESSION_DELAY, new_config)],);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_root_cannot_set_config() {
|
||||
new_test_ext(Default::default()).execute_with(|| {
|
||||
assert!(Configuration::set_validation_upgrade_delay(RuntimeOrigin::signed(1), 100).is_err());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_externally_accessible() {
|
||||
// This test verifies that the value can be accessed through the well known keys and the
|
||||
// host configuration decodes into the abridged version.
|
||||
|
||||
use pezkuwi_primitives::{well_known_keys, AbridgedHostConfiguration};
|
||||
|
||||
new_test_ext(Default::default()).execute_with(|| {
|
||||
let mut ground_truth = HostConfiguration::default();
|
||||
ground_truth.async_backing_params =
|
||||
AsyncBackingParams { allowed_ancestry_len: 111, max_candidate_depth: 222 };
|
||||
|
||||
// Make sure that the configuration is stored in the storage.
|
||||
ActiveConfig::<Test>::put(ground_truth.clone());
|
||||
|
||||
// Extract the active config via the well known key.
|
||||
let raw_active_config = sp_io::storage::get(well_known_keys::ACTIVE_CONFIG)
|
||||
.expect("config must be present in storage under ACTIVE_CONFIG");
|
||||
let abridged_config = AbridgedHostConfiguration::decode(&mut &raw_active_config[..])
|
||||
.expect("HostConfiguration must be decodable into AbridgedHostConfiguration");
|
||||
|
||||
assert_eq!(
|
||||
abridged_config,
|
||||
AbridgedHostConfiguration {
|
||||
max_code_size: ground_truth.max_code_size,
|
||||
max_head_data_size: ground_truth.max_head_data_size,
|
||||
max_upward_queue_count: ground_truth.max_upward_queue_count,
|
||||
max_upward_queue_size: ground_truth.max_upward_queue_size,
|
||||
max_upward_message_size: ground_truth.max_upward_message_size,
|
||||
max_upward_message_num_per_candidate: ground_truth
|
||||
.max_upward_message_num_per_candidate,
|
||||
hrmp_max_message_num_per_candidate: ground_truth.hrmp_max_message_num_per_candidate,
|
||||
validation_upgrade_cooldown: ground_truth.validation_upgrade_cooldown,
|
||||
validation_upgrade_delay: ground_truth.validation_upgrade_delay,
|
||||
async_backing_params: ground_truth.async_backing_params,
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn active_config_hrmp_channel_size_and_capacity_ratio_works() {
|
||||
frame_support::parameter_types! {
|
||||
pub Ratio100: Percent = Percent::from_percent(100);
|
||||
pub Ratio50: Percent = Percent::from_percent(50);
|
||||
}
|
||||
|
||||
let mut genesis: MockGenesisConfig = Default::default();
|
||||
genesis.configuration.config.hrmp_channel_max_message_size = 1024;
|
||||
genesis.configuration.config.hrmp_channel_max_capacity = 100;
|
||||
|
||||
new_test_ext(genesis).execute_with(|| {
|
||||
let active_config = configuration::ActiveConfig::<Test>::get();
|
||||
assert_eq!(active_config.hrmp_channel_max_message_size, 1024);
|
||||
assert_eq!(active_config.hrmp_channel_max_capacity, 100);
|
||||
|
||||
assert_eq!(
|
||||
ActiveConfigHrmpChannelSizeAndCapacityRatio::<Test, Ratio100>::get(),
|
||||
(1024, 100)
|
||||
);
|
||||
assert_eq!(ActiveConfigHrmpChannelSizeAndCapacityRatio::<Test, Ratio50>::get(), (512, 50));
|
||||
|
||||
// change ActiveConfig
|
||||
assert_ok!(Configuration::set_hrmp_channel_max_message_size(
|
||||
RuntimeOrigin::root(),
|
||||
active_config.hrmp_channel_max_message_size * 4
|
||||
));
|
||||
assert_ok!(Configuration::set_hrmp_channel_max_capacity(
|
||||
RuntimeOrigin::root(),
|
||||
active_config.hrmp_channel_max_capacity * 4
|
||||
));
|
||||
on_new_session(1);
|
||||
on_new_session(2);
|
||||
let active_config = configuration::ActiveConfig::<Test>::get();
|
||||
assert_eq!(active_config.hrmp_channel_max_message_size, 4096);
|
||||
assert_eq!(active_config.hrmp_channel_max_capacity, 400);
|
||||
|
||||
assert_eq!(
|
||||
ActiveConfigHrmpChannelSizeAndCapacityRatio::<Test, Ratio100>::get(),
|
||||
(4096, 400)
|
||||
);
|
||||
assert_eq!(
|
||||
ActiveConfigHrmpChannelSizeAndCapacityRatio::<Test, Ratio50>::get(),
|
||||
(2048, 200)
|
||||
);
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// 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/>.
|
||||
|
||||
//! Coretime pallet benchmarking.
|
||||
|
||||
#![cfg(feature = "runtime-benchmarks")]
|
||||
|
||||
use super::*;
|
||||
use frame_benchmarking::v2::*;
|
||||
use frame_support::traits::OriginTrait;
|
||||
use pallet_broker::CoreIndex as BrokerCoreIndex;
|
||||
|
||||
#[benchmarks]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
use assigner_coretime::PartsOf57600;
|
||||
|
||||
#[benchmark]
|
||||
fn request_revenue_at() {
|
||||
let root_origin = <T as frame_system::Config>::RuntimeOrigin::root();
|
||||
let mhr = <T as on_demand::Config>::MaxHistoricalRevenue::get();
|
||||
frame_system::Pallet::<T>::set_block_number((mhr + 2).into());
|
||||
let minimum_balance = <T as on_demand::Config>::Currency::minimum_balance();
|
||||
let rev: BoundedVec<
|
||||
<<T as on_demand::Config>::Currency as frame_support::traits::Currency<
|
||||
T::AccountId,
|
||||
>>::Balance,
|
||||
T::MaxHistoricalRevenue,
|
||||
> = BoundedVec::try_from((1..=mhr).map(|v| minimum_balance * v.into()).collect::<Vec<_>>())
|
||||
.unwrap();
|
||||
on_demand::Revenue::<T>::put(rev);
|
||||
|
||||
crate::paras::Heads::<T>::insert(ParaId::from(T::BrokerId::get()), vec![1, 2, 3]);
|
||||
|
||||
<T as on_demand::Config>::Currency::make_free_balance_be(
|
||||
&<on_demand::Pallet<T>>::account_id(),
|
||||
minimum_balance * (mhr * (mhr + 1)).into(),
|
||||
);
|
||||
|
||||
#[extrinsic_call]
|
||||
_(root_origin as <T as frame_system::Config>::RuntimeOrigin, mhr + 1)
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn request_core_count() {
|
||||
// Setup
|
||||
let root_origin = <T as frame_system::Config>::RuntimeOrigin::root();
|
||||
|
||||
#[extrinsic_call]
|
||||
_(
|
||||
root_origin as <T as frame_system::Config>::RuntimeOrigin,
|
||||
// random core count
|
||||
100,
|
||||
)
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn assign_core(s: Linear<1, 100>) {
|
||||
// Setup
|
||||
let root_origin = <T as frame_system::Config>::RuntimeOrigin::root();
|
||||
|
||||
// Use parameterized assignment count
|
||||
let mut assignments: Vec<(CoreAssignment, PartsOf57600)> = vec![0u16; s as usize - 1]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, parts)| {
|
||||
(CoreAssignment::Task(index as u32), PartsOf57600::new_saturating(parts))
|
||||
})
|
||||
.collect();
|
||||
// Parts must add up to exactly 57600. Here we add all the parts in one assignment, as
|
||||
// it won't effect the weight and splitting up the parts into even groupings may not
|
||||
// work for every value `s`.
|
||||
assignments.push((CoreAssignment::Task(s as u32), PartsOf57600::FULL));
|
||||
|
||||
let core_index: BrokerCoreIndex = 0;
|
||||
|
||||
#[extrinsic_call]
|
||||
_(
|
||||
root_origin as <T as frame_system::Config>::RuntimeOrigin,
|
||||
core_index,
|
||||
BlockNumberFor::<T>::from(5u32),
|
||||
assignments,
|
||||
Some(BlockNumberFor::<T>::from(20u32)),
|
||||
)
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn credit_account() {
|
||||
// Setup
|
||||
let root_origin = <T as frame_system::Config>::RuntimeOrigin::root();
|
||||
let who: T::AccountId = whitelisted_caller();
|
||||
|
||||
#[extrinsic_call]
|
||||
_(root_origin as <T as frame_system::Config>::RuntimeOrigin, who, 1_000_000u32.into())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
// 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/>.
|
||||
|
||||
//! Migrations for the Coretime pallet.
|
||||
|
||||
pub use v_coretime::{GetLegacyLease, MigrateToCoretime};
|
||||
|
||||
mod v_coretime {
|
||||
use crate::{
|
||||
assigner_coretime, configuration,
|
||||
coretime::{mk_coretime_call, Config, PartsOf57600, WeightInfo},
|
||||
};
|
||||
use alloc::{vec, vec::Vec};
|
||||
#[cfg(feature = "try-runtime")]
|
||||
use codec::Decode;
|
||||
#[cfg(feature = "try-runtime")]
|
||||
use codec::Encode;
|
||||
use core::{iter, result};
|
||||
#[cfg(feature = "try-runtime")]
|
||||
use frame_support::ensure;
|
||||
use frame_support::{
|
||||
traits::{OnRuntimeUpgrade, PalletInfoAccess, StorageVersion},
|
||||
weights::Weight,
|
||||
};
|
||||
use frame_system::pallet_prelude::BlockNumberFor;
|
||||
use pallet_broker::{CoreAssignment, CoreMask, ScheduleItem};
|
||||
use pezkuwi_primitives::{CoreIndex, Id as ParaId};
|
||||
use pezkuwi_teyrchain_primitives::primitives::IsSystem;
|
||||
use sp_arithmetic::traits::SaturatedConversion;
|
||||
use sp_core::Get;
|
||||
use sp_runtime::BoundedVec;
|
||||
use xcm::prelude::{
|
||||
send_xcm, Instruction, Junction, Location, SendError, SendXcm, WeightLimit, Xcm,
|
||||
};
|
||||
|
||||
/// Return information about a legacy lease of a teyrchain.
|
||||
pub trait GetLegacyLease<N> {
|
||||
/// If teyrchain is a lease holding teyrchain, return the block at which the lease expires.
|
||||
fn get_teyrchain_lease_in_blocks(para: ParaId) -> Option<N>;
|
||||
// All teyrchains holding a lease, no matter if there are gaps in the slots or not.
|
||||
fn get_all_teyrchains_with_leases() -> Vec<ParaId>;
|
||||
}
|
||||
|
||||
/// Migrate a chain to use coretime.
|
||||
///
|
||||
/// This assumes that the `Coretime` and the `AssignerCoretime` pallets are added at the same
|
||||
/// time to a runtime.
|
||||
pub struct MigrateToCoretime<T, SendXcm, LegacyLease, const TIMESLICE_PERIOD: u32>(
|
||||
core::marker::PhantomData<(T, SendXcm, LegacyLease)>,
|
||||
);
|
||||
|
||||
impl<
|
||||
T: Config,
|
||||
XcmSender: SendXcm,
|
||||
LegacyLease: GetLegacyLease<BlockNumberFor<T>>,
|
||||
const TIMESLICE_PERIOD: u32,
|
||||
> MigrateToCoretime<T, XcmSender, LegacyLease, TIMESLICE_PERIOD>
|
||||
{
|
||||
fn already_migrated() -> bool {
|
||||
// We are using the assigner coretime because the coretime pallet doesn't has any
|
||||
// storage data. But both pallets are introduced at the same time, so this is fine.
|
||||
let name_hash = assigner_coretime::Pallet::<T>::name_hash();
|
||||
let mut next_key = name_hash.to_vec();
|
||||
let storage_version_key = StorageVersion::storage_key::<assigner_coretime::Pallet<T>>();
|
||||
|
||||
loop {
|
||||
match sp_io::storage::next_key(&next_key) {
|
||||
// StorageVersion is initialized before, so we need to ignore it.
|
||||
Some(key) if &key == &storage_version_key => {
|
||||
next_key = key;
|
||||
},
|
||||
// If there is any other key with the prefix of the pallet,
|
||||
// we already have executed the migration.
|
||||
Some(key) if key.starts_with(&name_hash) => {
|
||||
log::info!("`MigrateToCoretime` already executed!");
|
||||
return true;
|
||||
},
|
||||
// Any other key/no key means that we did not yet have migrated.
|
||||
None | Some(_) => return false,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<
|
||||
T: Config + crate::dmp::Config,
|
||||
XcmSender: SendXcm,
|
||||
LegacyLease: GetLegacyLease<BlockNumberFor<T>>,
|
||||
const TIMESLICE_PERIOD: u32,
|
||||
> OnRuntimeUpgrade for MigrateToCoretime<T, XcmSender, LegacyLease, TIMESLICE_PERIOD>
|
||||
{
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
if Self::already_migrated() {
|
||||
return Weight::zero();
|
||||
}
|
||||
|
||||
log::info!("Migrating existing teyrchains to coretime.");
|
||||
migrate_to_coretime::<T, XcmSender, LegacyLease, TIMESLICE_PERIOD>()
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::DispatchError> {
|
||||
if Self::already_migrated() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let legacy_paras = LegacyLease::get_all_teyrchains_with_leases();
|
||||
let config = configuration::ActiveConfig::<T>::get();
|
||||
let total_core_count = config.scheduler_params.num_cores + legacy_paras.len() as u32;
|
||||
|
||||
let dmp_queue_size =
|
||||
crate::dmp::Pallet::<T>::dmq_contents(T::BrokerId::get().into()).len() as u32;
|
||||
|
||||
let total_core_count = total_core_count as u32;
|
||||
|
||||
Ok((total_core_count, dmp_queue_size).encode())
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(state: Vec<u8>) -> Result<(), sp_runtime::DispatchError> {
|
||||
if state.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::trace!("Running post_upgrade()");
|
||||
|
||||
let (prev_core_count, prev_dmp_queue_size) =
|
||||
<(u32, u32)>::decode(&mut &state[..]).unwrap();
|
||||
|
||||
let dmp_queue_size =
|
||||
crate::dmp::Pallet::<T>::dmq_contents(T::BrokerId::get().into()).len() as u32;
|
||||
let config = configuration::ActiveConfig::<T>::get();
|
||||
let new_core_count = config.scheduler_params.num_cores;
|
||||
ensure!(new_core_count == prev_core_count, "Total number of cores need to not change.");
|
||||
ensure!(
|
||||
dmp_queue_size > prev_dmp_queue_size,
|
||||
"There should have been enqueued at least one DMP messages."
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate to Coretime.
|
||||
//
|
||||
// NOTE: Also migrates `num_cores` config value in configuration::ActiveConfig.
|
||||
fn migrate_to_coretime<
|
||||
T: Config,
|
||||
XcmSender: SendXcm,
|
||||
LegacyLease: GetLegacyLease<BlockNumberFor<T>>,
|
||||
const TIMESLICE_PERIOD: u32,
|
||||
>() -> Weight {
|
||||
let legacy_paras = LegacyLease::get_all_teyrchains_with_leases();
|
||||
let legacy_count = legacy_paras.len() as u32;
|
||||
let now = frame_system::Pallet::<T>::block_number();
|
||||
for (core, para_id) in legacy_paras.into_iter().enumerate() {
|
||||
let r = assigner_coretime::Pallet::<T>::assign_core(
|
||||
CoreIndex(core as u32),
|
||||
now,
|
||||
vec![(CoreAssignment::Task(para_id.into()), PartsOf57600::FULL)],
|
||||
None,
|
||||
);
|
||||
if let Err(err) = r {
|
||||
log::error!(
|
||||
"Creating assignment for existing para failed: {:?}, error: {:?}",
|
||||
para_id,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let config = configuration::ActiveConfig::<T>::get();
|
||||
for on_demand in 0..config.scheduler_params.num_cores {
|
||||
let core = CoreIndex(legacy_count.saturating_add(on_demand as _));
|
||||
let r = assigner_coretime::Pallet::<T>::assign_core(
|
||||
core,
|
||||
now,
|
||||
vec![(CoreAssignment::Pool, PartsOf57600::FULL)],
|
||||
None,
|
||||
);
|
||||
if let Err(err) = r {
|
||||
log::error!("Creating assignment for existing on-demand core, failed: {:?}", err);
|
||||
}
|
||||
}
|
||||
let total_cores = config.scheduler_params.num_cores + legacy_count;
|
||||
configuration::ActiveConfig::<T>::mutate(|c| {
|
||||
c.scheduler_params.num_cores = total_cores;
|
||||
});
|
||||
|
||||
if let Err(err) = migrate_send_assignments_to_coretime_chain::<
|
||||
T,
|
||||
XcmSender,
|
||||
LegacyLease,
|
||||
TIMESLICE_PERIOD,
|
||||
>() {
|
||||
log::error!("Sending legacy chain data to coretime chain failed: {:?}", err);
|
||||
}
|
||||
|
||||
let single_weight = <T as Config>::WeightInfo::assign_core(1);
|
||||
single_weight
|
||||
.saturating_mul(u64::from(
|
||||
legacy_count.saturating_add(config.scheduler_params.num_cores),
|
||||
))
|
||||
// Second read from sending assignments to the coretime chain.
|
||||
.saturating_add(T::DbWeight::get().reads_writes(2, 1))
|
||||
}
|
||||
|
||||
fn migrate_send_assignments_to_coretime_chain<
|
||||
T: Config,
|
||||
XcmSender: SendXcm,
|
||||
LegacyLease: GetLegacyLease<BlockNumberFor<T>>,
|
||||
const TIMESLICE_PERIOD: u32,
|
||||
>() -> result::Result<(), SendError> {
|
||||
let legacy_paras = LegacyLease::get_all_teyrchains_with_leases();
|
||||
let legacy_paras_count = legacy_paras.len();
|
||||
let (system_chains, lease_holding): (Vec<_>, Vec<_>) =
|
||||
legacy_paras.into_iter().partition(IsSystem::is_system);
|
||||
|
||||
let reservations = system_chains.into_iter().map(|p| {
|
||||
let schedule = BoundedVec::truncate_from(vec![ScheduleItem {
|
||||
mask: CoreMask::complete(),
|
||||
assignment: CoreAssignment::Task(p.into()),
|
||||
}]);
|
||||
mk_coretime_call::<T>(crate::coretime::CoretimeCalls::Reserve(schedule))
|
||||
});
|
||||
|
||||
let mut leases = lease_holding.into_iter().filter_map(|p| {
|
||||
log::trace!(target: "coretime-migration", "Preparing sending of lease holding para {:?}", p);
|
||||
let Some(valid_until) = LegacyLease::get_teyrchain_lease_in_blocks(p) else {
|
||||
log::error!("Lease holding chain with no lease information?!");
|
||||
return None
|
||||
};
|
||||
let valid_until: u32 = match valid_until.try_into() {
|
||||
Ok(val) => val,
|
||||
Err(_) => {
|
||||
log::error!("Converting block number to u32 failed!");
|
||||
return None
|
||||
},
|
||||
};
|
||||
let time_slice = valid_until.div_ceil(TIMESLICE_PERIOD);
|
||||
log::trace!(target: "coretime-migration", "Sending of lease holding para {:?}, valid_until: {:?}, time_slice: {:?}", p, valid_until, time_slice);
|
||||
Some(mk_coretime_call::<T>(crate::coretime::CoretimeCalls::SetLease(p.into(), time_slice)))
|
||||
});
|
||||
|
||||
let core_count: u16 = configuration::ActiveConfig::<T>::get()
|
||||
.scheduler_params
|
||||
.num_cores
|
||||
.saturated_into();
|
||||
let set_core_count = iter::once(mk_coretime_call::<T>(
|
||||
crate::coretime::CoretimeCalls::NotifyCoreCount(core_count),
|
||||
));
|
||||
|
||||
let pool = (legacy_paras_count..core_count.into()).map(|_| {
|
||||
let schedule = BoundedVec::truncate_from(vec![ScheduleItem {
|
||||
mask: CoreMask::complete(),
|
||||
assignment: CoreAssignment::Pool,
|
||||
}]);
|
||||
// Reserved cores will come before lease cores, so cores will change their assignments
|
||||
// when coretime chain sends us their assign_core calls -> Good test.
|
||||
mk_coretime_call::<T>(crate::coretime::CoretimeCalls::Reserve(schedule))
|
||||
});
|
||||
|
||||
let message_content = iter::once(Instruction::UnpaidExecution {
|
||||
weight_limit: WeightLimit::Unlimited,
|
||||
check_origin: None,
|
||||
});
|
||||
|
||||
let reservation_content = message_content.clone().chain(reservations).collect();
|
||||
let leases_content_1 = message_content
|
||||
.clone()
|
||||
.chain(leases.by_ref().take(legacy_paras_count / 2)) // split in two messages to avoid overweighted XCM
|
||||
.collect();
|
||||
let leases_content_2 = message_content.clone().chain(leases).collect();
|
||||
let set_core_count_content = message_content.clone().chain(set_core_count).collect();
|
||||
// If `pool_content` is empty don't send a blank XCM message
|
||||
let messages = if core_count as usize > legacy_paras_count {
|
||||
let pool_content = message_content.clone().chain(pool).collect();
|
||||
vec![
|
||||
Xcm(reservation_content),
|
||||
Xcm(pool_content),
|
||||
Xcm(leases_content_1),
|
||||
Xcm(leases_content_2),
|
||||
Xcm(set_core_count_content),
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
Xcm(reservation_content),
|
||||
Xcm(leases_content_1),
|
||||
Xcm(leases_content_2),
|
||||
Xcm(set_core_count_content),
|
||||
]
|
||||
};
|
||||
|
||||
for message in messages {
|
||||
send_xcm::<XcmSender>(
|
||||
Location::new(0, Junction::Teyrchain(T::BrokerId::get())),
|
||||
message,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
// 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/>.
|
||||
|
||||
//! Extrinsics implementing the relay chain side of the Coretime interface.
|
||||
//!
|
||||
//! <https://github.com/polkadot-fellows/RFCs/blob/main/text/0005-coretime-interface.md>
|
||||
|
||||
use alloc::{vec, vec::Vec};
|
||||
use core::result;
|
||||
use frame_support::{
|
||||
pallet_prelude::*,
|
||||
traits::{defensive_prelude::*, Currency},
|
||||
};
|
||||
use frame_system::pallet_prelude::*;
|
||||
pub use pallet::*;
|
||||
use pallet_broker::{CoreAssignment, CoreIndex as BrokerCoreIndex};
|
||||
use pezkuwi_primitives::{Balance, BlockNumber, CoreIndex, Id as ParaId};
|
||||
use sp_arithmetic::traits::SaturatedConversion;
|
||||
use sp_runtime::traits::TryConvert;
|
||||
use xcm::prelude::*;
|
||||
use xcm_executor::traits::TransactAsset;
|
||||
|
||||
use crate::{
|
||||
assigner_coretime::{self, PartsOf57600},
|
||||
initializer::{OnNewSession, SessionChangeNotification},
|
||||
on_demand,
|
||||
origin::{ensure_teyrchain, Origin},
|
||||
};
|
||||
|
||||
mod benchmarking;
|
||||
pub mod migration;
|
||||
|
||||
const LOG_TARGET: &str = "runtime::teyrchains::coretime";
|
||||
|
||||
pub trait WeightInfo {
|
||||
fn request_core_count() -> Weight;
|
||||
fn request_revenue_at() -> Weight;
|
||||
fn credit_account() -> Weight;
|
||||
fn assign_core(s: u32) -> Weight;
|
||||
}
|
||||
|
||||
/// A weight info that is only suitable for testing.
|
||||
pub struct TestWeightInfo;
|
||||
|
||||
impl WeightInfo for TestWeightInfo {
|
||||
fn request_core_count() -> Weight {
|
||||
Weight::MAX
|
||||
}
|
||||
fn request_revenue_at() -> Weight {
|
||||
Weight::MAX
|
||||
}
|
||||
fn credit_account() -> Weight {
|
||||
Weight::MAX
|
||||
}
|
||||
fn assign_core(_s: u32) -> Weight {
|
||||
Weight::MAX
|
||||
}
|
||||
}
|
||||
|
||||
/// Shorthand for the Balance type the runtime is using.
|
||||
pub type BalanceOf<T> = <<T as on_demand::Config>::Currency as Currency<
|
||||
<T as frame_system::Config>::AccountId,
|
||||
>>::Balance;
|
||||
|
||||
/// Broker pallet index on the coretime chain. Used to
|
||||
///
|
||||
/// construct remote calls. The codec index must correspond to the index of `Broker` in the
|
||||
/// `construct_runtime` of the coretime chain.
|
||||
#[derive(Encode, Decode)]
|
||||
enum BrokerRuntimePallets {
|
||||
#[codec(index = 50)]
|
||||
Broker(CoretimeCalls),
|
||||
}
|
||||
|
||||
/// Call encoding for the calls needed from the Broker pallet.
|
||||
#[derive(Encode, Decode)]
|
||||
enum CoretimeCalls {
|
||||
#[codec(index = 1)]
|
||||
Reserve(pallet_broker::Schedule),
|
||||
#[codec(index = 3)]
|
||||
SetLease(pallet_broker::TaskId, pallet_broker::Timeslice),
|
||||
#[codec(index = 19)]
|
||||
NotifyCoreCount(u16),
|
||||
#[codec(index = 20)]
|
||||
NotifyRevenue((BlockNumber, Balance)),
|
||||
#[codec(index = 99)]
|
||||
SwapLeases(ParaId, ParaId),
|
||||
}
|
||||
|
||||
#[frame_support::pallet]
|
||||
pub mod pallet {
|
||||
|
||||
use crate::configuration;
|
||||
use sp_runtime::traits::TryConvert;
|
||||
use xcm::latest::InteriorLocation;
|
||||
use xcm_executor::traits::TransactAsset;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[pallet::pallet]
|
||||
#[pallet::without_storage_info]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: frame_system::Config + assigner_coretime::Config + on_demand::Config {
|
||||
type RuntimeOrigin: From<<Self as frame_system::Config>::RuntimeOrigin>
|
||||
+ Into<result::Result<Origin, <Self as Config>::RuntimeOrigin>>;
|
||||
#[allow(deprecated)]
|
||||
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
|
||||
/// The ParaId of the coretime chain.
|
||||
#[pallet::constant]
|
||||
type BrokerId: Get<u32>;
|
||||
/// The coretime chain pot location.
|
||||
#[pallet::constant]
|
||||
type BrokerPotLocation: Get<InteriorLocation>;
|
||||
/// Something that provides the weight of this pallet.
|
||||
type WeightInfo: WeightInfo;
|
||||
/// The XCM sender.
|
||||
type SendXcm: SendXcm;
|
||||
/// The asset transactor.
|
||||
type AssetTransactor: TransactAsset;
|
||||
/// AccountId to Location converter
|
||||
type AccountToLocation: for<'a> TryConvert<&'a Self::AccountId, Location>;
|
||||
|
||||
/// Maximum weight for any XCM transact call that should be executed on the coretime chain.
|
||||
///
|
||||
/// Basically should be `max_weight(set_leases, reserve, notify_core_count)`.
|
||||
type MaxXcmTransactWeight: Get<Weight>;
|
||||
}
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
pub enum Event<T: Config> {
|
||||
/// The broker chain has asked for revenue information for a specific block.
|
||||
RevenueInfoRequested { when: BlockNumberFor<T> },
|
||||
/// A core has received a new assignment from the broker chain.
|
||||
CoreAssigned { core: CoreIndex },
|
||||
}
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T> {
|
||||
/// The paraid making the call is not the coretime brokerage system teyrchain.
|
||||
NotBroker,
|
||||
/// Requested revenue information `when` parameter was in the future from the current
|
||||
/// block height.
|
||||
RequestedFutureRevenue,
|
||||
/// Failed to transfer assets to the coretime chain
|
||||
AssetTransferFailed,
|
||||
}
|
||||
|
||||
#[pallet::hooks]
|
||||
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Request the configuration to be updated with the specified number of cores. Warning:
|
||||
/// Since this only schedules a configuration update, it takes two sessions to come into
|
||||
/// effect.
|
||||
///
|
||||
/// - `origin`: Root or the Coretime Chain
|
||||
/// - `count`: total number of cores
|
||||
#[pallet::weight(<T as Config>::WeightInfo::request_core_count())]
|
||||
#[pallet::call_index(1)]
|
||||
pub fn request_core_count(origin: OriginFor<T>, count: u16) -> DispatchResult {
|
||||
// Ignore requests not coming from the coretime chain or root.
|
||||
Self::ensure_root_or_para(origin, <T as Config>::BrokerId::get().into())?;
|
||||
|
||||
configuration::Pallet::<T>::set_coretime_cores_unchecked(u32::from(count))
|
||||
}
|
||||
|
||||
/// Request to claim the instantaneous coretime sales revenue starting from the block it was
|
||||
/// last claimed until and up to the block specified. The claimed amount value is sent back
|
||||
/// to the Coretime chain in a `notify_revenue` message. At the same time, the amount is
|
||||
/// teleported to the Coretime chain.
|
||||
#[pallet::weight(<T as Config>::WeightInfo::request_revenue_at())]
|
||||
#[pallet::call_index(2)]
|
||||
pub fn request_revenue_at(origin: OriginFor<T>, when: BlockNumber) -> DispatchResult {
|
||||
// Ignore requests not coming from the Coretime Chain or Root.
|
||||
Self::ensure_root_or_para(origin, <T as Config>::BrokerId::get().into())?;
|
||||
Self::notify_revenue(when)
|
||||
}
|
||||
|
||||
#[pallet::weight(<T as Config>::WeightInfo::credit_account())]
|
||||
#[pallet::call_index(3)]
|
||||
pub fn credit_account(
|
||||
origin: OriginFor<T>,
|
||||
who: T::AccountId,
|
||||
amount: BalanceOf<T>,
|
||||
) -> DispatchResult {
|
||||
// Ignore requests not coming from the coretime chain or root.
|
||||
Self::ensure_root_or_para(origin, <T as Config>::BrokerId::get().into())?;
|
||||
|
||||
on_demand::Pallet::<T>::credit_account(who, amount.saturated_into());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Receive instructions from the `ExternalBrokerOrigin`, detailing how a specific core is
|
||||
/// to be used.
|
||||
///
|
||||
/// Parameters:
|
||||
/// -`origin`: The `ExternalBrokerOrigin`, assumed to be the coretime chain.
|
||||
/// -`core`: The core that should be scheduled.
|
||||
/// -`begin`: The starting blockheight of the instruction.
|
||||
/// -`assignment`: How the blockspace should be utilised.
|
||||
/// -`end_hint`: An optional hint as to when this particular set of instructions will end.
|
||||
// The broker pallet's `CoreIndex` definition is `u16` but on the relay chain it's `struct
|
||||
// CoreIndex(u32)`
|
||||
#[pallet::call_index(4)]
|
||||
#[pallet::weight(<T as Config>::WeightInfo::assign_core(assignment.len() as u32))]
|
||||
pub fn assign_core(
|
||||
origin: OriginFor<T>,
|
||||
core: BrokerCoreIndex,
|
||||
begin: BlockNumberFor<T>,
|
||||
assignment: Vec<(CoreAssignment, PartsOf57600)>,
|
||||
end_hint: Option<BlockNumberFor<T>>,
|
||||
) -> DispatchResult {
|
||||
// Ignore requests not coming from the coretime chain or root.
|
||||
Self::ensure_root_or_para(origin, T::BrokerId::get().into())?;
|
||||
|
||||
let core = u32::from(core).into();
|
||||
|
||||
<assigner_coretime::Pallet<T>>::assign_core(core, begin, assignment, end_hint)?;
|
||||
Self::deposit_event(Event::<T>::CoreAssigned { core });
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Ensure the origin is one of Root or the `para` itself.
|
||||
fn ensure_root_or_para(
|
||||
origin: <T as frame_system::Config>::RuntimeOrigin,
|
||||
id: ParaId,
|
||||
) -> DispatchResult {
|
||||
if let Ok(caller_id) = ensure_teyrchain(<T as Config>::RuntimeOrigin::from(origin.clone()))
|
||||
{
|
||||
// Check if matching para id...
|
||||
ensure!(caller_id == id, Error::<T>::NotBroker);
|
||||
} else {
|
||||
// Check if root...
|
||||
ensure_root(origin.clone())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn initializer_on_new_session(notification: &SessionChangeNotification<BlockNumberFor<T>>) {
|
||||
let old_core_count = notification.prev_config.scheduler_params.num_cores;
|
||||
let new_core_count = notification.new_config.scheduler_params.num_cores;
|
||||
if new_core_count != old_core_count {
|
||||
let core_count: u16 = new_core_count.saturated_into();
|
||||
let message = Xcm(vec![
|
||||
Instruction::UnpaidExecution {
|
||||
weight_limit: WeightLimit::Unlimited,
|
||||
check_origin: None,
|
||||
},
|
||||
mk_coretime_call::<T>(crate::coretime::CoretimeCalls::NotifyCoreCount(core_count)),
|
||||
]);
|
||||
if let Err(err) = send_xcm::<T::SendXcm>(
|
||||
Location::new(0, [Junction::Teyrchain(T::BrokerId::get())]),
|
||||
message,
|
||||
) {
|
||||
log::error!(target: LOG_TARGET, "Sending `NotifyCoreCount` to coretime chain failed: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Provide the amount of revenue accumulated from Instantaneous Coretime Sales from Relay-chain
|
||||
/// block number last_until to until, not including until itself. last_until is defined as being
|
||||
/// the until argument of the last notify_revenue message sent, or zero for the first call. If
|
||||
/// revenue is None, this indicates that the information is no longer available. This explicitly
|
||||
/// disregards the possibility of multiple teyrchains requesting and being notified of revenue
|
||||
/// information.
|
||||
///
|
||||
/// The Relay-chain must be configured to ensure that only a single revenue information
|
||||
/// destination exists.
|
||||
pub fn notify_revenue(until: BlockNumber) -> DispatchResult {
|
||||
let now = <frame_system::Pallet<T>>::block_number();
|
||||
let until_bnf: BlockNumberFor<T> = until.into();
|
||||
|
||||
// When cannot be in the future.
|
||||
ensure!(until_bnf <= now, Error::<T>::RequestedFutureRevenue);
|
||||
|
||||
let amount = <on_demand::Pallet<T>>::claim_revenue_until(until_bnf);
|
||||
log::debug!(target: LOG_TARGET, "Revenue info requested: {:?}", amount);
|
||||
|
||||
let raw_revenue: Balance = amount.try_into().map_err(|_| {
|
||||
log::error!(target: LOG_TARGET, "Converting on demand revenue for `NotifyRevenue` failed");
|
||||
Error::<T>::AssetTransferFailed
|
||||
})?;
|
||||
|
||||
do_notify_revenue::<T>(until, raw_revenue).map_err(|err| {
|
||||
log::error!(target: LOG_TARGET, "notify_revenue failed: {err:?}");
|
||||
Error::<T>::AssetTransferFailed
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Handle legacy swaps in coretime. Notifies coretime chain that a lease swap has occurred via
|
||||
// XCM message. This function is meant to be used in an implementation of `OnSwap` trait.
|
||||
pub fn on_legacy_lease_swap(one: ParaId, other: ParaId) {
|
||||
let message = Xcm(vec![
|
||||
Instruction::UnpaidExecution {
|
||||
weight_limit: WeightLimit::Unlimited,
|
||||
check_origin: None,
|
||||
},
|
||||
mk_coretime_call::<T>(crate::coretime::CoretimeCalls::SwapLeases(one, other)),
|
||||
]);
|
||||
if let Err(err) = send_xcm::<T::SendXcm>(
|
||||
Location::new(0, [Junction::Teyrchain(T::BrokerId::get())]),
|
||||
message,
|
||||
) {
|
||||
log::error!(target: LOG_TARGET, "Sending `SwapLeases` to coretime chain failed: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> OnNewSession<BlockNumberFor<T>> for Pallet<T> {
|
||||
fn on_new_session(notification: &SessionChangeNotification<BlockNumberFor<T>>) {
|
||||
Self::initializer_on_new_session(notification);
|
||||
}
|
||||
}
|
||||
|
||||
fn mk_coretime_call<T: Config>(call: crate::coretime::CoretimeCalls) -> Instruction<()> {
|
||||
Instruction::Transact {
|
||||
origin_kind: OriginKind::Superuser,
|
||||
fallback_max_weight: Some(T::MaxXcmTransactWeight::get()),
|
||||
call: BrokerRuntimePallets::Broker(call).encode().into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn do_notify_revenue<T: Config>(when: BlockNumber, raw_revenue: Balance) -> Result<(), XcmError> {
|
||||
let dest = Junction::Teyrchain(T::BrokerId::get()).into_location();
|
||||
let mut message = vec![Instruction::UnpaidExecution {
|
||||
weight_limit: WeightLimit::Unlimited,
|
||||
check_origin: None,
|
||||
}];
|
||||
let asset = Asset { id: Location::here().into(), fun: Fungible(raw_revenue) };
|
||||
let dummy_xcm_context = XcmContext { origin: None, message_id: [0; 32], topic: None };
|
||||
|
||||
if raw_revenue > 0 {
|
||||
let on_demand_pot =
|
||||
T::AccountToLocation::try_convert(&<on_demand::Pallet<T>>::account_id()).map_err(
|
||||
|err| {
|
||||
log::error!(
|
||||
target: LOG_TARGET,
|
||||
"Failed to convert on-demand pot account to XCM location: {err:?}",
|
||||
);
|
||||
XcmError::InvalidLocation
|
||||
},
|
||||
)?;
|
||||
|
||||
let withdrawn = T::AssetTransactor::withdraw_asset(&asset, &on_demand_pot, None)?;
|
||||
|
||||
T::AssetTransactor::can_check_out(&dest, &asset, &dummy_xcm_context)?;
|
||||
|
||||
let assets_reanchored = Into::<Assets>::into(withdrawn)
|
||||
.reanchored(&dest, &Here.into())
|
||||
.defensive_map_err(|_| XcmError::ReanchorFailed)?;
|
||||
|
||||
message.extend(
|
||||
[
|
||||
ReceiveTeleportedAsset(assets_reanchored),
|
||||
DepositAsset {
|
||||
assets: Wild(AllCounted(1)),
|
||||
beneficiary: T::BrokerPotLocation::get().into_location(),
|
||||
},
|
||||
]
|
||||
.into_iter(),
|
||||
);
|
||||
}
|
||||
|
||||
message.push(mk_coretime_call::<T>(CoretimeCalls::NotifyRevenue((when, raw_revenue))));
|
||||
|
||||
send_xcm::<T::SendXcm>(dest.clone(), Xcm(message))?;
|
||||
|
||||
if raw_revenue > 0 {
|
||||
T::AssetTransactor::check_out(&dest, &asset, &dummy_xcm_context);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
// 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 frame_benchmarking::v2::*;
|
||||
use frame_system::RawOrigin;
|
||||
use sp_runtime::traits::One;
|
||||
|
||||
#[benchmarks]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
#[benchmark]
|
||||
fn force_unfreeze() {
|
||||
Frozen::<T>::set(Some(One::one()));
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Root);
|
||||
|
||||
assert!(Frozen::<T>::get().is_none())
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(
|
||||
Pallet,
|
||||
crate::mock::new_test_ext(Default::default()),
|
||||
crate::mock::Test
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// 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/>.
|
||||
|
||||
//! Storage migration(s) related to disputes pallet
|
||||
|
||||
use frame_support::traits::StorageVersion;
|
||||
|
||||
pub mod v1 {
|
||||
use super::*;
|
||||
use crate::disputes::{Config, Pallet};
|
||||
use alloc::vec::Vec;
|
||||
use frame_support::{
|
||||
pallet_prelude::*, storage_alias, traits::OnRuntimeUpgrade, weights::Weight,
|
||||
};
|
||||
use pezkuwi_primitives::SessionIndex;
|
||||
|
||||
#[storage_alias]
|
||||
type SpamSlots<T: Config> = StorageMap<Pallet<T>, Twox64Concat, SessionIndex, Vec<u32>>;
|
||||
|
||||
pub struct MigrateToV1<T>(core::marker::PhantomData<T>);
|
||||
impl<T: Config> OnRuntimeUpgrade for MigrateToV1<T> {
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
let mut weight: Weight = Weight::zero();
|
||||
|
||||
if StorageVersion::get::<Pallet<T>>() < 1 {
|
||||
log::info!(target: crate::disputes::LOG_TARGET, "Migrating disputes storage to v1");
|
||||
weight += migrate_to_v1::<T>();
|
||||
StorageVersion::new(1).put::<Pallet<T>>();
|
||||
weight = weight.saturating_add(T::DbWeight::get().reads_writes(1, 1));
|
||||
} else {
|
||||
log::info!(
|
||||
target: crate::disputes::LOG_TARGET,
|
||||
"Disputes storage up to date - no need for migration"
|
||||
);
|
||||
}
|
||||
|
||||
weight
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::TryRuntimeError> {
|
||||
log::trace!(
|
||||
target: crate::disputes::LOG_TARGET,
|
||||
"SpamSlots before migration: {}",
|
||||
SpamSlots::<T>::iter().count()
|
||||
);
|
||||
ensure!(
|
||||
StorageVersion::get::<Pallet<T>>() == 0,
|
||||
"Storage version should be less than `1` before the migration",
|
||||
);
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(_state: Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
|
||||
log::trace!(target: crate::disputes::LOG_TARGET, "Running post_upgrade()");
|
||||
ensure!(
|
||||
StorageVersion::get::<Pallet<T>>() >= 1,
|
||||
"Storage version should be `1` after the migration"
|
||||
);
|
||||
ensure!(
|
||||
SpamSlots::<T>::iter().count() == 0,
|
||||
"SpamSlots should be empty after the migration"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Migrates the pallet storage to the most recent version, checking and setting the
|
||||
/// `StorageVersion`.
|
||||
pub fn migrate_to_v1<T: Config>() -> Weight {
|
||||
let mut weight: Weight = Weight::zero();
|
||||
|
||||
// SpamSlots should not contain too many keys so removing everything at once should be safe
|
||||
let res = SpamSlots::<T>::clear(u32::MAX, None);
|
||||
// `loops` is the number of iterations => used to calculate read weights
|
||||
// `backend` is the number of keys removed from the backend => used to calculate write
|
||||
// weights
|
||||
weight = weight
|
||||
.saturating_add(T::DbWeight::get().reads_writes(res.loops as u64, res.backend as u64));
|
||||
|
||||
weight
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,745 @@
|
||||
// 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/>.
|
||||
|
||||
//! Dispute slashing pallet.
|
||||
//!
|
||||
//! Once a dispute is concluded, we want to slash validators who were on the
|
||||
//! wrong side of the dispute.
|
||||
//!
|
||||
//! A dispute should always result in an offence. There are 3 possible
|
||||
//! offence types:
|
||||
//! - `ForInvalidBacked`: A major offence when a validator backed an
|
||||
//! invalid block. Main source of economic security.
|
||||
//! - `ForInvalidApproved`: A medium offence when a validator approved (NOT backed) an
|
||||
//! invalid block. Protects from lazy validators.
|
||||
//! - `AgainstValid`: A minor offence when a validator disputed a valid block.
|
||||
//! Protects from spam attacks.
|
||||
//!
|
||||
//! Past session slashing edgecase:
|
||||
//!
|
||||
//! The `offences` pallet from Substrate provides us with a way to do both.
|
||||
//! Currently, the interface expects us to provide staking information including
|
||||
//! nominator exposure in order to submit an offence.
|
||||
//!
|
||||
//! Normally, we'd able to fetch this information from the runtime as soon as
|
||||
//! the dispute is concluded. However, since a dispute can conclude several
|
||||
//! sessions after the candidate was backed (see `dispute_period` in
|
||||
//! `HostConfiguration`), we can't rely on this information being available
|
||||
//! in the context of the current block. The `babe` and `grandpa` equivocation
|
||||
//! handlers also have to deal with this problem.
|
||||
//!
|
||||
//! Our implementation looks simillar to the `grandpa
|
||||
//! equivocation` handler. Meaning, we submit an `offence` for the concluded
|
||||
//! disputes about the current session candidate directly from the runtime. If,
|
||||
//! however, the dispute is about a past session, we record unapplied slashes on
|
||||
//! chain, without `FullIdentification` of the offenders. Later on, a block
|
||||
//! producer can submit an unsigned transaction with `KeyOwnershipProof` of an
|
||||
//! offender and submit it to the runtime to produce an offence.
|
||||
|
||||
use crate::{disputes, initializer::ValidatorSetCount, session_info::IdentificationTuple};
|
||||
use frame_support::{
|
||||
dispatch::Pays,
|
||||
traits::{Defensive, Get, KeyOwnerProofSystem, ValidatorSet, ValidatorSetWithIdentification},
|
||||
weights::Weight,
|
||||
};
|
||||
use frame_system::pallet_prelude::BlockNumberFor;
|
||||
|
||||
use alloc::{
|
||||
boxed::Box,
|
||||
collections::{btree_map::Entry, btree_set::BTreeSet},
|
||||
vec,
|
||||
vec::Vec,
|
||||
};
|
||||
use pezkuwi_primitives::{
|
||||
slashing::{DisputeProof, DisputesTimeSlot, PendingSlashes},
|
||||
CandidateHash, DisputeOffenceKind, SessionIndex, ValidatorId, ValidatorIndex,
|
||||
};
|
||||
use scale_info::TypeInfo;
|
||||
use sp_runtime::{
|
||||
traits::Convert,
|
||||
transaction_validity::{
|
||||
InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity,
|
||||
TransactionValidityError, ValidTransaction,
|
||||
},
|
||||
KeyTypeId, Perbill,
|
||||
};
|
||||
use sp_session::{GetSessionNumber, GetValidatorCount};
|
||||
use sp_staking::offence::{Kind, Offence, OffenceError, ReportOffence};
|
||||
|
||||
const LOG_TARGET: &str = "runtime::teyrchains::slashing";
|
||||
|
||||
// These are constants, but we want to make them configurable
|
||||
// via `HostConfiguration` in the future.
|
||||
const SLASH_FOR_INVALID_BACKED: Perbill = Perbill::from_percent(100);
|
||||
const SLASH_FOR_INVALID_APPROVED: Perbill = Perbill::from_percent(2);
|
||||
const SLASH_AGAINST_VALID: Perbill = Perbill::zero();
|
||||
const DEFENSIVE_PROOF: &'static str = "disputes module should bail on old session";
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
pub mod benchmarking;
|
||||
|
||||
/// The benchmarking configuration.
|
||||
pub trait BenchmarkingConfiguration {
|
||||
const MAX_VALIDATORS: u32;
|
||||
}
|
||||
|
||||
pub struct BenchConfig<const M: u32>;
|
||||
|
||||
impl<const M: u32> BenchmarkingConfiguration for BenchConfig<M> {
|
||||
const MAX_VALIDATORS: u32 = M;
|
||||
}
|
||||
|
||||
/// An offence that is filed against the validators that lost a dispute.
|
||||
#[derive(TypeInfo)]
|
||||
#[cfg_attr(feature = "std", derive(Clone, PartialEq, Eq))]
|
||||
pub struct SlashingOffence<KeyOwnerIdentification> {
|
||||
/// The size of the validator set in that session.
|
||||
pub validator_set_count: ValidatorSetCount,
|
||||
/// Should be unique per dispute.
|
||||
pub time_slot: DisputesTimeSlot,
|
||||
/// Staking information about the validators that lost the dispute
|
||||
/// needed for slashing.
|
||||
pub offenders: Vec<KeyOwnerIdentification>,
|
||||
/// What fraction of the total exposure that should be slashed for
|
||||
/// this offence.
|
||||
pub slash_fraction: Perbill,
|
||||
/// The type of slashing offence.
|
||||
pub kind: DisputeOffenceKind,
|
||||
}
|
||||
|
||||
impl<Offender> Offence<Offender> for SlashingOffence<Offender>
|
||||
where
|
||||
Offender: Clone,
|
||||
{
|
||||
const ID: Kind = *b"disputes:slashin";
|
||||
|
||||
type TimeSlot = DisputesTimeSlot;
|
||||
|
||||
fn offenders(&self) -> Vec<Offender> {
|
||||
self.offenders.clone()
|
||||
}
|
||||
|
||||
fn session_index(&self) -> SessionIndex {
|
||||
self.time_slot.session_index
|
||||
}
|
||||
|
||||
fn validator_set_count(&self) -> ValidatorSetCount {
|
||||
self.validator_set_count
|
||||
}
|
||||
|
||||
fn time_slot(&self) -> Self::TimeSlot {
|
||||
self.time_slot.clone()
|
||||
}
|
||||
|
||||
fn slash_fraction(&self, _offenders: u32) -> Perbill {
|
||||
self.slash_fraction
|
||||
}
|
||||
}
|
||||
|
||||
impl<KeyOwnerIdentification> SlashingOffence<KeyOwnerIdentification> {
|
||||
fn new(
|
||||
session_index: SessionIndex,
|
||||
candidate_hash: CandidateHash,
|
||||
validator_set_count: ValidatorSetCount,
|
||||
offenders: Vec<KeyOwnerIdentification>,
|
||||
kind: DisputeOffenceKind,
|
||||
) -> Self {
|
||||
let time_slot = DisputesTimeSlot::new(session_index, candidate_hash);
|
||||
let slash_fraction = match kind {
|
||||
DisputeOffenceKind::ForInvalidBacked => SLASH_FOR_INVALID_BACKED,
|
||||
DisputeOffenceKind::ForInvalidApproved => SLASH_FOR_INVALID_APPROVED,
|
||||
DisputeOffenceKind::AgainstValid => SLASH_AGAINST_VALID,
|
||||
};
|
||||
Self { time_slot, validator_set_count, offenders, slash_fraction, kind }
|
||||
}
|
||||
}
|
||||
|
||||
/// This type implements `SlashingHandler`.
|
||||
pub struct SlashValidatorsForDisputes<C> {
|
||||
_phantom: core::marker::PhantomData<C>,
|
||||
}
|
||||
|
||||
impl<C> Default for SlashValidatorsForDisputes<C> {
|
||||
fn default() -> Self {
|
||||
Self { _phantom: Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> SlashValidatorsForDisputes<Pallet<T>>
|
||||
where
|
||||
T: Config<KeyOwnerIdentification = IdentificationTuple<T>>,
|
||||
{
|
||||
/// If in the current session, returns the identified validators. `None`
|
||||
/// otherwise.
|
||||
fn maybe_identify_validators(
|
||||
session_index: SessionIndex,
|
||||
validators: impl IntoIterator<Item = ValidatorIndex>,
|
||||
) -> Option<Vec<IdentificationTuple<T>>> {
|
||||
// We use `ValidatorSet::session_index` and not
|
||||
// `shared::CurrentSessionIndex::<T>::get()` because at the first block of a new era,
|
||||
// the `IdentificationOf` of a validator in the previous session might be
|
||||
// missing, while `shared` pallet would return the same session index as being
|
||||
// updated at the end of the block.
|
||||
let current_session = T::ValidatorSet::session_index();
|
||||
if session_index == current_session {
|
||||
let account_keys = crate::session_info::AccountKeys::<T>::get(session_index);
|
||||
let account_ids = account_keys.defensive_unwrap_or_default();
|
||||
|
||||
let fully_identified = validators
|
||||
.into_iter()
|
||||
.flat_map(|i| account_ids.get(i.0 as usize).cloned())
|
||||
.filter_map(|id| {
|
||||
<T::ValidatorSet as ValidatorSetWithIdentification<T::AccountId>>::IdentificationOf::convert(
|
||||
id.clone()
|
||||
).map(|full_id| (id, full_id))
|
||||
})
|
||||
.collect::<Vec<IdentificationTuple<T>>>();
|
||||
return Some(fully_identified);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn do_punish(
|
||||
session_index: SessionIndex,
|
||||
candidate_hash: CandidateHash,
|
||||
kind: DisputeOffenceKind,
|
||||
losers: impl IntoIterator<Item = ValidatorIndex>,
|
||||
) {
|
||||
let losers: BTreeSet<_> = losers.into_iter().collect();
|
||||
if losers.is_empty() {
|
||||
return;
|
||||
}
|
||||
let session_info = crate::session_info::Sessions::<T>::get(session_index);
|
||||
let session_info = match session_info.defensive_proof(DEFENSIVE_PROOF) {
|
||||
Some(info) => info,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let maybe_offenders =
|
||||
Self::maybe_identify_validators(session_index, losers.iter().cloned());
|
||||
if let Some(offenders) = maybe_offenders {
|
||||
let validator_set_count = session_info.discovery_keys.len() as ValidatorSetCount;
|
||||
let offence = SlashingOffence::new(
|
||||
session_index,
|
||||
candidate_hash,
|
||||
validator_set_count,
|
||||
offenders,
|
||||
kind,
|
||||
);
|
||||
// This is the first time we report an offence for this dispute,
|
||||
// so it is not a duplicate.
|
||||
let _ = T::HandleReports::report_offence(offence);
|
||||
return;
|
||||
}
|
||||
|
||||
let keys = losers
|
||||
.into_iter()
|
||||
.filter_map(|i| session_info.validators.get(i).cloned().map(|id| (i, id)))
|
||||
.collect();
|
||||
let unapplied = PendingSlashes { keys, kind };
|
||||
|
||||
let append = |old: &mut Option<PendingSlashes>| {
|
||||
let old = old
|
||||
.get_or_insert(PendingSlashes { keys: Default::default(), kind: unapplied.kind });
|
||||
debug_assert_eq!(old.kind, unapplied.kind);
|
||||
|
||||
old.keys.extend(unapplied.keys)
|
||||
};
|
||||
<UnappliedSlashes<T>>::mutate(session_index, candidate_hash, append);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> disputes::SlashingHandler<BlockNumberFor<T>> for SlashValidatorsForDisputes<Pallet<T>>
|
||||
where
|
||||
T: Config<KeyOwnerIdentification = IdentificationTuple<T>>,
|
||||
{
|
||||
fn punish_for_invalid(
|
||||
session_index: SessionIndex,
|
||||
candidate_hash: CandidateHash,
|
||||
losers: impl IntoIterator<Item = ValidatorIndex>,
|
||||
backers: impl IntoIterator<Item = ValidatorIndex>,
|
||||
) {
|
||||
let losers: Vec<_> = losers.into_iter().collect();
|
||||
let backers: BTreeSet<_> = backers.into_iter().collect();
|
||||
|
||||
if losers.is_empty() || backers.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let (loosing_backers, loosing_approvers): (Vec<_>, Vec<_>) =
|
||||
losers.into_iter().partition(|v| backers.contains(v));
|
||||
|
||||
if !loosing_backers.is_empty() {
|
||||
Self::do_punish(
|
||||
session_index,
|
||||
candidate_hash,
|
||||
DisputeOffenceKind::ForInvalidBacked,
|
||||
loosing_backers,
|
||||
);
|
||||
}
|
||||
if !loosing_approvers.is_empty() {
|
||||
Self::do_punish(
|
||||
session_index,
|
||||
candidate_hash,
|
||||
DisputeOffenceKind::ForInvalidApproved,
|
||||
loosing_approvers,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn punish_against_valid(
|
||||
session_index: SessionIndex,
|
||||
candidate_hash: CandidateHash,
|
||||
losers: impl IntoIterator<Item = ValidatorIndex>,
|
||||
_backers: impl IntoIterator<Item = ValidatorIndex>,
|
||||
) {
|
||||
let kind = DisputeOffenceKind::AgainstValid;
|
||||
Self::do_punish(session_index, candidate_hash, kind, losers);
|
||||
}
|
||||
|
||||
fn initializer_initialize(now: BlockNumberFor<T>) -> Weight {
|
||||
Pallet::<T>::initializer_initialize(now)
|
||||
}
|
||||
|
||||
fn initializer_finalize() {
|
||||
Pallet::<T>::initializer_finalize()
|
||||
}
|
||||
|
||||
fn initializer_on_new_session(session_index: SessionIndex) {
|
||||
Pallet::<T>::initializer_on_new_session(session_index)
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait that defines methods to report an offence (after the slashing report
|
||||
/// has been validated) and for submitting a transaction to report a slash (from
|
||||
/// an offchain context).
|
||||
pub trait HandleReports<T: Config> {
|
||||
/// The longevity, in blocks, that the offence report is valid for. When
|
||||
/// using the staking pallet this should be equal to the bonding duration
|
||||
/// (in blocks, not eras).
|
||||
type ReportLongevity: Get<u64>;
|
||||
|
||||
/// Report an offence.
|
||||
fn report_offence(
|
||||
offence: SlashingOffence<T::KeyOwnerIdentification>,
|
||||
) -> Result<(), OffenceError>;
|
||||
|
||||
/// Returns true if the offenders at the given time slot has already been
|
||||
/// reported.
|
||||
fn is_known_offence(
|
||||
offenders: &[T::KeyOwnerIdentification],
|
||||
time_slot: &DisputesTimeSlot,
|
||||
) -> bool;
|
||||
|
||||
/// Create and dispatch a slashing report extrinsic.
|
||||
/// This should be called offchain.
|
||||
fn submit_unsigned_slashing_report(
|
||||
dispute_proof: DisputeProof,
|
||||
key_owner_proof: T::KeyOwnerProof,
|
||||
) -> Result<(), sp_runtime::TryRuntimeError>;
|
||||
}
|
||||
|
||||
impl<T: Config> HandleReports<T> for () {
|
||||
type ReportLongevity = ();
|
||||
|
||||
fn report_offence(
|
||||
_offence: SlashingOffence<T::KeyOwnerIdentification>,
|
||||
) -> Result<(), OffenceError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_known_offence(
|
||||
_offenders: &[T::KeyOwnerIdentification],
|
||||
_time_slot: &DisputesTimeSlot,
|
||||
) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn submit_unsigned_slashing_report(
|
||||
_dispute_proof: DisputeProof,
|
||||
_key_owner_proof: T::KeyOwnerProof,
|
||||
) -> Result<(), sp_runtime::TryRuntimeError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait WeightInfo {
|
||||
fn report_dispute_lost_unsigned(validator_count: ValidatorSetCount) -> Weight;
|
||||
}
|
||||
|
||||
pub struct TestWeightInfo;
|
||||
impl WeightInfo for TestWeightInfo {
|
||||
fn report_dispute_lost_unsigned(_validator_count: ValidatorSetCount) -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
}
|
||||
|
||||
pub use pallet::*;
|
||||
#[frame_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
use frame_support::pallet_prelude::*;
|
||||
use frame_system::pallet_prelude::*;
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: frame_system::Config + crate::disputes::Config {
|
||||
/// The proof of key ownership, used for validating slashing reports.
|
||||
/// The proof must include the session index and validator count of the
|
||||
/// session at which the offence occurred.
|
||||
type KeyOwnerProof: Parameter + GetSessionNumber + GetValidatorCount;
|
||||
|
||||
/// The identification of a key owner, used when reporting slashes.
|
||||
type KeyOwnerIdentification: Parameter;
|
||||
|
||||
/// A system for proving ownership of keys, i.e. that a given key was
|
||||
/// part of a validator set, needed for validating slashing reports.
|
||||
type KeyOwnerProofSystem: KeyOwnerProofSystem<
|
||||
(KeyTypeId, ValidatorId),
|
||||
Proof = Self::KeyOwnerProof,
|
||||
IdentificationTuple = Self::KeyOwnerIdentification,
|
||||
>;
|
||||
|
||||
/// The slashing report handling subsystem, defines methods to report an
|
||||
/// offence (after the slashing report has been validated) and for
|
||||
/// submitting a transaction to report a slash (from an offchain
|
||||
/// context). NOTE: when enabling slashing report handling (i.e. this
|
||||
/// type isn't set to `()`) you must use this pallet's
|
||||
/// `ValidateUnsigned` in the runtime definition.
|
||||
type HandleReports: HandleReports<Self>;
|
||||
|
||||
/// Weight information for extrinsics in this pallet.
|
||||
type WeightInfo: WeightInfo;
|
||||
|
||||
/// Benchmarking configuration.
|
||||
type BenchmarkingConfig: BenchmarkingConfiguration;
|
||||
}
|
||||
|
||||
#[pallet::pallet]
|
||||
#[pallet::without_storage_info]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
/// Validators pending dispute slashes.
|
||||
#[pallet::storage]
|
||||
pub(crate) type UnappliedSlashes<T> = StorageDoubleMap<
|
||||
_,
|
||||
Twox64Concat,
|
||||
SessionIndex,
|
||||
Blake2_128Concat,
|
||||
CandidateHash,
|
||||
PendingSlashes,
|
||||
>;
|
||||
|
||||
/// `ValidatorSetCount` per session.
|
||||
#[pallet::storage]
|
||||
pub(super) type ValidatorSetCounts<T> =
|
||||
StorageMap<_, Twox64Concat, SessionIndex, ValidatorSetCount>;
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T> {
|
||||
/// The key ownership proof is invalid.
|
||||
InvalidKeyOwnershipProof,
|
||||
/// The session index is too old or invalid.
|
||||
InvalidSessionIndex,
|
||||
/// The candidate hash is invalid.
|
||||
InvalidCandidateHash,
|
||||
/// There is no pending slash for the given validator index and time
|
||||
/// slot.
|
||||
InvalidValidatorIndex,
|
||||
/// The validator index does not match the validator id.
|
||||
ValidatorIndexIdMismatch,
|
||||
/// The given slashing report is valid but already previously reported.
|
||||
DuplicateSlashingReport,
|
||||
}
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
#[pallet::call_index(0)]
|
||||
#[pallet::weight(<T as Config>::WeightInfo::report_dispute_lost_unsigned(
|
||||
key_owner_proof.validator_count()
|
||||
))]
|
||||
pub fn report_dispute_lost_unsigned(
|
||||
origin: OriginFor<T>,
|
||||
// box to decrease the size of the call
|
||||
dispute_proof: Box<DisputeProof>,
|
||||
key_owner_proof: T::KeyOwnerProof,
|
||||
) -> DispatchResultWithPostInfo {
|
||||
ensure_none(origin)?;
|
||||
let validator_set_count = key_owner_proof.validator_count() as ValidatorSetCount;
|
||||
// check the membership proof to extract the offender's id
|
||||
let key =
|
||||
(pezkuwi_primitives::TEYRCHAIN_KEY_TYPE_ID, dispute_proof.validator_id.clone());
|
||||
let offender = T::KeyOwnerProofSystem::check_proof(key, key_owner_proof)
|
||||
.ok_or(Error::<T>::InvalidKeyOwnershipProof)?;
|
||||
|
||||
let session_index = dispute_proof.time_slot.session_index;
|
||||
|
||||
// check that there is a pending slash for the given
|
||||
// validator index and candidate hash
|
||||
let candidate_hash = dispute_proof.time_slot.candidate_hash;
|
||||
let try_remove = |v: &mut Option<PendingSlashes>| -> Result<(), DispatchError> {
|
||||
let pending = v.as_mut().ok_or(Error::<T>::InvalidCandidateHash)?;
|
||||
if pending.kind != dispute_proof.kind {
|
||||
return Err(Error::<T>::InvalidCandidateHash.into());
|
||||
}
|
||||
|
||||
match pending.keys.entry(dispute_proof.validator_index) {
|
||||
Entry::Vacant(_) => return Err(Error::<T>::InvalidValidatorIndex.into()),
|
||||
// check that `validator_index` matches `validator_id`
|
||||
Entry::Occupied(e) if e.get() != &dispute_proof.validator_id =>
|
||||
return Err(Error::<T>::ValidatorIndexIdMismatch.into()),
|
||||
Entry::Occupied(e) => {
|
||||
e.remove(); // the report is correct
|
||||
},
|
||||
}
|
||||
|
||||
// if the last validator is slashed for this dispute, clean up the storage
|
||||
if pending.keys.is_empty() {
|
||||
*v = None;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
};
|
||||
|
||||
<UnappliedSlashes<T>>::try_mutate_exists(&session_index, &candidate_hash, try_remove)?;
|
||||
|
||||
let offence = SlashingOffence::new(
|
||||
session_index,
|
||||
candidate_hash,
|
||||
validator_set_count,
|
||||
vec![offender],
|
||||
dispute_proof.kind,
|
||||
);
|
||||
|
||||
<T::HandleReports as HandleReports<T>>::report_offence(offence)
|
||||
.map_err(|_| Error::<T>::DuplicateSlashingReport)?;
|
||||
|
||||
Ok(Pays::No.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::validate_unsigned]
|
||||
impl<T: Config> ValidateUnsigned for Pallet<T> {
|
||||
type Call = Call<T>;
|
||||
fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity {
|
||||
Self::validate_unsigned(source, call)
|
||||
}
|
||||
|
||||
fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> {
|
||||
Self::pre_dispatch(call)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Called by the initializer to initialize the disputes slashing module.
|
||||
fn initializer_initialize(_now: BlockNumberFor<T>) -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
|
||||
/// Called by the initializer to finalize the disputes slashing pallet.
|
||||
fn initializer_finalize() {}
|
||||
|
||||
/// Called by the initializer to note a new session in the disputes slashing
|
||||
/// pallet.
|
||||
fn initializer_on_new_session(session_index: SessionIndex) {
|
||||
// This should be small, as disputes are limited by spam slots, so no limit is
|
||||
// fine.
|
||||
const REMOVE_LIMIT: u32 = u32::MAX;
|
||||
|
||||
let config = crate::configuration::ActiveConfig::<T>::get();
|
||||
if session_index <= config.dispute_period + 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
let old_session = session_index - config.dispute_period - 1;
|
||||
let _ = <UnappliedSlashes<T>>::clear_prefix(old_session, REMOVE_LIMIT, None);
|
||||
}
|
||||
|
||||
pub(crate) fn unapplied_slashes() -> Vec<(SessionIndex, CandidateHash, PendingSlashes)> {
|
||||
<UnappliedSlashes<T>>::iter().collect()
|
||||
}
|
||||
|
||||
pub(crate) fn submit_unsigned_slashing_report(
|
||||
dispute_proof: DisputeProof,
|
||||
key_ownership_proof: <T as Config>::KeyOwnerProof,
|
||||
) -> Option<()> {
|
||||
T::HandleReports::submit_unsigned_slashing_report(dispute_proof, key_ownership_proof).ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// Methods for the `ValidateUnsigned` implementation:
|
||||
///
|
||||
/// It restricts calls to `report_dispute_lost_unsigned` to local calls (i.e.
|
||||
/// extrinsics generated on this node) or that already in a block. This
|
||||
/// guarantees that only block authors can include unsigned slashing reports.
|
||||
impl<T: Config> Pallet<T> {
|
||||
pub fn validate_unsigned(source: TransactionSource, call: &Call<T>) -> TransactionValidity {
|
||||
if let Call::report_dispute_lost_unsigned { dispute_proof, key_owner_proof } = call {
|
||||
// discard slashing report not coming from the local node
|
||||
match source {
|
||||
TransactionSource::Local | TransactionSource::InBlock => { /* allowed */ },
|
||||
_ => {
|
||||
log::warn!(
|
||||
target: LOG_TARGET,
|
||||
"rejecting unsigned transaction because it is not local/in-block."
|
||||
);
|
||||
|
||||
return InvalidTransaction::Call.into();
|
||||
},
|
||||
}
|
||||
|
||||
// check report staleness
|
||||
is_known_offence::<T>(dispute_proof, key_owner_proof)?;
|
||||
|
||||
let longevity = <T::HandleReports as HandleReports<T>>::ReportLongevity::get();
|
||||
|
||||
let tag_prefix = match dispute_proof.kind {
|
||||
DisputeOffenceKind::ForInvalidBacked => "DisputeForInvalidBacked",
|
||||
DisputeOffenceKind::ForInvalidApproved => "DisputeForInvalidApproved",
|
||||
DisputeOffenceKind::AgainstValid => "DisputeAgainstValid",
|
||||
};
|
||||
|
||||
ValidTransaction::with_tag_prefix(tag_prefix)
|
||||
// We assign the maximum priority for any report.
|
||||
.priority(TransactionPriority::max_value())
|
||||
// Only one report for the same offender at the same slot.
|
||||
.and_provides((dispute_proof.time_slot.clone(), dispute_proof.validator_id.clone()))
|
||||
.longevity(longevity)
|
||||
// We don't propagate this. This can never be included on a remote node.
|
||||
.propagate(false)
|
||||
.build()
|
||||
} else {
|
||||
InvalidTransaction::Call.into()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pre_dispatch(call: &Call<T>) -> Result<(), TransactionValidityError> {
|
||||
if let Call::report_dispute_lost_unsigned { dispute_proof, key_owner_proof } = call {
|
||||
is_known_offence::<T>(dispute_proof, key_owner_proof)
|
||||
} else {
|
||||
Err(InvalidTransaction::Call.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_known_offence<T: Config>(
|
||||
dispute_proof: &DisputeProof,
|
||||
key_owner_proof: &T::KeyOwnerProof,
|
||||
) -> Result<(), TransactionValidityError> {
|
||||
// check the membership proof to extract the offender's id
|
||||
let key = (pezkuwi_primitives::TEYRCHAIN_KEY_TYPE_ID, dispute_proof.validator_id.clone());
|
||||
|
||||
let offender = T::KeyOwnerProofSystem::check_proof(key, key_owner_proof.clone())
|
||||
.ok_or(InvalidTransaction::BadProof)?;
|
||||
|
||||
// check if the offence has already been reported,
|
||||
// and if so then we can discard the report.
|
||||
let is_known_offence = <T::HandleReports as HandleReports<T>>::is_known_offence(
|
||||
&[offender],
|
||||
&dispute_proof.time_slot,
|
||||
);
|
||||
|
||||
if is_known_offence {
|
||||
Err(InvalidTransaction::Stale.into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Actual `HandleReports` implementation.
|
||||
///
|
||||
/// When configured properly, should be instantiated with
|
||||
/// `T::KeyOwnerIdentification, Offences, ReportLongevity` parameters.
|
||||
pub struct SlashingReportHandler<I, R, L> {
|
||||
_phantom: core::marker::PhantomData<(I, R, L)>,
|
||||
}
|
||||
|
||||
impl<I, R, L> Default for SlashingReportHandler<I, R, L> {
|
||||
fn default() -> Self {
|
||||
Self { _phantom: Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, R, L> HandleReports<T> for SlashingReportHandler<T::KeyOwnerIdentification, R, L>
|
||||
where
|
||||
T: Config + frame_system::offchain::CreateBare<Call<T>>,
|
||||
R: ReportOffence<
|
||||
T::AccountId,
|
||||
T::KeyOwnerIdentification,
|
||||
SlashingOffence<T::KeyOwnerIdentification>,
|
||||
>,
|
||||
L: Get<u64>,
|
||||
{
|
||||
type ReportLongevity = L;
|
||||
|
||||
fn report_offence(
|
||||
offence: SlashingOffence<T::KeyOwnerIdentification>,
|
||||
) -> Result<(), OffenceError> {
|
||||
let reporters = Vec::new();
|
||||
R::report_offence(reporters, offence)
|
||||
}
|
||||
|
||||
fn is_known_offence(
|
||||
offenders: &[T::KeyOwnerIdentification],
|
||||
time_slot: &DisputesTimeSlot,
|
||||
) -> bool {
|
||||
<R as ReportOffence<
|
||||
T::AccountId,
|
||||
T::KeyOwnerIdentification,
|
||||
SlashingOffence<T::KeyOwnerIdentification>,
|
||||
>>::is_known_offence(offenders, time_slot)
|
||||
}
|
||||
|
||||
fn submit_unsigned_slashing_report(
|
||||
dispute_proof: DisputeProof,
|
||||
key_owner_proof: <T as Config>::KeyOwnerProof,
|
||||
) -> Result<(), sp_runtime::TryRuntimeError> {
|
||||
use frame_system::offchain::{CreateBare, SubmitTransaction};
|
||||
|
||||
let session_index = dispute_proof.time_slot.session_index;
|
||||
let validator_index = dispute_proof.validator_index.0;
|
||||
let kind = dispute_proof.kind;
|
||||
|
||||
let call = Call::report_dispute_lost_unsigned {
|
||||
dispute_proof: Box::new(dispute_proof),
|
||||
key_owner_proof,
|
||||
};
|
||||
|
||||
let xt = <T as CreateBare<Call<T>>>::create_bare(call.into());
|
||||
match SubmitTransaction::<T, Call<T>>::submit_transaction(xt) {
|
||||
Ok(()) => {
|
||||
log::info!(
|
||||
target: LOG_TARGET,
|
||||
"Submitted dispute slashing report, session({}), index({}), kind({:?})",
|
||||
session_index,
|
||||
validator_index,
|
||||
kind,
|
||||
);
|
||||
Ok(())
|
||||
},
|
||||
Err(()) => {
|
||||
log::error!(
|
||||
target: LOG_TARGET,
|
||||
"Error submitting dispute slashing report, session({}), index({}), kind({:?})",
|
||||
session_index,
|
||||
validator_index,
|
||||
kind,
|
||||
);
|
||||
Err(sp_runtime::DispatchError::Other(""))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
// 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::{disputes::SlashingHandler, initializer, shared};
|
||||
use codec::Decode;
|
||||
use frame_benchmarking::v2::*;
|
||||
use frame_support::traits::{OnFinalize, OnInitialize};
|
||||
use frame_system::{pallet_prelude::BlockNumberFor, RawOrigin};
|
||||
use pallet_staking::testing_utils::create_validators;
|
||||
use pezkuwi_primitives::{Hash, TEYRCHAIN_KEY_TYPE_ID};
|
||||
use sp_runtime::traits::{One, OpaqueKeys, StaticLookup};
|
||||
use sp_session::MembershipProof;
|
||||
|
||||
// Candidate hash of the disputed candidate.
|
||||
const CANDIDATE_HASH: CandidateHash = CandidateHash(Hash::zero());
|
||||
|
||||
// Simplify getting the value in the benchmark
|
||||
pub const fn max_validators_for<T: super::Config>() -> u32 {
|
||||
<<T>::BenchmarkingConfig as BenchmarkingConfiguration>::MAX_VALIDATORS
|
||||
}
|
||||
|
||||
pub trait Config:
|
||||
pallet_session::Config
|
||||
+ pallet_session::historical::Config
|
||||
+ pallet_staking::Config
|
||||
+ super::Config
|
||||
+ shared::Config
|
||||
+ initializer::Config
|
||||
{
|
||||
}
|
||||
|
||||
fn setup_validator_set<T>(n: u32) -> (SessionIndex, MembershipProof, ValidatorId)
|
||||
where
|
||||
T: Config,
|
||||
{
|
||||
pallet_staking::ValidatorCount::<T>::put(n);
|
||||
|
||||
let balance_factor = 1000;
|
||||
// create validators and set random session keys
|
||||
for (n, who) in create_validators::<T>(n, balance_factor).unwrap().into_iter().enumerate() {
|
||||
use rand::{RngCore, SeedableRng};
|
||||
|
||||
let validator = T::Lookup::lookup(who).unwrap();
|
||||
let controller = pallet_staking::Pallet::<T>::bonded(&validator).unwrap();
|
||||
|
||||
let keys = {
|
||||
const SESSION_KEY_LEN: usize = 32;
|
||||
let key_ids = T::Keys::key_ids();
|
||||
let mut keys_len = key_ids.len() * SESSION_KEY_LEN;
|
||||
if key_ids.contains(&sp_core::crypto::key_types::BEEFY) {
|
||||
// BEEFY key is 33 bytes long, not 32.
|
||||
keys_len += 1;
|
||||
}
|
||||
let mut keys = vec![0u8; keys_len];
|
||||
let mut rng = rand_chacha::ChaCha12Rng::seed_from_u64(n as u64);
|
||||
rng.fill_bytes(&mut keys);
|
||||
keys
|
||||
};
|
||||
|
||||
let keys: T::Keys = Decode::decode(&mut &keys[..]).expect("wrong number of session keys?");
|
||||
let proof: Vec<u8> = vec![];
|
||||
|
||||
whitelist_account!(controller);
|
||||
pallet_session::Pallet::<T>::ensure_can_pay_key_deposit(&controller).unwrap();
|
||||
pallet_session::Pallet::<T>::set_keys(RawOrigin::Signed(controller).into(), keys, proof)
|
||||
.expect("session::set_keys should work");
|
||||
}
|
||||
|
||||
pallet_session::Pallet::<T>::on_initialize(BlockNumberFor::<T>::one());
|
||||
initializer::Pallet::<T>::on_initialize(BlockNumberFor::<T>::one());
|
||||
|
||||
// signal to `pallet-staking`'s `ElectionProvider` to be ready asap.
|
||||
use frame_election_provider_support::ElectionProvider;
|
||||
<<T as pallet_staking::Config>::ElectionProvider as ElectionProvider>::asap();
|
||||
|
||||
// skip sessions until the new validator set is enacted
|
||||
while pallet_session::Pallet::<T>::validators().len() < n as usize {
|
||||
pallet_session::Pallet::<T>::rotate_session();
|
||||
}
|
||||
initializer::Pallet::<T>::on_finalize(BlockNumberFor::<T>::one());
|
||||
|
||||
let session_index = crate::shared::CurrentSessionIndex::<T>::get();
|
||||
let session_info = crate::session_info::Sessions::<T>::get(session_index);
|
||||
let session_info = session_info.unwrap();
|
||||
let validator_id = session_info.validators.get(ValidatorIndex::from(0)).unwrap().clone();
|
||||
let key = (TEYRCHAIN_KEY_TYPE_ID, validator_id.clone());
|
||||
let key_owner_proof = pallet_session::historical::Pallet::<T>::prove(key).unwrap();
|
||||
|
||||
// rotate a session to make sure `key_owner_proof` is historical
|
||||
initializer::Pallet::<T>::on_initialize(BlockNumberFor::<T>::one());
|
||||
pallet_session::Pallet::<T>::rotate_session();
|
||||
initializer::Pallet::<T>::on_finalize(BlockNumberFor::<T>::one());
|
||||
|
||||
let idx = crate::shared::CurrentSessionIndex::<T>::get();
|
||||
assert!(
|
||||
idx > session_index,
|
||||
"session rotation should work for teyrchain pallets: {} <= {}",
|
||||
idx,
|
||||
session_index,
|
||||
);
|
||||
|
||||
(session_index, key_owner_proof, validator_id)
|
||||
}
|
||||
|
||||
/// Submits a single `ForInvalid` dispute.
|
||||
fn setup_dispute<T>(session_index: SessionIndex, validator_id: ValidatorId) -> DisputeProof
|
||||
where
|
||||
T: Config,
|
||||
{
|
||||
let current_session = T::ValidatorSet::session_index();
|
||||
assert_ne!(session_index, current_session);
|
||||
|
||||
let validator_index = ValidatorIndex(0);
|
||||
let losers = [validator_index].into_iter();
|
||||
let backers = losers.clone();
|
||||
|
||||
T::SlashingHandler::punish_for_invalid(session_index, CANDIDATE_HASH, losers, backers);
|
||||
|
||||
let unapplied = <UnappliedSlashes<T>>::get(session_index, CANDIDATE_HASH);
|
||||
assert_eq!(unapplied.unwrap().keys.len(), 1);
|
||||
|
||||
dispute_proof(session_index, validator_id, validator_index)
|
||||
}
|
||||
|
||||
/// Creates a `ForInvalid` dispute proof.
|
||||
fn dispute_proof(
|
||||
session_index: SessionIndex,
|
||||
validator_id: ValidatorId,
|
||||
validator_index: ValidatorIndex,
|
||||
) -> DisputeProof {
|
||||
let kind = DisputeOffenceKind::ForInvalidBacked;
|
||||
let time_slot = DisputesTimeSlot::new(session_index, CANDIDATE_HASH);
|
||||
|
||||
DisputeProof { time_slot, kind, validator_index, validator_id }
|
||||
}
|
||||
|
||||
#[benchmarks(where T: Config<KeyOwnerProof = MembershipProof>)]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
#[benchmark]
|
||||
fn report_dispute_lost_unsigned(n: Linear<4, { max_validators_for::<T>() }>) {
|
||||
let (session_index, key_owner_proof, validator_id) = setup_validator_set::<T>(n);
|
||||
|
||||
// submit a single `ForInvalid` dispute for a past session.
|
||||
let dispute_proof = setup_dispute::<T>(session_index, validator_id);
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::None, Box::new(dispute_proof), key_owner_proof);
|
||||
|
||||
let unapplied = <UnappliedSlashes<T>>::get(session_index, CANDIDATE_HASH);
|
||||
assert!(unapplied.is_none());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,358 @@
|
||||
// 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/>.
|
||||
|
||||
//! To prevent Out of Memory errors on the `DownwardMessageQueue`, an
|
||||
//! exponential fee factor (`DeliveryFeeFactor`) is set. The fee factor
|
||||
//! increments exponentially after the number of messages in the
|
||||
//! `DownwardMessageQueue` passes a threshold. This threshold is set as:
|
||||
//!
|
||||
//! ```ignore
|
||||
//! // Maximum max sized messages that can be send to
|
||||
//! // the DownwardMessageQueue before it runs out of memory
|
||||
//! max_messages = MAX_POSSIBLE_ALLOCATION / max_downward_message_size
|
||||
//! threshold = max_messages / THRESHOLD_FACTOR
|
||||
//! ```
|
||||
//! Based on the THRESHOLD_FACTOR, the threshold is set as a fraction of the
|
||||
//! total messages. The `DeliveryFeeFactor` increases for a message over the
|
||||
//! threshold by:
|
||||
//!
|
||||
//! `DeliveryFeeFactor = DeliveryFeeFactor *
|
||||
//! (EXPONENTIAL_FEE_BASE + MESSAGE_SIZE_FEE_BASE * encoded_message_size_in_KB)`
|
||||
//!
|
||||
//! And decreases when the number of messages in the `DownwardMessageQueue` fall
|
||||
//! below the threshold by:
|
||||
//!
|
||||
//! `DeliveryFeeFactor = DeliveryFeeFactor / EXPONENTIAL_FEE_BASE`
|
||||
//!
|
||||
//! As an extra defensive measure, a `max_messages` hard
|
||||
//! limit is set to the number of messages in the DownwardMessageQueue. Messages
|
||||
//! that would increase the number of messages in the queue above this hard
|
||||
//! limit are dropped.
|
||||
|
||||
use crate::{
|
||||
configuration::{self, HostConfiguration},
|
||||
initializer, paras, FeeTracker, GetMinFeeFactor,
|
||||
};
|
||||
use alloc::vec::Vec;
|
||||
use core::fmt;
|
||||
use frame_support::pallet_prelude::*;
|
||||
use frame_system::pallet_prelude::BlockNumberFor;
|
||||
use pezkuwi_primitives::{DownwardMessage, Hash, Id as ParaId, InboundDownwardMessage};
|
||||
use sp_core::MAX_POSSIBLE_ALLOCATION;
|
||||
use sp_runtime::{
|
||||
traits::{BlakeTwo256, Hash as HashT, SaturatedConversion},
|
||||
FixedU128,
|
||||
};
|
||||
use xcm::latest::SendError;
|
||||
|
||||
pub use pallet::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
const THRESHOLD_FACTOR: u32 = 2;
|
||||
|
||||
/// An error sending a downward message.
|
||||
#[derive(Debug)]
|
||||
pub enum QueueDownwardMessageError {
|
||||
/// The message being sent exceeds the configured max message size.
|
||||
ExceedsMaxMessageSize,
|
||||
/// The destination is unknown.
|
||||
Unroutable,
|
||||
}
|
||||
|
||||
impl From<QueueDownwardMessageError> for SendError {
|
||||
fn from(err: QueueDownwardMessageError) -> Self {
|
||||
match err {
|
||||
QueueDownwardMessageError::ExceedsMaxMessageSize => SendError::ExceedsMaxMessageSize,
|
||||
QueueDownwardMessageError::Unroutable => SendError::Unroutable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An error returned by [`Pallet::check_processed_downward_messages`] that indicates an acceptance
|
||||
/// check didn't pass.
|
||||
pub(crate) enum ProcessedDownwardMessagesAcceptanceErr {
|
||||
/// If there are pending messages then `processed_downward_messages` should be at least 1,
|
||||
AdvancementRule,
|
||||
/// `processed_downward_messages` should not be greater than the number of pending messages.
|
||||
Underflow { processed_downward_messages: u32, dmq_length: u32 },
|
||||
}
|
||||
|
||||
impl fmt::Debug for ProcessedDownwardMessagesAcceptanceErr {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
use ProcessedDownwardMessagesAcceptanceErr::*;
|
||||
match *self {
|
||||
AdvancementRule => {
|
||||
write!(fmt, "DMQ is not empty, but processed_downward_messages is 0",)
|
||||
},
|
||||
Underflow { processed_downward_messages, dmq_length } => write!(
|
||||
fmt,
|
||||
"processed_downward_messages = {}, but dmq_length is only {}",
|
||||
processed_downward_messages, dmq_length,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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 + paras::Config {}
|
||||
|
||||
/// The downward messages addressed for a certain para.
|
||||
#[pallet::storage]
|
||||
pub type DownwardMessageQueues<T: Config> = StorageMap<
|
||||
_,
|
||||
Twox64Concat,
|
||||
ParaId,
|
||||
Vec<InboundDownwardMessage<BlockNumberFor<T>>>,
|
||||
ValueQuery,
|
||||
>;
|
||||
|
||||
/// A mapping that stores the downward message queue MQC head for each para.
|
||||
///
|
||||
/// Each link in this chain has a form:
|
||||
/// `(prev_head, B, H(M))`, where
|
||||
/// - `prev_head`: is the previous head hash or zero if none.
|
||||
/// - `B`: is the relay-chain block number in which a message was appended.
|
||||
/// - `H(M)`: is the hash of the message being appended.
|
||||
#[pallet::storage]
|
||||
pub(crate) type DownwardMessageQueueHeads<T: Config> =
|
||||
StorageMap<_, Twox64Concat, ParaId, Hash, ValueQuery>;
|
||||
|
||||
/// The factor to multiply the base delivery fee by.
|
||||
#[pallet::storage]
|
||||
pub(crate) type DeliveryFeeFactor<T: Config> =
|
||||
StorageMap<_, Twox64Concat, ParaId, FixedU128, ValueQuery, GetMinFeeFactor<Pallet<T>>>;
|
||||
}
|
||||
/// Routines and getters related to downward message passing.
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Block initialization logic, called by initializer.
|
||||
pub(crate) fn initializer_initialize(_now: BlockNumberFor<T>) -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
|
||||
/// Block finalization logic, called by initializer.
|
||||
pub(crate) fn initializer_finalize() {}
|
||||
|
||||
/// Called by the initializer to note that a new session has started.
|
||||
pub(crate) fn initializer_on_new_session(
|
||||
_notification: &initializer::SessionChangeNotification<BlockNumberFor<T>>,
|
||||
outgoing_paras: &[ParaId],
|
||||
) {
|
||||
Self::perform_outgoing_para_cleanup(outgoing_paras);
|
||||
}
|
||||
|
||||
/// Iterate over all paras that were noted for offboarding and remove all the data
|
||||
/// associated with them.
|
||||
fn perform_outgoing_para_cleanup(outgoing: &[ParaId]) {
|
||||
for outgoing_para in outgoing {
|
||||
Self::clean_dmp_after_outgoing(outgoing_para);
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove all relevant storage items for an outgoing teyrchain.
|
||||
fn clean_dmp_after_outgoing(outgoing_para: &ParaId) {
|
||||
DownwardMessageQueues::<T>::remove(outgoing_para);
|
||||
DownwardMessageQueueHeads::<T>::remove(outgoing_para);
|
||||
}
|
||||
|
||||
/// Determine whether enqueuing a downward message to a specific recipient para would result
|
||||
/// in an error. If this returns `Ok(())` the caller can be certain that a call to
|
||||
/// `queue_downward_message` with the same parameters will be successful.
|
||||
pub fn can_queue_downward_message(
|
||||
config: &HostConfiguration<BlockNumberFor<T>>,
|
||||
para: &ParaId,
|
||||
msg: &DownwardMessage,
|
||||
) -> Result<(), QueueDownwardMessageError> {
|
||||
let serialized_len = msg.len() as u32;
|
||||
if serialized_len > config.max_downward_message_size {
|
||||
return Err(QueueDownwardMessageError::ExceedsMaxMessageSize);
|
||||
}
|
||||
|
||||
// Hard limit on Queue size
|
||||
if Self::dmq_length(*para) > Self::dmq_max_length(config.max_downward_message_size) {
|
||||
return Err(QueueDownwardMessageError::ExceedsMaxMessageSize);
|
||||
}
|
||||
|
||||
// If the head exists, we assume the teyrchain is legit and exists.
|
||||
if !paras::Heads::<T>::contains_key(para) {
|
||||
return Err(QueueDownwardMessageError::Unroutable);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Enqueue a downward message to a specific recipient para.
|
||||
///
|
||||
/// When encoded, the message should not exceed the `config.max_downward_message_size`.
|
||||
/// Otherwise, the message won't be sent and `Err` will be returned.
|
||||
///
|
||||
/// It is possible to send a downward message to a non-existent para. That, however, would lead
|
||||
/// to a dangling storage. If the caller cannot statically prove that the recipient exists
|
||||
/// then the caller should perform a runtime check.
|
||||
pub fn queue_downward_message(
|
||||
config: &HostConfiguration<BlockNumberFor<T>>,
|
||||
para: ParaId,
|
||||
msg: DownwardMessage,
|
||||
) -> Result<(), QueueDownwardMessageError> {
|
||||
let serialized_len = msg.len();
|
||||
Self::can_queue_downward_message(config, ¶, &msg)?;
|
||||
|
||||
let inbound =
|
||||
InboundDownwardMessage { msg, sent_at: frame_system::Pallet::<T>::block_number() };
|
||||
|
||||
// obtain the new link in the MQC and update the head.
|
||||
DownwardMessageQueueHeads::<T>::mutate(para, |head| {
|
||||
let new_head =
|
||||
BlakeTwo256::hash_of(&(*head, inbound.sent_at, T::Hashing::hash_of(&inbound.msg)));
|
||||
*head = new_head;
|
||||
});
|
||||
|
||||
let q_len = DownwardMessageQueues::<T>::mutate(para, |v| {
|
||||
v.push(inbound);
|
||||
v.len()
|
||||
});
|
||||
|
||||
let threshold =
|
||||
Self::dmq_max_length(config.max_downward_message_size).saturating_div(THRESHOLD_FACTOR);
|
||||
if q_len > (threshold as usize) {
|
||||
Self::increase_fee_factor(para, serialized_len as u128);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks if the number of processed downward messages is valid.
|
||||
pub(crate) fn check_processed_downward_messages(
|
||||
para: ParaId,
|
||||
relay_parent_number: BlockNumberFor<T>,
|
||||
processed_downward_messages: u32,
|
||||
) -> Result<(), ProcessedDownwardMessagesAcceptanceErr> {
|
||||
let dmq_length = Self::dmq_length(para);
|
||||
|
||||
if dmq_length > 0 && processed_downward_messages == 0 {
|
||||
// The advancement rule is for at least one downwards message to be processed
|
||||
// if the queue is non-empty at the relay-parent. Downwards messages are annotated
|
||||
// with the block number, so we compare the earliest (first) against the relay parent.
|
||||
let contents = Self::dmq_contents(para);
|
||||
|
||||
// sanity: if dmq_length is >0 this should always be 'Some'.
|
||||
if contents.get(0).map_or(false, |msg| msg.sent_at <= relay_parent_number) {
|
||||
return Err(ProcessedDownwardMessagesAcceptanceErr::AdvancementRule);
|
||||
}
|
||||
}
|
||||
|
||||
// Note that we might be allowing a teyrchain to signal that it's processed
|
||||
// messages that hadn't been placed in the queue at the relay_parent.
|
||||
// only 'stupid' teyrchains would do it and we don't (and can't) force anyone
|
||||
// to act on messages, so the lenient approach is fine here.
|
||||
if dmq_length < processed_downward_messages {
|
||||
return Err(ProcessedDownwardMessagesAcceptanceErr::Underflow {
|
||||
processed_downward_messages,
|
||||
dmq_length,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prunes the specified number of messages from the downward message queue of the given para.
|
||||
pub(crate) fn prune_dmq(para: ParaId, processed_downward_messages: u32) {
|
||||
let q_len = DownwardMessageQueues::<T>::mutate(para, |q| {
|
||||
let processed_downward_messages = processed_downward_messages as usize;
|
||||
if processed_downward_messages > q.len() {
|
||||
// reaching this branch is unexpected due to the constraint established by
|
||||
// `check_processed_downward_messages`. But better be safe than sorry.
|
||||
q.clear();
|
||||
} else {
|
||||
*q = q.split_off(processed_downward_messages);
|
||||
}
|
||||
q.len()
|
||||
});
|
||||
|
||||
let config = configuration::ActiveConfig::<T>::get();
|
||||
let threshold =
|
||||
Self::dmq_max_length(config.max_downward_message_size).saturating_div(THRESHOLD_FACTOR);
|
||||
if q_len <= (threshold as usize) {
|
||||
Self::decrease_fee_factor(para);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the Head of Message Queue Chain for the given para or `None` if there is none
|
||||
/// associated with it.
|
||||
#[cfg(test)]
|
||||
fn dmq_mqc_head(para: ParaId) -> Hash {
|
||||
DownwardMessageQueueHeads::<T>::get(¶)
|
||||
}
|
||||
|
||||
/// Returns the number of pending downward messages addressed to the given para.
|
||||
///
|
||||
/// Returns 0 if the para doesn't have an associated downward message queue.
|
||||
pub(crate) fn dmq_length(para: ParaId) -> u32 {
|
||||
DownwardMessageQueues::<T>::decode_len(¶)
|
||||
.unwrap_or(0)
|
||||
.saturated_into::<u32>()
|
||||
}
|
||||
|
||||
fn dmq_max_length(max_downward_message_size: u32) -> u32 {
|
||||
MAX_POSSIBLE_ALLOCATION.checked_div(max_downward_message_size).unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Returns the downward message queue contents for the given para.
|
||||
///
|
||||
/// The most recent messages are the latest in the vector.
|
||||
pub(crate) fn dmq_contents(
|
||||
recipient: ParaId,
|
||||
) -> Vec<InboundDownwardMessage<BlockNumberFor<T>>> {
|
||||
DownwardMessageQueues::<T>::get(&recipient)
|
||||
}
|
||||
|
||||
/// Make the teyrchain reachable for downward messages.
|
||||
///
|
||||
/// Only useable in benchmarks or tests.
|
||||
#[cfg(any(feature = "runtime-benchmarks", feature = "std"))]
|
||||
pub fn make_teyrchain_reachable(para: impl Into<ParaId>) {
|
||||
let para = para.into();
|
||||
crate::paras::Heads::<T>::insert(para, para.encode());
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> FeeTracker for Pallet<T> {
|
||||
type Id = ParaId;
|
||||
|
||||
fn get_fee_factor(id: Self::Id) -> FixedU128 {
|
||||
DeliveryFeeFactor::<T>::get(id)
|
||||
}
|
||||
|
||||
fn set_fee_factor(id: Self::Id, val: FixedU128) {
|
||||
<DeliveryFeeFactor<T>>::set(id, val);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
impl<T: Config> crate::EnsureForTeyrchain for Pallet<T> {
|
||||
fn ensure(para: ParaId) {
|
||||
Self::make_teyrchain_reachable(para);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
// 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::{
|
||||
configuration::ActiveConfig,
|
||||
mock::{new_test_ext, Dmp, MockGenesisConfig, Paras, System, Test},
|
||||
};
|
||||
use codec::Encode;
|
||||
use frame_support::assert_ok;
|
||||
use hex_literal::hex;
|
||||
use pezkuwi_primitives::BlockNumber;
|
||||
use sp_arithmetic::traits::Saturating;
|
||||
|
||||
pub(crate) fn run_to_block(to: BlockNumber, new_session: Option<Vec<BlockNumber>>) {
|
||||
while System::block_number() < to {
|
||||
let b = System::block_number();
|
||||
Paras::initializer_finalize(b);
|
||||
Dmp::initializer_finalize();
|
||||
if new_session.as_ref().map_or(false, |v| v.contains(&(b + 1))) {
|
||||
Dmp::initializer_on_new_session(&Default::default(), &Vec::new());
|
||||
}
|
||||
System::on_finalize(b);
|
||||
|
||||
System::on_initialize(b + 1);
|
||||
System::set_block_number(b + 1);
|
||||
|
||||
Paras::initializer_finalize(b + 1);
|
||||
Dmp::initializer_initialize(b + 1);
|
||||
}
|
||||
}
|
||||
|
||||
fn default_genesis_config() -> MockGenesisConfig {
|
||||
MockGenesisConfig {
|
||||
configuration: crate::configuration::GenesisConfig {
|
||||
config: crate::configuration::HostConfiguration {
|
||||
max_downward_message_size: 1024,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn queue_downward_message(
|
||||
para_id: ParaId,
|
||||
msg: DownwardMessage,
|
||||
) -> Result<(), QueueDownwardMessageError> {
|
||||
Dmp::queue_downward_message(&configuration::ActiveConfig::<Test>::get(), para_id, msg)
|
||||
}
|
||||
|
||||
fn register_paras(paras: &[ParaId]) {
|
||||
paras.iter().for_each(|p| {
|
||||
Dmp::make_teyrchain_reachable(*p);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clean_dmp_works() {
|
||||
let a = ParaId::from(1312);
|
||||
let b = ParaId::from(228);
|
||||
let c = ParaId::from(123);
|
||||
|
||||
new_test_ext(default_genesis_config()).execute_with(|| {
|
||||
register_paras(&[a, b, c]);
|
||||
|
||||
// enqueue downward messages to A, B and C.
|
||||
queue_downward_message(a, vec![1, 2, 3]).unwrap();
|
||||
queue_downward_message(b, vec![4, 5, 6]).unwrap();
|
||||
queue_downward_message(c, vec![7, 8, 9]).unwrap();
|
||||
|
||||
let notification = crate::initializer::SessionChangeNotification::default();
|
||||
let outgoing_paras = vec![a, b];
|
||||
Dmp::initializer_on_new_session(¬ification, &outgoing_paras);
|
||||
|
||||
assert!(DownwardMessageQueues::<Test>::get(&a).is_empty());
|
||||
assert!(DownwardMessageQueues::<Test>::get(&b).is_empty());
|
||||
assert!(!DownwardMessageQueues::<Test>::get(&c).is_empty());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dmq_length_and_head_updated_properly() {
|
||||
let a = ParaId::from(1312);
|
||||
let b = ParaId::from(228);
|
||||
|
||||
new_test_ext(default_genesis_config()).execute_with(|| {
|
||||
register_paras(&[a, b]);
|
||||
|
||||
assert_eq!(Dmp::dmq_length(a), 0);
|
||||
assert_eq!(Dmp::dmq_length(b), 0);
|
||||
|
||||
queue_downward_message(a, vec![1, 2, 3]).unwrap();
|
||||
|
||||
assert_eq!(Dmp::dmq_length(a), 1);
|
||||
assert_eq!(Dmp::dmq_length(b), 0);
|
||||
assert!(!Dmp::dmq_mqc_head(a).is_zero());
|
||||
assert!(Dmp::dmq_mqc_head(b).is_zero());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dmq_fail_if_para_does_not_exist() {
|
||||
let a = ParaId::from(1312);
|
||||
|
||||
new_test_ext(default_genesis_config()).execute_with(|| {
|
||||
assert_eq!(Dmp::dmq_length(a), 0);
|
||||
|
||||
assert!(matches!(
|
||||
queue_downward_message(a, vec![1, 2, 3]),
|
||||
Err(QueueDownwardMessageError::Unroutable)
|
||||
));
|
||||
|
||||
assert_eq!(Dmp::dmq_length(a), 0);
|
||||
assert!(Dmp::dmq_mqc_head(a).is_zero());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dmp_mqc_head_fixture() {
|
||||
let a = ParaId::from(2000);
|
||||
|
||||
new_test_ext(default_genesis_config()).execute_with(|| {
|
||||
register_paras(&[a]);
|
||||
|
||||
run_to_block(2, None);
|
||||
assert!(Dmp::dmq_mqc_head(a).is_zero());
|
||||
queue_downward_message(a, vec![1, 2, 3]).unwrap();
|
||||
|
||||
run_to_block(3, None);
|
||||
queue_downward_message(a, vec![4, 5, 6]).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Dmp::dmq_mqc_head(a),
|
||||
hex!["88dc00db8cc9d22aa62b87807705831f164387dfa49f80a8600ed1cbe1704b6b"].into(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_processed_downward_messages() {
|
||||
let a = ParaId::from(1312);
|
||||
|
||||
new_test_ext(default_genesis_config()).execute_with(|| {
|
||||
register_paras(&[a]);
|
||||
|
||||
let block_number = System::block_number();
|
||||
|
||||
// processed_downward_messages=0 is allowed when the DMQ is empty.
|
||||
assert!(Dmp::check_processed_downward_messages(a, block_number, 0).is_ok());
|
||||
|
||||
queue_downward_message(a, vec![1, 2, 3]).unwrap();
|
||||
queue_downward_message(a, vec![4, 5, 6]).unwrap();
|
||||
queue_downward_message(a, vec![7, 8, 9]).unwrap();
|
||||
|
||||
// 0 doesn't pass if the DMQ has msgs.
|
||||
assert!(Dmp::check_processed_downward_messages(a, block_number, 0).is_err());
|
||||
// a candidate can consume up to 3 messages
|
||||
assert!(Dmp::check_processed_downward_messages(a, block_number, 1).is_ok());
|
||||
assert!(Dmp::check_processed_downward_messages(a, block_number, 2).is_ok());
|
||||
assert!(Dmp::check_processed_downward_messages(a, block_number, 3).is_ok());
|
||||
// there is no 4 messages in the queue
|
||||
assert!(Dmp::check_processed_downward_messages(a, block_number, 4).is_err());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_processed_downward_messages_advancement_rule() {
|
||||
let a = ParaId::from(1312);
|
||||
|
||||
new_test_ext(default_genesis_config()).execute_with(|| {
|
||||
register_paras(&[a]);
|
||||
|
||||
let block_number = System::block_number();
|
||||
|
||||
run_to_block(block_number + 1, None);
|
||||
let advanced_block_number = System::block_number();
|
||||
|
||||
queue_downward_message(a, vec![1, 2, 3]).unwrap();
|
||||
queue_downward_message(a, vec![4, 5, 6]).unwrap();
|
||||
|
||||
// The queue was empty at genesis, 0 is OK despite it being non-empty in the further block.
|
||||
assert!(Dmp::check_processed_downward_messages(a, block_number, 0).is_ok());
|
||||
// For the advanced block number, however, the rule is broken in case of 0.
|
||||
assert!(Dmp::check_processed_downward_messages(a, advanced_block_number, 0).is_err());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dmq_pruning() {
|
||||
let a = ParaId::from(1312);
|
||||
|
||||
new_test_ext(default_genesis_config()).execute_with(|| {
|
||||
register_paras(&[a]);
|
||||
|
||||
assert_eq!(Dmp::dmq_length(a), 0);
|
||||
|
||||
queue_downward_message(a, vec![1, 2, 3]).unwrap();
|
||||
queue_downward_message(a, vec![4, 5, 6]).unwrap();
|
||||
queue_downward_message(a, vec![7, 8, 9]).unwrap();
|
||||
assert_eq!(Dmp::dmq_length(a), 3);
|
||||
|
||||
// pruning 0 elements shouldn't change anything.
|
||||
Dmp::prune_dmq(a, 0);
|
||||
assert_eq!(Dmp::dmq_length(a), 3);
|
||||
|
||||
Dmp::prune_dmq(a, 2);
|
||||
assert_eq!(Dmp::dmq_length(a), 1);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queue_downward_message_critical() {
|
||||
let a = ParaId::from(1312);
|
||||
|
||||
let mut genesis = default_genesis_config();
|
||||
genesis.configuration.config.max_downward_message_size = 7;
|
||||
|
||||
new_test_ext(genesis).execute_with(|| {
|
||||
register_paras(&[a]);
|
||||
|
||||
let smol = [0; 3].to_vec();
|
||||
let big = [0; 8].to_vec();
|
||||
|
||||
// still within limits
|
||||
assert_eq!(smol.encode().len(), 4);
|
||||
assert!(queue_downward_message(a, smol).is_ok());
|
||||
|
||||
// that's too big
|
||||
assert_eq!(big.encode().len(), 9);
|
||||
assert!(queue_downward_message(a, big).is_err());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_dmq_mqc_head_is_externally_accessible() {
|
||||
use hex_literal::hex;
|
||||
use pezkuwi_primitives::well_known_keys;
|
||||
|
||||
let a = ParaId::from(2020);
|
||||
|
||||
new_test_ext(default_genesis_config()).execute_with(|| {
|
||||
register_paras(&[a]);
|
||||
|
||||
let head = sp_io::storage::get(&well_known_keys::dmq_mqc_head(a));
|
||||
assert_eq!(head, None);
|
||||
|
||||
queue_downward_message(a, vec![1, 2, 3]).unwrap();
|
||||
|
||||
let head = sp_io::storage::get(&well_known_keys::dmq_mqc_head(a));
|
||||
assert_eq!(
|
||||
head,
|
||||
Some(
|
||||
hex!["434f8579a2297dfea851bf6be33093c83a78b655a53ae141a7894494c0010589"]
|
||||
.to_vec()
|
||||
.into()
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_fee_increase_and_decrease() {
|
||||
let a = ParaId::from(123);
|
||||
|
||||
let mut genesis = default_genesis_config();
|
||||
genesis.configuration.config.max_downward_message_size = 16777216;
|
||||
new_test_ext(genesis).execute_with(|| {
|
||||
register_paras(&[a]);
|
||||
|
||||
let initial = Pallet::<Test>::MIN_FEE_FACTOR;
|
||||
assert_eq!(DeliveryFeeFactor::<Test>::get(a), initial);
|
||||
|
||||
// Under fee limit
|
||||
queue_downward_message(a, vec![1]).unwrap();
|
||||
assert_eq!(DeliveryFeeFactor::<Test>::get(a), initial);
|
||||
|
||||
// Limit reached so fee is increased
|
||||
queue_downward_message(a, vec![1]).unwrap();
|
||||
let result = Pallet::<Test>::MIN_FEE_FACTOR.saturating_mul(Dmp::EXPONENTIAL_FEE_BASE);
|
||||
assert_eq!(DeliveryFeeFactor::<Test>::get(a), result);
|
||||
|
||||
Dmp::prune_dmq(a, 1);
|
||||
assert_eq!(DeliveryFeeFactor::<Test>::get(a), initial);
|
||||
|
||||
// 10 Kb message adds additional 0.001 per KB fee factor
|
||||
let big_message = [0; 10240].to_vec();
|
||||
let msg_len_in_kb = big_message.len().saturating_div(1024) as u32;
|
||||
let result = initial.saturating_mul(
|
||||
Dmp::EXPONENTIAL_FEE_BASE +
|
||||
Dmp::MESSAGE_SIZE_FEE_BASE.saturating_mul(FixedU128::from_u32(msg_len_in_kb)),
|
||||
);
|
||||
queue_downward_message(a, big_message).unwrap();
|
||||
assert_eq!(DeliveryFeeFactor::<Test>::get(a), result);
|
||||
|
||||
queue_downward_message(a, vec![1]).unwrap();
|
||||
let result = result.saturating_mul(Dmp::EXPONENTIAL_FEE_BASE);
|
||||
assert_eq!(DeliveryFeeFactor::<Test>::get(a), result);
|
||||
|
||||
Dmp::prune_dmq(a, 3);
|
||||
let result = result / Dmp::EXPONENTIAL_FEE_BASE;
|
||||
assert_eq!(DeliveryFeeFactor::<Test>::get(a), result);
|
||||
assert_eq!(Dmp::dmq_length(a), 0);
|
||||
|
||||
// Messages under limit will keep decreasing fee factor until base fee factor is reached
|
||||
queue_downward_message(a, vec![1]).unwrap();
|
||||
Dmp::prune_dmq(a, 1);
|
||||
queue_downward_message(a, vec![1]).unwrap();
|
||||
Dmp::prune_dmq(a, 1);
|
||||
assert_eq!(DeliveryFeeFactor::<Test>::get(a), initial);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_fee_factor_reaches_high_value() {
|
||||
let a = ParaId::from(123);
|
||||
let mut genesis = default_genesis_config();
|
||||
genesis.configuration.config.max_downward_message_size = 51200;
|
||||
new_test_ext(genesis).execute_with(|| {
|
||||
register_paras(&[a]);
|
||||
|
||||
let max_messages =
|
||||
Dmp::dmq_max_length(ActiveConfig::<Test>::get().max_downward_message_size);
|
||||
let mut total_fee_factor = FixedU128::from_float(1.0);
|
||||
for _ in 1..max_messages {
|
||||
assert_ok!(queue_downward_message(a, vec![]));
|
||||
total_fee_factor = total_fee_factor + (DeliveryFeeFactor::<Test>::get(a));
|
||||
}
|
||||
assert!(total_fee_factor > FixedU128::from_u32(100_000_000));
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,572 @@
|
||||
// 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/>.
|
||||
|
||||
#![cfg(feature = "runtime-benchmarks")]
|
||||
|
||||
use crate::{
|
||||
configuration::Pallet as Configuration,
|
||||
hrmp::{Pallet as Hrmp, *},
|
||||
paras::{Pallet as Paras, ParaKind, TeyrchainsCache},
|
||||
shared::Pallet as Shared,
|
||||
};
|
||||
use frame_benchmarking::{v2::*, whitelisted_caller};
|
||||
use frame_support::{assert_ok, traits::Currency};
|
||||
|
||||
type BalanceOf<T> =
|
||||
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
|
||||
|
||||
fn register_teyrchain_with_balance<T: Config>(id: ParaId, balance: BalanceOf<T>) {
|
||||
let mut teyrchains = TeyrchainsCache::new();
|
||||
Paras::<T>::initialize_para_now(
|
||||
&mut teyrchains,
|
||||
id,
|
||||
&crate::paras::ParaGenesisArgs {
|
||||
para_kind: ParaKind::Teyrchain,
|
||||
genesis_head: vec![1].into(),
|
||||
validation_code: vec![1].into(),
|
||||
},
|
||||
);
|
||||
T::Currency::make_free_balance_be(&id.into_account_truncating(), balance);
|
||||
}
|
||||
|
||||
fn assert_last_event<T: Config>(generic_event: <T as Config>::RuntimeEvent) {
|
||||
let events = frame_system::Pallet::<T>::events();
|
||||
let system_event: <T as frame_system::Config>::RuntimeEvent = generic_event.into();
|
||||
// compare to the last event record
|
||||
let frame_system::EventRecord { event, .. } = &events[events.len() - 1];
|
||||
assert_eq!(event, &system_event);
|
||||
}
|
||||
|
||||
fn assert_has_event<T: Config>(generic_event: <T as Config>::RuntimeEvent) {
|
||||
let events = frame_system::Pallet::<T>::events();
|
||||
let system_event: <T as frame_system::Config>::RuntimeEvent = generic_event.into();
|
||||
|
||||
assert!(events.iter().any(|record| record.event == system_event));
|
||||
}
|
||||
|
||||
/// Enumerates the phase in the setup process of a channel between two teyrchains.
|
||||
enum TeyrchainSetupStep {
|
||||
/// A channel open has been requested
|
||||
Requested,
|
||||
/// A channel open has been requested and accepted.
|
||||
Accepted,
|
||||
/// A channel open has been requested and accepted, and a session has passed and is now open.
|
||||
Established,
|
||||
/// A channel open has been requested and accepted, and a session has passed and is now
|
||||
/// open, and now it has been requested to close down.
|
||||
CloseRequested,
|
||||
}
|
||||
|
||||
fn establish_para_connection<T: Config>(
|
||||
from: u32,
|
||||
to: u32,
|
||||
until: TeyrchainSetupStep,
|
||||
) -> [(ParaId, crate::Origin); 2]
|
||||
where
|
||||
<T as frame_system::Config>::RuntimeOrigin: From<crate::Origin>,
|
||||
{
|
||||
let config = configuration::ActiveConfig::<T>::get();
|
||||
let ed = T::Currency::minimum_balance();
|
||||
let deposit: BalanceOf<T> = config.hrmp_sender_deposit.unique_saturated_into();
|
||||
let capacity = config.hrmp_channel_max_capacity;
|
||||
let message_size = config.hrmp_channel_max_message_size;
|
||||
assert!(message_size > 0, "Invalid genesis for benchmarking");
|
||||
assert!(capacity > 0, "Invalid genesis for benchmarking");
|
||||
|
||||
let sender: ParaId = from.into();
|
||||
let sender_origin: crate::Origin = from.into();
|
||||
|
||||
let recipient: ParaId = to.into();
|
||||
let recipient_origin: crate::Origin = to.into();
|
||||
|
||||
let output = [(sender, sender_origin.clone()), (recipient, recipient_origin.clone())];
|
||||
|
||||
// Make both a teyrchain if they are already not.
|
||||
if !Paras::<T>::is_teyrchain(sender) {
|
||||
register_teyrchain_with_balance::<T>(sender, deposit + ed);
|
||||
}
|
||||
if !Paras::<T>::is_teyrchain(recipient) {
|
||||
register_teyrchain_with_balance::<T>(recipient, deposit + ed);
|
||||
}
|
||||
|
||||
assert_ok!(Hrmp::<T>::hrmp_init_open_channel(
|
||||
sender_origin.clone().into(),
|
||||
recipient,
|
||||
capacity,
|
||||
message_size
|
||||
));
|
||||
|
||||
if matches!(until, TeyrchainSetupStep::Requested) {
|
||||
return output;
|
||||
}
|
||||
|
||||
assert_ok!(Hrmp::<T>::hrmp_accept_open_channel(recipient_origin.into(), sender));
|
||||
if matches!(until, TeyrchainSetupStep::Accepted) {
|
||||
return output;
|
||||
}
|
||||
|
||||
Hrmp::<T>::process_hrmp_open_channel_requests(&configuration::ActiveConfig::<T>::get());
|
||||
if matches!(until, TeyrchainSetupStep::Established) {
|
||||
return output;
|
||||
}
|
||||
|
||||
let channel_id = HrmpChannelId { sender, recipient };
|
||||
assert_ok!(Hrmp::<T>::hrmp_close_channel(sender_origin.clone().into(), channel_id));
|
||||
if matches!(until, TeyrchainSetupStep::CloseRequested) {
|
||||
// NOTE: this is just for expressiveness, otherwise the if-statement is 100% useless.
|
||||
return output;
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
/// Prefix value for account generation. These numbers are used as seeds to create distinct (para)
|
||||
/// accounts.
|
||||
///
|
||||
/// To maintain sensibility created accounts should always be unique and never overlap. For example,
|
||||
/// if for some benchmarking component `c`, accounts are being created `for s in 0..c` with seed
|
||||
/// `PREFIX_0 + s`, then we must assert that `c <= PREFIX_1`, meaning that it won't overlap with
|
||||
/// `PREFIX_2`.
|
||||
///
|
||||
/// These values are chosen large enough so that the likelihood of any clash is already very low.
|
||||
const PREFIX_0: u32 = 10_000;
|
||||
const PREFIX_1: u32 = PREFIX_0 * 2;
|
||||
const MAX_UNIQUE_CHANNELS: u32 = 128;
|
||||
|
||||
// Compile-time assertions for channel bounds
|
||||
const _: () = {
|
||||
assert!(MAX_UNIQUE_CHANNELS < PREFIX_0);
|
||||
assert!(HRMP_MAX_INBOUND_CHANNELS_BOUND < PREFIX_0);
|
||||
assert!(HRMP_MAX_OUTBOUND_CHANNELS_BOUND < PREFIX_0);
|
||||
};
|
||||
|
||||
#[benchmarks(where <T as frame_system::Config>::RuntimeOrigin: From<crate::Origin>)]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
#[benchmark]
|
||||
fn hrmp_init_open_channel() {
|
||||
let sender_id: ParaId = 1u32.into();
|
||||
let sender_origin: crate::Origin = 1u32.into();
|
||||
|
||||
let recipient_id: ParaId = 2u32.into();
|
||||
|
||||
// make sure para is registered, and has enough balance.
|
||||
let ed = T::Currency::minimum_balance();
|
||||
let deposit: BalanceOf<T> = configuration::ActiveConfig::<T>::get()
|
||||
.hrmp_sender_deposit
|
||||
.unique_saturated_into();
|
||||
register_teyrchain_with_balance::<T>(sender_id, deposit + ed);
|
||||
register_teyrchain_with_balance::<T>(recipient_id, deposit + ed);
|
||||
|
||||
let capacity = configuration::ActiveConfig::<T>::get().hrmp_channel_max_capacity;
|
||||
let message_size = configuration::ActiveConfig::<T>::get().hrmp_channel_max_message_size;
|
||||
|
||||
#[extrinsic_call]
|
||||
_(sender_origin, recipient_id, capacity, message_size);
|
||||
|
||||
assert_last_event::<T>(
|
||||
Event::<T>::OpenChannelRequested {
|
||||
sender: sender_id,
|
||||
recipient: recipient_id,
|
||||
proposed_max_capacity: capacity,
|
||||
proposed_max_message_size: message_size,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn hrmp_accept_open_channel() {
|
||||
let [(sender, _), (recipient, recipient_origin)] =
|
||||
establish_para_connection::<T>(1, 2, TeyrchainSetupStep::Requested);
|
||||
|
||||
#[extrinsic_call]
|
||||
_(recipient_origin, sender);
|
||||
|
||||
assert_last_event::<T>(Event::<T>::OpenChannelAccepted { sender, recipient }.into());
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn hrmp_close_channel() {
|
||||
let [(sender, sender_origin), (recipient, _)] =
|
||||
establish_para_connection::<T>(1, 2, TeyrchainSetupStep::Established);
|
||||
let channel_id = HrmpChannelId { sender, recipient };
|
||||
|
||||
#[extrinsic_call]
|
||||
_(sender_origin, channel_id.clone());
|
||||
|
||||
assert_last_event::<T>(
|
||||
Event::<T>::ChannelClosed { by_teyrchain: sender, channel_id }.into(),
|
||||
);
|
||||
}
|
||||
|
||||
// NOTE: a single teyrchain should have the maximum number of allowed ingress and egress
|
||||
// channels.
|
||||
#[benchmark]
|
||||
fn force_clean_hrmp(
|
||||
// ingress channels to a single leaving teyrchain that need to be closed.
|
||||
i: Linear<0, { HRMP_MAX_INBOUND_CHANNELS_BOUND - 1 }>,
|
||||
// egress channels to a single leaving teyrchain that need to be closed.
|
||||
e: Linear<0, { HRMP_MAX_OUTBOUND_CHANNELS_BOUND - 1 }>,
|
||||
) {
|
||||
// first, update the configs to support this many open channels...
|
||||
assert_ok!(Configuration::<T>::set_hrmp_max_teyrchain_outbound_channels(
|
||||
frame_system::RawOrigin::Root.into(),
|
||||
e + 1
|
||||
));
|
||||
assert_ok!(Configuration::<T>::set_hrmp_max_teyrchain_inbound_channels(
|
||||
frame_system::RawOrigin::Root.into(),
|
||||
i + 1
|
||||
));
|
||||
assert_ok!(Configuration::<T>::set_max_downward_message_size(
|
||||
frame_system::RawOrigin::Root.into(),
|
||||
1024
|
||||
));
|
||||
// .. and enact it.
|
||||
Configuration::<T>::initializer_on_new_session(&Shared::<T>::scheduled_session());
|
||||
|
||||
let config = configuration::ActiveConfig::<T>::get();
|
||||
let deposit: BalanceOf<T> = config.hrmp_sender_deposit.unique_saturated_into();
|
||||
|
||||
let para: ParaId = 1u32.into();
|
||||
register_teyrchain_with_balance::<T>(para, deposit);
|
||||
T::Currency::make_free_balance_be(¶.into_account_truncating(), deposit * 256u32.into());
|
||||
|
||||
for ingress_para_id in 0..i {
|
||||
// establish ingress channels to `para`.
|
||||
let ingress_para_id = ingress_para_id + PREFIX_0;
|
||||
let _ = establish_para_connection::<T>(
|
||||
ingress_para_id,
|
||||
para.into(),
|
||||
TeyrchainSetupStep::Established,
|
||||
);
|
||||
}
|
||||
|
||||
// nothing should be left unprocessed.
|
||||
assert_eq!(HrmpOpenChannelRequestsList::<T>::decode_len().unwrap_or_default(), 0);
|
||||
|
||||
for egress_para_id in 0..e {
|
||||
// establish egress channels to `para`.
|
||||
let egress_para_id = egress_para_id + PREFIX_1;
|
||||
let _ = establish_para_connection::<T>(
|
||||
para.into(),
|
||||
egress_para_id,
|
||||
TeyrchainSetupStep::Established,
|
||||
);
|
||||
}
|
||||
|
||||
// nothing should be left unprocessed.
|
||||
assert_eq!(HrmpOpenChannelRequestsList::<T>::decode_len().unwrap_or_default(), 0);
|
||||
|
||||
// all in all, we have created this many channels.
|
||||
assert_eq!(HrmpChannels::<T>::iter().count() as u32, i + e);
|
||||
|
||||
#[extrinsic_call]
|
||||
_(frame_system::Origin::<T>::Root, para, i, e);
|
||||
|
||||
// all in all, all of them must be gone by now.
|
||||
assert_eq!(HrmpChannels::<T>::iter().count() as u32, 0);
|
||||
// borrow this function from the tests to make sure state is clear, given that we do a lot
|
||||
// of out-of-ordinary ops here.
|
||||
Hrmp::<T>::assert_storage_consistency_exhaustive();
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn force_process_hrmp_open(
|
||||
// number of channels that need to be processed. Worse case is an N-M relation: unique
|
||||
// sender and recipients for all channels.
|
||||
c: Linear<0, MAX_UNIQUE_CHANNELS>,
|
||||
) {
|
||||
for id in 0..c {
|
||||
let _ = establish_para_connection::<T>(
|
||||
PREFIX_0 + id,
|
||||
PREFIX_1 + id,
|
||||
TeyrchainSetupStep::Accepted,
|
||||
);
|
||||
}
|
||||
assert_eq!(HrmpOpenChannelRequestsList::<T>::decode_len().unwrap_or_default() as u32, c);
|
||||
|
||||
#[extrinsic_call]
|
||||
_(frame_system::Origin::<T>::Root, c);
|
||||
|
||||
assert_eq!(HrmpOpenChannelRequestsList::<T>::decode_len().unwrap_or_default() as u32, 0);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn force_process_hrmp_close(
|
||||
// number of channels that need to be processed. Worse case is an N-M relation: unique
|
||||
// sender and recipients for all channels.
|
||||
c: Linear<0, MAX_UNIQUE_CHANNELS>,
|
||||
) {
|
||||
for id in 0..c {
|
||||
let _ = establish_para_connection::<T>(
|
||||
PREFIX_0 + id,
|
||||
PREFIX_1 + id,
|
||||
TeyrchainSetupStep::CloseRequested,
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(HrmpCloseChannelRequestsList::<T>::decode_len().unwrap_or_default() as u32, c);
|
||||
|
||||
#[extrinsic_call]
|
||||
_(frame_system::Origin::<T>::Root, c);
|
||||
|
||||
assert_eq!(HrmpCloseChannelRequestsList::<T>::decode_len().unwrap_or_default() as u32, 0);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn hrmp_cancel_open_request(
|
||||
// number of items already existing in the `HrmpOpenChannelRequestsList`, other than the
|
||||
// one that we remove.
|
||||
c: Linear<0, MAX_UNIQUE_CHANNELS>,
|
||||
) {
|
||||
for id in 0..c {
|
||||
let _ = establish_para_connection::<T>(
|
||||
PREFIX_0 + id,
|
||||
PREFIX_1 + id,
|
||||
TeyrchainSetupStep::Requested,
|
||||
);
|
||||
}
|
||||
|
||||
let [(sender, sender_origin), (recipient, _)] =
|
||||
establish_para_connection::<T>(1, 2, TeyrchainSetupStep::Requested);
|
||||
assert_eq!(
|
||||
HrmpOpenChannelRequestsList::<T>::decode_len().unwrap_or_default() as u32,
|
||||
c + 1
|
||||
);
|
||||
let channel_id = HrmpChannelId { sender, recipient };
|
||||
|
||||
#[extrinsic_call]
|
||||
_(sender_origin, channel_id, c + 1);
|
||||
|
||||
assert_eq!(HrmpOpenChannelRequestsList::<T>::decode_len().unwrap_or_default() as u32, c);
|
||||
}
|
||||
|
||||
// worse case will be `n` teyrchain channel requests, where in all of them either the sender or
|
||||
// the recipient need to be cleaned. This enforces the deposit of at least one to be processed.
|
||||
// No code path for triggering two deposit process exists.
|
||||
#[benchmark]
|
||||
fn clean_open_channel_requests(c: Linear<0, MAX_UNIQUE_CHANNELS>) {
|
||||
for id in 0..c {
|
||||
let _ = establish_para_connection::<T>(
|
||||
PREFIX_0 + id,
|
||||
PREFIX_1 + id,
|
||||
TeyrchainSetupStep::Requested,
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(HrmpOpenChannelRequestsList::<T>::decode_len().unwrap_or_default() as u32, c);
|
||||
let outgoing = (0..c).map(|id| (id + PREFIX_1).into()).collect::<Vec<ParaId>>();
|
||||
let config = configuration::ActiveConfig::<T>::get();
|
||||
|
||||
#[block]
|
||||
{
|
||||
Hrmp::<T>::clean_open_channel_requests(&config, &outgoing);
|
||||
}
|
||||
|
||||
assert_eq!(HrmpOpenChannelRequestsList::<T>::decode_len().unwrap_or_default() as u32, 0);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn force_open_hrmp_channel(
|
||||
// Weight parameter only accepts `u32`, `0` and `1` used to represent `false` and `true`,
|
||||
// respectively.
|
||||
c: Linear<0, 1>,
|
||||
) {
|
||||
let sender_id: ParaId = 1u32.into();
|
||||
let sender_origin: crate::Origin = 1u32.into();
|
||||
let recipient_id: ParaId = 2u32.into();
|
||||
|
||||
// Make sure para is registered. The sender does actually need the normal deposit because it
|
||||
// is first going the "init" route.
|
||||
let ed = T::Currency::minimum_balance();
|
||||
let sender_deposit: BalanceOf<T> = configuration::ActiveConfig::<T>::get()
|
||||
.hrmp_sender_deposit
|
||||
.unique_saturated_into();
|
||||
register_teyrchain_with_balance::<T>(sender_id, sender_deposit + ed);
|
||||
register_teyrchain_with_balance::<T>(recipient_id, Zero::zero());
|
||||
|
||||
let capacity = configuration::ActiveConfig::<T>::get().hrmp_channel_max_capacity;
|
||||
let message_size = configuration::ActiveConfig::<T>::get().hrmp_channel_max_message_size;
|
||||
|
||||
let channel_id = HrmpChannelId { sender: sender_id, recipient: recipient_id };
|
||||
if c == 1 {
|
||||
// this will consume more weight if a channel _request_ already exists, because it
|
||||
// will need to clear the request.
|
||||
assert_ok!(Hrmp::<T>::hrmp_init_open_channel(
|
||||
sender_origin.clone().into(),
|
||||
recipient_id,
|
||||
capacity,
|
||||
message_size
|
||||
));
|
||||
assert!(HrmpOpenChannelRequests::<T>::get(&channel_id).is_some());
|
||||
} else {
|
||||
if HrmpOpenChannelRequests::<T>::get(&channel_id).is_some() {
|
||||
assert_ok!(Hrmp::<T>::hrmp_cancel_open_request(
|
||||
sender_origin.clone().into(),
|
||||
channel_id.clone(),
|
||||
MAX_UNIQUE_CHANNELS,
|
||||
));
|
||||
}
|
||||
assert!(HrmpOpenChannelRequests::<T>::get(&channel_id).is_none());
|
||||
}
|
||||
|
||||
// but the _channel_ should not exist
|
||||
assert!(HrmpChannels::<T>::get(&channel_id).is_none());
|
||||
|
||||
#[extrinsic_call]
|
||||
_(frame_system::Origin::<T>::Root, sender_id, recipient_id, capacity, message_size);
|
||||
|
||||
assert_last_event::<T>(
|
||||
Event::<T>::HrmpChannelForceOpened {
|
||||
sender: sender_id,
|
||||
recipient: recipient_id,
|
||||
proposed_max_capacity: capacity,
|
||||
proposed_max_message_size: message_size,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn establish_system_channel() {
|
||||
let sender_id: ParaId = 1u32.into();
|
||||
let recipient_id: ParaId = 2u32.into();
|
||||
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
let config = configuration::ActiveConfig::<T>::get();
|
||||
|
||||
// make sure para is registered, and has zero balance.
|
||||
register_teyrchain_with_balance::<T>(sender_id, Zero::zero());
|
||||
register_teyrchain_with_balance::<T>(recipient_id, Zero::zero());
|
||||
|
||||
let capacity = config.hrmp_channel_max_capacity;
|
||||
let message_size = config.hrmp_channel_max_message_size;
|
||||
|
||||
#[extrinsic_call]
|
||||
_(frame_system::RawOrigin::Signed(caller), sender_id, recipient_id);
|
||||
|
||||
assert_last_event::<T>(
|
||||
Event::<T>::HrmpSystemChannelOpened {
|
||||
sender: sender_id,
|
||||
recipient: recipient_id,
|
||||
proposed_max_capacity: capacity,
|
||||
proposed_max_message_size: message_size,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn poke_channel_deposits() {
|
||||
let sender_id: ParaId = 1u32.into();
|
||||
let recipient_id: ParaId = 2u32.into();
|
||||
let channel_id = HrmpChannelId { sender: sender_id, recipient: recipient_id };
|
||||
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
let config = configuration::ActiveConfig::<T>::get();
|
||||
|
||||
// make sure para is registered, and has balance to reserve.
|
||||
let ed = T::Currency::minimum_balance();
|
||||
let sender_deposit: BalanceOf<T> = config.hrmp_sender_deposit.unique_saturated_into();
|
||||
let recipient_deposit: BalanceOf<T> = config.hrmp_recipient_deposit.unique_saturated_into();
|
||||
register_teyrchain_with_balance::<T>(sender_id, ed + sender_deposit);
|
||||
register_teyrchain_with_balance::<T>(recipient_id, ed + recipient_deposit);
|
||||
|
||||
// Our normal establishment won't actually reserve deposits, so just insert them directly.
|
||||
HrmpChannels::<T>::insert(
|
||||
&channel_id,
|
||||
HrmpChannel {
|
||||
sender_deposit: config.hrmp_sender_deposit,
|
||||
recipient_deposit: config.hrmp_recipient_deposit,
|
||||
max_capacity: config.hrmp_channel_max_capacity,
|
||||
max_total_size: config.hrmp_channel_max_total_size,
|
||||
max_message_size: config.hrmp_channel_max_message_size,
|
||||
msg_count: 0,
|
||||
total_size: 0,
|
||||
mqc_head: None,
|
||||
},
|
||||
);
|
||||
// Actually reserve the deposits.
|
||||
let _ = T::Currency::reserve(&sender_id.into_account_truncating(), sender_deposit);
|
||||
let _ = T::Currency::reserve(&recipient_id.into_account_truncating(), recipient_deposit);
|
||||
|
||||
#[extrinsic_call]
|
||||
_(frame_system::RawOrigin::Signed(caller), sender_id, recipient_id);
|
||||
|
||||
assert_last_event::<T>(
|
||||
Event::<T>::OpenChannelDepositsUpdated { sender: sender_id, recipient: recipient_id }
|
||||
.into(),
|
||||
);
|
||||
let channel = HrmpChannels::<T>::get(&channel_id).unwrap();
|
||||
// Check that the deposit was updated in the channel state.
|
||||
assert_eq!(channel.sender_deposit, 0);
|
||||
assert_eq!(channel.recipient_deposit, 0);
|
||||
// And that the funds were unreserved.
|
||||
assert_eq!(
|
||||
T::Currency::reserved_balance(&sender_id.into_account_truncating()),
|
||||
0u128.unique_saturated_into()
|
||||
);
|
||||
assert_eq!(
|
||||
T::Currency::reserved_balance(&recipient_id.into_account_truncating()),
|
||||
0u128.unique_saturated_into()
|
||||
);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn establish_channel_with_system() {
|
||||
let sender_id = 1u32;
|
||||
let recipient_id: ParaId = 2u32.into();
|
||||
|
||||
let sender_origin: crate::Origin = sender_id.into();
|
||||
|
||||
// make sure para is registered, and has zero balance.
|
||||
register_teyrchain_with_balance::<T>(sender_id.into(), Zero::zero());
|
||||
register_teyrchain_with_balance::<T>(recipient_id, Zero::zero());
|
||||
|
||||
#[extrinsic_call]
|
||||
_(sender_origin, recipient_id);
|
||||
|
||||
let (max_message_size, max_capacity) = T::DefaultChannelSizeAndCapacityWithSystem::get();
|
||||
|
||||
assert_has_event::<T>(
|
||||
Event::<T>::HrmpSystemChannelOpened {
|
||||
sender: sender_id.into(),
|
||||
recipient: recipient_id,
|
||||
proposed_max_capacity: max_capacity,
|
||||
proposed_max_message_size: max_message_size,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
assert_has_event::<T>(
|
||||
Event::<T>::HrmpSystemChannelOpened {
|
||||
sender: recipient_id,
|
||||
recipient: sender_id.into(),
|
||||
proposed_max_capacity: max_capacity,
|
||||
proposed_max_message_size: max_message_size,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(
|
||||
Hrmp,
|
||||
crate::mock::new_test_ext(crate::hrmp::tests::GenesisConfigBuilder::default().build()),
|
||||
crate::mock::Test
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,156 @@
|
||||
// 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 bitvec::{bitvec, prelude::Lsb0};
|
||||
use frame_benchmarking::v2::*;
|
||||
use pallet_message_queue as mq;
|
||||
use pezkuwi_primitives::{
|
||||
CandidateCommitments, CommittedCandidateReceiptV2 as CommittedCandidateReceipt, HrmpChannelId,
|
||||
OutboundHrmpMessage, SessionIndex,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
builder::generate_validator_pairs,
|
||||
configuration,
|
||||
hrmp::{HrmpChannel, HrmpChannels},
|
||||
initializer, HeadData, ValidationCode,
|
||||
};
|
||||
|
||||
fn create_candidate_commitments<T: crate::hrmp::pallet::Config>(
|
||||
para_id: ParaId,
|
||||
head_data: HeadData,
|
||||
max_msg_len: usize,
|
||||
ump_msg_count: u32,
|
||||
hrmp_msg_count: u32,
|
||||
code_upgrade: bool,
|
||||
) -> CandidateCommitments {
|
||||
let upward_messages = {
|
||||
let unbounded = create_messages(max_msg_len, ump_msg_count as _);
|
||||
BoundedVec::truncate_from(unbounded)
|
||||
};
|
||||
|
||||
let horizontal_messages = {
|
||||
let unbounded = create_messages(max_msg_len, hrmp_msg_count as _);
|
||||
|
||||
for n in 0..unbounded.len() {
|
||||
let channel_id = HrmpChannelId { sender: para_id, recipient: para_id + n as u32 + 1 };
|
||||
HrmpChannels::<T>::insert(
|
||||
&channel_id,
|
||||
HrmpChannel {
|
||||
sender_deposit: 42,
|
||||
recipient_deposit: 42,
|
||||
max_capacity: 10_000_000,
|
||||
max_total_size: 1_000_000_000,
|
||||
max_message_size: 10_000_000,
|
||||
msg_count: 0,
|
||||
total_size: 0,
|
||||
mqc_head: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let unbounded = unbounded
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(n, data)| OutboundHrmpMessage { recipient: para_id + n as u32 + 1, data })
|
||||
.collect();
|
||||
BoundedVec::truncate_from(unbounded)
|
||||
};
|
||||
|
||||
let new_validation_code = code_upgrade.then_some(ValidationCode(vec![42_u8; 1024]));
|
||||
|
||||
CandidateCommitments::<u32> {
|
||||
upward_messages,
|
||||
horizontal_messages,
|
||||
new_validation_code,
|
||||
head_data,
|
||||
processed_downward_messages: 0,
|
||||
hrmp_watermark: 10,
|
||||
}
|
||||
}
|
||||
|
||||
fn create_messages(msg_len: usize, n_msgs: usize) -> Vec<Vec<u8>> {
|
||||
let best_number = 73_u8; // Chuck Norris of numbers
|
||||
vec![vec![best_number; msg_len]; n_msgs]
|
||||
}
|
||||
|
||||
#[benchmarks(where T: mq::Config + configuration::Config + initializer::Config)]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
#[benchmark]
|
||||
fn enact_candidate(u: Linear<0, 2>, h: Linear<0, 2>, c: Linear<0, 1>) {
|
||||
let para = 42_u32.into(); // not especially important.
|
||||
|
||||
let max_len = mq::MaxMessageLenOf::<T>::get() as usize;
|
||||
|
||||
let config = configuration::ActiveConfig::<T>::get();
|
||||
let n_validators = config.max_validators.unwrap_or(500);
|
||||
let validators = generate_validator_pairs::<T>(n_validators);
|
||||
|
||||
let session = SessionIndex::from(0_u32);
|
||||
initializer::Pallet::<T>::test_trigger_on_new_session(
|
||||
false,
|
||||
session,
|
||||
validators.iter().map(|(a, v)| (a, v.clone())),
|
||||
None,
|
||||
);
|
||||
let backing_group_size = config.scheduler_params.max_validators_per_core.unwrap_or(5);
|
||||
let head_data = HeadData(vec![0xFF; 1024]);
|
||||
|
||||
let relay_parent_number = BlockNumberFor::<T>::from(10_u32);
|
||||
let commitments = create_candidate_commitments::<T>(para, head_data, max_len, u, h, c != 0);
|
||||
let backers = bitvec![u8, Lsb0; 1; backing_group_size as usize];
|
||||
let availability_votes = bitvec![u8, Lsb0; 1; n_validators as usize];
|
||||
let core_index = CoreIndex::from(0);
|
||||
let backing_group = GroupIndex::from(0);
|
||||
|
||||
let descriptor = CandidateDescriptor::<T::Hash>::new(
|
||||
para,
|
||||
Default::default(),
|
||||
CoreIndex(0),
|
||||
1,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
ValidationCode(vec![1, 2, 3]).hash(),
|
||||
);
|
||||
|
||||
let receipt = CommittedCandidateReceipt::<T::Hash> { descriptor, commitments };
|
||||
|
||||
Pallet::<T>::receive_upward_messages(para, &vec![vec![0; max_len]; 1]);
|
||||
|
||||
#[block]
|
||||
{
|
||||
Pallet::<T>::enact_candidate(
|
||||
relay_parent_number,
|
||||
receipt,
|
||||
backers,
|
||||
availability_votes,
|
||||
core_index,
|
||||
backing_group,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite! {
|
||||
Pallet,
|
||||
crate::mock::new_test_ext(Default::default()),
|
||||
crate::mock::Test
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
// 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.
|
||||
|
||||
pub use v1::MigrateToV1;
|
||||
|
||||
pub mod v0 {
|
||||
use crate::inclusion::{Config, Pallet};
|
||||
use bitvec::{order::Lsb0 as BitOrderLsb0, vec::BitVec};
|
||||
use codec::{Decode, Encode};
|
||||
use frame_support::{storage_alias, Twox64Concat};
|
||||
use frame_system::pallet_prelude::BlockNumberFor;
|
||||
use pezkuwi_primitives::{
|
||||
AvailabilityBitfield, CandidateCommitments, CandidateDescriptorV2 as CandidateDescriptor,
|
||||
CandidateHash, CoreIndex, GroupIndex, Id as ParaId, ValidatorIndex,
|
||||
};
|
||||
use scale_info::TypeInfo;
|
||||
|
||||
#[derive(Encode, Decode, PartialEq, TypeInfo, Clone, Debug)]
|
||||
pub struct CandidatePendingAvailability<H, N> {
|
||||
pub core: CoreIndex,
|
||||
pub hash: CandidateHash,
|
||||
pub descriptor: CandidateDescriptor<H>,
|
||||
pub availability_votes: BitVec<u8, BitOrderLsb0>,
|
||||
pub backers: BitVec<u8, BitOrderLsb0>,
|
||||
pub relay_parent_number: N,
|
||||
pub backed_in_number: N,
|
||||
pub backing_group: GroupIndex,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, TypeInfo, Debug, PartialEq)]
|
||||
pub struct AvailabilityBitfieldRecord<N> {
|
||||
pub bitfield: AvailabilityBitfield,
|
||||
pub submitted_at: N,
|
||||
}
|
||||
|
||||
#[storage_alias]
|
||||
pub type PendingAvailability<T: Config> = StorageMap<
|
||||
Pallet<T>,
|
||||
Twox64Concat,
|
||||
ParaId,
|
||||
CandidatePendingAvailability<<T as frame_system::Config>::Hash, BlockNumberFor<T>>,
|
||||
>;
|
||||
|
||||
#[storage_alias]
|
||||
pub type PendingAvailabilityCommitments<T: Config> =
|
||||
StorageMap<Pallet<T>, Twox64Concat, ParaId, CandidateCommitments>;
|
||||
|
||||
#[storage_alias]
|
||||
pub type AvailabilityBitfields<T: Config> = StorageMap<
|
||||
Pallet<T>,
|
||||
Twox64Concat,
|
||||
ValidatorIndex,
|
||||
AvailabilityBitfieldRecord<BlockNumberFor<T>>,
|
||||
>;
|
||||
}
|
||||
|
||||
mod v1 {
|
||||
use super::v0::{
|
||||
AvailabilityBitfields, PendingAvailability as V0PendingAvailability,
|
||||
PendingAvailabilityCommitments as V0PendingAvailabilityCommitments,
|
||||
};
|
||||
use crate::inclusion::{
|
||||
CandidatePendingAvailability as V1CandidatePendingAvailability, Config, Pallet,
|
||||
PendingAvailability as V1PendingAvailability,
|
||||
};
|
||||
use alloc::{collections::vec_deque::VecDeque, vec::Vec};
|
||||
use frame_support::{traits::UncheckedOnRuntimeUpgrade, weights::Weight};
|
||||
use sp_core::Get;
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
use codec::{Decode, Encode};
|
||||
#[cfg(feature = "try-runtime")]
|
||||
use frame_support::{
|
||||
ensure,
|
||||
traits::{GetStorageVersion, StorageVersion},
|
||||
};
|
||||
|
||||
pub struct VersionUncheckedMigrateToV1<T>(core::marker::PhantomData<T>);
|
||||
|
||||
impl<T: Config> UncheckedOnRuntimeUpgrade for VersionUncheckedMigrateToV1<T> {
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::TryRuntimeError> {
|
||||
log::trace!(target: crate::inclusion::LOG_TARGET, "Running pre_upgrade() for inclusion MigrateToV1");
|
||||
let candidates_before_upgrade = V0PendingAvailability::<T>::iter().count();
|
||||
let commitments_before_upgrade = V0PendingAvailabilityCommitments::<T>::iter().count();
|
||||
|
||||
if candidates_before_upgrade != commitments_before_upgrade {
|
||||
log::warn!(
|
||||
target: crate::inclusion::LOG_TARGET,
|
||||
"Number of pending candidates differ from the number of pending commitments. {} vs {}",
|
||||
candidates_before_upgrade,
|
||||
commitments_before_upgrade
|
||||
);
|
||||
}
|
||||
|
||||
Ok((candidates_before_upgrade as u32).encode())
|
||||
}
|
||||
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
let mut weight: Weight = Weight::zero();
|
||||
|
||||
let v0_candidates: Vec<_> = V0PendingAvailability::<T>::drain().collect();
|
||||
|
||||
for (para_id, candidate) in v0_candidates {
|
||||
let commitments = V0PendingAvailabilityCommitments::<T>::take(para_id);
|
||||
// One write for each removal (one candidate and one commitment).
|
||||
weight = weight.saturating_add(T::DbWeight::get().writes(2));
|
||||
|
||||
if let Some(commitments) = commitments {
|
||||
let mut per_para = VecDeque::new();
|
||||
per_para.push_back(V1CandidatePendingAvailability {
|
||||
core: candidate.core,
|
||||
hash: candidate.hash,
|
||||
descriptor: candidate.descriptor,
|
||||
availability_votes: candidate.availability_votes,
|
||||
backers: candidate.backers,
|
||||
relay_parent_number: candidate.relay_parent_number,
|
||||
backed_in_number: candidate.backed_in_number,
|
||||
backing_group: candidate.backing_group,
|
||||
commitments,
|
||||
});
|
||||
V1PendingAvailability::<T>::insert(para_id, per_para);
|
||||
|
||||
weight = weight.saturating_add(T::DbWeight::get().writes(1));
|
||||
}
|
||||
}
|
||||
|
||||
// should've already been drained by the above for loop, but as a sanity check, in case
|
||||
// there are more commitments than candidates.
|
||||
// V0PendingAvailabilityCommitments should not contain too many keys so removing
|
||||
// everything at once should be safe
|
||||
let res = V0PendingAvailabilityCommitments::<T>::clear(u32::MAX, None);
|
||||
weight = weight.saturating_add(
|
||||
T::DbWeight::get().reads_writes(res.loops as u64, res.backend as u64),
|
||||
);
|
||||
|
||||
// AvailabilityBitfields should not contain too many keys so removing everything at once
|
||||
// should be safe.
|
||||
let res = AvailabilityBitfields::<T>::clear(u32::MAX, None);
|
||||
weight = weight.saturating_add(
|
||||
T::DbWeight::get().reads_writes(res.loops as u64, res.backend as u64),
|
||||
);
|
||||
|
||||
weight
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(state: Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
|
||||
log::trace!(target: crate::inclusion::LOG_TARGET, "Running post_upgrade() for inclusion MigrateToV1");
|
||||
ensure!(
|
||||
Pallet::<T>::on_chain_storage_version() >= StorageVersion::new(1),
|
||||
"Storage version should be >= 1 after the migration"
|
||||
);
|
||||
|
||||
let candidates_before_upgrade =
|
||||
u32::decode(&mut &state[..]).expect("Was properly encoded") as usize;
|
||||
let candidates_after_upgrade = V1PendingAvailability::<T>::iter().fold(
|
||||
0usize,
|
||||
|mut acc, (_paraid, para_candidates)| {
|
||||
acc += para_candidates.len();
|
||||
acc
|
||||
},
|
||||
);
|
||||
|
||||
ensure!(
|
||||
candidates_before_upgrade == candidates_after_upgrade,
|
||||
"Number of pending candidates should be the same as the one before the upgrade."
|
||||
);
|
||||
ensure!(
|
||||
V0PendingAvailability::<T>::iter().next() == None,
|
||||
"Pending availability candidates storage v0 should have been removed"
|
||||
);
|
||||
ensure!(
|
||||
V0PendingAvailabilityCommitments::<T>::iter().next() == None,
|
||||
"Pending availability commitments storage should have been removed"
|
||||
);
|
||||
ensure!(
|
||||
AvailabilityBitfields::<T>::iter().next() == None,
|
||||
"Availability bitfields storage should have been removed"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Migrate to v1 inclusion module storage.
|
||||
/// - merges the `PendingAvailabilityCommitments` into the `CandidatePendingAvailability`
|
||||
/// storage
|
||||
/// - removes the `AvailabilityBitfields` storage, which was never read.
|
||||
pub type MigrateToV1<T> = frame_support::migrations::VersionedMigration<
|
||||
0,
|
||||
1,
|
||||
VersionUncheckedMigrateToV1<T>,
|
||||
Pallet<T>,
|
||||
<T as frame_system::Config>::DbWeight,
|
||||
>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{v1::VersionUncheckedMigrateToV1, *};
|
||||
use crate::{
|
||||
inclusion::{
|
||||
CandidatePendingAvailability as V1CandidatePendingAvailability,
|
||||
PendingAvailability as V1PendingAvailability, *,
|
||||
},
|
||||
mock::{new_test_ext, MockGenesisConfig, Test},
|
||||
};
|
||||
use frame_support::traits::UncheckedOnRuntimeUpgrade;
|
||||
use pezkuwi_primitives::{AvailabilityBitfield, Id as ParaId};
|
||||
use pezkuwi_primitives_test_helpers::{
|
||||
dummy_candidate_commitments, dummy_candidate_descriptor_v2, dummy_hash,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn migrate_to_v1() {
|
||||
new_test_ext(MockGenesisConfig::default()).execute_with(|| {
|
||||
// No data to migrate.
|
||||
assert_eq!(
|
||||
<VersionUncheckedMigrateToV1<Test> as UncheckedOnRuntimeUpgrade>::on_runtime_upgrade(),
|
||||
Weight::zero()
|
||||
);
|
||||
assert!(V1PendingAvailability::<Test>::iter().next().is_none());
|
||||
|
||||
let mut expected = vec![];
|
||||
|
||||
for i in 1..5 {
|
||||
let descriptor = dummy_candidate_descriptor_v2(dummy_hash());
|
||||
v0::PendingAvailability::<Test>::insert(
|
||||
ParaId::from(i),
|
||||
v0::CandidatePendingAvailability {
|
||||
core: CoreIndex(i),
|
||||
descriptor: descriptor.clone(),
|
||||
relay_parent_number: i,
|
||||
hash: CandidateHash(dummy_hash()),
|
||||
availability_votes: Default::default(),
|
||||
backed_in_number: i,
|
||||
backers: Default::default(),
|
||||
backing_group: GroupIndex(i),
|
||||
},
|
||||
);
|
||||
v0::PendingAvailabilityCommitments::<Test>::insert(
|
||||
ParaId::from(i),
|
||||
dummy_candidate_commitments(HeadData(vec![i as _])),
|
||||
);
|
||||
|
||||
v0::AvailabilityBitfields::<Test>::insert(
|
||||
ValidatorIndex(i),
|
||||
v0::AvailabilityBitfieldRecord {
|
||||
bitfield: AvailabilityBitfield(Default::default()),
|
||||
submitted_at: i,
|
||||
},
|
||||
);
|
||||
|
||||
expected.push((
|
||||
ParaId::from(i),
|
||||
[V1CandidatePendingAvailability {
|
||||
core: CoreIndex(i),
|
||||
descriptor,
|
||||
relay_parent_number: i,
|
||||
hash: CandidateHash(dummy_hash()),
|
||||
availability_votes: Default::default(),
|
||||
backed_in_number: i,
|
||||
backers: Default::default(),
|
||||
backing_group: GroupIndex(i),
|
||||
commitments: dummy_candidate_commitments(HeadData(vec![i as _])),
|
||||
}]
|
||||
.into_iter()
|
||||
.collect::<VecDeque<_>>(),
|
||||
));
|
||||
}
|
||||
// add some wrong data also, candidates without commitments or commitments without
|
||||
// candidates.
|
||||
v0::PendingAvailability::<Test>::insert(
|
||||
ParaId::from(6),
|
||||
v0::CandidatePendingAvailability {
|
||||
core: CoreIndex(6),
|
||||
descriptor: dummy_candidate_descriptor_v2(dummy_hash()),
|
||||
relay_parent_number: 6,
|
||||
hash: CandidateHash(dummy_hash()),
|
||||
availability_votes: Default::default(),
|
||||
backed_in_number: 6,
|
||||
backers: Default::default(),
|
||||
backing_group: GroupIndex(6),
|
||||
},
|
||||
);
|
||||
v0::PendingAvailabilityCommitments::<Test>::insert(
|
||||
ParaId::from(7),
|
||||
dummy_candidate_commitments(HeadData(vec![7 as _])),
|
||||
);
|
||||
|
||||
// For tests, db weight is zero.
|
||||
assert_eq!(
|
||||
<VersionUncheckedMigrateToV1<Test> as UncheckedOnRuntimeUpgrade>::on_runtime_upgrade(),
|
||||
Weight::zero()
|
||||
);
|
||||
|
||||
assert_eq!(v0::PendingAvailabilityCommitments::<Test>::iter().next(), None);
|
||||
assert_eq!(v0::PendingAvailability::<Test>::iter().next(), None);
|
||||
assert_eq!(v0::AvailabilityBitfields::<Test>::iter().next(), None);
|
||||
|
||||
let mut actual = V1PendingAvailability::<Test>::iter().collect::<Vec<_>>();
|
||||
actual.sort_by(|(id1, _), (id2, _)| id1.cmp(id2));
|
||||
expected.sort_by(|(id1, _), (id2, _)| id1.cmp(id2));
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,358 @@
|
||||
// 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/>.
|
||||
|
||||
//! This module is responsible for maintaining a consistent initialization order for all other
|
||||
//! teyrchains modules. It's also responsible for finalization and session change notifications.
|
||||
//!
|
||||
//! This module can throw fatal errors if session-change notifications are received after
|
||||
//! initialization.
|
||||
|
||||
use crate::{
|
||||
configuration::{self, HostConfiguration},
|
||||
disputes::{self, DisputesHandler as _, SlashingHandler as _},
|
||||
dmp, hrmp, inclusion, paras, scheduler, session_info, shared,
|
||||
};
|
||||
use alloc::vec::Vec;
|
||||
use codec::{Decode, Encode};
|
||||
use frame_support::{
|
||||
traits::{OneSessionHandler, Randomness},
|
||||
weights::Weight,
|
||||
};
|
||||
use frame_system::limits::BlockWeights;
|
||||
use pezkuwi_primitives::{BlockNumber, ConsensusLog, SessionIndex, ValidatorId};
|
||||
use scale_info::TypeInfo;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
mod benchmarking;
|
||||
|
||||
pub use pallet::*;
|
||||
|
||||
/// Information about a session change that has just occurred.
|
||||
#[derive(Clone)]
|
||||
pub struct SessionChangeNotification<BlockNumber> {
|
||||
/// The new validators in the session.
|
||||
pub validators: Vec<ValidatorId>,
|
||||
/// The queued validators for the following session.
|
||||
pub queued: Vec<ValidatorId>,
|
||||
/// The configuration before handling the session change
|
||||
pub prev_config: HostConfiguration<BlockNumber>,
|
||||
/// The configuration after handling the session change.
|
||||
pub new_config: HostConfiguration<BlockNumber>,
|
||||
/// A secure random seed for the session, gathered from BABE.
|
||||
pub random_seed: [u8; 32],
|
||||
/// New session index.
|
||||
pub session_index: SessionIndex,
|
||||
}
|
||||
|
||||
/// Inform something about a new session.
|
||||
pub trait OnNewSession<N> {
|
||||
/// A new session was started.
|
||||
fn on_new_session(notification: &SessionChangeNotification<N>);
|
||||
}
|
||||
|
||||
impl<N> OnNewSession<N> for () {
|
||||
fn on_new_session(_: &SessionChangeNotification<N>) {}
|
||||
}
|
||||
|
||||
/// Number of validators (not only teyrchain) in a session.
|
||||
pub type ValidatorSetCount = u32;
|
||||
|
||||
impl<BlockNumber: Default + From<u32>> Default for SessionChangeNotification<BlockNumber> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
validators: Vec::new(),
|
||||
queued: Vec::new(),
|
||||
prev_config: HostConfiguration::default(),
|
||||
new_config: HostConfiguration::default(),
|
||||
random_seed: Default::default(),
|
||||
session_index: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, TypeInfo)]
|
||||
pub(crate) struct BufferedSessionChange {
|
||||
pub validators: Vec<ValidatorId>,
|
||||
pub queued: Vec<ValidatorId>,
|
||||
pub session_index: SessionIndex,
|
||||
}
|
||||
|
||||
pub trait WeightInfo {
|
||||
fn force_approve(d: u32) -> Weight;
|
||||
}
|
||||
|
||||
impl WeightInfo for () {
|
||||
fn force_approve(_: u32) -> Weight {
|
||||
BlockWeights::default().max_block
|
||||
}
|
||||
}
|
||||
|
||||
#[frame_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
use frame_support::pallet_prelude::*;
|
||||
use frame_system::pallet_prelude::*;
|
||||
|
||||
#[pallet::pallet]
|
||||
#[pallet::without_storage_info]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config:
|
||||
frame_system::Config
|
||||
+ configuration::Config
|
||||
+ shared::Config
|
||||
+ paras::Config
|
||||
+ scheduler::Config
|
||||
+ inclusion::Config
|
||||
+ session_info::Config
|
||||
+ disputes::Config
|
||||
+ dmp::Config
|
||||
+ hrmp::Config
|
||||
{
|
||||
/// A randomness beacon.
|
||||
type Randomness: Randomness<Self::Hash, BlockNumberFor<Self>>;
|
||||
/// An origin which is allowed to force updates to teyrchains.
|
||||
type ForceOrigin: EnsureOrigin<<Self as frame_system::Config>::RuntimeOrigin>;
|
||||
/// Temporary hack to call `Coretime::on_new_session` on chains that support `Coretime` or
|
||||
/// to disable it on the ones that don't support it. Can be removed and replaced by a simple
|
||||
/// bound to `coretime::Config` once all chains support it.
|
||||
type CoretimeOnNewSession: OnNewSession<BlockNumberFor<Self>>;
|
||||
/// Weight information for extrinsics in this pallet.
|
||||
type WeightInfo: WeightInfo;
|
||||
}
|
||||
|
||||
/// Whether the teyrchains modules have been initialized within this block.
|
||||
///
|
||||
/// Semantically a `bool`, but this guarantees it should never hit the trie,
|
||||
/// as this is cleared in `on_finalize` and Frame optimizes `None` values to be empty values.
|
||||
///
|
||||
/// As a `bool`, `set(false)` and `remove()` both lead to the next `get()` being false, but one
|
||||
/// of them writes to the trie and one does not. This confusion makes `Option<()>` more suitable
|
||||
/// for the semantics of this variable.
|
||||
#[pallet::storage]
|
||||
pub(super) type HasInitialized<T: Config> = StorageValue<_, ()>;
|
||||
|
||||
/// Buffered session changes.
|
||||
///
|
||||
/// Typically this will be empty or one element long. Apart from that this item never hits
|
||||
/// the storage.
|
||||
///
|
||||
/// However this is a `Vec` regardless to handle various edge cases that may occur at runtime
|
||||
/// upgrade boundaries or if governance intervenes.
|
||||
#[pallet::storage]
|
||||
pub(crate) type BufferedSessionChanges<T: Config> =
|
||||
StorageValue<_, Vec<BufferedSessionChange>, ValueQuery>;
|
||||
|
||||
#[pallet::hooks]
|
||||
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
|
||||
fn on_initialize(now: BlockNumberFor<T>) -> Weight {
|
||||
// The other modules are initialized in this order:
|
||||
// - Configuration
|
||||
// - Paras
|
||||
// - Scheduler
|
||||
// - Inclusion
|
||||
// - `SessionInfo`
|
||||
// - Disputes
|
||||
// - DMP
|
||||
// - UMP
|
||||
// - HRMP
|
||||
let total_weight = configuration::Pallet::<T>::initializer_initialize(now) +
|
||||
shared::Pallet::<T>::initializer_initialize(now) +
|
||||
paras::Pallet::<T>::initializer_initialize(now) +
|
||||
scheduler::Pallet::<T>::initializer_initialize(now) +
|
||||
inclusion::Pallet::<T>::initializer_initialize(now) +
|
||||
session_info::Pallet::<T>::initializer_initialize(now) +
|
||||
T::DisputesHandler::initializer_initialize(now) +
|
||||
T::SlashingHandler::initializer_initialize(now) +
|
||||
dmp::Pallet::<T>::initializer_initialize(now) +
|
||||
hrmp::Pallet::<T>::initializer_initialize(now);
|
||||
|
||||
HasInitialized::<T>::set(Some(()));
|
||||
|
||||
total_weight
|
||||
}
|
||||
|
||||
fn on_finalize(now: BlockNumberFor<T>) {
|
||||
// reverse initialization order.
|
||||
hrmp::Pallet::<T>::initializer_finalize();
|
||||
dmp::Pallet::<T>::initializer_finalize();
|
||||
T::SlashingHandler::initializer_finalize();
|
||||
T::DisputesHandler::initializer_finalize();
|
||||
session_info::Pallet::<T>::initializer_finalize();
|
||||
inclusion::Pallet::<T>::initializer_finalize();
|
||||
scheduler::Pallet::<T>::initializer_finalize();
|
||||
paras::Pallet::<T>::initializer_finalize(now);
|
||||
shared::Pallet::<T>::initializer_finalize();
|
||||
configuration::Pallet::<T>::initializer_finalize();
|
||||
|
||||
// Apply buffered session changes as the last thing. This way the runtime APIs and the
|
||||
// next block will observe the next session.
|
||||
//
|
||||
// Note that we only apply the last session as all others lasted less than a block
|
||||
// (weirdly).
|
||||
if let Some(BufferedSessionChange { session_index, validators, queued }) =
|
||||
BufferedSessionChanges::<T>::take().pop()
|
||||
{
|
||||
Self::apply_new_session(session_index, validators, queued);
|
||||
}
|
||||
|
||||
HasInitialized::<T>::take();
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Issue a signal to the consensus engine to forcibly act as though all teyrchain
|
||||
/// blocks in all relay chain blocks up to and including the given number in the current
|
||||
/// chain are valid and should be finalized.
|
||||
#[pallet::call_index(0)]
|
||||
#[pallet::weight((
|
||||
<T as Config>::WeightInfo::force_approve(
|
||||
frame_system::Pallet::<T>::digest().logs.len() as u32,
|
||||
),
|
||||
DispatchClass::Operational,
|
||||
))]
|
||||
pub fn force_approve(origin: OriginFor<T>, up_to: BlockNumber) -> DispatchResult {
|
||||
T::ForceOrigin::ensure_origin(origin)?;
|
||||
|
||||
frame_system::Pallet::<T>::deposit_log(ConsensusLog::ForceApprove(up_to).into());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
fn apply_new_session(
|
||||
session_index: SessionIndex,
|
||||
all_validators: Vec<ValidatorId>,
|
||||
queued: Vec<ValidatorId>,
|
||||
) {
|
||||
let random_seed = {
|
||||
let mut buf = [0u8; 32];
|
||||
// TODO: audit usage of randomness API
|
||||
// https://github.com/paritytech/polkadot/issues/2601
|
||||
let (random_hash, _) = T::Randomness::random(&b"paras"[..]);
|
||||
let len = core::cmp::min(32, random_hash.as_ref().len());
|
||||
buf[..len].copy_from_slice(&random_hash.as_ref()[..len]);
|
||||
buf
|
||||
};
|
||||
|
||||
let configuration::SessionChangeOutcome { prev_config, new_config } =
|
||||
configuration::Pallet::<T>::initializer_on_new_session(&session_index);
|
||||
let new_config = new_config.unwrap_or_else(|| prev_config.clone());
|
||||
|
||||
let validators = shared::Pallet::<T>::initializer_on_new_session(
|
||||
session_index,
|
||||
random_seed,
|
||||
&new_config,
|
||||
all_validators,
|
||||
);
|
||||
|
||||
let notification = SessionChangeNotification {
|
||||
validators,
|
||||
queued,
|
||||
prev_config,
|
||||
new_config,
|
||||
random_seed,
|
||||
session_index,
|
||||
};
|
||||
|
||||
let outgoing_paras = paras::Pallet::<T>::initializer_on_new_session(¬ification);
|
||||
scheduler::Pallet::<T>::initializer_on_new_session(¬ification);
|
||||
inclusion::Pallet::<T>::initializer_on_new_session(¬ification, &outgoing_paras);
|
||||
session_info::Pallet::<T>::initializer_on_new_session(¬ification);
|
||||
T::DisputesHandler::initializer_on_new_session(¬ification);
|
||||
T::SlashingHandler::initializer_on_new_session(session_index);
|
||||
dmp::Pallet::<T>::initializer_on_new_session(¬ification, &outgoing_paras);
|
||||
hrmp::Pallet::<T>::initializer_on_new_session(¬ification, &outgoing_paras);
|
||||
T::CoretimeOnNewSession::on_new_session(¬ification);
|
||||
}
|
||||
|
||||
/// Should be called when a new session occurs. Buffers the session notification to be applied
|
||||
/// at the end of the block. If `queued` is `None`, the `validators` are considered queued.
|
||||
fn on_new_session<'a, I: 'a>(
|
||||
_changed: bool,
|
||||
session_index: SessionIndex,
|
||||
validators: I,
|
||||
queued: Option<I>,
|
||||
) where
|
||||
I: Iterator<Item = (&'a T::AccountId, ValidatorId)>,
|
||||
{
|
||||
let validators: Vec<_> = validators.map(|(_, v)| v).collect();
|
||||
let queued: Vec<_> = if let Some(queued) = queued {
|
||||
queued.map(|(_, v)| v).collect()
|
||||
} else {
|
||||
validators.clone()
|
||||
};
|
||||
|
||||
if session_index == 0 {
|
||||
// Genesis session should be immediately enacted.
|
||||
Self::apply_new_session(0, validators, queued);
|
||||
} else {
|
||||
BufferedSessionChanges::<T>::mutate(|v| {
|
||||
v.push(BufferedSessionChange { validators, queued, session_index })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Allow to trigger `on_new_session` in tests, this is needed as long as `pallet_session` is not
|
||||
// implemented in mock.
|
||||
#[cfg(any(test, feature = "runtime-benchmarks"))]
|
||||
pub(crate) fn test_trigger_on_new_session<'a, I: 'a>(
|
||||
changed: bool,
|
||||
session_index: SessionIndex,
|
||||
validators: I,
|
||||
queued: Option<I>,
|
||||
) where
|
||||
I: Iterator<Item = (&'a T::AccountId, ValidatorId)>,
|
||||
{
|
||||
Self::on_new_session(changed, session_index, validators, queued)
|
||||
}
|
||||
|
||||
/// Return whether at the end of this block a new session will be initialized.
|
||||
pub(crate) fn upcoming_session_change() -> bool {
|
||||
!BufferedSessionChanges::<T>::get().is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> sp_runtime::BoundToRuntimeAppPublic for Pallet<T> {
|
||||
type Public = ValidatorId;
|
||||
}
|
||||
|
||||
impl<T: pallet_session::Config + Config> OneSessionHandler<T::AccountId> for Pallet<T> {
|
||||
type Key = ValidatorId;
|
||||
|
||||
fn on_genesis_session<'a, I: 'a>(validators: I)
|
||||
where
|
||||
I: Iterator<Item = (&'a T::AccountId, Self::Key)>,
|
||||
{
|
||||
Pallet::<T>::on_new_session(false, 0, validators, None);
|
||||
}
|
||||
|
||||
fn on_new_session<'a, I: 'a>(changed: bool, validators: I, queued: I)
|
||||
where
|
||||
I: Iterator<Item = (&'a T::AccountId, Self::Key)>,
|
||||
{
|
||||
let session_index = pallet_session::Pallet::<T>::current_index();
|
||||
Pallet::<T>::on_new_session(changed, session_index, validators, Some(queued));
|
||||
}
|
||||
|
||||
fn on_disabled(_i: u32) {}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// 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 frame_benchmarking::v2::*;
|
||||
use frame_system::RawOrigin;
|
||||
use pezkuwi_primitives::ConsensusLog;
|
||||
use sp_runtime::DigestItem;
|
||||
|
||||
// Random large number for the digest
|
||||
const DIGEST_MAX_LEN: u32 = 65536;
|
||||
|
||||
#[benchmarks]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
#[benchmark]
|
||||
fn force_approve(d: Linear<0, DIGEST_MAX_LEN>) -> Result<(), BenchmarkError> {
|
||||
for _ in 0..d {
|
||||
frame_system::Pallet::<T>::deposit_log(ConsensusLog::ForceApprove(d).into());
|
||||
}
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Root, d + 1);
|
||||
|
||||
assert_eq!(
|
||||
frame_system::Pallet::<T>::digest().logs.last().unwrap(),
|
||||
&DigestItem::from(ConsensusLog::ForceApprove(d + 1)),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(
|
||||
Pallet,
|
||||
crate::mock::new_test_ext(Default::default()),
|
||||
crate::mock::Test
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// 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::{
|
||||
mock::{new_test_ext, Dmp, Initializer, MockGenesisConfig, Paras, System, Test},
|
||||
paras::ParaKind,
|
||||
session_info,
|
||||
};
|
||||
use pezkuwi_primitives::{HeadData, Id as ParaId};
|
||||
use pezkuwi_primitives_test_helpers::dummy_validation_code;
|
||||
|
||||
use frame_support::{
|
||||
assert_ok,
|
||||
traits::{OnFinalize, OnInitialize},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn session_0_is_instantly_applied() {
|
||||
new_test_ext(Default::default()).execute_with(|| {
|
||||
Initializer::on_new_session(false, 0, Vec::new().into_iter(), Some(Vec::new().into_iter()));
|
||||
|
||||
let v = BufferedSessionChanges::<Test>::get();
|
||||
assert!(v.is_empty());
|
||||
|
||||
assert_eq!(session_info::EarliestStoredSession::<Test>::get(), 0);
|
||||
assert!(session_info::Sessions::<Test>::get(0).is_some());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_change_before_initialize_is_still_buffered_after() {
|
||||
new_test_ext(Default::default()).execute_with(|| {
|
||||
Initializer::on_new_session(false, 1, Vec::new().into_iter(), Some(Vec::new().into_iter()));
|
||||
|
||||
let now = System::block_number();
|
||||
Initializer::on_initialize(now);
|
||||
|
||||
let v = BufferedSessionChanges::<Test>::get();
|
||||
assert_eq!(v.len(), 1);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_change_applied_on_finalize() {
|
||||
new_test_ext(Default::default()).execute_with(|| {
|
||||
Initializer::on_initialize(1);
|
||||
Initializer::on_new_session(false, 1, Vec::new().into_iter(), Some(Vec::new().into_iter()));
|
||||
|
||||
Initializer::on_finalize(1);
|
||||
|
||||
assert!(BufferedSessionChanges::<Test>::get().is_empty());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sets_flag_on_initialize() {
|
||||
new_test_ext(Default::default()).execute_with(|| {
|
||||
Initializer::on_initialize(1);
|
||||
|
||||
assert!(HasInitialized::<Test>::get().is_some());
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clears_flag_on_finalize() {
|
||||
new_test_ext(Default::default()).execute_with(|| {
|
||||
Initializer::on_initialize(1);
|
||||
Initializer::on_finalize(1);
|
||||
|
||||
assert!(HasInitialized::<Test>::get().is_none());
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scheduled_cleanup_performed() {
|
||||
let a = ParaId::from(1312);
|
||||
let b = ParaId::from(228);
|
||||
let c = ParaId::from(123);
|
||||
|
||||
let mock_genesis = crate::paras::ParaGenesisArgs {
|
||||
para_kind: ParaKind::Teyrchain,
|
||||
genesis_head: HeadData(vec![4, 5, 6]),
|
||||
validation_code: dummy_validation_code(),
|
||||
};
|
||||
|
||||
new_test_ext(MockGenesisConfig {
|
||||
configuration: crate::configuration::GenesisConfig {
|
||||
config: crate::configuration::HostConfiguration {
|
||||
max_downward_message_size: 1024,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
paras: crate::paras::GenesisConfig {
|
||||
paras: vec![
|
||||
(a, mock_genesis.clone()),
|
||||
(b, mock_genesis.clone()),
|
||||
(c, mock_genesis.clone()),
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.execute_with(|| {
|
||||
// enqueue downward messages to A, B and C.
|
||||
assert_ok!(Dmp::queue_downward_message(
|
||||
&configuration::ActiveConfig::<Test>::get(),
|
||||
a,
|
||||
vec![1, 2, 3]
|
||||
));
|
||||
assert_ok!(Dmp::queue_downward_message(
|
||||
&configuration::ActiveConfig::<Test>::get(),
|
||||
b,
|
||||
vec![4, 5, 6]
|
||||
));
|
||||
assert_ok!(Dmp::queue_downward_message(
|
||||
&configuration::ActiveConfig::<Test>::get(),
|
||||
c,
|
||||
vec![7, 8, 9]
|
||||
));
|
||||
|
||||
assert_ok!(Paras::schedule_para_cleanup(a));
|
||||
assert_ok!(Paras::schedule_para_cleanup(b));
|
||||
|
||||
// Apply session 2 in the future
|
||||
Initializer::apply_new_session(2, vec![], vec![]);
|
||||
|
||||
assert!(Dmp::dmq_contents(a).is_empty());
|
||||
assert!(Dmp::dmq_contents(b).is_empty());
|
||||
assert!(!Dmp::dmq_contents(c).is_empty());
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
// 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/>.
|
||||
|
||||
//! Runtime modules for teyrchains code.
|
||||
//!
|
||||
//! It is crucial to include all the modules from this crate in the runtime, in
|
||||
//! particular the `Initializer` module, as it is responsible for initializing the state
|
||||
//! of the other modules.
|
||||
|
||||
#![cfg_attr(feature = "runtime-benchmarks", recursion_limit = "256")]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
pub mod assigner_coretime;
|
||||
pub mod configuration;
|
||||
pub mod coretime;
|
||||
pub mod disputes;
|
||||
pub mod dmp;
|
||||
pub mod hrmp;
|
||||
pub mod inclusion;
|
||||
pub mod initializer;
|
||||
pub mod metrics;
|
||||
pub mod on_demand;
|
||||
pub mod origin;
|
||||
pub mod paras;
|
||||
pub mod paras_inherent;
|
||||
pub mod reward_points;
|
||||
pub mod scheduler;
|
||||
pub mod session_info;
|
||||
pub mod shared;
|
||||
|
||||
pub mod runtime_api_impl;
|
||||
|
||||
mod util;
|
||||
|
||||
#[cfg(any(feature = "runtime-benchmarks", test))]
|
||||
mod builder;
|
||||
#[cfg(test)]
|
||||
mod mock;
|
||||
#[cfg(test)]
|
||||
mod ump_tests;
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
pub use origin::{ensure_teyrchain, Origin};
|
||||
pub use paras::{ParaLifecycle, UpgradeStrategy};
|
||||
use pezkuwi_primitives::{HeadData, Id as ParaId, ValidationCode};
|
||||
use sp_arithmetic::traits::Saturating;
|
||||
use sp_runtime::{traits::Get, DispatchResult, FixedU128};
|
||||
|
||||
/// Trait for tracking message delivery fees on a transport protocol.
|
||||
pub trait FeeTracker {
|
||||
/// Type used for assigning different fee factors to different destinations
|
||||
type Id: Copy;
|
||||
|
||||
/// Minimal delivery fee factor.
|
||||
const MIN_FEE_FACTOR: FixedU128 = FixedU128::from_u32(1);
|
||||
/// The factor that is used to increase the current message fee factor when the transport
|
||||
/// protocol is experiencing some lags.
|
||||
const EXPONENTIAL_FEE_BASE: FixedU128 = FixedU128::from_rational(105, 100); // 1.05
|
||||
/// The factor that is used to increase the current message fee factor for every sent kilobyte.
|
||||
const MESSAGE_SIZE_FEE_BASE: FixedU128 = FixedU128::from_rational(1, 1000); // 0.001
|
||||
|
||||
/// Returns the current message fee factor.
|
||||
fn get_fee_factor(id: Self::Id) -> FixedU128;
|
||||
|
||||
/// Sets the current message fee factor.
|
||||
fn set_fee_factor(id: Self::Id, val: FixedU128);
|
||||
|
||||
fn do_increase_fee_factor(fee_factor: &mut FixedU128, message_size: u128) {
|
||||
let message_size_factor = FixedU128::from(message_size.saturating_div(1024))
|
||||
.saturating_mul(Self::MESSAGE_SIZE_FEE_BASE);
|
||||
*fee_factor = fee_factor
|
||||
.saturating_mul(Self::EXPONENTIAL_FEE_BASE.saturating_add(message_size_factor));
|
||||
}
|
||||
|
||||
/// Increases the delivery fee factor by a factor based on message size and records the result.
|
||||
fn increase_fee_factor(id: Self::Id, message_size: u128) {
|
||||
let mut fee_factor = Self::get_fee_factor(id);
|
||||
Self::do_increase_fee_factor(&mut fee_factor, message_size);
|
||||
Self::set_fee_factor(id, fee_factor);
|
||||
}
|
||||
|
||||
fn do_decrease_fee_factor(fee_factor: &mut FixedU128) -> bool {
|
||||
const { assert!(Self::EXPONENTIAL_FEE_BASE.into_inner() >= FixedU128::from_u32(1).into_inner()) }
|
||||
|
||||
if *fee_factor == Self::MIN_FEE_FACTOR {
|
||||
return false;
|
||||
}
|
||||
|
||||
// This should never lead to a panic because of the static assert above.
|
||||
*fee_factor = Self::MIN_FEE_FACTOR.max(*fee_factor / Self::EXPONENTIAL_FEE_BASE);
|
||||
true
|
||||
}
|
||||
|
||||
/// Decreases the delivery fee factor by a constant factor and records the result.
|
||||
///
|
||||
/// Does not reduce the fee factor below the initial value, which is currently set as 1.
|
||||
///
|
||||
/// Returns `true` if the fee factor was actually decreased, `false` otherwise.
|
||||
fn decrease_fee_factor(id: Self::Id) -> bool {
|
||||
let mut fee_factor = Self::get_fee_factor(id);
|
||||
let res = Self::do_decrease_fee_factor(&mut fee_factor);
|
||||
Self::set_fee_factor(id, fee_factor);
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper struct used for accessing `FeeTracker::MIN_FEE_FACTOR`
|
||||
pub struct GetMinFeeFactor<T>(core::marker::PhantomData<T>);
|
||||
|
||||
impl<T: FeeTracker> Get<FixedU128> for GetMinFeeFactor<T> {
|
||||
fn get() -> FixedU128 {
|
||||
T::MIN_FEE_FACTOR
|
||||
}
|
||||
}
|
||||
|
||||
/// Schedule a para to be initialized at the start of the next session with the given genesis data.
|
||||
pub fn schedule_para_initialize<T: paras::Config>(
|
||||
id: ParaId,
|
||||
genesis: paras::ParaGenesisArgs,
|
||||
) -> Result<(), ()> {
|
||||
paras::Pallet::<T>::schedule_para_initialize(id, genesis).map_err(|_| ())
|
||||
}
|
||||
|
||||
/// Schedule a para to be cleaned up at the start of the next session.
|
||||
pub fn schedule_para_cleanup<T: paras::Config>(id: pezkuwi_primitives::Id) -> Result<(), ()> {
|
||||
paras::Pallet::<T>::schedule_para_cleanup(id).map_err(|_| ())
|
||||
}
|
||||
|
||||
/// Schedule a parathread (on-demand teyrchain) to be upgraded to a lease holding teyrchain.
|
||||
pub fn schedule_parathread_upgrade<T: paras::Config>(id: ParaId) -> Result<(), ()> {
|
||||
paras::Pallet::<T>::schedule_parathread_upgrade(id).map_err(|_| ())
|
||||
}
|
||||
|
||||
/// Schedule a lease holding teyrchain to be downgraded to an on-demand teyrchain.
|
||||
pub fn schedule_teyrchain_downgrade<T: paras::Config>(id: ParaId) -> Result<(), ()> {
|
||||
paras::Pallet::<T>::schedule_teyrchain_downgrade(id).map_err(|_| ())
|
||||
}
|
||||
|
||||
/// Schedules a validation code upgrade to a teyrchain with the given id.
|
||||
pub fn schedule_code_upgrade<T: paras::Config>(
|
||||
id: ParaId,
|
||||
new_code: ValidationCode,
|
||||
set_go_ahead: UpgradeStrategy,
|
||||
) -> DispatchResult {
|
||||
paras::Pallet::<T>::schedule_code_upgrade_external(id, new_code, set_go_ahead)
|
||||
}
|
||||
|
||||
/// Sets the current teyrchain head with the given id.
|
||||
pub fn set_current_head<T: paras::Config>(id: ParaId, new_head: HeadData) {
|
||||
paras::Pallet::<T>::set_current_head(id, new_head)
|
||||
}
|
||||
|
||||
/// Ensure more initialization for `ParaId` when benchmarking. (e.g. open HRMP channels, ...)
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
pub trait EnsureForTeyrchain {
|
||||
fn ensure(para_id: ParaId);
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
#[impl_trait_for_tuples::impl_for_tuples(30)]
|
||||
impl EnsureForTeyrchain for Tuple {
|
||||
fn ensure(para: ParaId) {
|
||||
for_tuples!( #(
|
||||
Tuple::ensure(para);
|
||||
)* );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// 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/>.
|
||||
|
||||
//! Runtime declaration of the teyrchain metrics.
|
||||
|
||||
use pezkuwi_primitives::metric_definitions::{
|
||||
TEYRCHAIN_CREATE_INHERENT_BITFIELDS_SIGNATURE_CHECKS,
|
||||
TEYRCHAIN_INHERENT_DATA_BITFIELDS_PROCESSED, TEYRCHAIN_INHERENT_DATA_CANDIDATES_PROCESSED,
|
||||
TEYRCHAIN_INHERENT_DATA_DISPUTE_SETS_PROCESSED, TEYRCHAIN_INHERENT_DATA_WEIGHT,
|
||||
TEYRCHAIN_VERIFY_DISPUTE_SIGNATURE,
|
||||
};
|
||||
use pezkuwi_runtime_metrics::{Counter, CounterVec, Histogram};
|
||||
|
||||
pub struct Metrics {
|
||||
/// Samples inherent data weight.
|
||||
inherent_data_weight: CounterVec,
|
||||
/// Counts how many inherent bitfields processed in `process_inherent_data`.
|
||||
bitfields_processed: Counter,
|
||||
/// Counts how many teyrchain candidates processed in `process_inherent_data`.
|
||||
candidates_processed: CounterVec,
|
||||
/// Counts dispute statements sets processed in `process_inherent_data`.
|
||||
dispute_sets_processed: CounterVec,
|
||||
/// Counts bitfield signature checks in `process_inherent_data`.
|
||||
bitfields_signature_checks: CounterVec,
|
||||
|
||||
/// Histogram with the time spent checking a validator signature of a dispute statement
|
||||
signature_timings: Histogram,
|
||||
}
|
||||
|
||||
impl Metrics {
|
||||
/// Sample the inherent data weight metric before filtering.
|
||||
pub fn on_before_filter(&self, value: u64) {
|
||||
self.inherent_data_weight.with_label_values(&["before-filter"]).inc_by(value);
|
||||
}
|
||||
|
||||
/// Sample the inherent data weight metric after filtering.
|
||||
pub fn on_after_filter(&self, value: u64) {
|
||||
self.inherent_data_weight.with_label_values(&["after-filter"]).inc_by(value);
|
||||
}
|
||||
|
||||
/// Increment the number of bitfields processed.
|
||||
pub fn on_bitfields_processed(&self, value: u64) {
|
||||
self.bitfields_processed.inc_by(value);
|
||||
}
|
||||
|
||||
/// Increment the number of teyrchain candidates included.
|
||||
pub fn on_candidates_included(&self, value: u64) {
|
||||
self.candidates_processed.with_label_values(&["included"]).inc_by(value);
|
||||
}
|
||||
|
||||
/// Increment the number of teyrchain candidates sanitized.
|
||||
pub fn on_candidates_sanitized(&self, value: u64) {
|
||||
self.candidates_processed.with_label_values(&["sanitized"]).inc_by(value);
|
||||
}
|
||||
|
||||
/// Increment the total number of teyrchain candidates received in `process_inherent_data`.
|
||||
pub fn on_candidates_processed_total(&self, value: u64) {
|
||||
self.candidates_processed.with_label_values(&["total"]).inc_by(value);
|
||||
}
|
||||
|
||||
/// Sample the relay chain freeze events causing runtime to not process candidates in
|
||||
/// `process_inherent_data`.
|
||||
pub fn on_relay_chain_freeze(&self) {
|
||||
self.dispute_sets_processed.with_label_values(&["frozen"]).inc();
|
||||
}
|
||||
|
||||
/// Increment the number of disputes that have concluded as invalid.
|
||||
pub fn on_disputes_concluded_invalid(&self, value: u64) {
|
||||
self.dispute_sets_processed
|
||||
.with_label_values(&["concluded_invalid"])
|
||||
.inc_by(value);
|
||||
}
|
||||
|
||||
/// Increment the number of disputes imported.
|
||||
pub fn on_disputes_imported(&self, value: u64) {
|
||||
self.dispute_sets_processed.with_label_values(&["imported"]).inc_by(value);
|
||||
}
|
||||
|
||||
pub fn on_valid_bitfield_signature(&self) {
|
||||
self.bitfields_signature_checks.with_label_values(&["valid"]).inc_by(1);
|
||||
}
|
||||
|
||||
pub fn on_invalid_bitfield_signature(&self) {
|
||||
self.bitfields_signature_checks.with_label_values(&["invalid"]).inc_by(1);
|
||||
}
|
||||
|
||||
pub fn on_signature_check_complete(&self, val: u128) {
|
||||
self.signature_timings.observe(val);
|
||||
}
|
||||
}
|
||||
|
||||
pub const METRICS: Metrics = Metrics {
|
||||
inherent_data_weight: CounterVec::new(TEYRCHAIN_INHERENT_DATA_WEIGHT),
|
||||
bitfields_processed: Counter::new(TEYRCHAIN_INHERENT_DATA_BITFIELDS_PROCESSED),
|
||||
candidates_processed: CounterVec::new(TEYRCHAIN_INHERENT_DATA_CANDIDATES_PROCESSED),
|
||||
dispute_sets_processed: CounterVec::new(TEYRCHAIN_INHERENT_DATA_DISPUTE_SETS_PROCESSED),
|
||||
bitfields_signature_checks: CounterVec::new(
|
||||
TEYRCHAIN_CREATE_INHERENT_BITFIELDS_SIGNATURE_CHECKS,
|
||||
),
|
||||
signature_timings: Histogram::new(TEYRCHAIN_VERIFY_DISPUTE_SIGNATURE),
|
||||
};
|
||||
@@ -0,0 +1,769 @@
|
||||
// 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/>.
|
||||
|
||||
//! Mocks for all the traits.
|
||||
|
||||
use crate::{
|
||||
assigner_coretime, configuration, coretime, disputes, dmp, hrmp,
|
||||
inclusion::{self, AggregateMessageOrigin, UmpQueueId},
|
||||
initializer, on_demand, origin, paras,
|
||||
paras::ParaKind,
|
||||
paras_inherent, scheduler,
|
||||
scheduler::common::AssignmentProvider,
|
||||
session_info, shared, ParaId,
|
||||
};
|
||||
use frame_support::pallet_prelude::*;
|
||||
use pezkuwi_primitives::CoreIndex;
|
||||
|
||||
use codec::Decode;
|
||||
use frame_support::{
|
||||
assert_ok, derive_impl,
|
||||
dispatch::GetDispatchInfo,
|
||||
parameter_types,
|
||||
traits::{
|
||||
Currency, ProcessMessage, ProcessMessageError, ValidatorSet, ValidatorSetWithIdentification,
|
||||
},
|
||||
weights::{Weight, WeightMeter},
|
||||
PalletId,
|
||||
};
|
||||
use frame_support_test::TestRandomness;
|
||||
use frame_system::{limits, EnsureRoot};
|
||||
use pezkuwi_primitives::{
|
||||
AuthorityDiscoveryId, Balance, BlockNumber, CandidateHash, Moment, SessionIndex, UpwardMessage,
|
||||
ValidationCode, ValidatorIndex,
|
||||
};
|
||||
use sp_core::{ConstU32, H256};
|
||||
use sp_io::TestExternalities;
|
||||
use sp_runtime::{
|
||||
traits::{AccountIdConversion, BlakeTwo256, IdentityLookup},
|
||||
transaction_validity::TransactionPriority,
|
||||
BuildStorage, FixedU128, Perbill, Permill,
|
||||
};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::{btree_map::BTreeMap, vec_deque::VecDeque, HashMap},
|
||||
};
|
||||
use xcm::{
|
||||
prelude::XcmVersion,
|
||||
v5::{Assets, InteriorLocation, Location, SendError, SendResult, SendXcm, Xcm, XcmHash},
|
||||
IntoVersion, VersionedXcm, WrapVersion,
|
||||
};
|
||||
|
||||
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>;
|
||||
type Block = frame_system::mocking::MockBlockU32<Test>;
|
||||
|
||||
frame_support::construct_runtime!(
|
||||
pub enum Test
|
||||
{
|
||||
System: frame_system,
|
||||
Balances: pallet_balances,
|
||||
MessageQueue: pallet_message_queue,
|
||||
Paras: paras,
|
||||
Configuration: configuration,
|
||||
ParasShared: shared,
|
||||
ParaInclusion: inclusion,
|
||||
ParaInherent: paras_inherent,
|
||||
Scheduler: scheduler,
|
||||
MockAssigner: mock_assigner,
|
||||
OnDemand: on_demand,
|
||||
CoretimeAssigner: assigner_coretime,
|
||||
Coretime: coretime,
|
||||
Initializer: initializer,
|
||||
Dmp: dmp,
|
||||
Hrmp: hrmp,
|
||||
TeyrchainsOrigin: origin,
|
||||
SessionInfo: session_info,
|
||||
Disputes: disputes,
|
||||
Babe: pallet_babe,
|
||||
}
|
||||
);
|
||||
|
||||
impl<C> frame_system::offchain::CreateTransactionBase<C> for Test
|
||||
where
|
||||
RuntimeCall: From<C>,
|
||||
{
|
||||
type Extrinsic = UncheckedExtrinsic;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
}
|
||||
|
||||
impl<C> frame_system::offchain::CreateBare<C> for Test
|
||||
where
|
||||
RuntimeCall: From<C>,
|
||||
{
|
||||
fn create_bare(call: Self::RuntimeCall) -> Self::Extrinsic {
|
||||
UncheckedExtrinsic::new_bare(call)
|
||||
}
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub static BlockWeights: frame_system::limits::BlockWeights =
|
||||
frame_system::limits::BlockWeights::simple_max(
|
||||
Weight::from_parts(4 * 1024 * 1024, u64::MAX),
|
||||
);
|
||||
pub static BlockLength: limits::BlockLength = limits::BlockLength::max_with_normal_ratio(u32::MAX, Perbill::from_percent(75));
|
||||
}
|
||||
|
||||
pub type AccountId = u64;
|
||||
|
||||
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
|
||||
impl frame_system::Config for Test {
|
||||
type BaseCallFilter = frame_support::traits::Everything;
|
||||
type BlockWeights = BlockWeights;
|
||||
type BlockLength = BlockLength;
|
||||
type DbWeight = ();
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type Nonce = u64;
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = u64;
|
||||
type Lookup = IdentityLookup<u64>;
|
||||
type Block = Block;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = pallet_balances::AccountData<u128>;
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type SystemWeightInfo = ();
|
||||
type SS58Prefix = ();
|
||||
type OnSetCode = ();
|
||||
type MaxConsumers = ConstU32<16>;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub static ExistentialDeposit: u64 = 1;
|
||||
}
|
||||
|
||||
#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)]
|
||||
impl pallet_balances::Config for Test {
|
||||
type Balance = Balance;
|
||||
type ExistentialDeposit = ExistentialDeposit;
|
||||
type AccountStore = System;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const EpochDuration: u64 = 10;
|
||||
pub const ExpectedBlockTime: Moment = 6_000;
|
||||
pub const ReportLongevity: u64 = 10;
|
||||
pub const MaxAuthorities: u32 = 100_000;
|
||||
}
|
||||
|
||||
impl pallet_babe::Config for Test {
|
||||
type EpochDuration = EpochDuration;
|
||||
type ExpectedBlockTime = ExpectedBlockTime;
|
||||
|
||||
// session module is the trigger
|
||||
type EpochChangeTrigger = pallet_babe::ExternalTrigger;
|
||||
type DisabledValidators = ();
|
||||
type WeightInfo = ();
|
||||
type MaxAuthorities = MaxAuthorities;
|
||||
type MaxNominators = ConstU32<0>;
|
||||
type KeyOwnerProof = sp_core::Void;
|
||||
type EquivocationReportSystem = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const MinimumPeriod: Moment = 6_000 / 2;
|
||||
}
|
||||
|
||||
impl pallet_timestamp::Config for Test {
|
||||
type Moment = Moment;
|
||||
type OnTimestampSet = ();
|
||||
type MinimumPeriod = MinimumPeriod;
|
||||
type WeightInfo = ();
|
||||
}
|
||||
|
||||
impl crate::initializer::Config for Test {
|
||||
type Randomness = TestRandomness<Self>;
|
||||
type ForceOrigin = frame_system::EnsureRoot<u64>;
|
||||
type WeightInfo = ();
|
||||
type CoretimeOnNewSession = Coretime;
|
||||
}
|
||||
|
||||
impl crate::configuration::Config for Test {
|
||||
type WeightInfo = crate::configuration::TestWeightInfo;
|
||||
}
|
||||
|
||||
pub struct MockDisabledValidators {}
|
||||
impl frame_support::traits::DisabledValidators for MockDisabledValidators {
|
||||
/// Returns true if the given validator is disabled.
|
||||
fn is_disabled(index: u32) -> bool {
|
||||
disabled_validators().iter().any(|v| *v == index)
|
||||
}
|
||||
|
||||
/// Returns a hardcoded list (`DISABLED_VALIDATORS`) of disabled validators
|
||||
fn disabled_validators() -> Vec<u32> {
|
||||
disabled_validators()
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::shared::Config for Test {
|
||||
type DisabledValidators = MockDisabledValidators;
|
||||
}
|
||||
|
||||
impl origin::Config for Test {}
|
||||
|
||||
parameter_types! {
|
||||
pub const ParasUnsignedPriority: TransactionPriority = TransactionPriority::max_value();
|
||||
}
|
||||
|
||||
/// A very dumb implementation of `EstimateNextSessionRotation`. At the moment of writing, this
|
||||
/// is more to satisfy type requirements rather than to test anything.
|
||||
pub struct TestNextSessionRotation;
|
||||
|
||||
impl frame_support::traits::EstimateNextSessionRotation<u32> for TestNextSessionRotation {
|
||||
fn average_session_length() -> u32 {
|
||||
10
|
||||
}
|
||||
|
||||
fn estimate_current_session_progress(_now: u32) -> (Option<Permill>, Weight) {
|
||||
(None, Weight::zero())
|
||||
}
|
||||
|
||||
fn estimate_next_session_rotation(_now: u32) -> (Option<u32>, Weight) {
|
||||
(None, Weight::zero())
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::paras::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type WeightInfo = crate::paras::TestWeightInfo;
|
||||
type UnsignedPriority = ParasUnsignedPriority;
|
||||
type QueueFootprinter = ParaInclusion;
|
||||
type NextSessionRotation = TestNextSessionRotation;
|
||||
type OnNewHead = ();
|
||||
type AssignCoretime = ();
|
||||
type Fungible = Balances;
|
||||
type CooldownRemovalMultiplier = ConstUint<1>;
|
||||
type AuthorizeCurrentCodeOrigin = EnsureRoot<AccountId>;
|
||||
}
|
||||
|
||||
impl crate::dmp::Config for Test {}
|
||||
|
||||
parameter_types! {
|
||||
pub const DefaultChannelSizeAndCapacityWithSystem: (u32, u32) = (4, 1);
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
pub static VERSION_WRAPPER: RefCell<BTreeMap<Location, Option<XcmVersion>>> = RefCell::new(BTreeMap::new());
|
||||
}
|
||||
/// Mock implementation of the [`WrapVersion`] trait which wraps XCM only for known/stored XCM
|
||||
/// versions in the `VERSION_WRAPPER`.
|
||||
pub struct TestUsesOnlyStoredVersionWrapper;
|
||||
impl WrapVersion for TestUsesOnlyStoredVersionWrapper {
|
||||
fn wrap_version<RuntimeCall: Decode + GetDispatchInfo>(
|
||||
dest: &Location,
|
||||
xcm: impl Into<VersionedXcm<RuntimeCall>>,
|
||||
) -> Result<VersionedXcm<RuntimeCall>, ()> {
|
||||
match VERSION_WRAPPER.with(|r| r.borrow().get(dest).map_or(None, |v| *v)) {
|
||||
Some(v) => xcm.into().into_version(v),
|
||||
None => return Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl TestUsesOnlyStoredVersionWrapper {
|
||||
pub fn set_version(location: Location, version: Option<XcmVersion>) {
|
||||
VERSION_WRAPPER.with(|r| {
|
||||
let _ = r.borrow_mut().entry(location).and_modify(|v| *v = version).or_insert(version);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::hrmp::Config for Test {
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type ChannelManager = frame_system::EnsureRoot<u64>;
|
||||
type Currency = pallet_balances::Pallet<Test>;
|
||||
type DefaultChannelSizeAndCapacityWithSystem = DefaultChannelSizeAndCapacityWithSystem;
|
||||
type VersionWrapper = TestUsesOnlyStoredVersionWrapper;
|
||||
type WeightInfo = crate::hrmp::TestWeightInfo;
|
||||
}
|
||||
|
||||
impl crate::disputes::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type RewardValidators = Self;
|
||||
type SlashingHandler = Self;
|
||||
type WeightInfo = crate::disputes::TestWeightInfo;
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
pub static REWARD_VALIDATORS: RefCell<Vec<(SessionIndex, Vec<ValidatorIndex>)>> = RefCell::new(Vec::new());
|
||||
pub static PUNISH_VALIDATORS_FOR: RefCell<Vec<(SessionIndex, Vec<ValidatorIndex>)>> = RefCell::new(Vec::new());
|
||||
pub static PUNISH_VALIDATORS_AGAINST: RefCell<Vec<(SessionIndex, Vec<ValidatorIndex>)>> = RefCell::new(Vec::new());
|
||||
pub static PUNISH_BACKERS_FOR: RefCell<Vec<(SessionIndex, Vec<ValidatorIndex>)>> = RefCell::new(Vec::new());
|
||||
}
|
||||
|
||||
impl crate::disputes::RewardValidators for Test {
|
||||
fn reward_dispute_statement(
|
||||
session: SessionIndex,
|
||||
validators: impl IntoIterator<Item = ValidatorIndex>,
|
||||
) {
|
||||
REWARD_VALIDATORS.with(|r| r.borrow_mut().push((session, validators.into_iter().collect())))
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::disputes::SlashingHandler<BlockNumber> for Test {
|
||||
fn punish_for_invalid(
|
||||
session: SessionIndex,
|
||||
_: CandidateHash,
|
||||
losers: impl IntoIterator<Item = ValidatorIndex>,
|
||||
backers: impl IntoIterator<Item = ValidatorIndex>,
|
||||
) {
|
||||
PUNISH_VALIDATORS_FOR
|
||||
.with(|r| r.borrow_mut().push((session, losers.into_iter().collect())));
|
||||
PUNISH_BACKERS_FOR.with(|r| r.borrow_mut().push((session, backers.into_iter().collect())));
|
||||
}
|
||||
|
||||
fn punish_against_valid(
|
||||
session: SessionIndex,
|
||||
_: CandidateHash,
|
||||
losers: impl IntoIterator<Item = ValidatorIndex>,
|
||||
_backers: impl IntoIterator<Item = ValidatorIndex>,
|
||||
) {
|
||||
PUNISH_VALIDATORS_AGAINST
|
||||
.with(|r| r.borrow_mut().push((session, losers.into_iter().collect())))
|
||||
}
|
||||
|
||||
fn initializer_initialize(_now: BlockNumber) -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
|
||||
fn initializer_finalize() {}
|
||||
|
||||
fn initializer_on_new_session(_: SessionIndex) {}
|
||||
}
|
||||
|
||||
impl crate::scheduler::Config for Test {
|
||||
type AssignmentProvider = MockAssigner;
|
||||
}
|
||||
|
||||
pub struct TestMessageQueueWeight;
|
||||
impl pallet_message_queue::WeightInfo for TestMessageQueueWeight {
|
||||
fn ready_ring_knit() -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
fn ready_ring_unknit() -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
fn service_queue_base() -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
fn service_page_base_completion() -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
fn service_page_base_no_completion() -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
fn service_page_item() -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
fn set_service_head() -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
fn bump_service_head() -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
fn reap_page() -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
fn execute_overweight_page_removed() -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
fn execute_overweight_page_updated() -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
}
|
||||
parameter_types! {
|
||||
pub const MessageQueueServiceWeight: Weight = Weight::from_all(500);
|
||||
}
|
||||
|
||||
pub type MessageQueueSize = u32;
|
||||
|
||||
impl pallet_message_queue::Config for Test {
|
||||
type Size = MessageQueueSize;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type WeightInfo = TestMessageQueueWeight;
|
||||
type MessageProcessor = TestProcessMessage;
|
||||
type QueueChangeHandler = ParaInclusion;
|
||||
type QueuePausedQuery = ();
|
||||
type HeapSize = ConstU32<65536>;
|
||||
type MaxStale = ConstU32<8>;
|
||||
type ServiceWeight = MessageQueueServiceWeight;
|
||||
type IdleMaxServiceWeight = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const OnDemandTrafficDefaultValue: FixedU128 = FixedU128::from_u32(1);
|
||||
// Production chains should keep this numbar around twice the
|
||||
// defined Timeslice for Coretime.
|
||||
pub const MaxHistoricalRevenue: BlockNumber = 2 * 5;
|
||||
pub const OnDemandPalletId: PalletId = PalletId(*b"py/ondmd");
|
||||
}
|
||||
|
||||
impl on_demand::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Currency = Balances;
|
||||
type TrafficDefaultValue = OnDemandTrafficDefaultValue;
|
||||
type WeightInfo = crate::on_demand::TestWeightInfo;
|
||||
type MaxHistoricalRevenue = MaxHistoricalRevenue;
|
||||
type PalletId = OnDemandPalletId;
|
||||
}
|
||||
|
||||
impl assigner_coretime::Config for Test {}
|
||||
|
||||
parameter_types! {
|
||||
pub const BrokerId: u32 = 10u32;
|
||||
pub MaxXcmTransactWeight: Weight = Weight::from_parts(10_000_000, 10_000);
|
||||
}
|
||||
|
||||
pub struct BrokerPot;
|
||||
impl Get<InteriorLocation> for BrokerPot {
|
||||
fn get() -> InteriorLocation {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
impl coretime::Config for Test {
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type BrokerId = BrokerId;
|
||||
type WeightInfo = crate::coretime::TestWeightInfo;
|
||||
type SendXcm = DummyXcmSender;
|
||||
type MaxXcmTransactWeight = MaxXcmTransactWeight;
|
||||
type BrokerPotLocation = BrokerPot;
|
||||
type AssetTransactor = ();
|
||||
type AccountToLocation = ();
|
||||
}
|
||||
|
||||
pub struct DummyXcmSender;
|
||||
impl SendXcm for DummyXcmSender {
|
||||
type Ticket = ();
|
||||
fn validate(_: &mut Option<Location>, _: &mut Option<Xcm<()>>) -> SendResult<Self::Ticket> {
|
||||
Ok(((), Assets::new()))
|
||||
}
|
||||
|
||||
/// Actually carry out the delivery operation for a previously validated message sending.
|
||||
fn deliver(_ticket: Self::Ticket) -> Result<XcmHash, SendError> {
|
||||
Ok([0u8; 32])
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InclusionWeightInfo;
|
||||
|
||||
impl crate::inclusion::WeightInfo for InclusionWeightInfo {
|
||||
fn enact_candidate(_u: u32, _h: u32, _c: u32) -> Weight {
|
||||
Weight::from_parts(1024 * 1024, 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::inclusion::Config for Test {
|
||||
type WeightInfo = InclusionWeightInfo;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type DisputesHandler = Disputes;
|
||||
type RewardValidators = TestRewardValidators;
|
||||
type MessageQueue = MessageQueue;
|
||||
}
|
||||
|
||||
impl crate::paras_inherent::Config for Test {
|
||||
type WeightInfo = crate::paras_inherent::TestWeightInfo;
|
||||
}
|
||||
|
||||
pub struct MockValidatorSet;
|
||||
|
||||
impl ValidatorSet<AccountId> for MockValidatorSet {
|
||||
type ValidatorId = AccountId;
|
||||
type ValidatorIdOf = ValidatorIdOf;
|
||||
fn session_index() -> SessionIndex {
|
||||
0
|
||||
}
|
||||
fn validators() -> Vec<Self::ValidatorId> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ValidatorSetWithIdentification<AccountId> for MockValidatorSet {
|
||||
type Identification = ();
|
||||
type IdentificationOf = FoolIdentificationOf;
|
||||
}
|
||||
|
||||
/// A mock assigner which acts as the scheduler's `AssignmentProvider` for tests. The mock
|
||||
/// assigner provides bare minimum functionality to test scheduler internals. Since they
|
||||
/// have no direct effect on scheduler state, AssignmentProvider functions such as
|
||||
/// `push_back_assignment` can be left empty.
|
||||
pub mod mock_assigner {
|
||||
use crate::scheduler::common::Assignment;
|
||||
|
||||
use super::*;
|
||||
pub use pallet::*;
|
||||
|
||||
#[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 + paras::Config {}
|
||||
|
||||
#[pallet::storage]
|
||||
pub(super) type MockAssignmentQueue<T: Config> =
|
||||
StorageValue<_, VecDeque<Assignment>, ValueQuery>;
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Adds a claim to the `MockAssignmentQueue` this claim can later be popped by the
|
||||
/// scheduler when filling the claim queue for tests.
|
||||
pub fn add_test_assignment(assignment: Assignment) {
|
||||
MockAssignmentQueue::<T>::mutate(|queue| queue.push_back(assignment));
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> AssignmentProvider<BlockNumber> for Pallet<T> {
|
||||
// With regards to popping_assignments, the scheduler just needs to be tested under
|
||||
// the following two conditions:
|
||||
// 1. An assignment is provided
|
||||
// 2. No assignment is provided
|
||||
// A simple assignment queue populated to fit each test fulfills these needs.
|
||||
fn pop_assignment_for_core(_core_idx: CoreIndex) -> Option<Assignment> {
|
||||
let mut queue: VecDeque<Assignment> = MockAssignmentQueue::<T>::get();
|
||||
let front = queue.pop_front();
|
||||
// Write changes to storage.
|
||||
MockAssignmentQueue::<T>::set(queue);
|
||||
front
|
||||
}
|
||||
|
||||
// We don't care about core affinity in the test assigner
|
||||
fn report_processed(_: Assignment) {}
|
||||
|
||||
fn push_back_assignment(assignment: Assignment) {
|
||||
Self::add_test_assignment(assignment);
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "runtime-benchmarks", test))]
|
||||
fn get_mock_assignment(_: CoreIndex, para_id: ParaId) -> Assignment {
|
||||
Assignment::Bulk(para_id)
|
||||
}
|
||||
|
||||
fn assignment_duplicated(_: &Assignment) {}
|
||||
}
|
||||
}
|
||||
|
||||
impl mock_assigner::pallet::Config for Test {}
|
||||
|
||||
pub struct FoolIdentificationOf;
|
||||
impl sp_runtime::traits::Convert<AccountId, Option<()>> for FoolIdentificationOf {
|
||||
fn convert(_: AccountId) -> Option<()> {
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ValidatorIdOf;
|
||||
impl sp_runtime::traits::Convert<AccountId, Option<AccountId>> for ValidatorIdOf {
|
||||
fn convert(a: AccountId) -> Option<AccountId> {
|
||||
Some(a)
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::session_info::Config for Test {
|
||||
type ValidatorSet = MockValidatorSet;
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
pub static DISCOVERY_AUTHORITIES: RefCell<Vec<AuthorityDiscoveryId>> = RefCell::new(Vec::new());
|
||||
}
|
||||
|
||||
pub fn discovery_authorities() -> Vec<AuthorityDiscoveryId> {
|
||||
DISCOVERY_AUTHORITIES.with(|r| r.borrow().clone())
|
||||
}
|
||||
|
||||
pub fn set_discovery_authorities(new: Vec<AuthorityDiscoveryId>) {
|
||||
DISCOVERY_AUTHORITIES.with(|r| *r.borrow_mut() = new);
|
||||
}
|
||||
|
||||
impl crate::session_info::AuthorityDiscoveryConfig for Test {
|
||||
fn authorities() -> Vec<AuthorityDiscoveryId> {
|
||||
discovery_authorities()
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
pub static BACKING_REWARDS: RefCell<HashMap<ValidatorIndex, usize>>
|
||||
= RefCell::new(HashMap::new());
|
||||
|
||||
pub static AVAILABILITY_REWARDS: RefCell<HashMap<ValidatorIndex, usize>>
|
||||
= RefCell::new(HashMap::new());
|
||||
|
||||
pub static DISABLED_VALIDATORS: RefCell<Vec<u32>> = RefCell::new(vec![]);
|
||||
}
|
||||
|
||||
pub fn backing_rewards() -> HashMap<ValidatorIndex, usize> {
|
||||
BACKING_REWARDS.with(|r| r.borrow().clone())
|
||||
}
|
||||
|
||||
pub fn availability_rewards() -> HashMap<ValidatorIndex, usize> {
|
||||
AVAILABILITY_REWARDS.with(|r| r.borrow().clone())
|
||||
}
|
||||
|
||||
pub fn disabled_validators() -> Vec<u32> {
|
||||
DISABLED_VALIDATORS.with(|r| r.borrow().clone())
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub static Processed: Vec<(ParaId, UpwardMessage)> = vec![];
|
||||
}
|
||||
|
||||
/// An implementation of a UMP sink that just records which messages were processed.
|
||||
///
|
||||
/// A message's weight is defined by the first 4 bytes of its data, which we decode into a
|
||||
/// `u32`.
|
||||
pub struct TestProcessMessage;
|
||||
impl ProcessMessage for TestProcessMessage {
|
||||
type Origin = AggregateMessageOrigin;
|
||||
|
||||
fn process_message(
|
||||
message: &[u8],
|
||||
origin: AggregateMessageOrigin,
|
||||
meter: &mut WeightMeter,
|
||||
_id: &mut [u8; 32],
|
||||
) -> Result<bool, ProcessMessageError> {
|
||||
let para = match origin {
|
||||
AggregateMessageOrigin::Ump(UmpQueueId::Para(p)) => p,
|
||||
};
|
||||
|
||||
let required = match u32::decode(&mut &message[..]) {
|
||||
Ok(w) => Weight::from_parts(w as u64, w as u64),
|
||||
Err(_) => return Err(ProcessMessageError::Corrupt), // same as the real `ProcessMessage`
|
||||
};
|
||||
if meter.try_consume(required).is_err() {
|
||||
return Err(ProcessMessageError::Overweight(required));
|
||||
}
|
||||
|
||||
let mut processed = Processed::get();
|
||||
processed.push((para, message.to_vec()));
|
||||
Processed::set(processed);
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TestRewardValidators;
|
||||
|
||||
impl inclusion::RewardValidators for TestRewardValidators {
|
||||
fn reward_backing(v: impl IntoIterator<Item = ValidatorIndex>) {
|
||||
BACKING_REWARDS.with(|r| {
|
||||
let mut r = r.borrow_mut();
|
||||
for i in v {
|
||||
*r.entry(i).or_insert(0) += 1;
|
||||
}
|
||||
})
|
||||
}
|
||||
fn reward_bitfields(v: impl IntoIterator<Item = ValidatorIndex>) {
|
||||
AVAILABILITY_REWARDS.with(|r| {
|
||||
let mut r = r.borrow_mut();
|
||||
for i in v {
|
||||
*r.entry(i).or_insert(0) += 1;
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new set of test externalities.
|
||||
pub fn new_test_ext(state: MockGenesisConfig) -> TestExternalities {
|
||||
use sp_keystore::{testing::MemoryKeystore, KeystoreExt, KeystorePtr};
|
||||
use std::sync::Arc;
|
||||
|
||||
sp_tracing::try_init_simple();
|
||||
|
||||
BACKING_REWARDS.with(|r| r.borrow_mut().clear());
|
||||
AVAILABILITY_REWARDS.with(|r| r.borrow_mut().clear());
|
||||
|
||||
let mut t = state.system.build_storage().unwrap();
|
||||
state.configuration.assimilate_storage(&mut t).unwrap();
|
||||
state.paras.assimilate_storage(&mut t).unwrap();
|
||||
|
||||
let mut ext: TestExternalities = t.into();
|
||||
ext.register_extension(KeystoreExt(Arc::new(MemoryKeystore::new()) as KeystorePtr));
|
||||
|
||||
ext
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MockGenesisConfig {
|
||||
pub system: frame_system::GenesisConfig<Test>,
|
||||
pub configuration: crate::configuration::GenesisConfig<Test>,
|
||||
pub paras: crate::paras::GenesisConfig<Test>,
|
||||
}
|
||||
|
||||
pub fn assert_last_event(generic_event: RuntimeEvent) {
|
||||
let events = frame_system::Pallet::<Test>::events();
|
||||
let system_event: <Test as frame_system::Config>::RuntimeEvent = generic_event.into();
|
||||
// compare to the last event record
|
||||
let frame_system::EventRecord { event, .. } = &events[events.len() - 1];
|
||||
assert_eq!(event, &system_event);
|
||||
}
|
||||
|
||||
pub fn assert_last_events<E>(generic_events: E)
|
||||
where
|
||||
E: DoubleEndedIterator<Item = RuntimeEvent> + ExactSizeIterator,
|
||||
{
|
||||
for (i, (got, want)) in frame_system::Pallet::<Test>::events()
|
||||
.into_iter()
|
||||
.rev()
|
||||
.map(|e| e.event)
|
||||
.zip(generic_events.rev().map(<Test as frame_system::Config>::RuntimeEvent::from))
|
||||
.rev()
|
||||
.enumerate()
|
||||
{
|
||||
assert_eq!((i, got), (i, want));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn register_teyrchain_with_balance(id: ParaId, balance: Balance) {
|
||||
let validation_code: ValidationCode = vec![1].into();
|
||||
assert_ok!(Paras::schedule_para_initialize(
|
||||
id,
|
||||
crate::paras::ParaGenesisArgs {
|
||||
para_kind: ParaKind::Teyrchain,
|
||||
genesis_head: vec![1].into(),
|
||||
validation_code: validation_code.clone(),
|
||||
},
|
||||
));
|
||||
|
||||
assert_ok!(Paras::add_trusted_validation_code(RuntimeOrigin::root(), validation_code));
|
||||
<Test as crate::hrmp::Config>::Currency::make_free_balance_be(
|
||||
&id.into_account_truncating(),
|
||||
balance,
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn register_teyrchain(id: ParaId) {
|
||||
register_teyrchain_with_balance(id, 1000);
|
||||
}
|
||||
|
||||
pub(crate) fn deregister_teyrchain(id: ParaId) {
|
||||
assert_ok!(Paras::schedule_para_cleanup(id));
|
||||
}
|
||||
|
||||
/// Calls `schedule_para_cleanup` in a new storage transactions, since it assumes rollback on error.
|
||||
pub(crate) fn try_deregister_teyrchain(id: ParaId) -> crate::DispatchResult {
|
||||
frame_support::storage::transactional::with_storage_layer(|| Paras::schedule_para_cleanup(id))
|
||||
}
|
||||
|
||||
pub(crate) fn set_disabled_validators(disabled: Vec<u32>) {
|
||||
DISABLED_VALIDATORS.with(|d| *d.borrow_mut() = disabled)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// 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/>.
|
||||
|
||||
//! On demand assigner pallet benchmarking.
|
||||
|
||||
#![cfg(feature = "runtime-benchmarks")]
|
||||
|
||||
use super::{Pallet, *};
|
||||
use crate::{
|
||||
configuration::{HostConfiguration, Pallet as ConfigurationPallet},
|
||||
paras::{Pallet as ParasPallet, ParaGenesisArgs, ParaKind, TeyrchainsCache},
|
||||
shared::Pallet as ParasShared,
|
||||
};
|
||||
|
||||
use alloc::vec;
|
||||
use frame_benchmarking::v2::*;
|
||||
use frame_system::RawOrigin;
|
||||
use sp_runtime::traits::Bounded;
|
||||
|
||||
use pezkuwi_primitives::{
|
||||
HeadData, Id as ParaId, SessionIndex, ValidationCode, ON_DEMAND_DEFAULT_QUEUE_MAX_SIZE,
|
||||
};
|
||||
|
||||
// Constants for the benchmarking
|
||||
const SESSION_INDEX: SessionIndex = 1;
|
||||
|
||||
// Initialize a parathread for benchmarking.
|
||||
pub fn init_parathread<T>(para_id: ParaId)
|
||||
where
|
||||
T: Config + crate::paras::Config + crate::shared::Config,
|
||||
{
|
||||
ParasShared::<T>::set_session_index(SESSION_INDEX);
|
||||
let mut config = HostConfiguration::default();
|
||||
config.scheduler_params.num_cores = 1;
|
||||
ConfigurationPallet::<T>::force_set_active_config(config);
|
||||
let mut teyrchains = TeyrchainsCache::new();
|
||||
ParasPallet::<T>::initialize_para_now(
|
||||
&mut teyrchains,
|
||||
para_id,
|
||||
&ParaGenesisArgs {
|
||||
para_kind: ParaKind::Parathread,
|
||||
genesis_head: HeadData(vec![1, 2, 3, 4]),
|
||||
validation_code: ValidationCode(vec![1, 2, 3, 4]),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[benchmarks]
|
||||
mod benchmarks {
|
||||
/// We want to fill the queue to the maximum, so exactly one more item fits.
|
||||
const MAX_FILL_BENCH: u32 = ON_DEMAND_DEFAULT_QUEUE_MAX_SIZE.saturating_sub(1);
|
||||
|
||||
use super::*;
|
||||
#[benchmark]
|
||||
fn place_order_keep_alive(s: Linear<1, MAX_FILL_BENCH>) {
|
||||
// Setup
|
||||
let caller = whitelisted_caller();
|
||||
let para_id = ParaId::from(111u32);
|
||||
init_parathread::<T>(para_id);
|
||||
T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
|
||||
Pallet::<T>::populate_queue(para_id, s);
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Signed(caller.into()), BalanceOf::<T>::max_value(), para_id)
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn place_order_allow_death(s: Linear<1, MAX_FILL_BENCH>) {
|
||||
// Setup
|
||||
let caller = whitelisted_caller();
|
||||
let para_id = ParaId::from(111u32);
|
||||
init_parathread::<T>(para_id);
|
||||
T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
|
||||
|
||||
Pallet::<T>::populate_queue(para_id, s);
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Signed(caller.into()), BalanceOf::<T>::max_value(), para_id)
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn place_order_with_credits(s: Linear<1, MAX_FILL_BENCH>) {
|
||||
// Setup
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
let para_id = ParaId::from(111u32);
|
||||
init_parathread::<T>(para_id);
|
||||
Credits::<T>::insert(&caller, BalanceOf::<T>::max_value());
|
||||
|
||||
Pallet::<T>::populate_queue(para_id, s);
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Signed(caller.into()), BalanceOf::<T>::max_value(), para_id)
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(
|
||||
Pallet,
|
||||
crate::mock::new_test_ext(
|
||||
crate::on_demand::mock_helpers::GenesisConfigBuilder::default().build()
|
||||
),
|
||||
crate::mock::Test
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
// 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/>.
|
||||
|
||||
//! A module that is responsible for migration of storage.
|
||||
use super::*;
|
||||
use frame_support::{
|
||||
migrations::VersionedMigration, pallet_prelude::ValueQuery, storage_alias,
|
||||
traits::UncheckedOnRuntimeUpgrade, weights::Weight,
|
||||
};
|
||||
|
||||
mod v0 {
|
||||
use super::*;
|
||||
use alloc::collections::vec_deque::VecDeque;
|
||||
|
||||
#[derive(Encode, Decode, TypeInfo, Debug, PartialEq, Clone)]
|
||||
pub(super) struct EnqueuedOrder {
|
||||
pub para_id: ParaId,
|
||||
}
|
||||
|
||||
/// Keeps track of the multiplier used to calculate the current spot price for the on demand
|
||||
/// assigner.
|
||||
/// NOTE: Ignoring the `OnEmpty` field for the migration.
|
||||
#[storage_alias]
|
||||
pub(super) type SpotTraffic<T: Config> = StorageValue<Pallet<T>, FixedU128, ValueQuery>;
|
||||
|
||||
/// The order storage entry. Uses a VecDeque to be able to push to the front of the
|
||||
/// queue from the scheduler on session boundaries.
|
||||
/// NOTE: Ignoring the `OnEmpty` field for the migration.
|
||||
#[storage_alias]
|
||||
pub(super) type OnDemandQueue<T: Config> =
|
||||
StorageValue<Pallet<T>, VecDeque<EnqueuedOrder>, ValueQuery>;
|
||||
}
|
||||
|
||||
mod v1 {
|
||||
use super::*;
|
||||
|
||||
use crate::on_demand::LOG_TARGET;
|
||||
|
||||
/// Migration to V1
|
||||
pub struct UncheckedMigrateToV1<T>(core::marker::PhantomData<T>);
|
||||
impl<T: Config> UncheckedOnRuntimeUpgrade for UncheckedMigrateToV1<T> {
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
let mut weight: Weight = Weight::zero();
|
||||
|
||||
// Migrate the current traffic value
|
||||
let config = configuration::ActiveConfig::<T>::get();
|
||||
QueueStatus::<T>::mutate(|mut queue_status| {
|
||||
Pallet::<T>::update_spot_traffic(&config, &mut queue_status);
|
||||
|
||||
let v0_queue = v0::OnDemandQueue::<T>::take();
|
||||
// Process the v0 queue into v1.
|
||||
v0_queue.into_iter().for_each(|enqueued_order| {
|
||||
// Readding the old orders will use the new systems.
|
||||
Pallet::<T>::add_on_demand_order(
|
||||
queue_status,
|
||||
enqueued_order.para_id,
|
||||
QueuePushDirection::Back,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Remove the old storage.
|
||||
v0::OnDemandQueue::<T>::kill(); // 1 write
|
||||
v0::SpotTraffic::<T>::kill(); // 1 write
|
||||
|
||||
// Config read
|
||||
weight.saturating_accrue(T::DbWeight::get().reads(1));
|
||||
// QueueStatus read write (update_spot_traffic)
|
||||
weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
|
||||
// Kill x 2
|
||||
weight.saturating_accrue(T::DbWeight::get().writes(2));
|
||||
|
||||
log::info!(target: LOG_TARGET, "Migrated on demand assigner storage to v1");
|
||||
weight
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<alloc::vec::Vec<u8>, sp_runtime::TryRuntimeError> {
|
||||
let n: u32 = v0::OnDemandQueue::<T>::get().len() as u32;
|
||||
|
||||
log::info!(
|
||||
target: LOG_TARGET,
|
||||
"Number of orders waiting in the queue before: {n}",
|
||||
);
|
||||
|
||||
Ok(n.encode())
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(state: alloc::vec::Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
|
||||
log::info!(target: LOG_TARGET, "Running post_upgrade()");
|
||||
|
||||
ensure!(
|
||||
v0::OnDemandQueue::<T>::get().is_empty(),
|
||||
"OnDemandQueue should be empty after the migration"
|
||||
);
|
||||
|
||||
let expected_len = u32::decode(&mut &state[..]).unwrap();
|
||||
let queue_status_size = QueueStatus::<T>::get().size();
|
||||
ensure!(
|
||||
expected_len == queue_status_size,
|
||||
"Number of orders should be the same before and after migration"
|
||||
);
|
||||
|
||||
let n_affinity_entries: u32 =
|
||||
AffinityEntries::<T>::iter().map(|(_index, heap)| heap.len() as u32).sum();
|
||||
let n_para_id_affinity: u32 = ParaIdAffinity::<T>::iter()
|
||||
.map(|(_para_id, affinity)| affinity.count as u32)
|
||||
.sum();
|
||||
ensure!(
|
||||
n_para_id_affinity == n_affinity_entries,
|
||||
"Number of affinity entries should be the same as the counts in ParaIdAffinity"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Migrate `V0` to `V1` of the storage format.
|
||||
pub type MigrateV0ToV1<T> = VersionedMigration<
|
||||
0,
|
||||
1,
|
||||
v1::UncheckedMigrateToV1<T>,
|
||||
Pallet<T>,
|
||||
<T as frame_system::Config>::DbWeight,
|
||||
>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{v0, v1, UncheckedOnRuntimeUpgrade, Weight};
|
||||
use crate::mock::{new_test_ext, MockGenesisConfig, OnDemand, Test};
|
||||
use pezkuwi_primitives::Id as ParaId;
|
||||
|
||||
#[test]
|
||||
fn migration_to_v1_preserves_queue_ordering() {
|
||||
new_test_ext(MockGenesisConfig::default()).execute_with(|| {
|
||||
// Place orders for paraids 1..5
|
||||
for i in 1..=5 {
|
||||
v0::OnDemandQueue::<Test>::mutate(|queue| {
|
||||
queue.push_back(v0::EnqueuedOrder { para_id: ParaId::new(i) })
|
||||
});
|
||||
}
|
||||
|
||||
// Queue has 5 orders
|
||||
let old_queue = v0::OnDemandQueue::<Test>::get();
|
||||
assert_eq!(old_queue.len(), 5);
|
||||
// New queue has 0 orders
|
||||
assert_eq!(OnDemand::get_queue_status().size(), 0);
|
||||
|
||||
// For tests, db weight is zero.
|
||||
assert_eq!(
|
||||
<v1::UncheckedMigrateToV1<Test> as UncheckedOnRuntimeUpgrade>::on_runtime_upgrade(),
|
||||
Weight::zero()
|
||||
);
|
||||
|
||||
// New queue has 5 orders
|
||||
assert_eq!(OnDemand::get_queue_status().size(), 5);
|
||||
|
||||
// Compare each entry from the old queue with the entry in the new queue.
|
||||
old_queue.iter().zip(OnDemand::get_free_entries().iter()).for_each(
|
||||
|(old_enq, new_enq)| {
|
||||
assert_eq!(old_enq.para_id, new_enq.para_id);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// 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 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<ParaId>,
|
||||
}
|
||||
|
||||
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,813 @@
|
||||
// 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 sp_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 frame_support::{
|
||||
pallet_prelude::*,
|
||||
traits::{
|
||||
defensive_prelude::*,
|
||||
Currency,
|
||||
ExistenceRequirement::{self, AllowDeath, KeepAlive},
|
||||
WithdrawReasons,
|
||||
},
|
||||
PalletId,
|
||||
};
|
||||
use frame_system::{pallet_prelude::*, Pallet as System};
|
||||
use pezkuwi_primitives::{CoreIndex, Id as ParaId};
|
||||
use sp_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,
|
||||
}
|
||||
|
||||
#[frame_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: frame_system::Config + configuration::Config + paras::Config {
|
||||
/// The runtime's definition of an event.
|
||||
#[allow(deprecated)]
|
||||
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_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 frame_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 = <frame_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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,882 @@
|
||||
// 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::{
|
||||
initializer::SessionChangeNotification,
|
||||
mock::{
|
||||
new_test_ext, Balances, OnDemand, Paras, ParasShared, RuntimeOrigin, Scheduler, System,
|
||||
Test,
|
||||
},
|
||||
on_demand::{
|
||||
self,
|
||||
mock_helpers::GenesisConfigBuilder,
|
||||
types::{QueueIndex, ReverseQueueIndex},
|
||||
Error,
|
||||
},
|
||||
paras::{ParaGenesisArgs, ParaKind},
|
||||
};
|
||||
use core::cmp::{Ord, Ordering};
|
||||
use frame_support::{assert_noop, assert_ok};
|
||||
use pallet_balances::Error as BalancesError;
|
||||
use pezkuwi_primitives::{BlockNumber, SessionIndex, ValidationCode, ON_DEMAND_MAX_QUEUE_MAX_SIZE};
|
||||
use sp_runtime::traits::BadOrigin;
|
||||
|
||||
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(¬ification_with_session_index);
|
||||
Scheduler::initializer_on_new_session(¬ification_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 place_order_run_to_blocknumber(para_id: ParaId, blocknumber: Option<BlockNumber>) {
|
||||
let alice = 100u64;
|
||||
let amt = 10_000_000u128;
|
||||
|
||||
Balances::make_free_balance_be(&alice, amt);
|
||||
|
||||
if let Some(bn) = blocknumber {
|
||||
run_to_block(bn, |n| if n == bn { Some(Default::default()) } else { None });
|
||||
}
|
||||
#[allow(deprecated)]
|
||||
OnDemand::place_order_allow_death(RuntimeOrigin::signed(alice), amt, para_id).unwrap()
|
||||
}
|
||||
|
||||
fn place_order_run_to_101(para_id: ParaId) {
|
||||
place_order_run_to_blocknumber(para_id, Some(101));
|
||||
}
|
||||
|
||||
fn place_order(para_id: ParaId) {
|
||||
place_order_run_to_blocknumber(para_id, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spot_traffic_capacity_zero_returns_none() {
|
||||
match OnDemand::calculate_spot_traffic(
|
||||
FixedU128::from(u128::MAX),
|
||||
0u32,
|
||||
u32::MAX,
|
||||
Perbill::from_percent(100),
|
||||
Perbill::from_percent(1),
|
||||
) {
|
||||
Ok(_) => panic!("Error"),
|
||||
Err(e) => assert_eq!(e, SpotTrafficCalculationErr::QueueCapacityIsZero),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spot_traffic_queue_size_larger_than_capacity_returns_none() {
|
||||
match OnDemand::calculate_spot_traffic(
|
||||
FixedU128::from(u128::MAX),
|
||||
1u32,
|
||||
2u32,
|
||||
Perbill::from_percent(100),
|
||||
Perbill::from_percent(1),
|
||||
) {
|
||||
Ok(_) => panic!("Error"),
|
||||
Err(e) => assert_eq!(e, SpotTrafficCalculationErr::QueueSizeLargerThanCapacity),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spot_traffic_calculation_identity() {
|
||||
match OnDemand::calculate_spot_traffic(
|
||||
FixedU128::from_u32(1),
|
||||
1000,
|
||||
100,
|
||||
Perbill::from_percent(10),
|
||||
Perbill::from_percent(3),
|
||||
) {
|
||||
Ok(res) => {
|
||||
assert_eq!(res, FixedU128::from_u32(1))
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spot_traffic_calculation_u32_max() {
|
||||
match OnDemand::calculate_spot_traffic(
|
||||
FixedU128::from_u32(1),
|
||||
u32::MAX,
|
||||
u32::MAX,
|
||||
Perbill::from_percent(100),
|
||||
Perbill::from_percent(3),
|
||||
) {
|
||||
Ok(res) => {
|
||||
assert_eq!(res, FixedU128::from_u32(1))
|
||||
},
|
||||
_ => panic!("Error"),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spot_traffic_calculation_u32_traffic_max() {
|
||||
match OnDemand::calculate_spot_traffic(
|
||||
FixedU128::from(u128::MAX),
|
||||
u32::MAX,
|
||||
u32::MAX,
|
||||
Perbill::from_percent(1),
|
||||
Perbill::from_percent(1),
|
||||
) {
|
||||
Ok(res) => assert_eq!(res, FixedU128::from(u128::MAX)),
|
||||
_ => panic!("Error"),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sustained_target_increases_spot_traffic() {
|
||||
let mut traffic = FixedU128::from_u32(1u32);
|
||||
for _ in 0..50 {
|
||||
traffic = OnDemand::calculate_spot_traffic(
|
||||
traffic,
|
||||
100,
|
||||
12,
|
||||
Perbill::from_percent(10),
|
||||
Perbill::from_percent(100),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
assert_eq!(traffic, FixedU128::from_inner(2_718_103_312_071_174_015u128))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spot_traffic_can_decrease() {
|
||||
let traffic = FixedU128::from_u32(100u32);
|
||||
match OnDemand::calculate_spot_traffic(
|
||||
traffic,
|
||||
100u32,
|
||||
0u32,
|
||||
Perbill::from_percent(100),
|
||||
Perbill::from_percent(100),
|
||||
) {
|
||||
Ok(new_traffic) => {
|
||||
assert_eq!(new_traffic, FixedU128::from_inner(50_000_000_000_000_000_000u128))
|
||||
},
|
||||
_ => panic!("Error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spot_traffic_decreases_over_time() {
|
||||
let mut traffic = FixedU128::from_u32(100u32);
|
||||
for _ in 0..5 {
|
||||
traffic = OnDemand::calculate_spot_traffic(
|
||||
traffic,
|
||||
100u32,
|
||||
0u32,
|
||||
Perbill::from_percent(100),
|
||||
Perbill::from_percent(100),
|
||||
)
|
||||
.unwrap();
|
||||
println!("{traffic}");
|
||||
}
|
||||
assert_eq!(traffic, FixedU128::from_inner(3_125_000_000_000_000_000u128))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spot_traffic_decreases_between_idle_blocks() {
|
||||
// Testing spot traffic assumptions, but using the mock runtime and default on demand
|
||||
// configuration values. Ensuring that blocks with no on demand activity (idle)
|
||||
// decrease traffic.
|
||||
|
||||
let para_id = ParaId::from(111);
|
||||
|
||||
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
|
||||
// Initialize the parathread and wait for it to be ready.
|
||||
schedule_blank_para(para_id, ParaKind::Parathread);
|
||||
assert!(!Paras::is_parathread(para_id));
|
||||
run_to_block(100, |n| if n == 100 { Some(Default::default()) } else { None });
|
||||
assert!(Paras::is_parathread(para_id));
|
||||
|
||||
// Set the spot traffic to a large number
|
||||
OnDemand::set_queue_status(QueueStatusType {
|
||||
traffic: FixedU128::from_u32(10),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
assert_eq!(OnDemand::get_queue_status().traffic, FixedU128::from_u32(10));
|
||||
|
||||
// Run to block 101 and ensure that the traffic decreases.
|
||||
run_to_block(101, |n| if n == 100 { Some(Default::default()) } else { None });
|
||||
assert!(OnDemand::get_queue_status().traffic < FixedU128::from_u32(10));
|
||||
|
||||
// Run to block 102 and observe that we've hit the default traffic value.
|
||||
run_to_block(102, |n| if n == 100 { Some(Default::default()) } else { None });
|
||||
assert_eq!(OnDemand::get_queue_status().traffic, OnDemand::get_traffic_default_value());
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(deprecated)]
|
||||
fn place_order_works() {
|
||||
let alice = 1u64;
|
||||
let amt = 10_000_000u128;
|
||||
let para_id = ParaId::from(111);
|
||||
|
||||
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
|
||||
// Initialize the parathread and wait for it to be ready.
|
||||
schedule_blank_para(para_id, ParaKind::Parathread);
|
||||
|
||||
assert!(!Paras::is_parathread(para_id));
|
||||
|
||||
run_to_block(100, |n| if n == 100 { Some(Default::default()) } else { None });
|
||||
|
||||
assert!(Paras::is_parathread(para_id));
|
||||
|
||||
// Does not work unsigned
|
||||
assert_noop!(
|
||||
OnDemand::place_order_allow_death(RuntimeOrigin::none(), amt, para_id),
|
||||
BadOrigin
|
||||
);
|
||||
|
||||
// Does not work with max_amount lower than fee
|
||||
let low_max_amt = 1u128;
|
||||
assert_noop!(
|
||||
OnDemand::place_order_allow_death(RuntimeOrigin::signed(alice), low_max_amt, para_id,),
|
||||
Error::<Test>::SpotPriceHigherThanMaxAmount,
|
||||
);
|
||||
|
||||
// Does not work with insufficient balance
|
||||
assert_noop!(
|
||||
OnDemand::place_order_allow_death(RuntimeOrigin::signed(alice), amt, para_id),
|
||||
BalancesError::<Test, _>::InsufficientBalance
|
||||
);
|
||||
|
||||
// Works
|
||||
Balances::make_free_balance_be(&alice, amt);
|
||||
run_to_block(101, |n| if n == 101 { Some(Default::default()) } else { None });
|
||||
assert_ok!(OnDemand::place_order_allow_death(RuntimeOrigin::signed(alice), amt, para_id));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(deprecated)]
|
||||
fn place_order_keep_alive_keeps_alive() {
|
||||
let alice = 1u64;
|
||||
let amt = 1u128; // The same as crate::mock's EXISTENTIAL_DEPOSIT
|
||||
let max_amt = 10_000_000u128;
|
||||
let para_id = ParaId::from(111);
|
||||
|
||||
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
|
||||
let config = configuration::ActiveConfig::<Test>::get();
|
||||
|
||||
// Initialize the parathread and wait for it to be ready.
|
||||
schedule_blank_para(para_id, ParaKind::Parathread);
|
||||
Balances::make_free_balance_be(&alice, amt);
|
||||
|
||||
assert!(!Paras::is_parathread(para_id));
|
||||
run_to_block(100, |n| if n == 100 { Some(Default::default()) } else { None });
|
||||
assert!(Paras::is_parathread(para_id));
|
||||
|
||||
assert_noop!(
|
||||
OnDemand::place_order_keep_alive(RuntimeOrigin::signed(alice), max_amt, para_id),
|
||||
BalancesError::<Test, _>::InsufficientBalance
|
||||
);
|
||||
|
||||
Balances::make_free_balance_be(&alice, max_amt);
|
||||
assert_ok!(OnDemand::place_order_keep_alive(
|
||||
RuntimeOrigin::signed(alice),
|
||||
max_amt,
|
||||
para_id
|
||||
),);
|
||||
|
||||
let queue_status = QueueStatus::<Test>::get();
|
||||
let spot_price = queue_status.traffic.saturating_mul_int(
|
||||
config.scheduler_params.on_demand_base_fee.saturated_into::<BalanceOf<Test>>(),
|
||||
);
|
||||
assert_eq!(Balances::free_balance(&alice), max_amt.saturating_sub(spot_price));
|
||||
assert_eq!(
|
||||
FreeEntries::<Test>::get().pop(),
|
||||
Some(EnqueuedOrder::new(QueueIndex(0), para_id))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn place_order_with_credits() {
|
||||
let alice = 1u64;
|
||||
let initial_credit = 10_000_000u128;
|
||||
let para_id = ParaId::from(111);
|
||||
|
||||
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
|
||||
let config = configuration::ActiveConfig::<Test>::get();
|
||||
|
||||
// Initialize the parathread and wait for it to be ready.
|
||||
schedule_blank_para(para_id, ParaKind::Parathread);
|
||||
OnDemand::credit_account(alice, initial_credit);
|
||||
assert_eq!(Credits::<Test>::get(alice), initial_credit);
|
||||
|
||||
assert!(!Paras::is_parathread(para_id));
|
||||
run_to_block(100, |n| if n == 100 { Some(Default::default()) } else { None });
|
||||
assert!(Paras::is_parathread(para_id));
|
||||
|
||||
let queue_status = QueueStatus::<Test>::get();
|
||||
let spot_price = queue_status.traffic.saturating_mul_int(
|
||||
config.scheduler_params.on_demand_base_fee.saturated_into::<BalanceOf<Test>>(),
|
||||
);
|
||||
|
||||
// Create an order and pay for it with credits.
|
||||
assert_ok!(OnDemand::place_order_with_credits(
|
||||
RuntimeOrigin::signed(alice),
|
||||
initial_credit,
|
||||
para_id
|
||||
));
|
||||
assert_eq!(Credits::<Test>::get(alice), initial_credit.saturating_sub(spot_price));
|
||||
assert_eq!(
|
||||
FreeEntries::<Test>::get().pop(),
|
||||
Some(EnqueuedOrder::new(QueueIndex(0), para_id))
|
||||
);
|
||||
|
||||
// Insufficient credits:
|
||||
Credits::<Test>::insert(alice, 1u128);
|
||||
assert_noop!(
|
||||
OnDemand::place_order_with_credits(
|
||||
RuntimeOrigin::signed(alice),
|
||||
1_000_000u128,
|
||||
para_id
|
||||
),
|
||||
Error::<Test>::InsufficientCredits
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pop_assignment_for_core_works() {
|
||||
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
|
||||
let para_a = ParaId::from(111);
|
||||
let para_b = ParaId::from(110);
|
||||
schedule_blank_para(para_a, ParaKind::Parathread);
|
||||
schedule_blank_para(para_b, ParaKind::Parathread);
|
||||
|
||||
run_to_block(11, |n| if n == 11 { Some(Default::default()) } else { None });
|
||||
|
||||
// Pop should return none with empty queue
|
||||
assert_eq!(OnDemand::pop_assignment_for_core(CoreIndex(0)), None);
|
||||
|
||||
// Add enough assignments to the order queue.
|
||||
for _ in 0..2 {
|
||||
place_order(para_a);
|
||||
place_order(para_b);
|
||||
}
|
||||
|
||||
// Popped assignments should be for the correct paras and cores
|
||||
assert_eq!(
|
||||
OnDemand::pop_assignment_for_core(CoreIndex(0)).map(|a| a.para_id()),
|
||||
Some(para_a)
|
||||
);
|
||||
assert_eq!(
|
||||
OnDemand::pop_assignment_for_core(CoreIndex(1)).map(|a| a.para_id()),
|
||||
Some(para_b)
|
||||
);
|
||||
assert_eq!(
|
||||
OnDemand::pop_assignment_for_core(CoreIndex(0)).map(|a| a.para_id()),
|
||||
Some(para_a)
|
||||
);
|
||||
assert_eq!(
|
||||
OnDemand::pop_assignment_for_core(CoreIndex(1)).map(|a| a.para_id()),
|
||||
Some(para_b)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_back_assignment_works() {
|
||||
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
|
||||
let para_a = ParaId::from(111);
|
||||
let para_b = ParaId::from(110);
|
||||
schedule_blank_para(para_a, ParaKind::Parathread);
|
||||
schedule_blank_para(para_b, ParaKind::Parathread);
|
||||
|
||||
run_to_block(11, |n| if n == 11 { Some(Default::default()) } else { None });
|
||||
|
||||
// Add enough assignments to the order queue.
|
||||
place_order_run_to_101(para_a);
|
||||
place_order_run_to_101(para_b);
|
||||
|
||||
// Pop order a
|
||||
assert_eq!(OnDemand::pop_assignment_for_core(CoreIndex(0)).unwrap().para_id(), para_a);
|
||||
|
||||
// Para a should have affinity for core 0
|
||||
assert_eq!(OnDemand::get_affinity_map(para_a).unwrap().count, 1);
|
||||
assert_eq!(OnDemand::get_affinity_map(para_a).unwrap().core_index, CoreIndex(0));
|
||||
|
||||
// Push back order a
|
||||
OnDemand::push_back_assignment(para_a, CoreIndex(0));
|
||||
|
||||
// Para a should have no affinity
|
||||
assert_eq!(OnDemand::get_affinity_map(para_a).is_none(), true);
|
||||
|
||||
// Queue should contain orders a, b. A in front of b.
|
||||
assert_eq!(OnDemand::pop_assignment_for_core(CoreIndex(0)).unwrap().para_id(), para_a);
|
||||
assert_eq!(OnDemand::pop_assignment_for_core(CoreIndex(0)).unwrap().para_id(), para_b);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn affinity_prohibits_parallel_scheduling() {
|
||||
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
|
||||
let para_a = ParaId::from(111);
|
||||
let para_b = ParaId::from(222);
|
||||
|
||||
schedule_blank_para(para_a, ParaKind::Parathread);
|
||||
schedule_blank_para(para_b, ParaKind::Parathread);
|
||||
|
||||
run_to_block(11, |n| if n == 11 { Some(Default::default()) } else { None });
|
||||
|
||||
// There should be no affinity before starting.
|
||||
assert!(OnDemand::get_affinity_map(para_a).is_none());
|
||||
assert!(OnDemand::get_affinity_map(para_b).is_none());
|
||||
|
||||
// Add 2 assignments for para_a for every para_b.
|
||||
place_order_run_to_101(para_a);
|
||||
place_order_run_to_101(para_a);
|
||||
place_order_run_to_101(para_b);
|
||||
|
||||
// Approximate having 1 core.
|
||||
for _ in 0..3 {
|
||||
assert!(OnDemand::pop_assignment_for_core(CoreIndex(0)).is_some());
|
||||
}
|
||||
assert!(OnDemand::pop_assignment_for_core(CoreIndex(0)).is_none());
|
||||
|
||||
// Affinity on one core is meaningless.
|
||||
assert_eq!(OnDemand::get_affinity_map(para_a).unwrap().count, 2);
|
||||
assert_eq!(OnDemand::get_affinity_map(para_b).unwrap().count, 1);
|
||||
assert_eq!(
|
||||
OnDemand::get_affinity_map(para_a).unwrap().core_index,
|
||||
OnDemand::get_affinity_map(para_b).unwrap().core_index,
|
||||
);
|
||||
|
||||
// Clear affinity
|
||||
OnDemand::report_processed(para_a, 0.into());
|
||||
OnDemand::report_processed(para_a, 0.into());
|
||||
OnDemand::report_processed(para_b, 0.into());
|
||||
|
||||
// Add 2 assignments for para_a for every para_b.
|
||||
place_order_run_to_101(para_a);
|
||||
place_order_run_to_101(para_a);
|
||||
place_order_run_to_101(para_b);
|
||||
|
||||
// Approximate having 3 cores. CoreIndex 2 should be unable to obtain an assignment
|
||||
for _ in 0..3 {
|
||||
OnDemand::pop_assignment_for_core(CoreIndex(0));
|
||||
OnDemand::pop_assignment_for_core(CoreIndex(1));
|
||||
assert!(OnDemand::pop_assignment_for_core(CoreIndex(2)).is_none());
|
||||
}
|
||||
|
||||
// Affinity should be the same as before, but on different cores.
|
||||
assert_eq!(OnDemand::get_affinity_map(para_a).unwrap().count, 2);
|
||||
assert_eq!(OnDemand::get_affinity_map(para_b).unwrap().count, 1);
|
||||
assert_eq!(OnDemand::get_affinity_map(para_a).unwrap().core_index, CoreIndex(0));
|
||||
assert_eq!(OnDemand::get_affinity_map(para_b).unwrap().core_index, CoreIndex(1));
|
||||
|
||||
// Clear affinity
|
||||
OnDemand::report_processed(para_a, CoreIndex(0));
|
||||
OnDemand::report_processed(para_a, CoreIndex(0));
|
||||
OnDemand::report_processed(para_b, CoreIndex(1));
|
||||
|
||||
// There should be no affinity after clearing.
|
||||
assert!(OnDemand::get_affinity_map(para_a).is_none());
|
||||
assert!(OnDemand::get_affinity_map(para_b).is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn affinity_changes_work() {
|
||||
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
|
||||
let para_a = ParaId::from(111);
|
||||
let core_index = CoreIndex(0);
|
||||
schedule_blank_para(para_a, ParaKind::Parathread);
|
||||
|
||||
run_to_block(11, |n| if n == 11 { Some(Default::default()) } else { None });
|
||||
|
||||
// There should be no affinity before starting.
|
||||
assert!(OnDemand::get_affinity_map(para_a).is_none());
|
||||
|
||||
// Add enough assignments to the order queue.
|
||||
for _ in 0..10 {
|
||||
place_order_run_to_101(para_a);
|
||||
}
|
||||
|
||||
// There should be no affinity before the scheduler pops.
|
||||
assert!(OnDemand::get_affinity_map(para_a).is_none());
|
||||
|
||||
OnDemand::pop_assignment_for_core(core_index);
|
||||
|
||||
// Affinity count is 1 after popping.
|
||||
assert_eq!(OnDemand::get_affinity_map(para_a).unwrap().count, 1);
|
||||
|
||||
OnDemand::report_processed(para_a, 0.into());
|
||||
OnDemand::pop_assignment_for_core(core_index);
|
||||
|
||||
// Affinity count is 1 after popping with a previous para.
|
||||
assert_eq!(OnDemand::get_affinity_map(para_a).unwrap().count, 1);
|
||||
|
||||
for _ in 0..3 {
|
||||
OnDemand::pop_assignment_for_core(core_index);
|
||||
}
|
||||
|
||||
// Affinity count is 4 after popping 3 times without a previous para.
|
||||
assert_eq!(OnDemand::get_affinity_map(para_a).unwrap().count, 4);
|
||||
|
||||
for _ in 0..5 {
|
||||
OnDemand::report_processed(para_a, 0.into());
|
||||
assert!(OnDemand::pop_assignment_for_core(core_index).is_some());
|
||||
}
|
||||
|
||||
// Affinity count should still be 4 but queue should be empty.
|
||||
assert!(OnDemand::pop_assignment_for_core(core_index).is_none());
|
||||
assert_eq!(OnDemand::get_affinity_map(para_a).unwrap().count, 4);
|
||||
|
||||
// Pop 4 times and get to exactly 0 (None) affinity.
|
||||
for _ in 0..4 {
|
||||
OnDemand::report_processed(para_a, 0.into());
|
||||
assert!(OnDemand::pop_assignment_for_core(core_index).is_none());
|
||||
}
|
||||
assert!(OnDemand::get_affinity_map(para_a).is_none());
|
||||
|
||||
// Decreasing affinity beyond 0 should still be None.
|
||||
OnDemand::report_processed(para_a, 0.into());
|
||||
assert!(OnDemand::pop_assignment_for_core(core_index).is_none());
|
||||
assert!(OnDemand::get_affinity_map(para_a).is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_affinity_for_a_core_must_come_from_free_entries() {
|
||||
// If affinity count for a core was zero before, and is 1 now, then the entry
|
||||
// must have come from free_entries.
|
||||
let teyrchains =
|
||||
vec![ParaId::from(111), ParaId::from(222), ParaId::from(333), ParaId::from(444)];
|
||||
let core_indices = vec![CoreIndex(0), CoreIndex(1), CoreIndex(2), CoreIndex(3)];
|
||||
|
||||
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
|
||||
teyrchains.iter().for_each(|chain| {
|
||||
schedule_blank_para(*chain, ParaKind::Parathread);
|
||||
});
|
||||
|
||||
run_to_block(11, |n| if n == 11 { Some(Default::default()) } else { None });
|
||||
|
||||
// Place orders for all chains.
|
||||
teyrchains.iter().for_each(|chain| {
|
||||
place_order_run_to_101(*chain);
|
||||
});
|
||||
|
||||
// There are 4 entries in free_entries.
|
||||
let start_free_entries = OnDemand::get_free_entries().len();
|
||||
assert_eq!(start_free_entries, 4);
|
||||
|
||||
// Pop assignments on all cores.
|
||||
core_indices.iter().enumerate().for_each(|(n, core_index)| {
|
||||
// There is no affinity on the core prior to popping.
|
||||
assert!(OnDemand::get_affinity_entries(*core_index).is_empty());
|
||||
|
||||
// There's always an order to be popped for each core.
|
||||
let free_entries = OnDemand::get_free_entries();
|
||||
let next_order = free_entries.peek();
|
||||
|
||||
// There is no affinity on the paraid prior to popping.
|
||||
assert!(OnDemand::get_affinity_map(next_order.unwrap().para_id).is_none());
|
||||
|
||||
match OnDemand::pop_assignment_for_core(*core_index) {
|
||||
Some(assignment) => {
|
||||
// The popped assignment came from free entries.
|
||||
assert_eq!(start_free_entries - 1 - n, OnDemand::get_free_entries().len());
|
||||
// The popped assignment has the same para id as the next order.
|
||||
assert_eq!(assignment.para_id(), next_order.unwrap().para_id);
|
||||
},
|
||||
None => panic!("Should not happen"),
|
||||
}
|
||||
});
|
||||
|
||||
// All entries have been removed from free_entries.
|
||||
assert!(OnDemand::get_free_entries().is_empty());
|
||||
|
||||
// All chains have an affinity count of 1.
|
||||
teyrchains.iter().for_each(|chain| {
|
||||
assert_eq!(OnDemand::get_affinity_map(*chain).unwrap().count, 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn queue_index_ordering_is_unsound_over_max_size() {
|
||||
// NOTE: Unsoundness proof. If the number goes sufficiently over the max_queue_max_size
|
||||
// the overflow will cause an opposite comparison to what would be expected.
|
||||
let max_num = u32::MAX - ON_DEMAND_MAX_QUEUE_MAX_SIZE;
|
||||
// 0 < some large number.
|
||||
assert_eq!(QueueIndex(0).cmp(&QueueIndex(max_num + 1)), Ordering::Less);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queue_index_ordering_works() {
|
||||
// The largest accepted queue size.
|
||||
let max_num = ON_DEMAND_MAX_QUEUE_MAX_SIZE;
|
||||
|
||||
// 0 == 0
|
||||
assert_eq!(QueueIndex(0).cmp(&QueueIndex(0)), Ordering::Equal);
|
||||
// 0 < 1
|
||||
assert_eq!(QueueIndex(0).cmp(&QueueIndex(1)), Ordering::Less);
|
||||
// 1 > 0
|
||||
assert_eq!(QueueIndex(1).cmp(&QueueIndex(0)), Ordering::Greater);
|
||||
// 0 < max_num
|
||||
assert_eq!(QueueIndex(0).cmp(&QueueIndex(max_num)), Ordering::Less);
|
||||
// 0 > max_num + 1
|
||||
assert_eq!(QueueIndex(0).cmp(&QueueIndex(max_num + 1)), Ordering::Less);
|
||||
|
||||
// Ordering within the bounds of ON_DEMAND_MAX_QUEUE_MAX_SIZE works.
|
||||
let mut v = vec![3, 6, 2, 1, 5, 4];
|
||||
v.sort_by_key(|&num| QueueIndex(num));
|
||||
assert_eq!(v, vec![1, 2, 3, 4, 5, 6]);
|
||||
|
||||
v = vec![max_num, 4, 5, 1, 6];
|
||||
v.sort_by_key(|&num| QueueIndex(num));
|
||||
assert_eq!(v, vec![1, 4, 5, 6, max_num]);
|
||||
|
||||
// Ordering with an element outside of the bounds of the max size also works.
|
||||
v = vec![max_num + 2, 0, 6, 2, 1, 5, 4];
|
||||
v.sort_by_key(|&num| QueueIndex(num));
|
||||
assert_eq!(v, vec![0, 1, 2, 4, 5, 6, max_num + 2]);
|
||||
|
||||
// Numbers way above the max size will overflow
|
||||
v = vec![u32::MAX - 1, u32::MAX, 6, 2, 1, 5, 4];
|
||||
v.sort_by_key(|&num| QueueIndex(num));
|
||||
assert_eq!(v, vec![u32::MAX - 1, u32::MAX, 1, 2, 4, 5, 6]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reverse_queue_index_does_reverse() {
|
||||
let mut v = vec![1, 2, 3, 4, 5, 6];
|
||||
|
||||
// Basic reversal of a vector.
|
||||
v.sort_by_key(|&num| ReverseQueueIndex(num));
|
||||
assert_eq!(v, vec![6, 5, 4, 3, 2, 1]);
|
||||
|
||||
// Example from rust docs on `Reverse`. Should work identically.
|
||||
v.sort_by_key(|&num| (num > 3, ReverseQueueIndex(num)));
|
||||
assert_eq!(v, vec![3, 2, 1, 6, 5, 4]);
|
||||
|
||||
let mut v2 = vec![1, 2, u32::MAX];
|
||||
v2.sort_by_key(|&num| ReverseQueueIndex(num));
|
||||
assert_eq!(v2, vec![2, 1, u32::MAX]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queue_status_size_fn_works() {
|
||||
// Add orders to the on demand queue, and make sure that they are properly represented
|
||||
// by the QueueStatusType::size fn.
|
||||
let teyrchains = vec![ParaId::from(111), ParaId::from(222), ParaId::from(333)];
|
||||
let core_indices = vec![CoreIndex(0), CoreIndex(1)];
|
||||
|
||||
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
|
||||
teyrchains.iter().for_each(|chain| {
|
||||
schedule_blank_para(*chain, ParaKind::Parathread);
|
||||
});
|
||||
|
||||
assert_eq!(OnDemand::get_queue_status().size(), 0);
|
||||
|
||||
run_to_block(11, |n| if n == 11 { Some(Default::default()) } else { None });
|
||||
|
||||
// Place orders for all chains.
|
||||
teyrchains.iter().for_each(|chain| {
|
||||
// 2 per chain for a total of 6
|
||||
place_order_run_to_101(*chain);
|
||||
place_order_run_to_101(*chain);
|
||||
});
|
||||
|
||||
// 6 orders in free entries
|
||||
assert_eq!(OnDemand::get_free_entries().len(), 6);
|
||||
// 6 orders via queue status size
|
||||
assert_eq!(
|
||||
OnDemand::get_free_entries().len(),
|
||||
OnDemand::get_queue_status().size() as usize
|
||||
);
|
||||
|
||||
core_indices.iter().for_each(|core_index| {
|
||||
OnDemand::pop_assignment_for_core(*core_index);
|
||||
});
|
||||
|
||||
// There should be 2 orders in the scheduler's claimqueue,
|
||||
// 2 in assorted AffinityMaps and 2 in free.
|
||||
// ParaId 111
|
||||
assert_eq!(OnDemand::get_affinity_entries(core_indices[0]).len(), 1);
|
||||
// ParaId 222
|
||||
assert_eq!(OnDemand::get_affinity_entries(core_indices[1]).len(), 1);
|
||||
// Free entries are from ParaId 333
|
||||
assert_eq!(OnDemand::get_free_entries().len(), 2);
|
||||
// For a total size of 4.
|
||||
assert_eq!(OnDemand::get_queue_status().size(), 4)
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revenue_information_fetching_works() {
|
||||
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
|
||||
let para_a = ParaId::from(111);
|
||||
schedule_blank_para(para_a, ParaKind::Parathread);
|
||||
// Mock assigner sets max revenue history to 10.
|
||||
run_to_block(10, |n| if n == 10 { Some(Default::default()) } else { None });
|
||||
let revenue = OnDemand::claim_revenue_until(10);
|
||||
|
||||
// No revenue should be recorded.
|
||||
assert_eq!(revenue, 0);
|
||||
|
||||
// Place one order
|
||||
place_order_run_to_blocknumber(para_a, Some(11));
|
||||
let revenue = OnDemand::get_revenue();
|
||||
let amt = OnDemand::claim_revenue_until(11);
|
||||
|
||||
// Revenue until the current block is still zero as "until" is non-inclusive
|
||||
assert_eq!(amt, 0);
|
||||
|
||||
let amt = OnDemand::claim_revenue_until(12);
|
||||
|
||||
// Revenue for a single order should be recorded and shouldn't have been pruned by the
|
||||
// previous call
|
||||
assert_eq!(amt, revenue[0]);
|
||||
|
||||
run_to_block(12, |n| if n == 12 { Some(Default::default()) } else { None });
|
||||
let revenue = OnDemand::claim_revenue_until(13);
|
||||
|
||||
// No revenue should be recorded.
|
||||
assert_eq!(revenue, 0);
|
||||
|
||||
// Place many orders
|
||||
place_order(para_a);
|
||||
place_order(para_a);
|
||||
|
||||
run_to_block(13, |n| if n == 13 { Some(Default::default()) } else { None });
|
||||
|
||||
place_order(para_a);
|
||||
|
||||
run_to_block(14, |n| if n == 14 { Some(Default::default()) } else { None });
|
||||
|
||||
let revenue = OnDemand::claim_revenue_until(15);
|
||||
|
||||
// All 3 orders should be accounted for.
|
||||
assert_eq!(revenue, 30_000);
|
||||
|
||||
// Place one order
|
||||
place_order_run_to_blocknumber(para_a, Some(16));
|
||||
|
||||
let revenue = OnDemand::claim_revenue_until(15);
|
||||
|
||||
// Order is not in range of the revenue_until call
|
||||
assert_eq!(revenue, 0);
|
||||
|
||||
run_to_block(20, |n| if n == 20 { Some(Default::default()) } else { None });
|
||||
let revenue = OnDemand::claim_revenue_until(21);
|
||||
assert_eq!(revenue, 10_000);
|
||||
|
||||
// Make sure overdue revenue is accumulated
|
||||
for i in 21..=35 {
|
||||
run_to_block(i, |n| if n % 10 == 0 { Some(Default::default()) } else { None });
|
||||
place_order(para_a);
|
||||
}
|
||||
let revenue = OnDemand::claim_revenue_until(36);
|
||||
assert_eq!(revenue, 150_000);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pot_account_is_immortal() {
|
||||
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
|
||||
let para_a = ParaId::from(111);
|
||||
let pot = OnDemand::account_id();
|
||||
assert!(!System::account_exists(&pot));
|
||||
schedule_blank_para(para_a, ParaKind::Parathread);
|
||||
// Mock assigner sets max revenue history to 10.
|
||||
|
||||
run_to_block(10, |n| if n == 10 { Some(Default::default()) } else { None });
|
||||
place_order_run_to_blocknumber(para_a, Some(12));
|
||||
let purchase_revenue = Balances::free_balance(&pot);
|
||||
assert!(purchase_revenue > 0);
|
||||
|
||||
run_to_block(15, |_| None);
|
||||
let _imb = <Test as on_demand::Config>::Currency::withdraw(
|
||||
&pot,
|
||||
purchase_revenue,
|
||||
WithdrawReasons::FEE,
|
||||
ExistenceRequirement::AllowDeath,
|
||||
);
|
||||
assert_eq!(Balances::free_balance(&pot), 0);
|
||||
assert!(System::account_exists(&pot));
|
||||
assert_eq!(System::providers(&pot), 1);
|
||||
|
||||
// One more cycle to make sure providers are not increased on every transition from zero
|
||||
run_to_block(20, |n| if n == 20 { Some(Default::default()) } else { None });
|
||||
place_order_run_to_blocknumber(para_a, Some(22));
|
||||
let purchase_revenue = Balances::free_balance(&pot);
|
||||
assert!(purchase_revenue > 0);
|
||||
|
||||
run_to_block(25, |_| None);
|
||||
let _imb = <Test as on_demand::Config>::Currency::withdraw(
|
||||
&pot,
|
||||
purchase_revenue,
|
||||
WithdrawReasons::FEE,
|
||||
ExistenceRequirement::AllowDeath,
|
||||
);
|
||||
assert_eq!(Balances::free_balance(&pot), 0);
|
||||
assert!(System::account_exists(&pot));
|
||||
assert_eq!(System::providers(&pot), 1);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
// 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/>.
|
||||
|
||||
//! On demand module types.
|
||||
|
||||
use super::{alloc, pallet::Config};
|
||||
use alloc::collections::BinaryHeap;
|
||||
use core::cmp::{Ord, Ordering, PartialOrd};
|
||||
use frame_support::{
|
||||
pallet_prelude::{Decode, Encode, RuntimeDebug, TypeInfo},
|
||||
traits::Currency,
|
||||
};
|
||||
use pezkuwi_primitives::{CoreIndex, Id as ParaId, ON_DEMAND_MAX_QUEUE_MAX_SIZE};
|
||||
use sp_runtime::FixedU128;
|
||||
|
||||
/// Shorthand for the Balance type the runtime is using.
|
||||
pub type BalanceOf<T> =
|
||||
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
|
||||
|
||||
/// Meta data for full queue.
|
||||
///
|
||||
/// This includes elements with affinity and free entries.
|
||||
///
|
||||
/// The actual queue is implemented via multiple priority queues. One for each core, for entries
|
||||
/// which currently have a core affinity and one free queue, with entries without any affinity yet.
|
||||
///
|
||||
/// The design aims to have most queue accessess be O(1) or O(log(N)). Absolute worst case is O(N).
|
||||
/// Importantly this includes all accessess that happen in a single block. Even with 50 cores, the
|
||||
/// total complexity of all operations in the block should maintain above complexities. In
|
||||
/// particular O(N) stays O(N), it should never be O(N*cores).
|
||||
///
|
||||
/// More concrete rundown on complexity:
|
||||
///
|
||||
/// - insert: O(1) for placing an order, O(log(N)) for push backs.
|
||||
/// - pop_assignment_for_core: O(log(N)), O(N) worst case: Can only happen for one core, next core
|
||||
/// is already less work.
|
||||
/// - report_processed & push back: If affinity dropped to 0, then O(N) in the worst case. Again
|
||||
/// this divides per core.
|
||||
///
|
||||
/// Reads still exist, also improved slightly, but worst case we fetch all entries.
|
||||
#[derive(Encode, Decode, TypeInfo)]
|
||||
pub struct QueueStatusType {
|
||||
/// Last calculated traffic value.
|
||||
pub traffic: FixedU128,
|
||||
/// The next index to use.
|
||||
pub next_index: QueueIndex,
|
||||
/// Smallest index still in use.
|
||||
///
|
||||
/// In case of a completely empty queue (free + affinity queues), `next_index - smallest_index
|
||||
/// == 0`.
|
||||
pub smallest_index: QueueIndex,
|
||||
/// Indices that have been freed already.
|
||||
///
|
||||
/// But have a hole to `smallest_index`, so we can not yet bump `smallest_index`. This binary
|
||||
/// heap is roughly bounded in the number of on demand cores:
|
||||
///
|
||||
/// For a single core, elements will always be processed in order. With each core added, a
|
||||
/// level of out of order execution is added.
|
||||
pub freed_indices: BinaryHeap<ReverseQueueIndex>,
|
||||
}
|
||||
|
||||
impl Default for QueueStatusType {
|
||||
fn default() -> QueueStatusType {
|
||||
QueueStatusType {
|
||||
traffic: FixedU128::default(),
|
||||
next_index: QueueIndex(0),
|
||||
smallest_index: QueueIndex(0),
|
||||
freed_indices: BinaryHeap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QueueStatusType {
|
||||
/// How many orders are queued in total?
|
||||
///
|
||||
/// This includes entries which have core affinity.
|
||||
pub fn size(&self) -> u32 {
|
||||
self.next_index
|
||||
.0
|
||||
.overflowing_sub(self.smallest_index.0)
|
||||
.0
|
||||
.saturating_sub(self.freed_indices.len() as u32)
|
||||
}
|
||||
|
||||
/// Get current next index
|
||||
///
|
||||
/// to use for an element newly pushed to the back of the queue.
|
||||
pub fn push_back(&mut self) -> QueueIndex {
|
||||
let QueueIndex(next_index) = self.next_index;
|
||||
self.next_index = QueueIndex(next_index.overflowing_add(1).0);
|
||||
QueueIndex(next_index)
|
||||
}
|
||||
|
||||
/// Push something to the front of the queue
|
||||
pub fn push_front(&mut self) -> QueueIndex {
|
||||
self.smallest_index = QueueIndex(self.smallest_index.0.overflowing_sub(1).0);
|
||||
self.smallest_index
|
||||
}
|
||||
|
||||
/// The given index is no longer part of the queue.
|
||||
///
|
||||
/// This updates `smallest_index` if need be.
|
||||
pub fn consume_index(&mut self, removed_index: QueueIndex) {
|
||||
if removed_index != self.smallest_index {
|
||||
self.freed_indices.push(removed_index.reverse());
|
||||
return;
|
||||
}
|
||||
let mut index = self.smallest_index.0.overflowing_add(1).0;
|
||||
// Even more to advance?
|
||||
while self.freed_indices.peek() == Some(&ReverseQueueIndex(index)) {
|
||||
index = index.overflowing_add(1).0;
|
||||
self.freed_indices.pop();
|
||||
}
|
||||
self.smallest_index = QueueIndex(index);
|
||||
}
|
||||
}
|
||||
|
||||
/// Type used for priority indices.
|
||||
// NOTE: The `Ord` implementation for this type is unsound in the general case.
|
||||
// Do not use it for anything but it's intended purpose.
|
||||
#[derive(Encode, Decode, TypeInfo, Debug, PartialEq, Clone, Eq, Copy)]
|
||||
pub struct QueueIndex(pub u32);
|
||||
|
||||
/// QueueIndex with reverse ordering.
|
||||
///
|
||||
/// Same as `Reverse(QueueIndex)`, but with all the needed traits implemented.
|
||||
#[derive(Encode, Decode, TypeInfo, Debug, PartialEq, Clone, Eq, Copy)]
|
||||
pub struct ReverseQueueIndex(pub u32);
|
||||
|
||||
impl QueueIndex {
|
||||
fn reverse(self) -> ReverseQueueIndex {
|
||||
ReverseQueueIndex(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for QueueIndex {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
let diff = self.0.overflowing_sub(other.0).0;
|
||||
if diff == 0 {
|
||||
Ordering::Equal
|
||||
} else if diff <= ON_DEMAND_MAX_QUEUE_MAX_SIZE {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
Ordering::Less
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for QueueIndex {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for ReverseQueueIndex {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
QueueIndex(other.0).cmp(&QueueIndex(self.0))
|
||||
}
|
||||
}
|
||||
impl PartialOrd for ReverseQueueIndex {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(&other))
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal representation of an order after it has been enqueued already.
|
||||
///
|
||||
/// This data structure is provided for a min BinaryHeap (Ord compares in reverse order with regards
|
||||
/// to its elements)
|
||||
#[derive(Encode, Decode, TypeInfo, Debug, PartialEq, Clone, Eq)]
|
||||
pub struct EnqueuedOrder {
|
||||
pub para_id: ParaId,
|
||||
pub idx: QueueIndex,
|
||||
}
|
||||
|
||||
impl EnqueuedOrder {
|
||||
pub fn new(idx: QueueIndex, para_id: ParaId) -> Self {
|
||||
Self { idx, para_id }
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for EnqueuedOrder {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
match other.idx.partial_cmp(&self.idx) {
|
||||
Some(Ordering::Equal) => other.para_id.partial_cmp(&self.para_id),
|
||||
o => o,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for EnqueuedOrder {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
match other.idx.cmp(&self.idx) {
|
||||
Ordering::Equal => other.para_id.cmp(&self.para_id),
|
||||
o => o,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Keeps track of how many assignments a scheduler currently has at a specific `CoreIndex` for a
|
||||
/// specific `ParaId`.
|
||||
#[derive(Encode, Decode, Default, Clone, Copy, TypeInfo)]
|
||||
#[cfg_attr(test, derive(PartialEq, RuntimeDebug))]
|
||||
pub struct CoreAffinityCount {
|
||||
pub core_index: CoreIndex,
|
||||
pub count: u32,
|
||||
}
|
||||
|
||||
/// An indicator as to which end of the `OnDemandQueue` an assignment will be placed.
|
||||
#[cfg_attr(test, derive(RuntimeDebug))]
|
||||
pub enum QueuePushDirection {
|
||||
Back,
|
||||
Front,
|
||||
}
|
||||
|
||||
/// Errors that can happen during spot traffic calculation.
|
||||
#[derive(PartialEq, RuntimeDebug)]
|
||||
pub enum SpotTrafficCalculationErr {
|
||||
/// The order queue capacity is at 0.
|
||||
QueueCapacityIsZero,
|
||||
/// The queue size is larger than the queue capacity.
|
||||
QueueSizeLargerThanCapacity,
|
||||
/// Arithmetic error during division, either division by 0 or over/underflow.
|
||||
Division,
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// 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/>.
|
||||
|
||||
//! Declaration of the teyrchain specific origin and a pallet that hosts it.
|
||||
|
||||
use core::result;
|
||||
use pezkuwi_primitives::Id as ParaId;
|
||||
use sp_runtime::traits::BadOrigin;
|
||||
|
||||
pub use pallet::*;
|
||||
|
||||
/// Ensure that the origin `o` represents a teyrchain.
|
||||
/// Returns `Ok` with the teyrchain ID that effected the extrinsic or an `Err` otherwise.
|
||||
pub fn ensure_teyrchain<OuterOrigin>(o: OuterOrigin) -> result::Result<ParaId, BadOrigin>
|
||||
where
|
||||
OuterOrigin: Into<result::Result<Origin, OuterOrigin>>,
|
||||
{
|
||||
match o.into() {
|
||||
Ok(Origin::Teyrchain(id)) => Ok(id),
|
||||
_ => Err(BadOrigin),
|
||||
}
|
||||
}
|
||||
|
||||
/// There is no way to register an origin type in `construct_runtime` without a pallet the origin
|
||||
/// belongs to.
|
||||
///
|
||||
/// This module fulfills only the single purpose of housing the `Origin` in `construct_runtime`.
|
||||
// ideally, though, the `construct_runtime` should support a free-standing origin.
|
||||
#[frame_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
use frame_support::pallet_prelude::*;
|
||||
|
||||
#[pallet::pallet]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: frame_system::Config {}
|
||||
|
||||
/// Origin for the teyrchains.
|
||||
#[pallet::origin]
|
||||
#[derive(
|
||||
PartialEq,
|
||||
Eq,
|
||||
Clone,
|
||||
Encode,
|
||||
Decode,
|
||||
DecodeWithMemTracking,
|
||||
Debug,
|
||||
scale_info::TypeInfo,
|
||||
MaxEncodedLen,
|
||||
)]
|
||||
pub enum Origin {
|
||||
/// It comes from a teyrchain.
|
||||
Teyrchain(ParaId),
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for Origin {
|
||||
fn from(id: u32) -> Origin {
|
||||
Origin::Teyrchain(id.into())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
// 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::configuration::HostConfiguration;
|
||||
use alloc::vec;
|
||||
use frame_benchmarking::v2::*;
|
||||
use frame_support::traits::fungible::Mutate;
|
||||
use frame_system::{pallet_prelude::BlockNumberFor, RawOrigin};
|
||||
use pezkuwi_primitives::{
|
||||
HeadData, Id as ParaId, ValidationCode, MAX_CODE_SIZE, MAX_HEAD_DATA_SIZE,
|
||||
};
|
||||
use sp_runtime::traits::{One, Saturating};
|
||||
|
||||
pub mod mmr_setup;
|
||||
mod pvf_check;
|
||||
|
||||
use self::pvf_check::{VoteCause, VoteOutcome};
|
||||
|
||||
// 2 ^ 10, because binary search time complexity is O(log(2, n)) and n = 1024 gives us a big and
|
||||
// round number.
|
||||
// Due to the limited number of teyrchains, the number of pruning, upcoming upgrades and cooldowns
|
||||
// shouldn't exceed this number.
|
||||
const SAMPLE_SIZE: u32 = 1024;
|
||||
|
||||
fn assert_last_event<T: Config>(generic_event: <T as Config>::RuntimeEvent) {
|
||||
let events = frame_system::Pallet::<T>::events();
|
||||
let system_event: <T as frame_system::Config>::RuntimeEvent = generic_event.into();
|
||||
// compare to the last event record
|
||||
let frame_system::EventRecord { event, .. } = &events[events.len() - 1];
|
||||
assert_eq!(event, &system_event);
|
||||
}
|
||||
|
||||
fn generate_disordered_pruning<T: Config>() {
|
||||
let mut needs_pruning = Vec::new();
|
||||
|
||||
for i in 0..SAMPLE_SIZE {
|
||||
let id = ParaId::from(i);
|
||||
let block_number = BlockNumberFor::<T>::from(1000u32);
|
||||
needs_pruning.push((id, block_number));
|
||||
}
|
||||
|
||||
PastCodePruning::<T>::put(needs_pruning);
|
||||
}
|
||||
|
||||
pub(crate) fn generate_disordered_upgrades<T: Config>() {
|
||||
let mut upgrades = Vec::new();
|
||||
let mut cooldowns = Vec::new();
|
||||
|
||||
for i in 0..SAMPLE_SIZE {
|
||||
let id = ParaId::from(i);
|
||||
let block_number = BlockNumberFor::<T>::from(1000u32);
|
||||
upgrades.push((id, block_number));
|
||||
cooldowns.push((id, block_number));
|
||||
}
|
||||
|
||||
UpcomingUpgrades::<T>::put(upgrades);
|
||||
UpgradeCooldowns::<T>::put(cooldowns);
|
||||
}
|
||||
|
||||
fn generate_disordered_actions_queue<T: Config>() {
|
||||
let mut queue = Vec::new();
|
||||
let next_session = shared::CurrentSessionIndex::<T>::get().saturating_add(One::one());
|
||||
|
||||
for _ in 0..SAMPLE_SIZE {
|
||||
let id = ParaId::from(1000);
|
||||
queue.push(id);
|
||||
}
|
||||
|
||||
ActionsQueue::<T>::mutate(next_session, |v| {
|
||||
*v = queue;
|
||||
});
|
||||
}
|
||||
|
||||
#[benchmarks]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
#[benchmark]
|
||||
fn force_set_current_code(c: Linear<MIN_CODE_SIZE, MAX_CODE_SIZE>) {
|
||||
let new_code = ValidationCode(vec![0; c as usize]);
|
||||
let para_id = ParaId::from(c as u32);
|
||||
CurrentCodeHash::<T>::insert(¶_id, new_code.hash());
|
||||
generate_disordered_pruning::<T>();
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Root, para_id, new_code);
|
||||
|
||||
assert_last_event::<T>(Event::CurrentCodeUpdated(para_id).into());
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn force_set_current_head(s: Linear<MIN_CODE_SIZE, MAX_HEAD_DATA_SIZE>) {
|
||||
let new_head = HeadData(vec![0; s as usize]);
|
||||
let para_id = ParaId::from(1000);
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Root, para_id, new_head);
|
||||
|
||||
assert_last_event::<T>(Event::CurrentHeadUpdated(para_id).into());
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn force_set_most_recent_context() {
|
||||
let para_id = ParaId::from(1000);
|
||||
let context = BlockNumberFor::<T>::from(1000u32);
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Root, para_id, context);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn force_schedule_code_upgrade(c: Linear<MIN_CODE_SIZE, MAX_CODE_SIZE>) {
|
||||
let new_code = ValidationCode(vec![0; c as usize]);
|
||||
let para_id = ParaId::from(c as u32);
|
||||
let block = BlockNumberFor::<T>::from(c);
|
||||
generate_disordered_upgrades::<T>();
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Root, para_id, new_code, block);
|
||||
|
||||
assert_last_event::<T>(Event::CodeUpgradeScheduled(para_id).into());
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn force_note_new_head(s: Linear<MIN_CODE_SIZE, MAX_HEAD_DATA_SIZE>) {
|
||||
let para_id = ParaId::from(1000);
|
||||
let new_head = HeadData(vec![0; s as usize]);
|
||||
let old_code_hash = ValidationCode(vec![0]).hash();
|
||||
CurrentCodeHash::<T>::insert(¶_id, old_code_hash);
|
||||
frame_system::Pallet::<T>::set_block_number(10u32.into());
|
||||
// schedule an expired code upgrade for this `para_id` so that force_note_new_head would use
|
||||
// the worst possible code path
|
||||
let expired = frame_system::Pallet::<T>::block_number().saturating_sub(One::one());
|
||||
let config = HostConfiguration::<BlockNumberFor<T>>::default();
|
||||
generate_disordered_pruning::<T>();
|
||||
Pallet::<T>::schedule_code_upgrade(
|
||||
para_id,
|
||||
ValidationCode(vec![0u8; MIN_CODE_SIZE as usize]),
|
||||
expired,
|
||||
&config,
|
||||
UpgradeStrategy::SetGoAheadSignal,
|
||||
);
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Root, para_id, new_head);
|
||||
|
||||
assert_last_event::<T>(Event::NewHeadNoted(para_id).into());
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn force_queue_action() {
|
||||
let para_id = ParaId::from(1000);
|
||||
generate_disordered_actions_queue::<T>();
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Root, para_id);
|
||||
|
||||
let next_session =
|
||||
crate::shared::CurrentSessionIndex::<T>::get().saturating_add(One::one());
|
||||
assert_last_event::<T>(Event::ActionQueued(para_id, next_session).into());
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn add_trusted_validation_code(c: Linear<MIN_CODE_SIZE, MAX_CODE_SIZE>) {
|
||||
let new_code = ValidationCode(vec![0; c as usize]);
|
||||
|
||||
pvf_check::prepare_bypassing_bench::<T>(new_code.clone());
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Root, new_code);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn poke_unused_validation_code() {
|
||||
let code_hash = [0; 32].into();
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Root, code_hash);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn include_pvf_check_statement() {
|
||||
let (stmt, signature) = pvf_check::prepare_inclusion_bench::<T>();
|
||||
|
||||
#[block]
|
||||
{
|
||||
let _ =
|
||||
Pallet::<T>::include_pvf_check_statement(RawOrigin::None.into(), stmt, signature);
|
||||
}
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn include_pvf_check_statement_finalize_upgrade_accept() {
|
||||
let (stmt, signature) =
|
||||
pvf_check::prepare_finalization_bench::<T>(VoteCause::Upgrade, VoteOutcome::Accept);
|
||||
|
||||
#[block]
|
||||
{
|
||||
let _ =
|
||||
Pallet::<T>::include_pvf_check_statement(RawOrigin::None.into(), stmt, signature);
|
||||
}
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn include_pvf_check_statement_finalize_upgrade_reject() {
|
||||
let (stmt, signature) =
|
||||
pvf_check::prepare_finalization_bench::<T>(VoteCause::Upgrade, VoteOutcome::Reject);
|
||||
|
||||
#[block]
|
||||
{
|
||||
let _ =
|
||||
Pallet::<T>::include_pvf_check_statement(RawOrigin::None.into(), stmt, signature);
|
||||
}
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn include_pvf_check_statement_finalize_onboarding_accept() {
|
||||
let (stmt, signature) =
|
||||
pvf_check::prepare_finalization_bench::<T>(VoteCause::Onboarding, VoteOutcome::Accept);
|
||||
|
||||
#[block]
|
||||
{
|
||||
let _ =
|
||||
Pallet::<T>::include_pvf_check_statement(RawOrigin::None.into(), stmt, signature);
|
||||
}
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn include_pvf_check_statement_finalize_onboarding_reject() {
|
||||
let (stmt, signature) =
|
||||
pvf_check::prepare_finalization_bench::<T>(VoteCause::Onboarding, VoteOutcome::Reject);
|
||||
|
||||
#[block]
|
||||
{
|
||||
let _ =
|
||||
Pallet::<T>::include_pvf_check_statement(RawOrigin::None.into(), stmt, signature);
|
||||
}
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn remove_upgrade_cooldown() -> Result<(), BenchmarkError> {
|
||||
let para_id = ParaId::from(1000);
|
||||
let old_code_hash = ValidationCode(vec![0]).hash();
|
||||
CurrentCodeHash::<T>::insert(¶_id, old_code_hash);
|
||||
frame_system::Pallet::<T>::set_block_number(10u32.into());
|
||||
let inclusion = frame_system::Pallet::<T>::block_number().saturating_add(10u32.into());
|
||||
let config = HostConfiguration::<BlockNumberFor<T>>::default();
|
||||
Pallet::<T>::schedule_code_upgrade(
|
||||
para_id,
|
||||
ValidationCode(vec![0u8; MIN_CODE_SIZE as usize]),
|
||||
inclusion,
|
||||
&config,
|
||||
UpgradeStrategy::SetGoAheadSignal,
|
||||
);
|
||||
|
||||
let who: T::AccountId = whitelisted_caller();
|
||||
|
||||
T::Fungible::mint_into(
|
||||
&who,
|
||||
T::CooldownRemovalMultiplier::get().saturating_mul(1_000_000u32.into()),
|
||||
)?;
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Signed(who), para_id);
|
||||
|
||||
assert_last_event::<T>(Event::UpgradeCooldownRemoved { para_id }.into());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn authorize_force_set_current_code_hash() {
|
||||
let para_id = ParaId::from(1000);
|
||||
let code = ValidationCode(vec![0; 32]);
|
||||
let new_code_hash = code.hash();
|
||||
let valid_period = BlockNumberFor::<T>::from(1_000_000_u32);
|
||||
ParaLifecycles::<T>::insert(¶_id, ParaLifecycle::Teyrchain);
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Root, para_id, new_code_hash, valid_period);
|
||||
|
||||
assert_last_event::<T>(
|
||||
Event::CodeAuthorized {
|
||||
para_id,
|
||||
code_hash: new_code_hash,
|
||||
expire_at: frame_system::Pallet::<T>::block_number().saturating_add(valid_period),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn apply_authorized_force_set_current_code(c: Linear<MIN_CODE_SIZE, MAX_CODE_SIZE>) {
|
||||
let code = ValidationCode(vec![0; c as usize]);
|
||||
let para_id = ParaId::from(1000);
|
||||
let expire_at =
|
||||
frame_system::Pallet::<T>::block_number().saturating_add(BlockNumberFor::<T>::from(c));
|
||||
AuthorizedCodeHash::<T>::insert(
|
||||
¶_id,
|
||||
AuthorizedCodeHashAndExpiry::from((code.hash(), expire_at)),
|
||||
);
|
||||
generate_disordered_pruning::<T>();
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Root, para_id, code);
|
||||
|
||||
assert_last_event::<T>(Event::CurrentCodeUpdated(para_id).into());
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(
|
||||
Pallet,
|
||||
crate::mock::new_test_ext(Default::default()),
|
||||
crate::mock::Test
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// 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/>.
|
||||
|
||||
//! Implements benchmarking setup for the `merkle-mountain-range` pallet.
|
||||
|
||||
use crate::paras::*;
|
||||
use pallet_mmr::BenchmarkHelper;
|
||||
use sp_std::vec;
|
||||
|
||||
/// Struct to setup benchmarks for the `merkle-mountain-range` pallet.
|
||||
pub struct MmrSetup<T>(core::marker::PhantomData<T>);
|
||||
|
||||
impl<T> BenchmarkHelper for MmrSetup<T>
|
||||
where
|
||||
T: Config,
|
||||
{
|
||||
fn setup() {
|
||||
// Create a head with 1024 bytes of data.
|
||||
let head = vec![42u8; 1024];
|
||||
|
||||
for para in 0..MAX_PARA_HEADS {
|
||||
let id = (para as u32).into();
|
||||
let h = head.clone().into();
|
||||
Pallet::<T>::heads_insert(&id, h);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
// 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/>.
|
||||
|
||||
//! This module focuses on the benchmarking of the `include_pvf_check_statement` dispatchable.
|
||||
|
||||
use crate::{configuration, paras::*, shared::Pallet as ParasShared};
|
||||
use alloc::{vec, vec::Vec};
|
||||
use frame_support::assert_ok;
|
||||
use frame_system::RawOrigin;
|
||||
use pezkuwi_primitives::{HeadData, Id as ParaId, ValidationCode, ValidatorId, ValidatorIndex};
|
||||
use sp_application_crypto::RuntimeAppPublic;
|
||||
|
||||
// Constants for the benchmarking
|
||||
const SESSION_INDEX: SessionIndex = 1;
|
||||
const VALIDATOR_NUM: usize = 800;
|
||||
const CAUSES_NUM: usize = 100;
|
||||
fn validation_code() -> ValidationCode {
|
||||
ValidationCode(vec![1, 2, 3, 4, 5, 6, 7, 8, 9])
|
||||
}
|
||||
fn old_validation_code() -> ValidationCode {
|
||||
ValidationCode(vec![9, 8, 7, 6, 5, 4, 3, 2, 1])
|
||||
}
|
||||
|
||||
/// Prepares the PVF check statement and the validator signature to pass into
|
||||
/// `include_pvf_check_statement` during benchmarking phase.
|
||||
///
|
||||
/// It won't trigger finalization, so we expect the benchmarking will only measure the performance
|
||||
/// of only vote accounting.
|
||||
pub fn prepare_inclusion_bench<T>() -> (PvfCheckStatement, ValidatorSignature)
|
||||
where
|
||||
T: Config + shared::Config,
|
||||
{
|
||||
initialize::<T>();
|
||||
// we do not plan to trigger finalization, thus the cause is inconsequential.
|
||||
initialize_pvf_active_vote::<T>(VoteCause::Onboarding, CAUSES_NUM);
|
||||
|
||||
// `unwrap` cannot panic here since the `initialize` function should initialize validators count
|
||||
// to be more than 0.
|
||||
//
|
||||
// VoteDirection doesn't matter here as well.
|
||||
let stmt_n_sig = generate_statements::<T>(VoteOutcome::Accept).next().unwrap();
|
||||
|
||||
stmt_n_sig
|
||||
}
|
||||
|
||||
/// Prepares conditions for benchmarking of the finalization part of `include_pvf_check_statement`.
|
||||
///
|
||||
/// This function will initialize a PVF pre-check vote, then submit a number of PVF pre-checking
|
||||
/// statements so that to achieve the quorum only one statement is left. This statement is returned
|
||||
/// from this function and is expected to be passed into `include_pvf_check_statement` during the
|
||||
/// benchmarking phase.
|
||||
pub fn prepare_finalization_bench<T>(
|
||||
cause: VoteCause,
|
||||
outcome: VoteOutcome,
|
||||
) -> (PvfCheckStatement, ValidatorSignature)
|
||||
where
|
||||
T: Config + shared::Config,
|
||||
{
|
||||
initialize::<T>();
|
||||
initialize_pvf_active_vote::<T>(cause, CAUSES_NUM);
|
||||
|
||||
let mut stmts = generate_statements::<T>(outcome).collect::<Vec<_>>();
|
||||
// this should be ensured by the `initialize` function.
|
||||
assert!(stmts.len() > 2);
|
||||
|
||||
// stash the last statement to be used in the benchmarking phase.
|
||||
let stmt_n_sig = stmts.pop().unwrap();
|
||||
|
||||
for (stmt, sig) in stmts {
|
||||
let r = Pallet::<T>::include_pvf_check_statement(RawOrigin::None.into(), stmt, sig);
|
||||
assert!(r.is_ok());
|
||||
}
|
||||
|
||||
stmt_n_sig
|
||||
}
|
||||
|
||||
/// Prepares storage for invoking `add_trusted_validation_code` with several paras initializing to
|
||||
/// the same code.
|
||||
pub fn prepare_bypassing_bench<T>(validation_code: ValidationCode)
|
||||
where
|
||||
T: Config + shared::Config,
|
||||
{
|
||||
// Suppose a sensible number of paras initialize to the same code.
|
||||
const PARAS_NUM: usize = 10;
|
||||
|
||||
initialize::<T>();
|
||||
for i in 0..PARAS_NUM {
|
||||
let id = ParaId::from(i as u32);
|
||||
assert_ok!(Pallet::<T>::schedule_para_initialize(
|
||||
id,
|
||||
ParaGenesisArgs {
|
||||
para_kind: ParaKind::Teyrchain,
|
||||
genesis_head: HeadData(vec![1, 2, 3, 4]),
|
||||
validation_code: validation_code.clone(),
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// What caused the PVF pre-checking vote?
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
||||
pub enum VoteCause {
|
||||
Onboarding,
|
||||
Upgrade,
|
||||
}
|
||||
|
||||
/// The outcome of the PVF pre-checking vote.
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
||||
pub enum VoteOutcome {
|
||||
Accept,
|
||||
Reject,
|
||||
}
|
||||
|
||||
fn initialize<T>()
|
||||
where
|
||||
T: Config + shared::Config,
|
||||
{
|
||||
// 0. generate a list of validators
|
||||
let validators = (0..VALIDATOR_NUM)
|
||||
.map(|_| <ValidatorId as RuntimeAppPublic>::generate_pair(None))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// 1. Make sure PVF pre-checking is enabled in the config.
|
||||
let config = configuration::ActiveConfig::<T>::get();
|
||||
configuration::Pallet::<T>::force_set_active_config(config.clone());
|
||||
|
||||
// 2. initialize a new session with deterministic validator set.
|
||||
ParasShared::<T>::set_active_validators_ascending(validators.clone());
|
||||
ParasShared::<T>::set_session_index(SESSION_INDEX);
|
||||
}
|
||||
|
||||
/// Creates a new PVF pre-checking active vote.
|
||||
///
|
||||
/// The subject of the vote (i.e. validation code) and the cause (upgrade/onboarding) is specified
|
||||
/// by the test setup.
|
||||
fn initialize_pvf_active_vote<T>(vote_cause: VoteCause, causes_num: usize)
|
||||
where
|
||||
T: Config + shared::Config,
|
||||
{
|
||||
for i in 0..causes_num {
|
||||
let id = ParaId::from(i as u32);
|
||||
|
||||
if vote_cause == VoteCause::Upgrade {
|
||||
// we do care about validation code being actually different, since there is a check
|
||||
// that prevents upgrading to the same code.
|
||||
let old_validation_code = old_validation_code();
|
||||
let validation_code = validation_code();
|
||||
|
||||
let mut teyrchains = TeyrchainsCache::new();
|
||||
Pallet::<T>::initialize_para_now(
|
||||
&mut teyrchains,
|
||||
id,
|
||||
&ParaGenesisArgs {
|
||||
para_kind: ParaKind::Teyrchain,
|
||||
genesis_head: HeadData(vec![1, 2, 3, 4]),
|
||||
validation_code: old_validation_code,
|
||||
},
|
||||
);
|
||||
// don't care about performance here, but we do care about robustness. So dump the cache
|
||||
// asap.
|
||||
drop(teyrchains);
|
||||
|
||||
Pallet::<T>::schedule_code_upgrade(
|
||||
id,
|
||||
validation_code,
|
||||
/* relay_parent_number */ 1u32.into(),
|
||||
&configuration::ActiveConfig::<T>::get(),
|
||||
UpgradeStrategy::SetGoAheadSignal,
|
||||
);
|
||||
} else {
|
||||
let r = Pallet::<T>::schedule_para_initialize(
|
||||
id,
|
||||
ParaGenesisArgs {
|
||||
para_kind: ParaKind::Teyrchain,
|
||||
genesis_head: HeadData(vec![1, 2, 3, 4]),
|
||||
validation_code: validation_code(),
|
||||
},
|
||||
);
|
||||
assert!(r.is_ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a list of votes combined with signatures for the active validator set. The number of
|
||||
/// votes is equal to the minimum number of votes required to reach the threshold for either accept
|
||||
/// or reject.
|
||||
fn generate_statements<T>(
|
||||
vote_outcome: VoteOutcome,
|
||||
) -> impl Iterator<Item = (PvfCheckStatement, ValidatorSignature)>
|
||||
where
|
||||
T: Config + shared::Config,
|
||||
{
|
||||
let validators = shared::ActiveValidatorKeys::<T>::get();
|
||||
|
||||
let accept_threshold = pezkuwi_primitives::supermajority_threshold(validators.len());
|
||||
let required_votes = match vote_outcome {
|
||||
VoteOutcome::Accept => accept_threshold,
|
||||
VoteOutcome::Reject => validators.len() - accept_threshold,
|
||||
};
|
||||
(0..required_votes).map(move |validator_index| {
|
||||
let stmt = PvfCheckStatement {
|
||||
accept: vote_outcome == VoteOutcome::Accept,
|
||||
subject: validation_code().hash(),
|
||||
session_index: SESSION_INDEX,
|
||||
|
||||
validator_index: ValidatorIndex(validator_index as u32),
|
||||
};
|
||||
let signature = validators[validator_index].sign(&stmt.signing_payload()).unwrap();
|
||||
|
||||
(stmt, signature)
|
||||
})
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,259 @@
|
||||
// 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/>.
|
||||
#![cfg(feature = "runtime-benchmarks")]
|
||||
|
||||
use super::*;
|
||||
use crate::{inclusion, ParaId};
|
||||
use alloc::collections::btree_map::BTreeMap;
|
||||
use core::cmp::{max, min};
|
||||
use frame_benchmarking::v2::*;
|
||||
use frame_system::RawOrigin;
|
||||
|
||||
use pezkuwi_primitives::{node_features::FeatureIndex, GroupIndex};
|
||||
|
||||
use crate::builder::BenchBuilder;
|
||||
|
||||
#[benchmarks]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
#[benchmark]
|
||||
fn enter_empty() -> Result<(), BenchmarkError> {
|
||||
let scenario = BenchBuilder::<T>::new().build();
|
||||
|
||||
let mut benchmark = scenario.data.clone();
|
||||
|
||||
benchmark.bitfields.clear();
|
||||
benchmark.backed_candidates.clear();
|
||||
benchmark.disputes.clear();
|
||||
|
||||
#[extrinsic_call]
|
||||
enter(RawOrigin::None, benchmark);
|
||||
|
||||
// Assert that the block was not discarded
|
||||
assert!(Included::<T>::get().is_some());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Variant over `v`, the number of dispute statements in a dispute statement set. This gives the
|
||||
// weight of a single dispute statement set.
|
||||
// The number of statements needs to be at least a third of the validator set size.
|
||||
#[benchmark]
|
||||
fn enter_variable_disputes(
|
||||
v: Linear<400, { BenchBuilder::<T>::fallback_max_validators() }>,
|
||||
) -> Result<(), BenchmarkError> {
|
||||
let scenario = BenchBuilder::<T>::new().set_dispute_sessions(&[2]).build();
|
||||
|
||||
let mut benchmark = scenario.data.clone();
|
||||
let dispute = benchmark.disputes.pop().unwrap();
|
||||
|
||||
benchmark.bitfields.clear();
|
||||
benchmark.backed_candidates.clear();
|
||||
benchmark.disputes.clear();
|
||||
|
||||
benchmark.disputes.push(dispute);
|
||||
benchmark.disputes.get_mut(0).unwrap().statements.drain(v as usize..);
|
||||
|
||||
#[extrinsic_call]
|
||||
enter(RawOrigin::None, benchmark);
|
||||
|
||||
// Assert that the block was not discarded
|
||||
assert!(Included::<T>::get().is_some());
|
||||
|
||||
// Assert that there are on-chain votes that got scraped
|
||||
let onchain_votes = OnChainVotes::<T>::get();
|
||||
assert!(onchain_votes.is_some());
|
||||
let vote = onchain_votes.unwrap();
|
||||
|
||||
// Ensure that the votes are for the correct session
|
||||
assert_eq!(vote.session, scenario._session);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// The weight of one bitfield.
|
||||
#[benchmark]
|
||||
fn enter_bitfields() -> Result<(), BenchmarkError> {
|
||||
let cores_with_backed: BTreeMap<_, _> =
|
||||
vec![(0, BenchBuilder::<T>::fallback_max_validators())].into_iter().collect();
|
||||
|
||||
let scenario = BenchBuilder::<T>::new()
|
||||
.set_backed_and_concluding_paras(cores_with_backed)
|
||||
.build();
|
||||
|
||||
let mut benchmark = scenario.data.clone();
|
||||
let bitfield = benchmark.bitfields.pop().unwrap();
|
||||
|
||||
benchmark.bitfields.clear();
|
||||
benchmark.backed_candidates.clear();
|
||||
benchmark.disputes.clear();
|
||||
|
||||
benchmark.bitfields.push(bitfield);
|
||||
|
||||
#[extrinsic_call]
|
||||
enter(RawOrigin::None, benchmark);
|
||||
|
||||
// Assert that the block was not discarded
|
||||
assert!(Included::<T>::get().is_some());
|
||||
// Assert that there are on-chain votes that got scraped
|
||||
let onchain_votes = OnChainVotes::<T>::get();
|
||||
assert!(onchain_votes.is_some());
|
||||
let vote = onchain_votes.unwrap();
|
||||
// Ensure that the votes are for the correct session
|
||||
assert_eq!(vote.session, scenario._session);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Variant over `v`, the amount of validity votes for a backed candidate. This gives the weight
|
||||
// of a single backed candidate.
|
||||
#[benchmark]
|
||||
fn enter_backed_candidates_variable(
|
||||
v: Linear<
|
||||
{ BenchBuilder::<T>::fallback_min_backing_votes() },
|
||||
{
|
||||
max(
|
||||
BenchBuilder::<T>::fallback_min_backing_votes() + 1,
|
||||
BenchBuilder::<T>::fallback_max_validators_per_core(),
|
||||
)
|
||||
},
|
||||
>,
|
||||
) -> Result<(), BenchmarkError> {
|
||||
configuration::Pallet::<T>::set_node_feature(
|
||||
RawOrigin::Root.into(),
|
||||
FeatureIndex::CandidateReceiptV2 as u8,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
let cores_with_backed: BTreeMap<_, _> = vec![(0, v)] // The backed candidate will have `v` validity votes.
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let scenario = BenchBuilder::<T>::new()
|
||||
.set_backed_in_inherent_paras(cores_with_backed.clone())
|
||||
.build();
|
||||
|
||||
let mut benchmark = scenario.data.clone();
|
||||
|
||||
// There is 1 backed,
|
||||
assert_eq!(benchmark.backed_candidates.len(), 1);
|
||||
// with `v` validity votes.
|
||||
let votes = min(
|
||||
scheduler::Pallet::<T>::group_validators(GroupIndex::from(0)).unwrap().len(),
|
||||
v as usize,
|
||||
);
|
||||
assert_eq!(benchmark.backed_candidates.get(0).unwrap().validity_votes().len(), votes);
|
||||
|
||||
benchmark.bitfields.clear();
|
||||
benchmark.disputes.clear();
|
||||
|
||||
#[extrinsic_call]
|
||||
enter(RawOrigin::None, benchmark);
|
||||
|
||||
// Assert that the block was not discarded
|
||||
assert!(Included::<T>::get().is_some());
|
||||
// Assert that there are on-chain votes that got scraped
|
||||
let onchain_votes = OnChainVotes::<T>::get();
|
||||
assert!(onchain_votes.is_some());
|
||||
let vote = onchain_votes.unwrap();
|
||||
// Ensure that the votes are for the correct session
|
||||
assert_eq!(vote.session, scenario._session);
|
||||
// Ensure that there are an expected number of candidates
|
||||
let header = BenchBuilder::<T>::header(scenario._block_number);
|
||||
// Traverse candidates and assert descriptors are as expected
|
||||
for (para_id, backing_validators) in
|
||||
vote.backing_validators_per_candidate.iter().enumerate()
|
||||
{
|
||||
let descriptor = backing_validators.0.descriptor();
|
||||
assert_eq!(ParaId::from(para_id), descriptor.para_id());
|
||||
assert_eq!(header.hash(), descriptor.relay_parent());
|
||||
assert_eq!(backing_validators.1.len(), votes);
|
||||
}
|
||||
|
||||
assert_eq!(inclusion::PendingAvailability::<T>::iter().count(), cores_with_backed.len());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn enter_backed_candidate_code_upgrade() -> Result<(), BenchmarkError> {
|
||||
configuration::Pallet::<T>::set_node_feature(
|
||||
RawOrigin::Root.into(),
|
||||
FeatureIndex::CandidateReceiptV2 as u8,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// For now we always assume worst case code size. In the future we could vary over this.
|
||||
let v = crate::configuration::ActiveConfig::<T>::get().max_code_size;
|
||||
|
||||
let cores_with_backed: BTreeMap<_, _> =
|
||||
vec![(0, BenchBuilder::<T>::fallback_min_backing_votes())].into_iter().collect();
|
||||
|
||||
let scenario = BenchBuilder::<T>::new()
|
||||
.set_backed_in_inherent_paras(cores_with_backed.clone())
|
||||
.set_code_upgrade(v)
|
||||
.build();
|
||||
|
||||
let mut benchmark = scenario.data.clone();
|
||||
|
||||
let votes = min(
|
||||
scheduler::Pallet::<T>::group_validators(GroupIndex::from(0)).unwrap().len(),
|
||||
BenchBuilder::<T>::fallback_min_backing_votes() as usize,
|
||||
);
|
||||
|
||||
// There is 1 backed
|
||||
assert_eq!(benchmark.backed_candidates.len(), 1);
|
||||
assert_eq!(benchmark.backed_candidates.get(0).unwrap().validity_votes().len(), votes,);
|
||||
|
||||
benchmark.bitfields.clear();
|
||||
benchmark.disputes.clear();
|
||||
crate::paras::benchmarking::generate_disordered_upgrades::<T>();
|
||||
|
||||
#[extrinsic_call]
|
||||
enter(RawOrigin::None, benchmark);
|
||||
|
||||
// Assert that the block was not discarded
|
||||
assert!(Included::<T>::get().is_some());
|
||||
// Assert that there are on-chain votes that got scraped
|
||||
let onchain_votes = OnChainVotes::<T>::get();
|
||||
assert!(onchain_votes.is_some());
|
||||
let vote = onchain_votes.unwrap();
|
||||
// Ensure that the votes are for the correct session
|
||||
assert_eq!(vote.session, scenario._session);
|
||||
// Ensure that there are an expected number of candidates
|
||||
let header = BenchBuilder::<T>::header(scenario._block_number);
|
||||
// Traverse candidates and assert descriptors are as expected
|
||||
for (para_id, backing_validators) in
|
||||
vote.backing_validators_per_candidate.iter().enumerate()
|
||||
{
|
||||
let descriptor = backing_validators.0.descriptor();
|
||||
assert_eq!(ParaId::from(para_id), descriptor.para_id());
|
||||
assert_eq!(header.hash(), descriptor.relay_parent());
|
||||
assert_eq!(backing_validators.1.len(), votes,);
|
||||
}
|
||||
|
||||
assert_eq!(inclusion::PendingAvailability::<T>::iter().count(), cores_with_backed.len());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite! {
|
||||
Pallet,
|
||||
crate::mock::new_test_ext(Default::default()),
|
||||
crate::mock::Test
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// 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 alloc::vec::Vec;
|
||||
use core::cmp::Ordering;
|
||||
|
||||
/// A helper trait to allow calling retain while getting access
|
||||
/// to the index of the item in the `vec`.
|
||||
pub trait IndexedRetain<T> {
|
||||
/// Retains only the elements specified by the predicate.
|
||||
///
|
||||
/// In other words, remove all elements `e` residing at
|
||||
/// index `i` such that `f(i, &e)` returns `false`. This method
|
||||
/// operates in place, visiting each element exactly once in the
|
||||
/// original order, and preserves the order of the retained elements.
|
||||
fn indexed_retain(&mut self, f: impl FnMut(usize, &T) -> bool);
|
||||
}
|
||||
|
||||
impl<T> IndexedRetain<T> for Vec<T> {
|
||||
fn indexed_retain(&mut self, mut f: impl FnMut(usize, &T) -> bool) {
|
||||
let mut idx = 0_usize;
|
||||
self.retain(move |item| {
|
||||
let ret = f(idx, item);
|
||||
idx += 1_usize;
|
||||
ret
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper trait until `is_sorted_by` is stabilized.
|
||||
/// TODO: <https://github.com/rust-lang/rust/issues/53485>
|
||||
pub trait IsSortedBy<T> {
|
||||
fn is_sorted_by<F>(self, cmp: F) -> bool
|
||||
where
|
||||
F: FnMut(&T, &T) -> Ordering;
|
||||
}
|
||||
|
||||
impl<'x, T, X> IsSortedBy<T> for X
|
||||
where
|
||||
X: 'x + IntoIterator<Item = &'x T>,
|
||||
T: 'x,
|
||||
{
|
||||
fn is_sorted_by<F>(self, mut cmp: F) -> bool
|
||||
where
|
||||
F: FnMut(&T, &T) -> Ordering,
|
||||
{
|
||||
let mut iter = self.into_iter();
|
||||
let mut previous: &T = if let Some(previous) = iter.next() {
|
||||
previous
|
||||
} else {
|
||||
// empty is always sorted
|
||||
return true;
|
||||
};
|
||||
while let Some(cursor) = iter.next() {
|
||||
match cmp(&previous, &cursor) {
|
||||
Ordering::Greater => return false,
|
||||
_ => {
|
||||
// ordering is ok
|
||||
},
|
||||
}
|
||||
previous = cursor;
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn is_sorted_simple() {
|
||||
let v = vec![1_i32, 2, 3, 1000];
|
||||
assert!(IsSortedBy::<i32>::is_sorted_by(v.as_slice(), |a: &i32, b: &i32| { a.cmp(b) }));
|
||||
assert!(!IsSortedBy::<i32>::is_sorted_by(&v, |a, b| { b.cmp(a) }));
|
||||
|
||||
let v = vec![8_i32, 8, 8, 8];
|
||||
assert!(IsSortedBy::<i32>::is_sorted_by(v.as_slice(), |a: &i32, b: &i32| { a.cmp(b) }));
|
||||
assert!(IsSortedBy::<i32>::is_sorted_by(v.as_slice(), |a: &i32, b: &i32| { b.cmp(a) }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_not_sorted() {
|
||||
let v = vec![7, 1, 3];
|
||||
assert!(!IsSortedBy::is_sorted_by(&v, |a, b| { a.cmp(b) }));
|
||||
assert!(!IsSortedBy::is_sorted_by(&v, |a, b| { b.cmp(a) }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_is_sorted() {
|
||||
let v = Vec::<u8>::new();
|
||||
assert!(IsSortedBy::is_sorted_by(&v, |_a, _b| { unreachable!() }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_items_is_sorted() {
|
||||
let v = vec![7_u8];
|
||||
assert!(IsSortedBy::is_sorted_by(&v, |_a, _b| { unreachable!() }));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,213 @@
|
||||
// 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/>.
|
||||
|
||||
//! We use benchmarks to get time weights, for proof_size we manually use the size of the input
|
||||
//! data, which will be part of the block. This is because we don't care about the storage proof on
|
||||
//! the relay chain, but we do care about the size of the block, by putting the tx in the
|
||||
//! proof_size we can use the already existing weight limiting code to limit the used size as well.
|
||||
|
||||
use crate::{configuration, inclusion};
|
||||
use codec::{Encode, WrapperTypeEncode};
|
||||
use pezkuwi_primitives::{
|
||||
CheckedMultiDisputeStatementSet, MultiDisputeStatementSet, UncheckedSignedAvailabilityBitfield,
|
||||
UncheckedSignedAvailabilityBitfields,
|
||||
};
|
||||
|
||||
use super::{BackedCandidate, Config, DisputeStatementSet, Weight};
|
||||
|
||||
pub trait WeightInfo {
|
||||
/// The weight of processing an empty teyrchain inherent.
|
||||
fn enter_empty() -> Weight;
|
||||
/// Variant over `v`, the count of dispute statements in a dispute statement set. This gives the
|
||||
/// weight of a single dispute statement set.
|
||||
fn enter_variable_disputes(v: u32) -> Weight;
|
||||
/// The weight of one bitfield.
|
||||
fn enter_bitfields() -> Weight;
|
||||
/// Variant over `v`, the count of validity votes for a backed candidate. This gives the weight
|
||||
/// of a single backed candidate.
|
||||
fn enter_backed_candidates_variable(v: u32) -> Weight;
|
||||
/// The weight of a single backed candidate with a code upgrade.
|
||||
fn enter_backed_candidate_code_upgrade() -> Weight;
|
||||
}
|
||||
|
||||
pub struct TestWeightInfo;
|
||||
// `WeightInfo` impl for unit and integration tests. Based off of the `max_block` weight for the
|
||||
// mock.
|
||||
#[cfg(not(feature = "runtime-benchmarks"))]
|
||||
impl WeightInfo for TestWeightInfo {
|
||||
fn enter_empty() -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
fn enter_variable_disputes(v: u32) -> Weight {
|
||||
// MAX Block Weight should fit 4 disputes
|
||||
Weight::from_parts(80_000 * v as u64 + 80_000, 0)
|
||||
}
|
||||
fn enter_bitfields() -> Weight {
|
||||
// MAX Block Weight should fit 4 backed candidates
|
||||
Weight::from_parts(40_000u64, 0)
|
||||
}
|
||||
fn enter_backed_candidates_variable(v: u32) -> Weight {
|
||||
// MAX Block Weight should fit 4 backed candidates
|
||||
Weight::from_parts(40_000 * v as u64 + 40_000, 0)
|
||||
}
|
||||
fn enter_backed_candidate_code_upgrade() -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
}
|
||||
// To simplify benchmarks running as tests, we set all the weights to 0. `enter` will exit early
|
||||
// when if the data causes it to be over weight, but we don't want that to block a benchmark from
|
||||
// running as a test.
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
impl WeightInfo for TestWeightInfo {
|
||||
fn enter_empty() -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
fn enter_variable_disputes(_v: u32) -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
fn enter_bitfields() -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
fn enter_backed_candidates_variable(_v: u32) -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
fn enter_backed_candidate_code_upgrade() -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paras_inherent_total_weight<T: Config>(
|
||||
backed_candidates: &[BackedCandidate<<T as frame_system::Config>::Hash>],
|
||||
bitfields: &UncheckedSignedAvailabilityBitfields,
|
||||
disputes: &MultiDisputeStatementSet,
|
||||
) -> Weight {
|
||||
let weight = backed_candidates_weight::<T>(backed_candidates)
|
||||
.saturating_add(signed_bitfields_weight::<T>(bitfields))
|
||||
.saturating_add(multi_dispute_statement_sets_weight::<T>(disputes))
|
||||
.saturating_add(enact_candidates_max_weight::<T>(bitfields));
|
||||
// Relay chain blocks pre-dispatch weight can be set to any high enough value
|
||||
// but the proof size is irrelevant for the relay chain either way.
|
||||
weight.set_proof_size(u64::MAX)
|
||||
}
|
||||
|
||||
pub fn multi_dispute_statement_sets_weight<T: Config>(
|
||||
disputes: &MultiDisputeStatementSet,
|
||||
) -> Weight {
|
||||
set_proof_size_to_tx_size(
|
||||
disputes
|
||||
.iter()
|
||||
.map(|d| dispute_statement_set_weight::<T, _>(d))
|
||||
.fold(Weight::zero(), |acc_weight, weight| acc_weight.saturating_add(weight)),
|
||||
disputes,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn checked_multi_dispute_statement_sets_weight<T: Config>(
|
||||
disputes: &CheckedMultiDisputeStatementSet,
|
||||
) -> Weight {
|
||||
set_proof_size_to_tx_size(
|
||||
disputes
|
||||
.iter()
|
||||
.map(|d| dispute_statement_set_weight::<T, _>(d))
|
||||
.fold(Weight::zero(), |acc_weight, weight| acc_weight.saturating_add(weight)),
|
||||
disputes,
|
||||
)
|
||||
}
|
||||
|
||||
/// Get time weights from benchmarks and set proof size to tx size.
|
||||
pub fn dispute_statement_set_weight<T, D>(statement_set: D) -> Weight
|
||||
where
|
||||
T: Config,
|
||||
D: AsRef<DisputeStatementSet> + WrapperTypeEncode + Sized + Encode,
|
||||
{
|
||||
set_proof_size_to_tx_size(
|
||||
<<T as Config>::WeightInfo as WeightInfo>::enter_variable_disputes(
|
||||
statement_set.as_ref().statements.len() as u32,
|
||||
)
|
||||
.saturating_sub(<<T as Config>::WeightInfo as WeightInfo>::enter_empty()),
|
||||
statement_set,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn signed_bitfields_weight<T: Config>(
|
||||
bitfields: &UncheckedSignedAvailabilityBitfields,
|
||||
) -> Weight {
|
||||
set_proof_size_to_tx_size(
|
||||
<<T as Config>::WeightInfo as WeightInfo>::enter_bitfields()
|
||||
.saturating_sub(<<T as Config>::WeightInfo as WeightInfo>::enter_empty())
|
||||
.saturating_mul(bitfields.len() as u64),
|
||||
bitfields,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn signed_bitfield_weight<T: Config>(bitfield: &UncheckedSignedAvailabilityBitfield) -> Weight {
|
||||
set_proof_size_to_tx_size(
|
||||
<<T as Config>::WeightInfo as WeightInfo>::enter_bitfields()
|
||||
.saturating_sub(<<T as Config>::WeightInfo as WeightInfo>::enter_empty()),
|
||||
bitfield,
|
||||
)
|
||||
}
|
||||
|
||||
/// Worst case scenario is all candidates have been enacted
|
||||
/// and process a maximum number of messages.
|
||||
pub fn enact_candidates_max_weight<T: Config>(
|
||||
bitfields: &UncheckedSignedAvailabilityBitfields,
|
||||
) -> Weight {
|
||||
let config = configuration::ActiveConfig::<T>::get();
|
||||
let max_ump_msgs = config.max_upward_message_num_per_candidate;
|
||||
let max_hrmp_msgs = config.hrmp_max_message_num_per_candidate;
|
||||
// No bitfields - no enacted candidates
|
||||
let bitfield_size = bitfields.first().map(|b| b.unchecked_payload().0.len()).unwrap_or(0);
|
||||
set_proof_size_to_tx_size(
|
||||
<<T as inclusion::Config>::WeightInfo as inclusion::WeightInfo>::enact_candidate(
|
||||
max_ump_msgs,
|
||||
max_hrmp_msgs,
|
||||
1, // runtime upgrade
|
||||
)
|
||||
.saturating_mul(bitfield_size as u64),
|
||||
bitfields,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn backed_candidate_weight<T: frame_system::Config + Config>(
|
||||
candidate: &BackedCandidate<T::Hash>,
|
||||
) -> Weight {
|
||||
set_proof_size_to_tx_size(
|
||||
if candidate.candidate().commitments.new_validation_code.is_some() {
|
||||
<<T as Config>::WeightInfo as WeightInfo>::enter_backed_candidate_code_upgrade()
|
||||
} else {
|
||||
<<T as Config>::WeightInfo as WeightInfo>::enter_backed_candidates_variable(
|
||||
candidate.validity_votes().len() as u32,
|
||||
)
|
||||
}
|
||||
.saturating_sub(<<T as Config>::WeightInfo as WeightInfo>::enter_empty()),
|
||||
candidate,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn backed_candidates_weight<T: frame_system::Config + Config>(
|
||||
candidates: &[BackedCandidate<T::Hash>],
|
||||
) -> Weight {
|
||||
candidates
|
||||
.iter()
|
||||
.map(|c| backed_candidate_weight::<T>(c))
|
||||
.fold(Weight::zero(), |acc, x| acc.saturating_add(x))
|
||||
}
|
||||
|
||||
/// Set proof_size component of `Weight` to tx size.
|
||||
fn set_proof_size_to_tx_size<Arg: Encode>(weight: Weight, arg: Arg) -> Weight {
|
||||
weight.set_proof_size(arg.encoded_size() as u64)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// 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/>.
|
||||
|
||||
//! An implementation of the `RewardValidators` trait used by `inclusion` that employs
|
||||
//! `pallet-staking` to compute the rewards.
|
||||
//!
|
||||
//! Based on <https://research.web3.foundation/en/latest/polkadot/overview/2-token-economics.html>
|
||||
//! which doesn't currently mention availability bitfields. As such, we don't reward them
|
||||
//! for the time being, although we will build schemes to do so in the future.
|
||||
|
||||
use crate::{session_info, shared};
|
||||
use alloc::collections::btree_set::BTreeSet;
|
||||
use frame_support::traits::{Defensive, RewardsReporter, ValidatorSet};
|
||||
use pezkuwi_primitives::{SessionIndex, ValidatorIndex};
|
||||
|
||||
/// The amount of era points given by backing a candidate that is included.
|
||||
pub const BACKING_POINTS: u32 = 20;
|
||||
/// The amount of era points given by dispute voting on a candidate.
|
||||
pub const DISPUTE_STATEMENT_POINTS: u32 = 20;
|
||||
|
||||
/// Rewards validators for participating in teyrchains with era points in pallet-staking.
|
||||
pub struct RewardValidatorsWithEraPoints<C, R>(core::marker::PhantomData<(C, R)>);
|
||||
|
||||
impl<C, R> RewardValidatorsWithEraPoints<C, R>
|
||||
where
|
||||
C: session_info::Config,
|
||||
C::ValidatorSet: ValidatorSet<C::AccountId, ValidatorId = C::AccountId>,
|
||||
R: RewardsReporter<C::AccountId>,
|
||||
{
|
||||
/// Reward validators in session with points, but only if they are in the active set.
|
||||
fn reward_only_active(
|
||||
session_index: SessionIndex,
|
||||
indices: impl IntoIterator<Item = ValidatorIndex>,
|
||||
points: u32,
|
||||
) {
|
||||
let validators = session_info::AccountKeys::<C>::get(&session_index);
|
||||
let validators = match validators
|
||||
.defensive_proof("account_keys are present for dispute_period sessions")
|
||||
{
|
||||
Some(validators) => validators,
|
||||
None => return,
|
||||
};
|
||||
// limit rewards to the active validator set
|
||||
let active_set: BTreeSet<_> = C::ValidatorSet::validators().into_iter().collect();
|
||||
|
||||
let rewards = indices
|
||||
.into_iter()
|
||||
.filter_map(|i| validators.get(i.0 as usize).cloned())
|
||||
.filter(|v| active_set.contains(v))
|
||||
.map(|v| (v, points));
|
||||
|
||||
R::reward_by_ids(rewards);
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, R> crate::inclusion::RewardValidators for RewardValidatorsWithEraPoints<C, R>
|
||||
where
|
||||
C: shared::Config + session_info::Config,
|
||||
C::ValidatorSet: ValidatorSet<C::AccountId, ValidatorId = C::AccountId>,
|
||||
R: RewardsReporter<C::AccountId>,
|
||||
{
|
||||
fn reward_backing(indices: impl IntoIterator<Item = ValidatorIndex>) {
|
||||
let session_index = shared::CurrentSessionIndex::<C>::get();
|
||||
Self::reward_only_active(session_index, indices, BACKING_POINTS);
|
||||
}
|
||||
|
||||
fn reward_bitfields(_validators: impl IntoIterator<Item = ValidatorIndex>) {}
|
||||
}
|
||||
|
||||
impl<C, R> crate::disputes::RewardValidators for RewardValidatorsWithEraPoints<C, R>
|
||||
where
|
||||
C: session_info::Config,
|
||||
C::ValidatorSet: ValidatorSet<C::AccountId, ValidatorId = C::AccountId>,
|
||||
R: RewardsReporter<C::AccountId>,
|
||||
{
|
||||
fn reward_dispute_statement(
|
||||
session: SessionIndex,
|
||||
validators: impl IntoIterator<Item = ValidatorIndex>,
|
||||
) {
|
||||
Self::reward_only_active(session, validators, DISPUTE_STATEMENT_POINTS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// 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/>.
|
||||
|
||||
//! Runtime API implementations for Teyrchains.
|
||||
//!
|
||||
//! These are exposed as different modules using different sets of primitives.
|
||||
//! At the moment there is a `v2` module for the current stable API and
|
||||
//! `vstaging` module for all staging methods.
|
||||
//! When new version of the stable API is released it will be based on `v2` and
|
||||
//! will contain methods from `vstaging`.
|
||||
//! The promotion consists of the following steps:
|
||||
//! 1. Bump the version of the stable module (e.g. `v2` becomes `v3`)
|
||||
//! 2. Move methods from `vstaging` to `v3`. The new stable version should include all methods from
|
||||
//! `vstaging` tagged with the new version number (e.g. all `v3` methods).
|
||||
|
||||
pub mod v13;
|
||||
pub mod vstaging;
|
||||
@@ -0,0 +1,581 @@
|
||||
// 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.
|
||||
|
||||
//! A module exporting runtime API implementation functions for all runtime APIs using `v5`
|
||||
//! primitives.
|
||||
//!
|
||||
//! Runtimes implementing the v13 runtime API are recommended to forward directly to these
|
||||
//! functions.
|
||||
|
||||
use crate::{
|
||||
configuration, disputes, dmp, hrmp, inclusion, initializer, paras, paras_inherent, scheduler,
|
||||
session_info, shared,
|
||||
};
|
||||
use alloc::{
|
||||
collections::{btree_map::BTreeMap, vec_deque::VecDeque},
|
||||
vec,
|
||||
vec::Vec,
|
||||
};
|
||||
use frame_support::traits::{GetStorageVersion, StorageVersion};
|
||||
use frame_system::pallet_prelude::*;
|
||||
use pezkuwi_primitives::{
|
||||
async_backing::{
|
||||
AsyncBackingParams, BackingState, CandidatePendingAvailability, Constraints,
|
||||
InboundHrmpLimitations, OutboundHrmpChannelLimitations,
|
||||
},
|
||||
slashing, ApprovalVotingParams, AuthorityDiscoveryId, CandidateEvent, CandidateHash,
|
||||
CommittedCandidateReceiptV2 as CommittedCandidateReceipt, CoreIndex, CoreState, DisputeState,
|
||||
ExecutorParams, GroupIndex, GroupRotationInfo, Hash, Id as ParaId, InboundDownwardMessage,
|
||||
InboundHrmpMessage, NodeFeatures, OccupiedCore, OccupiedCoreAssumption,
|
||||
PersistedValidationData, PvfCheckStatement, ScrapedOnChainVotes, SessionIndex, SessionInfo,
|
||||
ValidationCode, ValidationCodeHash, ValidatorId, ValidatorIndex, ValidatorSignature,
|
||||
};
|
||||
use sp_runtime::traits::One;
|
||||
|
||||
/// Implementation for the `validators` function of the runtime API.
|
||||
pub fn validators<T: initializer::Config>() -> Vec<ValidatorId> {
|
||||
shared::ActiveValidatorKeys::<T>::get()
|
||||
}
|
||||
|
||||
/// Implementation for the `validator_groups` function of the runtime API.
|
||||
pub fn validator_groups<T: initializer::Config>(
|
||||
) -> (Vec<Vec<ValidatorIndex>>, GroupRotationInfo<BlockNumberFor<T>>) {
|
||||
// This formula needs to be the same as the one we use
|
||||
// when populating group_responsible in `availability_cores`
|
||||
let now = frame_system::Pallet::<T>::block_number() + One::one();
|
||||
|
||||
let groups = scheduler::ValidatorGroups::<T>::get();
|
||||
let rotation_info = scheduler::Pallet::<T>::group_rotation_info(now);
|
||||
|
||||
(groups, rotation_info)
|
||||
}
|
||||
|
||||
/// Implementation for the `availability_cores` function of the runtime API.
|
||||
pub fn availability_cores<T: initializer::Config>() -> Vec<CoreState<T::Hash, BlockNumberFor<T>>> {
|
||||
let time_out_for = scheduler::Pallet::<T>::availability_timeout_predicate();
|
||||
|
||||
let group_responsible_for =
|
||||
|backed_in_number, core_index| match scheduler::Pallet::<T>::group_assigned_to_core(
|
||||
core_index,
|
||||
backed_in_number,
|
||||
) {
|
||||
Some(g) => g,
|
||||
None => {
|
||||
log::warn!(
|
||||
target: "runtime::pezkuwi-api::v2",
|
||||
"Could not determine the group responsible for core extracted \
|
||||
from list of cores for some prior block in same session",
|
||||
);
|
||||
|
||||
GroupIndex(0)
|
||||
},
|
||||
};
|
||||
|
||||
let claim_queue = scheduler::Pallet::<T>::get_claim_queue();
|
||||
let occupied_cores: BTreeMap<CoreIndex, inclusion::CandidatePendingAvailability<_, _>> =
|
||||
inclusion::Pallet::<T>::get_occupied_cores().collect();
|
||||
let n_cores = scheduler::Pallet::<T>::num_availability_cores();
|
||||
|
||||
(0..n_cores)
|
||||
.map(|core_idx| {
|
||||
let core_idx = CoreIndex(core_idx as u32);
|
||||
if let Some(pending_availability) = occupied_cores.get(&core_idx) {
|
||||
// Use the same block number for determining the responsible group as what the
|
||||
// backing subsystem would use when it calls validator_groups api.
|
||||
let backing_group_allocation_time =
|
||||
pending_availability.relay_parent_number() + One::one();
|
||||
CoreState::Occupied(OccupiedCore {
|
||||
next_up_on_available: scheduler::Pallet::<T>::next_up_on_available(core_idx),
|
||||
occupied_since: pending_availability.backed_in_number(),
|
||||
time_out_at: time_out_for(pending_availability.backed_in_number()).live_until,
|
||||
next_up_on_time_out: scheduler::Pallet::<T>::next_up_on_available(core_idx),
|
||||
availability: pending_availability.availability_votes().clone(),
|
||||
group_responsible: group_responsible_for(
|
||||
backing_group_allocation_time,
|
||||
pending_availability.core_occupied(),
|
||||
),
|
||||
candidate_hash: pending_availability.candidate_hash(),
|
||||
candidate_descriptor: pending_availability.candidate_descriptor().clone(),
|
||||
})
|
||||
} else {
|
||||
if let Some(assignment) = claim_queue.get(&core_idx).and_then(|q| q.front()) {
|
||||
CoreState::Scheduled(pezkuwi_primitives::ScheduledCore {
|
||||
para_id: assignment.para_id(),
|
||||
collator: None,
|
||||
})
|
||||
} else {
|
||||
CoreState::Free
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns current block number being processed and the corresponding root hash.
|
||||
fn current_relay_parent<T: frame_system::Config>(
|
||||
) -> (BlockNumberFor<T>, <T as frame_system::Config>::Hash) {
|
||||
use codec::Decode as _;
|
||||
let state_version = frame_system::Pallet::<T>::runtime_version().state_version();
|
||||
let relay_parent_number = frame_system::Pallet::<T>::block_number();
|
||||
let relay_parent_storage_root = T::Hash::decode(&mut &sp_io::storage::root(state_version)[..])
|
||||
.expect("storage root must decode to the Hash type; qed");
|
||||
(relay_parent_number, relay_parent_storage_root)
|
||||
}
|
||||
|
||||
fn with_assumption<Config, T, F>(
|
||||
para_id: ParaId,
|
||||
assumption: OccupiedCoreAssumption,
|
||||
build: F,
|
||||
) -> Option<T>
|
||||
where
|
||||
Config: inclusion::Config,
|
||||
F: FnOnce() -> Option<T>,
|
||||
{
|
||||
match assumption {
|
||||
OccupiedCoreAssumption::Included => {
|
||||
<inclusion::Pallet<Config>>::force_enact(para_id);
|
||||
build()
|
||||
},
|
||||
OccupiedCoreAssumption::TimedOut => build(),
|
||||
OccupiedCoreAssumption::Free =>
|
||||
if !<inclusion::Pallet<Config>>::candidates_pending_availability(para_id).is_empty() {
|
||||
None
|
||||
} else {
|
||||
build()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementation for the `persisted_validation_data` function of the runtime API.
|
||||
pub fn persisted_validation_data<T: initializer::Config>(
|
||||
para_id: ParaId,
|
||||
assumption: OccupiedCoreAssumption,
|
||||
) -> Option<PersistedValidationData<T::Hash, BlockNumberFor<T>>> {
|
||||
let (relay_parent_number, relay_parent_storage_root) = current_relay_parent::<T>();
|
||||
with_assumption::<T, _, _>(para_id, assumption, || {
|
||||
crate::util::make_persisted_validation_data::<T>(
|
||||
para_id,
|
||||
relay_parent_number,
|
||||
relay_parent_storage_root,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Implementation for the `assumed_validation_data` function of the runtime API.
|
||||
pub fn assumed_validation_data<T: initializer::Config>(
|
||||
para_id: ParaId,
|
||||
expected_persisted_validation_data_hash: Hash,
|
||||
) -> Option<(PersistedValidationData<T::Hash, BlockNumberFor<T>>, ValidationCodeHash)> {
|
||||
let (relay_parent_number, relay_parent_storage_root) = current_relay_parent::<T>();
|
||||
// This closure obtains the `persisted_validation_data` for the given `para_id` and matches
|
||||
// its hash against an expected one.
|
||||
let make_validation_data = || {
|
||||
crate::util::make_persisted_validation_data::<T>(
|
||||
para_id,
|
||||
relay_parent_number,
|
||||
relay_parent_storage_root,
|
||||
)
|
||||
.filter(|validation_data| validation_data.hash() == expected_persisted_validation_data_hash)
|
||||
};
|
||||
|
||||
let persisted_validation_data = make_validation_data().or_else(|| {
|
||||
// Try again with force enacting the pending candidates. This check only makes sense if
|
||||
// there are any pending candidates.
|
||||
(!inclusion::Pallet::<T>::candidates_pending_availability(para_id).is_empty())
|
||||
.then_some(())
|
||||
.and_then(|_| {
|
||||
inclusion::Pallet::<T>::force_enact(para_id);
|
||||
make_validation_data()
|
||||
})
|
||||
});
|
||||
// If we were successful, also query current validation code hash.
|
||||
persisted_validation_data.zip(paras::CurrentCodeHash::<T>::get(¶_id))
|
||||
}
|
||||
|
||||
/// Implementation for the `check_validation_outputs` function of the runtime API.
|
||||
pub fn check_validation_outputs<T: initializer::Config>(
|
||||
para_id: ParaId,
|
||||
outputs: pezkuwi_primitives::CandidateCommitments,
|
||||
) -> bool {
|
||||
let relay_parent_number = frame_system::Pallet::<T>::block_number();
|
||||
inclusion::Pallet::<T>::check_validation_outputs_for_runtime_api(
|
||||
para_id,
|
||||
relay_parent_number,
|
||||
outputs,
|
||||
)
|
||||
}
|
||||
|
||||
/// Implementation for the `session_index_for_child` function of the runtime API.
|
||||
pub fn session_index_for_child<T: initializer::Config>() -> SessionIndex {
|
||||
// Just returns the session index from `inclusion`. Runtime APIs follow
|
||||
// initialization so the initializer will have applied any pending session change
|
||||
// which is expected at the child of the block whose context the runtime API was invoked
|
||||
// in.
|
||||
//
|
||||
// Incidentally, this is also the rationale for why it is OK to query validators or
|
||||
// occupied cores or etc. and expect the correct response "for child".
|
||||
shared::CurrentSessionIndex::<T>::get()
|
||||
}
|
||||
|
||||
/// Implementation for the `AuthorityDiscoveryApi::authorities()` function of the runtime API.
|
||||
/// It is a heavy call, but currently only used for authority discovery, so it is fine.
|
||||
/// Gets next, current and some historical authority ids using `session_info` module.
|
||||
pub fn relevant_authority_ids<T: initializer::Config + pallet_authority_discovery::Config>(
|
||||
) -> Vec<AuthorityDiscoveryId> {
|
||||
let current_session_index = session_index_for_child::<T>();
|
||||
let earliest_stored_session = session_info::EarliestStoredSession::<T>::get();
|
||||
|
||||
// Due to `max_validators`, the `SessionInfo` stores only the validators who are actively
|
||||
// selected to participate in teyrchain consensus. We'd like all authorities for the current
|
||||
// and next sessions to be used in authority-discovery. The two sets likely have large overlap.
|
||||
let mut authority_ids = pallet_authority_discovery::Pallet::<T>::current_authorities().to_vec();
|
||||
authority_ids.extend(pallet_authority_discovery::Pallet::<T>::next_authorities().to_vec());
|
||||
|
||||
// Due to disputes, we'd like to remain connected to authorities of the previous few sessions.
|
||||
// For this, we don't need anyone other than the validators actively participating in consensus.
|
||||
for session_index in earliest_stored_session..current_session_index {
|
||||
let info = session_info::Sessions::<T>::get(session_index);
|
||||
if let Some(mut info) = info {
|
||||
authority_ids.append(&mut info.discovery_keys);
|
||||
}
|
||||
}
|
||||
|
||||
authority_ids.sort();
|
||||
authority_ids.dedup();
|
||||
|
||||
authority_ids
|
||||
}
|
||||
|
||||
/// Implementation for the `validation_code` function of the runtime API.
|
||||
pub fn validation_code<T: initializer::Config>(
|
||||
para_id: ParaId,
|
||||
assumption: OccupiedCoreAssumption,
|
||||
) -> Option<ValidationCode> {
|
||||
with_assumption::<T, _, _>(para_id, assumption, || paras::Pallet::<T>::current_code(¶_id))
|
||||
}
|
||||
|
||||
/// Implementation for the `candidate_pending_availability` function of the runtime API.
|
||||
#[deprecated(
|
||||
note = "`candidate_pending_availability` will be removed. Use `candidates_pending_availability` to query
|
||||
all candidates pending availability"
|
||||
)]
|
||||
pub fn candidate_pending_availability<T: initializer::Config>(
|
||||
para_id: ParaId,
|
||||
) -> Option<CommittedCandidateReceipt<T::Hash>> {
|
||||
inclusion::Pallet::<T>::first_candidate_pending_availability(para_id)
|
||||
}
|
||||
|
||||
/// Implementation for the `candidate_events` function of the runtime API.
|
||||
// NOTE: this runs without block initialization, as it accesses events.
|
||||
// this means it can run in a different session than other runtime APIs at the same block.
|
||||
pub fn candidate_events<T, F>(extract_event: F) -> Vec<CandidateEvent<T::Hash>>
|
||||
where
|
||||
T: initializer::Config,
|
||||
F: Fn(<T as frame_system::Config>::RuntimeEvent) -> Option<inclusion::Event<T>>,
|
||||
{
|
||||
use inclusion::Event as RawEvent;
|
||||
|
||||
frame_system::Pallet::<T>::read_events_no_consensus()
|
||||
.into_iter()
|
||||
.filter_map(|record| extract_event(record.event))
|
||||
.filter_map(|event| {
|
||||
Some(match event {
|
||||
RawEvent::<T>::CandidateBacked(c, h, core, group) =>
|
||||
CandidateEvent::CandidateBacked(c, h, core, group),
|
||||
RawEvent::<T>::CandidateIncluded(c, h, core, group) =>
|
||||
CandidateEvent::CandidateIncluded(c, h, core, group),
|
||||
RawEvent::<T>::CandidateTimedOut(c, h, core) =>
|
||||
CandidateEvent::CandidateTimedOut(c, h, core),
|
||||
// Not needed for candidate events.
|
||||
RawEvent::<T>::UpwardMessagesReceived { .. } => return None,
|
||||
RawEvent::<T>::__Ignore(_, _) => unreachable!("__Ignore cannot be used"),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get the session info for the given session, if stored.
|
||||
pub fn session_info<T: session_info::Config>(index: SessionIndex) -> Option<SessionInfo> {
|
||||
session_info::Sessions::<T>::get(index)
|
||||
}
|
||||
|
||||
/// Implementation for the `dmq_contents` function of the runtime API.
|
||||
pub fn dmq_contents<T: dmp::Config>(
|
||||
recipient: ParaId,
|
||||
) -> Vec<InboundDownwardMessage<BlockNumberFor<T>>> {
|
||||
dmp::Pallet::<T>::dmq_contents(recipient)
|
||||
}
|
||||
|
||||
/// Implementation for the `inbound_hrmp_channels_contents` function of the runtime API.
|
||||
pub fn inbound_hrmp_channels_contents<T: hrmp::Config>(
|
||||
recipient: ParaId,
|
||||
) -> BTreeMap<ParaId, Vec<InboundHrmpMessage<BlockNumberFor<T>>>> {
|
||||
hrmp::Pallet::<T>::inbound_hrmp_channels_contents(recipient)
|
||||
}
|
||||
|
||||
/// Implementation for the `validation_code_by_hash` function of the runtime API.
|
||||
pub fn validation_code_by_hash<T: paras::Config>(
|
||||
hash: ValidationCodeHash,
|
||||
) -> Option<ValidationCode> {
|
||||
paras::CodeByHash::<T>::get(hash)
|
||||
}
|
||||
|
||||
/// Disputes imported via means of on-chain imports.
|
||||
pub fn on_chain_votes<T: paras_inherent::Config>() -> Option<ScrapedOnChainVotes<T::Hash>> {
|
||||
paras_inherent::OnChainVotes::<T>::get()
|
||||
}
|
||||
|
||||
/// Submits an PVF pre-checking vote.
|
||||
pub fn submit_pvf_check_statement<T: paras::Config>(
|
||||
stmt: PvfCheckStatement,
|
||||
signature: ValidatorSignature,
|
||||
) {
|
||||
paras::Pallet::<T>::submit_pvf_check_statement(stmt, signature)
|
||||
}
|
||||
|
||||
/// Returns the list of all PVF code hashes that require pre-checking.
|
||||
pub fn pvfs_require_precheck<T: paras::Config>() -> Vec<ValidationCodeHash> {
|
||||
paras::Pallet::<T>::pvfs_require_precheck()
|
||||
}
|
||||
|
||||
/// Returns the validation code hash for the given teyrchain making the given
|
||||
/// `OccupiedCoreAssumption`.
|
||||
pub fn validation_code_hash<T>(
|
||||
para_id: ParaId,
|
||||
assumption: OccupiedCoreAssumption,
|
||||
) -> Option<ValidationCodeHash>
|
||||
where
|
||||
T: inclusion::Config,
|
||||
{
|
||||
with_assumption::<T, _, _>(para_id, assumption, || paras::CurrentCodeHash::<T>::get(¶_id))
|
||||
}
|
||||
|
||||
/// Implementation for `get_session_disputes` function from the runtime API
|
||||
pub fn get_session_disputes<T: disputes::Config>(
|
||||
) -> Vec<(SessionIndex, CandidateHash, DisputeState<BlockNumberFor<T>>)> {
|
||||
disputes::Pallet::<T>::disputes()
|
||||
}
|
||||
|
||||
/// Get session executor parameter set
|
||||
pub fn session_executor_params<T: session_info::Config>(
|
||||
session_index: SessionIndex,
|
||||
) -> Option<ExecutorParams> {
|
||||
session_info::SessionExecutorParams::<T>::get(session_index)
|
||||
}
|
||||
|
||||
/// Implementation of `unapplied_slashes` runtime API
|
||||
pub fn unapplied_slashes<T: disputes::slashing::Config>(
|
||||
) -> Vec<(SessionIndex, CandidateHash, slashing::LegacyPendingSlashes)> {
|
||||
disputes::slashing::Pallet::<T>::unapplied_slashes()
|
||||
.into_iter()
|
||||
.filter_map(|(session, candidate_hash, pending_slash)| {
|
||||
let legacy_pending_slash = slashing::LegacyPendingSlashes {
|
||||
keys: pending_slash.keys,
|
||||
kind: pending_slash.kind.try_into().ok()?,
|
||||
};
|
||||
Some((session, candidate_hash, legacy_pending_slash))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Implementation of `unapplied_slashes_v2` runtime API
|
||||
pub fn unapplied_slashes_v2<T: disputes::slashing::Config>(
|
||||
) -> Vec<(SessionIndex, CandidateHash, slashing::PendingSlashes)> {
|
||||
disputes::slashing::Pallet::<T>::unapplied_slashes()
|
||||
}
|
||||
|
||||
/// Implementation of `submit_report_dispute_lost` runtime API
|
||||
pub fn submit_unsigned_slashing_report<T: disputes::slashing::Config>(
|
||||
dispute_proof: slashing::DisputeProof,
|
||||
key_ownership_proof: slashing::OpaqueKeyOwnershipProof,
|
||||
) -> Option<()> {
|
||||
let key_ownership_proof = key_ownership_proof.decode()?;
|
||||
|
||||
disputes::slashing::Pallet::<T>::submit_unsigned_slashing_report(
|
||||
dispute_proof,
|
||||
key_ownership_proof,
|
||||
)
|
||||
}
|
||||
|
||||
/// Return the min backing votes threshold from the configuration.
|
||||
pub fn minimum_backing_votes<T: initializer::Config>() -> u32 {
|
||||
configuration::ActiveConfig::<T>::get().minimum_backing_votes
|
||||
}
|
||||
|
||||
// Helper function that returns the backing constraints given a teyrchain id.
|
||||
pub fn backing_constraints<T: initializer::Config>(
|
||||
para_id: ParaId,
|
||||
) -> Option<Constraints<BlockNumberFor<T>>> {
|
||||
let config = configuration::ActiveConfig::<T>::get();
|
||||
// Async backing is only expected to be enabled with a tracker capacity of 1.
|
||||
// Subsequent configuration update gets applied on new session, which always
|
||||
// clears the buffer.
|
||||
//
|
||||
// Thus, minimum relay parent is ensured to have asynchronous backing enabled.
|
||||
let now = frame_system::Pallet::<T>::block_number();
|
||||
|
||||
// Use the right storage depending on version to ensure #64 doesn't cause issues with this
|
||||
// migration.
|
||||
let min_relay_parent_number = if shared::Pallet::<T>::on_chain_storage_version() ==
|
||||
StorageVersion::new(0)
|
||||
{
|
||||
shared::migration::v0::AllowedRelayParents::<T>::get().hypothetical_earliest_block_number(
|
||||
now,
|
||||
config.scheduler_params.lookahead.saturating_sub(1),
|
||||
)
|
||||
} else {
|
||||
shared::AllowedRelayParents::<T>::get().hypothetical_earliest_block_number(
|
||||
now,
|
||||
config.scheduler_params.lookahead.saturating_sub(1),
|
||||
)
|
||||
};
|
||||
|
||||
let required_parent = paras::Heads::<T>::get(para_id)?;
|
||||
let validation_code_hash = paras::CurrentCodeHash::<T>::get(para_id)?;
|
||||
|
||||
let upgrade_restriction = paras::UpgradeRestrictionSignal::<T>::get(para_id);
|
||||
let future_validation_code =
|
||||
paras::FutureCodeUpgrades::<T>::get(para_id).and_then(|block_num| {
|
||||
// Only read the storage if there's a pending upgrade.
|
||||
Some(block_num).zip(paras::FutureCodeHash::<T>::get(para_id))
|
||||
});
|
||||
|
||||
let (ump_msg_count, ump_total_bytes) =
|
||||
inclusion::Pallet::<T>::relay_dispatch_queue_size(para_id);
|
||||
let ump_remaining = config.max_upward_queue_count - ump_msg_count;
|
||||
let ump_remaining_bytes = config.max_upward_queue_size - ump_total_bytes;
|
||||
|
||||
let dmp_remaining_messages = dmp::Pallet::<T>::dmq_contents(para_id)
|
||||
.into_iter()
|
||||
.map(|msg| msg.sent_at)
|
||||
.collect();
|
||||
|
||||
let valid_watermarks = hrmp::Pallet::<T>::valid_watermarks(para_id);
|
||||
let hrmp_inbound = InboundHrmpLimitations { valid_watermarks };
|
||||
let hrmp_channels_out = hrmp::Pallet::<T>::outbound_remaining_capacity(para_id)
|
||||
.into_iter()
|
||||
.map(|(para, (messages_remaining, bytes_remaining))| {
|
||||
(para, OutboundHrmpChannelLimitations { messages_remaining, bytes_remaining })
|
||||
})
|
||||
.collect();
|
||||
|
||||
Some(Constraints {
|
||||
min_relay_parent_number,
|
||||
max_pov_size: config.max_pov_size,
|
||||
max_code_size: config.max_code_size,
|
||||
max_head_data_size: Constraints::<BlockNumberFor<T>>::DEFAULT_MAX_HEAD_DATA_SIZE,
|
||||
ump_remaining,
|
||||
ump_remaining_bytes,
|
||||
max_ump_num_per_candidate: config.max_upward_message_num_per_candidate,
|
||||
dmp_remaining_messages,
|
||||
hrmp_inbound,
|
||||
hrmp_channels_out,
|
||||
max_hrmp_num_per_candidate: config.hrmp_max_message_num_per_candidate,
|
||||
required_parent,
|
||||
validation_code_hash,
|
||||
upgrade_restriction,
|
||||
future_validation_code,
|
||||
})
|
||||
}
|
||||
|
||||
/// Implementation for `ParaBackingState` function from the runtime API
|
||||
#[deprecated(note = "`backing_state` will be removed. Use `backing_constraints` and
|
||||
`candidates_pending_availability` instead.")]
|
||||
pub fn backing_state<T: initializer::Config>(
|
||||
para_id: ParaId,
|
||||
) -> Option<BackingState<T::Hash, BlockNumberFor<T>>> {
|
||||
let constraints = backing_constraints::<T>(para_id)?;
|
||||
|
||||
let pending_availability = {
|
||||
crate::inclusion::PendingAvailability::<T>::get(¶_id)
|
||||
.map(|pending_candidates| {
|
||||
pending_candidates
|
||||
.into_iter()
|
||||
.map(|candidate| {
|
||||
CandidatePendingAvailability {
|
||||
candidate_hash: candidate.candidate_hash(),
|
||||
descriptor: candidate.candidate_descriptor().clone(),
|
||||
commitments: candidate.candidate_commitments().clone(),
|
||||
relay_parent_number: candidate.relay_parent_number(),
|
||||
max_pov_size: constraints.max_pov_size, /* assume always same in
|
||||
* session. */
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(|| vec![])
|
||||
};
|
||||
|
||||
Some(BackingState { constraints, pending_availability })
|
||||
}
|
||||
|
||||
/// Implementation for `AsyncBackingParams` function from the runtime API
|
||||
#[deprecated = "AsyncBackingParams are going to be removed and ignored by relay chain validators, in favour of dynamically computed values based on the claim queue assignments"]
|
||||
pub fn async_backing_params<T: configuration::Config>() -> AsyncBackingParams {
|
||||
configuration::ActiveConfig::<T>::get().async_backing_params
|
||||
}
|
||||
|
||||
/// Implementation for `DisabledValidators`
|
||||
// CAVEAT: this should only be called on the node side
|
||||
// as it might produce incorrect results on session boundaries
|
||||
pub fn disabled_validators<T>() -> Vec<ValidatorIndex>
|
||||
where
|
||||
T: shared::Config,
|
||||
{
|
||||
<shared::Pallet<T>>::disabled_validators()
|
||||
}
|
||||
|
||||
/// Returns the current state of the node features.
|
||||
pub fn node_features<T: initializer::Config>() -> NodeFeatures {
|
||||
configuration::ActiveConfig::<T>::get().node_features
|
||||
}
|
||||
|
||||
/// Approval voting subsystem configuration parameters
|
||||
pub fn approval_voting_params<T: initializer::Config>() -> ApprovalVotingParams {
|
||||
configuration::ActiveConfig::<T>::get().approval_voting_params
|
||||
}
|
||||
|
||||
/// Returns the claimqueue from the scheduler
|
||||
pub fn claim_queue<T: scheduler::Config>() -> BTreeMap<CoreIndex, VecDeque<ParaId>> {
|
||||
let config = configuration::ActiveConfig::<T>::get();
|
||||
// Extra sanity, config should already never be smaller than 1:
|
||||
let n_lookahead = config.scheduler_params.lookahead.max(1);
|
||||
scheduler::Pallet::<T>::get_claim_queue()
|
||||
.into_iter()
|
||||
.map(|(core_index, entries)| {
|
||||
(
|
||||
core_index,
|
||||
entries.into_iter().map(|e| e.para_id()).take(n_lookahead as usize).collect(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns all the candidates that are pending availability for a given `ParaId`.
|
||||
/// Deprecates `candidate_pending_availability` in favor of supporting elastic scaling.
|
||||
pub fn candidates_pending_availability<T: initializer::Config>(
|
||||
para_id: ParaId,
|
||||
) -> Vec<CommittedCandidateReceipt<T::Hash>> {
|
||||
<inclusion::Pallet<T>>::candidates_pending_availability(para_id)
|
||||
}
|
||||
|
||||
/// Implementation for `validation_code_bomb_limit` function from the runtime API
|
||||
pub fn validation_code_bomb_limit<T: initializer::Config>() -> u32 {
|
||||
configuration::ActiveConfig::<T>::get().max_code_size *
|
||||
configuration::MAX_VALIDATION_CODE_COMPRESSION_RATIO
|
||||
}
|
||||
|
||||
/// Implementation for `scheduling_lookahead` function from the runtime API
|
||||
pub fn scheduling_lookahead<T: initializer::Config>() -> u32 {
|
||||
configuration::ActiveConfig::<T>::get().scheduler_params.lookahead
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// 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/>.
|
||||
|
||||
//! Put implementations of functions from staging APIs here.
|
||||
|
||||
use crate::{disputes, initializer, paras};
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use pezkuwi_primitives::{slashing, CandidateHash, Id as ParaId, SessionIndex};
|
||||
|
||||
/// Implementation of `para_ids` runtime API
|
||||
pub fn para_ids<T: initializer::Config>() -> Vec<ParaId> {
|
||||
paras::Heads::<T>::iter_keys().collect()
|
||||
}
|
||||
|
||||
/// Implementation of `unapplied_slashes_v2` runtime API
|
||||
pub fn unapplied_slashes_v2<T: disputes::slashing::Config>(
|
||||
) -> Vec<(SessionIndex, CandidateHash, slashing::PendingSlashes)> {
|
||||
disputes::slashing::Pallet::<T>::unapplied_slashes()
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
// 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 scheduler module for teyrchains and parathreads.
|
||||
//!
|
||||
//! This module is responsible for two main tasks:
|
||||
//! - Partitioning validators into groups and assigning groups to teyrchains and parathreads
|
||||
//! - Scheduling teyrchains and parathreads
|
||||
//!
|
||||
//! It aims to achieve these tasks with these goals in mind:
|
||||
//! - It should be possible to know at least a block ahead-of-time, ideally more, which validators
|
||||
//! are going to be assigned to which teyrchains.
|
||||
//! - Teyrchains that have a candidate pending availability in this fork of the chain should not be
|
||||
//! assigned.
|
||||
//! - Validator assignments should not be gameable. Malicious cartels should not be able to
|
||||
//! manipulate the scheduler to assign themselves as desired.
|
||||
//! - High or close to optimal throughput of teyrchains and parathreads. Work among validator groups
|
||||
//! should be balanced.
|
||||
//!
|
||||
//! The Scheduler manages resource allocation using the concept of "Availability Cores".
|
||||
//! There will be one availability core for each teyrchain, and a fixed number of cores
|
||||
//! used for multiplexing parathreads. Validators will be partitioned into groups, with the same
|
||||
//! number of groups as availability cores. Validator groups will be assigned to different
|
||||
//! availability cores over time.
|
||||
|
||||
use crate::{configuration, initializer::SessionChangeNotification, paras};
|
||||
use alloc::{
|
||||
collections::{btree_map::BTreeMap, btree_set::BTreeSet, vec_deque::VecDeque},
|
||||
vec::Vec,
|
||||
};
|
||||
use frame_support::{pallet_prelude::*, traits::Defensive};
|
||||
use frame_system::pallet_prelude::BlockNumberFor;
|
||||
pub use pezkuwi_core_primitives::v2::BlockNumber;
|
||||
use pezkuwi_primitives::{
|
||||
CoreIndex, GroupIndex, GroupRotationInfo, Id as ParaId, ScheduledCore, SchedulerParams,
|
||||
ValidatorIndex,
|
||||
};
|
||||
use sp_runtime::traits::One;
|
||||
|
||||
pub mod common;
|
||||
|
||||
use common::{Assignment, AssignmentProvider};
|
||||
|
||||
pub use pallet::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
const LOG_TARGET: &str = "runtime::teyrchains::scheduler";
|
||||
|
||||
pub mod migration;
|
||||
|
||||
#[frame_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
|
||||
const STORAGE_VERSION: StorageVersion = StorageVersion::new(3);
|
||||
|
||||
#[pallet::pallet]
|
||||
#[pallet::without_storage_info]
|
||||
#[pallet::storage_version(STORAGE_VERSION)]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: frame_system::Config + configuration::Config + paras::Config {
|
||||
type AssignmentProvider: AssignmentProvider<BlockNumberFor<Self>>;
|
||||
}
|
||||
|
||||
/// All the validator groups. One for each core. Indices are into `ActiveValidators` - not the
|
||||
/// broader set of Pezkuwi validators, but instead just the subset used for teyrchains during
|
||||
/// this session.
|
||||
///
|
||||
/// Bound: The number of cores is the sum of the numbers of teyrchains and parathread
|
||||
/// multiplexers. Reasonably, 100-1000. The dominant factor is the number of validators: safe
|
||||
/// upper bound at 10k.
|
||||
#[pallet::storage]
|
||||
pub type ValidatorGroups<T> = StorageValue<_, Vec<Vec<ValidatorIndex>>, ValueQuery>;
|
||||
|
||||
/// The block number where the session start occurred. Used to track how many group rotations
|
||||
/// have occurred.
|
||||
///
|
||||
/// Note that in the context of teyrchains modules the session change is signaled during
|
||||
/// the block and enacted at the end of the block (at the finalization stage, to be exact).
|
||||
/// Thus for all intents and purposes the effect of the session change is observed at the
|
||||
/// block following the session change, block number of which we save in this storage value.
|
||||
#[pallet::storage]
|
||||
pub type SessionStartBlock<T: Config> = StorageValue<_, BlockNumberFor<T>, ValueQuery>;
|
||||
|
||||
/// One entry for each availability core. The `VecDeque` represents the assignments to be
|
||||
/// scheduled on that core.
|
||||
#[pallet::storage]
|
||||
pub type ClaimQueue<T> = StorageValue<_, BTreeMap<CoreIndex, VecDeque<Assignment>>, ValueQuery>;
|
||||
|
||||
/// Availability timeout status of a core.
|
||||
pub(crate) struct AvailabilityTimeoutStatus<BlockNumber> {
|
||||
/// Is the core already timed out?
|
||||
///
|
||||
/// If this is true the core will be freed at this block.
|
||||
pub timed_out: bool,
|
||||
|
||||
/// When does this core timeout.
|
||||
///
|
||||
/// The block number the core times out. If `timed_out` is true, this will correspond to
|
||||
/// now (current block number).
|
||||
pub live_until: BlockNumber,
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Called by the initializer to initialize the scheduler pallet.
|
||||
pub(crate) fn initializer_initialize(_now: BlockNumberFor<T>) -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
|
||||
/// Called by the initializer to finalize the scheduler pallet.
|
||||
pub(crate) fn initializer_finalize() {}
|
||||
|
||||
/// Called by the initializer to note that a new session has started.
|
||||
pub(crate) fn initializer_on_new_session(
|
||||
notification: &SessionChangeNotification<BlockNumberFor<T>>,
|
||||
) {
|
||||
let SessionChangeNotification { validators, new_config, .. } = notification;
|
||||
let config = new_config;
|
||||
let assigner_cores = config.scheduler_params.num_cores;
|
||||
|
||||
let n_cores = core::cmp::max(
|
||||
assigner_cores,
|
||||
match config.scheduler_params.max_validators_per_core {
|
||||
Some(x) if x != 0 => validators.len() as u32 / x,
|
||||
_ => 0,
|
||||
},
|
||||
);
|
||||
|
||||
// shuffle validators into groups.
|
||||
if n_cores == 0 || validators.is_empty() {
|
||||
ValidatorGroups::<T>::set(Vec::new());
|
||||
} else {
|
||||
let group_base_size = validators
|
||||
.len()
|
||||
.checked_div(n_cores as usize)
|
||||
.defensive_proof("n_cores should not be 0")
|
||||
.unwrap_or(0);
|
||||
let n_larger_groups = validators
|
||||
.len()
|
||||
.checked_rem(n_cores as usize)
|
||||
.defensive_proof("n_cores should not be 0")
|
||||
.unwrap_or(0);
|
||||
|
||||
// Groups contain indices into the validators from the session change notification,
|
||||
// which are already shuffled.
|
||||
|
||||
let mut groups: Vec<Vec<ValidatorIndex>> = Vec::new();
|
||||
for i in 0..n_larger_groups {
|
||||
let offset = (group_base_size + 1) * i;
|
||||
groups.push(
|
||||
(0..group_base_size + 1)
|
||||
.map(|j| offset + j)
|
||||
.map(|j| ValidatorIndex(j as _))
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
for i in 0..(n_cores as usize - n_larger_groups) {
|
||||
let offset = (n_larger_groups * (group_base_size + 1)) + (i * group_base_size);
|
||||
groups.push(
|
||||
(0..group_base_size)
|
||||
.map(|j| offset + j)
|
||||
.map(|j| ValidatorIndex(j as _))
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
ValidatorGroups::<T>::set(groups);
|
||||
}
|
||||
|
||||
// Resize and populate claim queue.
|
||||
Self::maybe_resize_claim_queue();
|
||||
Self::populate_claim_queue_after_session_change();
|
||||
|
||||
let now = frame_system::Pallet::<T>::block_number() + One::one();
|
||||
SessionStartBlock::<T>::set(now);
|
||||
}
|
||||
|
||||
/// Get the validators in the given group, if the group index is valid for this session.
|
||||
pub(crate) fn group_validators(group_index: GroupIndex) -> Option<Vec<ValidatorIndex>> {
|
||||
ValidatorGroups::<T>::get().get(group_index.0 as usize).map(|g| g.clone())
|
||||
}
|
||||
|
||||
/// Get the number of cores.
|
||||
pub(crate) fn num_availability_cores() -> usize {
|
||||
ValidatorGroups::<T>::decode_len().unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Expected claim queue len. Can be different than the real length if for example we don't have
|
||||
/// assignments for a core.
|
||||
fn expected_claim_queue_len(config: &SchedulerParams<BlockNumberFor<T>>) -> u32 {
|
||||
core::cmp::min(config.num_cores, Self::num_availability_cores() as u32)
|
||||
}
|
||||
|
||||
/// Get the group assigned to a specific core by index at the current block number. Result
|
||||
/// undefined if the core index is unknown or the block number is less than the session start
|
||||
/// index.
|
||||
pub(crate) fn group_assigned_to_core(
|
||||
core: CoreIndex,
|
||||
at: BlockNumberFor<T>,
|
||||
) -> Option<GroupIndex> {
|
||||
let config = configuration::ActiveConfig::<T>::get();
|
||||
let session_start_block = SessionStartBlock::<T>::get();
|
||||
|
||||
if at < session_start_block {
|
||||
return None;
|
||||
}
|
||||
|
||||
let validator_groups = ValidatorGroups::<T>::get();
|
||||
|
||||
if core.0 as usize >= validator_groups.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let rotations_since_session_start: BlockNumberFor<T> =
|
||||
(at - session_start_block) / config.scheduler_params.group_rotation_frequency;
|
||||
|
||||
let rotations_since_session_start =
|
||||
<BlockNumberFor<T> as TryInto<u32>>::try_into(rotations_since_session_start)
|
||||
.unwrap_or(0);
|
||||
// Error case can only happen if rotations occur only once every u32::max(),
|
||||
// so functionally no difference in behavior.
|
||||
|
||||
let group_idx =
|
||||
(core.0 as usize + rotations_since_session_start as usize) % validator_groups.len();
|
||||
Some(GroupIndex(group_idx as u32))
|
||||
}
|
||||
|
||||
/// Returns a predicate that should be used for timing out occupied cores.
|
||||
///
|
||||
/// This only ever times out cores that have been occupied across a group rotation boundary.
|
||||
pub(crate) fn availability_timeout_predicate(
|
||||
) -> impl Fn(BlockNumberFor<T>) -> AvailabilityTimeoutStatus<BlockNumberFor<T>> {
|
||||
let config = configuration::ActiveConfig::<T>::get();
|
||||
let now = frame_system::Pallet::<T>::block_number();
|
||||
let rotation_info = Self::group_rotation_info(now);
|
||||
|
||||
let next_rotation = rotation_info.next_rotation_at();
|
||||
|
||||
let times_out = Self::availability_timeout_check_required();
|
||||
|
||||
move |pending_since| {
|
||||
let time_out_at = if times_out {
|
||||
// We are at the beginning of the rotation, here availability period is relevant.
|
||||
// Note: blocks backed in this rotation will never time out here as backed_in +
|
||||
// config.paras_availability_period will always be > now for these blocks, as
|
||||
// otherwise above condition would not be true.
|
||||
pending_since + config.scheduler_params.paras_availability_period
|
||||
} else {
|
||||
next_rotation + config.scheduler_params.paras_availability_period
|
||||
};
|
||||
|
||||
AvailabilityTimeoutStatus { timed_out: time_out_at <= now, live_until: time_out_at }
|
||||
}
|
||||
}
|
||||
|
||||
/// Is evaluation of `availability_timeout_predicate` necessary at the current block?
|
||||
///
|
||||
/// This can be used to avoid calling `availability_timeout_predicate` for each core in case
|
||||
/// this function returns false.
|
||||
pub(crate) fn availability_timeout_check_required() -> bool {
|
||||
let config = configuration::ActiveConfig::<T>::get();
|
||||
let now = frame_system::Pallet::<T>::block_number() + One::one();
|
||||
let rotation_info = Self::group_rotation_info(now);
|
||||
|
||||
let current_window =
|
||||
rotation_info.last_rotation_at() + config.scheduler_params.paras_availability_period;
|
||||
now < current_window
|
||||
}
|
||||
|
||||
/// Returns a helper for determining group rotation.
|
||||
pub(crate) fn group_rotation_info(
|
||||
now: BlockNumberFor<T>,
|
||||
) -> GroupRotationInfo<BlockNumberFor<T>> {
|
||||
let session_start_block = SessionStartBlock::<T>::get();
|
||||
let group_rotation_frequency = configuration::ActiveConfig::<T>::get()
|
||||
.scheduler_params
|
||||
.group_rotation_frequency;
|
||||
|
||||
GroupRotationInfo { session_start_block, now, group_rotation_frequency }
|
||||
}
|
||||
|
||||
/// Return the next thing that will be scheduled on this core assuming it is currently
|
||||
/// occupied and the candidate occupying it became available.
|
||||
pub(crate) fn next_up_on_available(core: CoreIndex) -> Option<ScheduledCore> {
|
||||
// Since this is being called from a runtime API, we need to workaround for #64.
|
||||
if Self::on_chain_storage_version() == StorageVersion::new(2) {
|
||||
migration::v2::ClaimQueue::<T>::get()
|
||||
.get(&core)
|
||||
.and_then(|a| a.front().map(|entry| entry.assignment.para_id()))
|
||||
} else {
|
||||
ClaimQueue::<T>::get()
|
||||
.get(&core)
|
||||
.and_then(|a| a.front().map(|assignment| assignment.para_id()))
|
||||
}
|
||||
.map(|para_id| ScheduledCore { para_id, collator: None })
|
||||
}
|
||||
|
||||
// Since this is being called from a runtime API, we need to workaround for #64.
|
||||
pub(crate) fn get_claim_queue() -> BTreeMap<CoreIndex, VecDeque<Assignment>> {
|
||||
if Self::on_chain_storage_version() == StorageVersion::new(2) {
|
||||
migration::v2::ClaimQueue::<T>::get()
|
||||
.into_iter()
|
||||
.map(|(core_index, entries)| {
|
||||
(core_index, entries.into_iter().map(|e| e.assignment).collect())
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
ClaimQueue::<T>::get()
|
||||
}
|
||||
}
|
||||
|
||||
/// For each core that isn't part of the `except_for` set, pop the first item of the claim queue
|
||||
/// and fill the queue from the assignment provider.
|
||||
pub(crate) fn advance_claim_queue(except_for: &BTreeSet<CoreIndex>) {
|
||||
let config = configuration::ActiveConfig::<T>::get();
|
||||
let expected_claim_queue_len = Self::expected_claim_queue_len(&config.scheduler_params);
|
||||
// Extra sanity, config should already never be smaller than 1:
|
||||
let n_lookahead = config.scheduler_params.lookahead.max(1);
|
||||
|
||||
for core_idx in 0..expected_claim_queue_len {
|
||||
let core_idx = CoreIndex::from(core_idx);
|
||||
|
||||
if !except_for.contains(&core_idx) {
|
||||
let core_idx = CoreIndex::from(core_idx);
|
||||
|
||||
if let Some(dropped_para) = Self::pop_front_of_claim_queue(&core_idx) {
|
||||
T::AssignmentProvider::report_processed(dropped_para);
|
||||
}
|
||||
|
||||
Self::fill_claim_queue(core_idx, n_lookahead);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// on new session
|
||||
fn maybe_resize_claim_queue() {
|
||||
let cq = ClaimQueue::<T>::get();
|
||||
let Some((old_max_core, _)) = cq.last_key_value() else { return };
|
||||
let config = configuration::ActiveConfig::<T>::get();
|
||||
let new_core_count = Self::expected_claim_queue_len(&config.scheduler_params);
|
||||
|
||||
if new_core_count < (old_max_core.0 + 1) {
|
||||
ClaimQueue::<T>::mutate(|cq| {
|
||||
let to_remove: Vec<_> =
|
||||
cq.range(CoreIndex(new_core_count)..=*old_max_core).map(|(k, _)| *k).collect();
|
||||
for key in to_remove {
|
||||
if let Some(dropped_assignments) = cq.remove(&key) {
|
||||
Self::push_back_to_assignment_provider(dropped_assignments.into_iter());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Populate the claim queue. To be called on new session, after all the other modules were
|
||||
// initialized.
|
||||
fn populate_claim_queue_after_session_change() {
|
||||
let config = configuration::ActiveConfig::<T>::get();
|
||||
// Extra sanity, config should already never be smaller than 1:
|
||||
let n_lookahead = config.scheduler_params.lookahead.max(1);
|
||||
let expected_claim_queue_len = Self::expected_claim_queue_len(&config.scheduler_params);
|
||||
|
||||
for core_idx in 0..expected_claim_queue_len {
|
||||
let core_idx = CoreIndex::from(core_idx);
|
||||
Self::fill_claim_queue(core_idx, n_lookahead);
|
||||
}
|
||||
}
|
||||
|
||||
/// Push some assignments back to the provider.
|
||||
fn push_back_to_assignment_provider(
|
||||
assignments: impl core::iter::DoubleEndedIterator<Item = Assignment>,
|
||||
) {
|
||||
// Push back in reverse order so that when we pop from the provider again,
|
||||
// the entries in the claim queue are in the same order as they are right
|
||||
// now.
|
||||
for assignment in assignments.rev() {
|
||||
T::AssignmentProvider::push_back_assignment(assignment);
|
||||
}
|
||||
}
|
||||
|
||||
fn fill_claim_queue(core_idx: CoreIndex, n_lookahead: u32) {
|
||||
ClaimQueue::<T>::mutate(|la| {
|
||||
let cq = la.entry(core_idx).or_default();
|
||||
|
||||
let mut n_lookahead_used = cq.len() as u32;
|
||||
|
||||
// If the claim queue used to be empty, we need to double the first assignment.
|
||||
// Otherwise, the para will only be able to get the collation in right at the next block
|
||||
// (synchronous backing).
|
||||
// Only do this if the configured lookahead is greater than 1. Otherwise, it doesn't
|
||||
// make sense.
|
||||
if n_lookahead_used == 0 && n_lookahead > 1 {
|
||||
if let Some(assignment) = T::AssignmentProvider::pop_assignment_for_core(core_idx) {
|
||||
T::AssignmentProvider::assignment_duplicated(&assignment);
|
||||
cq.push_back(assignment.clone());
|
||||
cq.push_back(assignment);
|
||||
n_lookahead_used += 2;
|
||||
}
|
||||
}
|
||||
|
||||
for _ in n_lookahead_used..n_lookahead {
|
||||
if let Some(assignment) = T::AssignmentProvider::pop_assignment_for_core(core_idx) {
|
||||
cq.push_back(assignment);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't end up pushing anything, remove the entry. We don't want to waste the
|
||||
// space if we've no assignments.
|
||||
if cq.is_empty() {
|
||||
la.remove(&core_idx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn pop_front_of_claim_queue(core_idx: &CoreIndex) -> Option<Assignment> {
|
||||
ClaimQueue::<T>::mutate(|cq| cq.get_mut(core_idx)?.pop_front())
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "try-runtime", test))]
|
||||
fn claim_queue_len() -> usize {
|
||||
ClaimQueue::<T>::get().iter().map(|la_vec| la_vec.1.len()).sum()
|
||||
}
|
||||
|
||||
#[cfg(all(not(feature = "runtime-benchmarks"), test))]
|
||||
pub(crate) fn claim_queue_is_empty() -> bool {
|
||||
Self::claim_queue_len() == 0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn set_validator_groups(validator_groups: Vec<Vec<ValidatorIndex>>) {
|
||||
ValidatorGroups::<T>::set(validator_groups);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn set_claim_queue(claim_queue: BTreeMap<CoreIndex, VecDeque<Assignment>>) {
|
||||
ClaimQueue::<T>::set(claim_queue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// 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/>.
|
||||
|
||||
//! Common traits and types used by the scheduler and assignment providers.
|
||||
|
||||
use scale_info::TypeInfo;
|
||||
use sp_runtime::{
|
||||
codec::{Decode, Encode},
|
||||
RuntimeDebug,
|
||||
};
|
||||
|
||||
use pezkuwi_primitives::{CoreIndex, Id as ParaId};
|
||||
|
||||
/// Assignment (ParaId -> CoreIndex).
|
||||
#[derive(Encode, Decode, TypeInfo, RuntimeDebug, Clone, PartialEq)]
|
||||
pub enum Assignment {
|
||||
/// A pool assignment.
|
||||
Pool {
|
||||
/// The assigned para id.
|
||||
para_id: ParaId,
|
||||
/// The core index the para got assigned to.
|
||||
core_index: CoreIndex,
|
||||
},
|
||||
/// A bulk assignment.
|
||||
Bulk(ParaId),
|
||||
}
|
||||
|
||||
impl Assignment {
|
||||
/// Returns the [`ParaId`] this assignment is associated to.
|
||||
pub fn para_id(&self) -> ParaId {
|
||||
match self {
|
||||
Self::Pool { para_id, .. } => *para_id,
|
||||
Self::Bulk(para_id) => *para_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AssignmentProvider<BlockNumber> {
|
||||
/// Pops an [`Assignment`] from the provider for a specified [`CoreIndex`].
|
||||
///
|
||||
/// This is where assignments come into existence.
|
||||
fn pop_assignment_for_core(core_idx: CoreIndex) -> Option<Assignment>;
|
||||
|
||||
/// A previously popped `Assignment` has been fully processed.
|
||||
///
|
||||
/// Report back to the assignment provider that an assignment is done and no longer present in
|
||||
/// the scheduler.
|
||||
///
|
||||
/// This is one way of the life of an assignment coming to an end.
|
||||
fn report_processed(assignment: Assignment);
|
||||
|
||||
/// Push back a previously popped assignment.
|
||||
///
|
||||
/// If the assignment could not be processed within the current session, it can be pushed back
|
||||
/// to the assignment provider in order to be popped again later.
|
||||
///
|
||||
/// This is the second way the life of an assignment can come to an end.
|
||||
fn push_back_assignment(assignment: Assignment);
|
||||
|
||||
/// Push some assignment for mocking/benchmarks purposes.
|
||||
///
|
||||
/// Useful for benchmarks and testing. The returned assignment is "valid" and can if need be
|
||||
/// passed into `report_processed` for example.
|
||||
#[cfg(any(feature = "runtime-benchmarks", test))]
|
||||
fn get_mock_assignment(core_idx: CoreIndex, para_id: ParaId) -> Assignment;
|
||||
|
||||
/// Report that an assignment was duplicated by the scheduler.
|
||||
fn assignment_duplicated(assignment: &Assignment);
|
||||
}
|
||||
@@ -0,0 +1,494 @@
|
||||
// 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/>.
|
||||
|
||||
//! A module that is responsible for migration of storage.
|
||||
|
||||
use super::*;
|
||||
use alloc::vec::Vec;
|
||||
use frame_support::{
|
||||
migrations::VersionedMigration, pallet_prelude::ValueQuery, storage_alias,
|
||||
traits::UncheckedOnRuntimeUpgrade, weights::Weight,
|
||||
};
|
||||
|
||||
/// Old/legacy assignment representation (v0).
|
||||
///
|
||||
/// `Assignment` used to be a concrete type with the same layout V0Assignment, identical on all
|
||||
/// assignment providers. This can be removed once storage has been migrated.
|
||||
#[derive(Encode, Decode, RuntimeDebug, TypeInfo, PartialEq, Clone)]
|
||||
struct V0Assignment {
|
||||
pub para_id: ParaId,
|
||||
}
|
||||
|
||||
/// Old scheduler with explicit parathreads and `Scheduled` storage instead of `ClaimQueue`.
|
||||
mod v0 {
|
||||
use super::*;
|
||||
use pezkuwi_primitives::{CollatorId, Id};
|
||||
|
||||
#[storage_alias]
|
||||
pub(super) type Scheduled<T: Config> = StorageValue<Pallet<T>, Vec<CoreAssignment>, ValueQuery>;
|
||||
|
||||
#[derive(Clone, Encode, Decode)]
|
||||
#[cfg_attr(feature = "std", derive(PartialEq))]
|
||||
pub struct ParathreadClaim(pub Id, pub CollatorId);
|
||||
|
||||
#[derive(Clone, Encode, Decode)]
|
||||
#[cfg_attr(feature = "std", derive(PartialEq))]
|
||||
pub struct ParathreadEntry {
|
||||
/// The claim.
|
||||
pub claim: ParathreadClaim,
|
||||
/// Number of retries.
|
||||
pub retries: u32,
|
||||
}
|
||||
|
||||
/// What is occupying a specific availability core.
|
||||
#[derive(Clone, Encode, Decode)]
|
||||
#[cfg_attr(feature = "std", derive(PartialEq))]
|
||||
pub enum CoreOccupied {
|
||||
/// A parathread.
|
||||
Parathread(ParathreadEntry),
|
||||
/// A teyrchain.
|
||||
Teyrchain,
|
||||
}
|
||||
|
||||
/// The actual type isn't important, as we only delete the key in the state.
|
||||
#[storage_alias]
|
||||
pub(crate) type AvailabilityCores<T: Config> =
|
||||
StorageValue<Pallet<T>, Vec<Option<CoreOccupied>>, ValueQuery>;
|
||||
|
||||
/// The actual type isn't important, as we only delete the key in the state.
|
||||
#[storage_alias]
|
||||
pub(super) type ParathreadQueue<T: Config> = StorageValue<Pallet<T>, (), ValueQuery>;
|
||||
|
||||
#[storage_alias]
|
||||
pub(super) type ParathreadClaimIndex<T: Config> = StorageValue<Pallet<T>, (), ValueQuery>;
|
||||
|
||||
/// The assignment type.
|
||||
#[derive(Clone, Encode, Decode, TypeInfo, RuntimeDebug)]
|
||||
#[cfg_attr(feature = "std", derive(PartialEq))]
|
||||
pub enum AssignmentKind {
|
||||
/// A teyrchain.
|
||||
Teyrchain,
|
||||
/// A parathread.
|
||||
Parathread(CollatorId, u32),
|
||||
}
|
||||
|
||||
/// How a free core is scheduled to be assigned.
|
||||
#[derive(Clone, Encode, Decode, TypeInfo, RuntimeDebug)]
|
||||
#[cfg_attr(feature = "std", derive(PartialEq))]
|
||||
pub struct CoreAssignment {
|
||||
/// The core that is assigned.
|
||||
pub core: CoreIndex,
|
||||
/// The unique ID of the para that is assigned to the core.
|
||||
pub para_id: ParaId,
|
||||
/// The kind of the assignment.
|
||||
pub kind: AssignmentKind,
|
||||
/// The index of the validator group assigned to the core.
|
||||
pub group_idx: GroupIndex,
|
||||
}
|
||||
}
|
||||
|
||||
// `ClaimQueue` got introduced.
|
||||
//
|
||||
// - Items are `Option` for some weird reason.
|
||||
// - Assignments only consist of `ParaId`, `Assignment` is a concrete type (Same as V0Assignment).
|
||||
mod v1 {
|
||||
use frame_support::{
|
||||
pallet_prelude::ValueQuery, storage_alias, traits::UncheckedOnRuntimeUpgrade,
|
||||
weights::Weight,
|
||||
};
|
||||
use frame_system::pallet_prelude::BlockNumberFor;
|
||||
|
||||
use super::*;
|
||||
use crate::scheduler;
|
||||
|
||||
#[storage_alias]
|
||||
pub(super) type ClaimQueue<T: Config> = StorageValue<
|
||||
Pallet<T>,
|
||||
BTreeMap<CoreIndex, VecDeque<Option<ParasEntry<BlockNumberFor<T>>>>>,
|
||||
ValueQuery,
|
||||
>;
|
||||
|
||||
#[storage_alias]
|
||||
pub(super) type AvailabilityCores<T: Config> =
|
||||
StorageValue<Pallet<T>, Vec<CoreOccupied<BlockNumberFor<T>>>, ValueQuery>;
|
||||
|
||||
#[derive(Encode, Decode, TypeInfo, RuntimeDebug, PartialEq)]
|
||||
pub(super) enum CoreOccupied<N> {
|
||||
/// No candidate is waiting availability on this core right now (the core is not occupied).
|
||||
Free,
|
||||
/// A para is currently waiting for availability/inclusion on this core.
|
||||
Paras(ParasEntry<N>),
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, TypeInfo, RuntimeDebug, PartialEq)]
|
||||
pub(super) struct ParasEntry<N> {
|
||||
/// The underlying `Assignment`
|
||||
pub(super) assignment: V0Assignment,
|
||||
/// The number of times the entry has timed out in availability already.
|
||||
pub(super) availability_timeouts: u32,
|
||||
/// The block height until this entry needs to be backed.
|
||||
///
|
||||
/// If missed the entry will be removed from the claim queue without ever having occupied
|
||||
/// the core.
|
||||
pub(super) ttl: N,
|
||||
}
|
||||
|
||||
impl<N> ParasEntry<N> {
|
||||
/// Create a new `ParasEntry`.
|
||||
pub(super) fn new(assignment: V0Assignment, now: N) -> Self {
|
||||
ParasEntry { assignment, availability_timeouts: 0, ttl: now }
|
||||
}
|
||||
|
||||
/// Return `Id` from the underlying `Assignment`.
|
||||
pub(super) fn para_id(&self) -> ParaId {
|
||||
self.assignment.para_id
|
||||
}
|
||||
}
|
||||
|
||||
fn add_to_claimqueue<T: Config>(core_idx: CoreIndex, pe: ParasEntry<BlockNumberFor<T>>) {
|
||||
ClaimQueue::<T>::mutate(|la| {
|
||||
la.entry(core_idx).or_default().push_back(Some(pe));
|
||||
});
|
||||
}
|
||||
|
||||
/// Migration to V1
|
||||
pub struct UncheckedMigrateToV1<T>(core::marker::PhantomData<T>);
|
||||
impl<T: Config> UncheckedOnRuntimeUpgrade for UncheckedMigrateToV1<T> {
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
let mut weight: Weight = Weight::zero();
|
||||
|
||||
v0::ParathreadQueue::<T>::kill();
|
||||
v0::ParathreadClaimIndex::<T>::kill();
|
||||
|
||||
let now = frame_system::Pallet::<T>::block_number();
|
||||
let scheduled = v0::Scheduled::<T>::take();
|
||||
let sched_len = scheduled.len() as u64;
|
||||
for core_assignment in scheduled {
|
||||
let core_idx = core_assignment.core;
|
||||
let assignment = V0Assignment { para_id: core_assignment.para_id };
|
||||
let pe = v1::ParasEntry::new(assignment, now);
|
||||
v1::add_to_claimqueue::<T>(core_idx, pe);
|
||||
}
|
||||
|
||||
let teyrchains = paras::Teyrchains::<T>::get();
|
||||
let availability_cores = v0::AvailabilityCores::<T>::take();
|
||||
let mut new_availability_cores = Vec::new();
|
||||
|
||||
for (core_index, core) in availability_cores.into_iter().enumerate() {
|
||||
let new_core = if let Some(core) = core {
|
||||
match core {
|
||||
v0::CoreOccupied::Teyrchain =>
|
||||
v1::CoreOccupied::Paras(v1::ParasEntry::new(
|
||||
V0Assignment { para_id: teyrchains[core_index] },
|
||||
now,
|
||||
)),
|
||||
v0::CoreOccupied::Parathread(entry) => v1::CoreOccupied::Paras(
|
||||
v1::ParasEntry::new(V0Assignment { para_id: entry.claim.0 }, now),
|
||||
),
|
||||
}
|
||||
} else {
|
||||
v1::CoreOccupied::Free
|
||||
};
|
||||
|
||||
new_availability_cores.push(new_core);
|
||||
}
|
||||
|
||||
v1::AvailabilityCores::<T>::set(new_availability_cores);
|
||||
|
||||
// 2x as once for Scheduled and once for Claimqueue
|
||||
weight.saturating_accrue(T::DbWeight::get().reads_writes(2 * sched_len, 2 * sched_len));
|
||||
// reading teyrchains + availability_cores, writing AvailabilityCores
|
||||
weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 1));
|
||||
// 2x kill
|
||||
weight.saturating_accrue(T::DbWeight::get().writes(2));
|
||||
|
||||
log::info!(target: scheduler::LOG_TARGET, "Migrated para scheduler storage to v1");
|
||||
|
||||
weight
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::DispatchError> {
|
||||
let n: u32 = v0::Scheduled::<T>::get().len() as u32 +
|
||||
v0::AvailabilityCores::<T>::get().iter().filter(|c| c.is_some()).count() as u32;
|
||||
|
||||
log::info!(
|
||||
target: crate::scheduler::LOG_TARGET,
|
||||
"Number of scheduled and waiting for availability before: {n}",
|
||||
);
|
||||
|
||||
Ok(n.encode())
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(state: Vec<u8>) -> Result<(), sp_runtime::DispatchError> {
|
||||
log::info!(target: crate::scheduler::LOG_TARGET, "Running post_upgrade()");
|
||||
|
||||
ensure!(
|
||||
v0::Scheduled::<T>::get().is_empty(),
|
||||
"Scheduled should be empty after the migration"
|
||||
);
|
||||
|
||||
let expected_len = u32::decode(&mut &state[..]).unwrap();
|
||||
let availability_cores_waiting = v1::AvailabilityCores::<T>::get()
|
||||
.into_iter()
|
||||
.filter(|c| !matches!(c, v1::CoreOccupied::Free))
|
||||
.count();
|
||||
|
||||
ensure!(
|
||||
Pallet::<T>::claim_queue_len() as u32 + availability_cores_waiting as u32 ==
|
||||
expected_len,
|
||||
"ClaimQueue and AvailabilityCores should have the correct length",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Migrate `V0` to `V1` of the storage format.
|
||||
pub type MigrateV0ToV1<T> = VersionedMigration<
|
||||
0,
|
||||
1,
|
||||
v1::UncheckedMigrateToV1<T>,
|
||||
Pallet<T>,
|
||||
<T as frame_system::Config>::DbWeight,
|
||||
>;
|
||||
|
||||
pub(crate) mod v2 {
|
||||
use super::*;
|
||||
use crate::scheduler;
|
||||
|
||||
#[derive(Encode, Decode, TypeInfo, RuntimeDebug, PartialEq)]
|
||||
pub(crate) enum CoreOccupied<N> {
|
||||
Free,
|
||||
Paras(ParasEntry<N>),
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, TypeInfo, RuntimeDebug, PartialEq)]
|
||||
pub(crate) struct ParasEntry<N> {
|
||||
pub assignment: Assignment,
|
||||
pub availability_timeouts: u32,
|
||||
pub ttl: N,
|
||||
}
|
||||
|
||||
// V2 (no Option wrapper) and new [`Assignment`].
|
||||
#[storage_alias]
|
||||
pub(crate) type ClaimQueue<T: Config> = StorageValue<
|
||||
Pallet<T>,
|
||||
BTreeMap<CoreIndex, VecDeque<ParasEntry<BlockNumberFor<T>>>>,
|
||||
ValueQuery,
|
||||
>;
|
||||
|
||||
#[storage_alias]
|
||||
pub(crate) type AvailabilityCores<T: Config> =
|
||||
StorageValue<Pallet<T>, Vec<CoreOccupied<BlockNumberFor<T>>>, ValueQuery>;
|
||||
|
||||
fn is_bulk<T: Config>(core_index: CoreIndex) -> bool {
|
||||
core_index.0 < paras::Teyrchains::<T>::decode_len().unwrap_or(0) as u32
|
||||
}
|
||||
|
||||
/// Migration to V2
|
||||
pub struct UncheckedMigrateToV2<T>(core::marker::PhantomData<T>);
|
||||
|
||||
impl<T: Config> UncheckedOnRuntimeUpgrade for UncheckedMigrateToV2<T> {
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
let mut weight: Weight = Weight::zero();
|
||||
|
||||
let old = v1::ClaimQueue::<T>::take();
|
||||
let new = old
|
||||
.into_iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k,
|
||||
v.into_iter()
|
||||
.flatten()
|
||||
.map(|p| {
|
||||
let assignment = if is_bulk::<T>(k) {
|
||||
Assignment::Bulk(p.para_id())
|
||||
} else {
|
||||
Assignment::Pool { para_id: p.para_id(), core_index: k }
|
||||
};
|
||||
|
||||
ParasEntry {
|
||||
assignment,
|
||||
availability_timeouts: p.availability_timeouts,
|
||||
ttl: p.ttl,
|
||||
}
|
||||
})
|
||||
.collect::<VecDeque<_>>(),
|
||||
)
|
||||
})
|
||||
.collect::<BTreeMap<CoreIndex, VecDeque<ParasEntry<BlockNumberFor<T>>>>>();
|
||||
|
||||
ClaimQueue::<T>::put(new);
|
||||
|
||||
let old = v1::AvailabilityCores::<T>::get();
|
||||
|
||||
let new = old
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(k, a)| match a {
|
||||
v1::CoreOccupied::Free => CoreOccupied::Free,
|
||||
v1::CoreOccupied::Paras(paras) => {
|
||||
let assignment = if is_bulk::<T>((k as u32).into()) {
|
||||
Assignment::Bulk(paras.para_id())
|
||||
} else {
|
||||
Assignment::Pool {
|
||||
para_id: paras.para_id(),
|
||||
core_index: (k as u32).into(),
|
||||
}
|
||||
};
|
||||
|
||||
CoreOccupied::Paras(ParasEntry {
|
||||
assignment,
|
||||
availability_timeouts: paras.availability_timeouts,
|
||||
ttl: paras.ttl,
|
||||
})
|
||||
},
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
AvailabilityCores::<T>::put(new);
|
||||
|
||||
weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
|
||||
|
||||
log::info!(target: scheduler::LOG_TARGET, "Migrating para scheduler storage to v2");
|
||||
|
||||
weight
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::DispatchError> {
|
||||
log::trace!(
|
||||
target: crate::scheduler::LOG_TARGET,
|
||||
"ClaimQueue before migration: {}",
|
||||
v1::ClaimQueue::<T>::get().len()
|
||||
);
|
||||
|
||||
let bytes = u32::to_be_bytes(v1::ClaimQueue::<T>::get().len() as u32);
|
||||
|
||||
Ok(bytes.to_vec())
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(state: Vec<u8>) -> Result<(), sp_runtime::DispatchError> {
|
||||
log::trace!(target: crate::scheduler::LOG_TARGET, "Running post_upgrade()");
|
||||
|
||||
let old_len = u32::from_be_bytes(state.try_into().unwrap());
|
||||
ensure!(
|
||||
v2::ClaimQueue::<T>::get().len() as u32 == old_len,
|
||||
"Old ClaimQueue completely moved to new ClaimQueue after migration"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Migrate `V1` to `V2` of the storage format.
|
||||
pub type MigrateV1ToV2<T> = VersionedMigration<
|
||||
1,
|
||||
2,
|
||||
v2::UncheckedMigrateToV2<T>,
|
||||
Pallet<T>,
|
||||
<T as frame_system::Config>::DbWeight,
|
||||
>;
|
||||
|
||||
/// Migration for TTL and availability timeout retries removal.
|
||||
/// AvailabilityCores storage is removed and ClaimQueue now holds `Assignment`s instead of
|
||||
/// `ParasEntryType`
|
||||
mod v3 {
|
||||
use super::*;
|
||||
use crate::scheduler;
|
||||
|
||||
#[storage_alias]
|
||||
pub(crate) type ClaimQueue<T: Config> =
|
||||
StorageValue<Pallet<T>, BTreeMap<CoreIndex, VecDeque<Assignment>>, ValueQuery>;
|
||||
/// Migration to V3
|
||||
pub struct UncheckedMigrateToV3<T>(core::marker::PhantomData<T>);
|
||||
|
||||
impl<T: Config> UncheckedOnRuntimeUpgrade for UncheckedMigrateToV3<T> {
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
let mut weight: Weight = Weight::zero();
|
||||
|
||||
// Migrate ClaimQueuee to new format.
|
||||
|
||||
let old = v2::ClaimQueue::<T>::take();
|
||||
let new = old
|
||||
.into_iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k,
|
||||
v.into_iter()
|
||||
.map(|paras_entry| paras_entry.assignment)
|
||||
.collect::<VecDeque<_>>(),
|
||||
)
|
||||
})
|
||||
.collect::<BTreeMap<CoreIndex, VecDeque<Assignment>>>();
|
||||
|
||||
v3::ClaimQueue::<T>::put(new);
|
||||
|
||||
// Clear AvailabilityCores storage
|
||||
v2::AvailabilityCores::<T>::kill();
|
||||
|
||||
weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2));
|
||||
|
||||
log::info!(target: scheduler::LOG_TARGET, "Migrating para scheduler storage to v3");
|
||||
|
||||
weight
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::DispatchError> {
|
||||
log::trace!(
|
||||
target: crate::scheduler::LOG_TARGET,
|
||||
"ClaimQueue before migration: {}",
|
||||
v2::ClaimQueue::<T>::get().len()
|
||||
);
|
||||
|
||||
let bytes = u32::to_be_bytes(v2::ClaimQueue::<T>::get().len() as u32);
|
||||
|
||||
Ok(bytes.to_vec())
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(state: Vec<u8>) -> Result<(), sp_runtime::DispatchError> {
|
||||
log::trace!(target: crate::scheduler::LOG_TARGET, "Running post_upgrade()");
|
||||
|
||||
let old_len = u32::from_be_bytes(state.try_into().unwrap());
|
||||
ensure!(
|
||||
v3::ClaimQueue::<T>::get().len() as u32 == old_len,
|
||||
"Old ClaimQueue completely moved to new ClaimQueue after migration"
|
||||
);
|
||||
|
||||
ensure!(
|
||||
!v2::AvailabilityCores::<T>::exists(),
|
||||
"AvailabilityCores storage should have been completely killed"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Migrate `V2` to `V3` of the storage format.
|
||||
pub type MigrateV2ToV3<T> = VersionedMigration<
|
||||
2,
|
||||
3,
|
||||
v3::UncheckedMigrateToV3<T>,
|
||||
Pallet<T>,
|
||||
<T as frame_system::Config>::DbWeight,
|
||||
>;
|
||||
@@ -0,0 +1,847 @@
|
||||
// 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 alloc::collections::btree_map::BTreeMap;
|
||||
use frame_support::assert_ok;
|
||||
use pezkuwi_primitives::{BlockNumber, SchedulerParams, SessionIndex, ValidationCode, ValidatorId};
|
||||
use sp_keyring::Sr25519Keyring;
|
||||
|
||||
use crate::{
|
||||
configuration::HostConfiguration,
|
||||
initializer::SessionChangeNotification,
|
||||
mock::{
|
||||
new_test_ext, Configuration, MockAssigner, MockGenesisConfig, Paras, ParasShared,
|
||||
RuntimeOrigin, Scheduler, System, Test,
|
||||
},
|
||||
paras::{ParaGenesisArgs, ParaKind},
|
||||
scheduler::{self, common::Assignment, ClaimQueue},
|
||||
};
|
||||
|
||||
fn register_para(id: ParaId) {
|
||||
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::Parathread, // This most closely mimics our test assigner
|
||||
}
|
||||
));
|
||||
|
||||
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(mut notification) = new_session(b + 1) {
|
||||
// We will make every session change trigger an action queue. Normally this may require
|
||||
// 2 or more session changes.
|
||||
if notification.session_index == SessionIndex::default() {
|
||||
notification.session_index = ParasShared::scheduled_session();
|
||||
}
|
||||
|
||||
Configuration::force_set_active_config(notification.new_config.clone());
|
||||
|
||||
Paras::initializer_on_new_session(¬ification);
|
||||
|
||||
Scheduler::initializer_on_new_session(¬ification);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
Scheduler::advance_claim_queue(&Default::default());
|
||||
}
|
||||
}
|
||||
|
||||
fn default_config() -> HostConfiguration<BlockNumber> {
|
||||
HostConfiguration {
|
||||
// This field does not affect anything that scheduler does. However, `HostConfiguration`
|
||||
// is still a subject to consistency test. It requires that
|
||||
// `minimum_validation_upgrade_delay` is greater than `chain_availability_period` and
|
||||
// `thread_availability_period`.
|
||||
minimum_validation_upgrade_delay: 6,
|
||||
#[allow(deprecated)]
|
||||
scheduler_params: SchedulerParams {
|
||||
group_rotation_frequency: 10,
|
||||
paras_availability_period: 3,
|
||||
lookahead: 2,
|
||||
num_cores: 3,
|
||||
max_availability_timeouts: 1,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn genesis_config(config: &HostConfiguration<BlockNumber>) -> MockGenesisConfig {
|
||||
MockGenesisConfig {
|
||||
configuration: crate::configuration::GenesisConfig { config: config.clone() },
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal access to assignments at the top of the claim queue.
|
||||
fn next_assignments() -> impl Iterator<Item = (CoreIndex, Assignment)> {
|
||||
let claim_queue = ClaimQueue::<Test>::get();
|
||||
claim_queue
|
||||
.into_iter()
|
||||
.filter_map(|(core_idx, v)| v.front().map(|a| (core_idx, a.clone())))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_change_shuffles_validators() {
|
||||
let mut config = default_config();
|
||||
// Need five cores for this test
|
||||
config.scheduler_params.num_cores = 5;
|
||||
let genesis_config = genesis_config(&config);
|
||||
|
||||
new_test_ext(genesis_config).execute_with(|| {
|
||||
assert!(ValidatorGroups::<Test>::get().is_empty());
|
||||
|
||||
run_to_block(1, |number| match number {
|
||||
1 => Some(SessionChangeNotification {
|
||||
new_config: config.clone(),
|
||||
validators: vec![
|
||||
ValidatorId::from(Sr25519Keyring::Alice.public()),
|
||||
ValidatorId::from(Sr25519Keyring::Bob.public()),
|
||||
ValidatorId::from(Sr25519Keyring::Charlie.public()),
|
||||
ValidatorId::from(Sr25519Keyring::Dave.public()),
|
||||
ValidatorId::from(Sr25519Keyring::Eve.public()),
|
||||
ValidatorId::from(Sr25519Keyring::Ferdie.public()),
|
||||
ValidatorId::from(Sr25519Keyring::One.public()),
|
||||
],
|
||||
random_seed: [99; 32],
|
||||
..Default::default()
|
||||
}),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
let groups = ValidatorGroups::<Test>::get();
|
||||
assert_eq!(groups.len(), 5);
|
||||
|
||||
// first two groups have the overflow.
|
||||
for i in 0..2 {
|
||||
assert_eq!(groups[i].len(), 2);
|
||||
}
|
||||
|
||||
for i in 2..5 {
|
||||
assert_eq!(groups[i].len(), 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_change_takes_only_max_per_core() {
|
||||
let config = {
|
||||
let mut config = default_config();
|
||||
// Simulate 2 cores between all usage types
|
||||
config.scheduler_params.num_cores = 2;
|
||||
config.scheduler_params.max_validators_per_core = Some(1);
|
||||
config
|
||||
};
|
||||
|
||||
let genesis_config = genesis_config(&config);
|
||||
|
||||
new_test_ext(genesis_config).execute_with(|| {
|
||||
run_to_block(1, |number| match number {
|
||||
1 => Some(SessionChangeNotification {
|
||||
new_config: config.clone(),
|
||||
validators: vec![
|
||||
ValidatorId::from(Sr25519Keyring::Alice.public()),
|
||||
ValidatorId::from(Sr25519Keyring::Bob.public()),
|
||||
ValidatorId::from(Sr25519Keyring::Charlie.public()),
|
||||
ValidatorId::from(Sr25519Keyring::Dave.public()),
|
||||
ValidatorId::from(Sr25519Keyring::Eve.public()),
|
||||
ValidatorId::from(Sr25519Keyring::Ferdie.public()),
|
||||
ValidatorId::from(Sr25519Keyring::One.public()),
|
||||
],
|
||||
random_seed: [99; 32],
|
||||
..Default::default()
|
||||
}),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
let groups = ValidatorGroups::<Test>::get();
|
||||
assert_eq!(groups.len(), 7);
|
||||
|
||||
// Every validator gets its own group, even though there are 2 cores.
|
||||
for i in 0..7 {
|
||||
assert_eq!(groups[i].len(), 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
// Test that `advance_claim_queue` doubles the first assignment only for a core that didn't use to
|
||||
// have any assignments.
|
||||
fn advance_claim_queue_doubles_assignment_only_if_empty() {
|
||||
let mut config = default_config();
|
||||
config.scheduler_params.lookahead = 3;
|
||||
config.scheduler_params.num_cores = 2;
|
||||
let genesis_config = genesis_config(&config);
|
||||
|
||||
let para_a = ParaId::from(3_u32);
|
||||
let para_b = ParaId::from(4_u32);
|
||||
let para_c = ParaId::from(5_u32);
|
||||
|
||||
let assignment_a = Assignment::Bulk(para_a);
|
||||
let assignment_b = Assignment::Bulk(para_b);
|
||||
let assignment_c = Assignment::Bulk(para_c);
|
||||
|
||||
new_test_ext(genesis_config).execute_with(|| {
|
||||
// Add 3 paras
|
||||
register_para(para_a);
|
||||
register_para(para_b);
|
||||
register_para(para_c);
|
||||
|
||||
// start a new session to activate, 2 validators for 2 cores.
|
||||
run_to_block(1, |number| match number {
|
||||
1 => Some(SessionChangeNotification {
|
||||
new_config: config.clone(),
|
||||
validators: vec![
|
||||
ValidatorId::from(Sr25519Keyring::Alice.public()),
|
||||
ValidatorId::from(Sr25519Keyring::Bob.public()),
|
||||
],
|
||||
..Default::default()
|
||||
}),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
// add some para assignments.
|
||||
MockAssigner::add_test_assignment(assignment_a.clone());
|
||||
MockAssigner::add_test_assignment(assignment_b.clone());
|
||||
MockAssigner::add_test_assignment(assignment_c.clone());
|
||||
|
||||
// This will call advance_claim_queue
|
||||
run_to_block(2, |_| None);
|
||||
|
||||
{
|
||||
assert_eq!(Scheduler::claim_queue_len(), 5);
|
||||
let mut claim_queue = scheduler::ClaimQueue::<Test>::get();
|
||||
|
||||
// Because the claim queue used to be empty, the first assignment is doubled for every
|
||||
// core so that the first para gets a fair shot at backing something.
|
||||
assert_eq!(
|
||||
claim_queue.remove(&CoreIndex(0)).unwrap(),
|
||||
[assignment_a.clone(), assignment_a, assignment_b]
|
||||
.into_iter()
|
||||
.collect::<VecDeque<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
claim_queue.remove(&CoreIndex(1)).unwrap(),
|
||||
[assignment_c.clone(), assignment_c].into_iter().collect::<VecDeque<_>>()
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
// Test that `advance_claim_queue` doesn't populate for cores which have no assignments.
|
||||
fn advance_claim_queue_no_entry_if_empty() {
|
||||
let mut config = default_config();
|
||||
config.scheduler_params.lookahead = 3;
|
||||
config.scheduler_params.num_cores = 2;
|
||||
let genesis_config = genesis_config(&config);
|
||||
|
||||
let para_a = ParaId::from(3_u32);
|
||||
let assignment_a = Assignment::Bulk(para_a);
|
||||
|
||||
new_test_ext(genesis_config).execute_with(|| {
|
||||
// Add 1 para
|
||||
register_para(para_a);
|
||||
|
||||
// start a new session to activate, 2 validators for 2 cores.
|
||||
run_to_block(1, |number| match number {
|
||||
1 => Some(SessionChangeNotification {
|
||||
new_config: config.clone(),
|
||||
validators: vec![
|
||||
ValidatorId::from(Sr25519Keyring::Alice.public()),
|
||||
ValidatorId::from(Sr25519Keyring::Bob.public()),
|
||||
],
|
||||
..Default::default()
|
||||
}),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
MockAssigner::add_test_assignment(assignment_a.clone());
|
||||
|
||||
// This will call advance_claim_queue
|
||||
run_to_block(3, |_| None);
|
||||
|
||||
{
|
||||
let mut claim_queue = scheduler::ClaimQueue::<Test>::get();
|
||||
|
||||
assert_eq!(
|
||||
claim_queue.remove(&CoreIndex(0)).unwrap(),
|
||||
[assignment_a].into_iter().collect::<VecDeque<_>>()
|
||||
);
|
||||
|
||||
// Even though core 1 exists, there's no assignment for it so it's not present in the
|
||||
// claim queue.
|
||||
assert!(claim_queue.remove(&CoreIndex(1)).is_none());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
// Test that `advance_claim_queue` only advances for cores that are not part of the `except_for`
|
||||
// set.
|
||||
fn advance_claim_queue_except_for() {
|
||||
let mut config = default_config();
|
||||
// NOTE: This test expects on demand cores to each get slotted on to a different core
|
||||
// and not fill up the claimqueue of each core first.
|
||||
config.scheduler_params.lookahead = 1;
|
||||
config.scheduler_params.num_cores = 3;
|
||||
|
||||
let genesis_config = genesis_config(&config);
|
||||
|
||||
let para_a = ParaId::from(1_u32);
|
||||
let para_b = ParaId::from(2_u32);
|
||||
let para_c = ParaId::from(3_u32);
|
||||
let para_d = ParaId::from(4_u32);
|
||||
let para_e = ParaId::from(5_u32);
|
||||
|
||||
let assignment_a = Assignment::Bulk(para_a);
|
||||
let assignment_b = Assignment::Bulk(para_b);
|
||||
let assignment_c = Assignment::Bulk(para_c);
|
||||
let assignment_d = Assignment::Bulk(para_d);
|
||||
let assignment_e = Assignment::Bulk(para_e);
|
||||
|
||||
new_test_ext(genesis_config).execute_with(|| {
|
||||
// add 5 paras
|
||||
register_para(para_a);
|
||||
register_para(para_b);
|
||||
register_para(para_c);
|
||||
register_para(para_d);
|
||||
register_para(para_e);
|
||||
|
||||
// start a new session to activate, 3 validators for 3 cores.
|
||||
run_to_block(1, |number| match number {
|
||||
1 => Some(SessionChangeNotification {
|
||||
new_config: config.clone(),
|
||||
validators: vec![
|
||||
ValidatorId::from(Sr25519Keyring::Alice.public()),
|
||||
ValidatorId::from(Sr25519Keyring::Bob.public()),
|
||||
ValidatorId::from(Sr25519Keyring::Charlie.public()),
|
||||
],
|
||||
..Default::default()
|
||||
}),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
// add a couple of para claims now that paras are live
|
||||
MockAssigner::add_test_assignment(assignment_a.clone());
|
||||
MockAssigner::add_test_assignment(assignment_c.clone());
|
||||
|
||||
run_to_block(2, |_| None);
|
||||
|
||||
Scheduler::advance_claim_queue(&Default::default());
|
||||
|
||||
// Queues of all cores should be empty
|
||||
assert_eq!(Scheduler::claim_queue_len(), 0);
|
||||
|
||||
MockAssigner::add_test_assignment(assignment_a.clone());
|
||||
MockAssigner::add_test_assignment(assignment_c.clone());
|
||||
MockAssigner::add_test_assignment(assignment_b.clone());
|
||||
MockAssigner::add_test_assignment(assignment_d.clone());
|
||||
MockAssigner::add_test_assignment(assignment_e.clone());
|
||||
|
||||
run_to_block(3, |_| None);
|
||||
|
||||
{
|
||||
let scheduled: BTreeMap<_, _> = next_assignments().collect();
|
||||
|
||||
assert_eq!(scheduled.len(), 3);
|
||||
assert_eq!(scheduled.get(&CoreIndex(0)).unwrap(), &Assignment::Bulk(para_a));
|
||||
assert_eq!(scheduled.get(&CoreIndex(1)).unwrap(), &Assignment::Bulk(para_c));
|
||||
assert_eq!(scheduled.get(&CoreIndex(2)).unwrap(), &Assignment::Bulk(para_b));
|
||||
}
|
||||
|
||||
// now note that cores 0 and 1 were freed.
|
||||
Scheduler::advance_claim_queue(&std::iter::once(CoreIndex(2)).collect());
|
||||
|
||||
{
|
||||
let scheduled: BTreeMap<_, _> = next_assignments().collect();
|
||||
|
||||
// 1 thing scheduled before, + 2 cores freed.
|
||||
assert_eq!(scheduled.len(), 3);
|
||||
assert_eq!(scheduled.get(&CoreIndex(0)).unwrap(), &Assignment::Bulk(para_d));
|
||||
assert_eq!(scheduled.get(&CoreIndex(1)).unwrap(), &Assignment::Bulk(para_e));
|
||||
assert_eq!(scheduled.get(&CoreIndex(2)).unwrap(), &Assignment::Bulk(para_b));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schedule_rotates_groups() {
|
||||
let on_demand_cores = 2;
|
||||
let config = {
|
||||
let mut config = default_config();
|
||||
config.scheduler_params.lookahead = 1;
|
||||
config.scheduler_params.num_cores = on_demand_cores;
|
||||
config
|
||||
};
|
||||
|
||||
let rotation_frequency = config.scheduler_params.group_rotation_frequency;
|
||||
|
||||
let genesis_config = genesis_config(&config);
|
||||
|
||||
let para_a = ParaId::from(1_u32);
|
||||
let para_b = ParaId::from(2_u32);
|
||||
|
||||
new_test_ext(genesis_config).execute_with(|| {
|
||||
register_para(para_a);
|
||||
register_para(para_b);
|
||||
|
||||
// start a new session to activate, 2 validators for 2 cores.
|
||||
run_to_block(1, |number| match number {
|
||||
1 => Some(SessionChangeNotification {
|
||||
new_config: config.clone(),
|
||||
validators: vec![
|
||||
ValidatorId::from(Sr25519Keyring::Alice.public()),
|
||||
ValidatorId::from(Sr25519Keyring::Eve.public()),
|
||||
],
|
||||
..Default::default()
|
||||
}),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
let session_start_block = scheduler::SessionStartBlock::<Test>::get();
|
||||
assert_eq!(session_start_block, 1);
|
||||
|
||||
let mut now = 2;
|
||||
run_to_block(now, |_| None);
|
||||
|
||||
let assert_groups_rotated = |rotations: u32, now: &BlockNumberFor<Test>| {
|
||||
assert_eq!(
|
||||
Scheduler::group_assigned_to_core(CoreIndex(0), *now).unwrap(),
|
||||
GroupIndex((0u32 + rotations) % on_demand_cores)
|
||||
);
|
||||
assert_eq!(
|
||||
Scheduler::group_assigned_to_core(CoreIndex(1), *now).unwrap(),
|
||||
GroupIndex((1u32 + rotations) % on_demand_cores)
|
||||
);
|
||||
};
|
||||
|
||||
assert_groups_rotated(0, &now);
|
||||
|
||||
// one block before first rotation.
|
||||
now = rotation_frequency;
|
||||
run_to_block(now, |_| None);
|
||||
|
||||
assert_groups_rotated(0, &now);
|
||||
|
||||
// first rotation.
|
||||
now = now + 1;
|
||||
run_to_block(now, |_| None);
|
||||
assert_groups_rotated(1, &now);
|
||||
|
||||
// one block before second rotation.
|
||||
now = rotation_frequency * 2;
|
||||
run_to_block(now, |_| None);
|
||||
assert_groups_rotated(1, &now);
|
||||
|
||||
// second rotation.
|
||||
now = now + 1;
|
||||
run_to_block(now, |_| None);
|
||||
assert_groups_rotated(2, &now);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn availability_predicate_works() {
|
||||
let genesis_config = genesis_config(&default_config());
|
||||
|
||||
let SchedulerParams { group_rotation_frequency, paras_availability_period, .. } =
|
||||
default_config().scheduler_params;
|
||||
|
||||
new_test_ext(genesis_config).execute_with(|| {
|
||||
run_to_block(1 + paras_availability_period, |_| None);
|
||||
|
||||
assert!(!Scheduler::availability_timeout_check_required());
|
||||
|
||||
run_to_block(1 + group_rotation_frequency, |_| None);
|
||||
|
||||
{
|
||||
let now = System::block_number();
|
||||
assert!(Scheduler::availability_timeout_check_required());
|
||||
let pred = Scheduler::availability_timeout_predicate();
|
||||
let last_rotation = Scheduler::group_rotation_info(now).last_rotation_at();
|
||||
|
||||
let would_be_timed_out = now - paras_availability_period;
|
||||
let should_not_be_timed_out = last_rotation;
|
||||
|
||||
assert!(pred(would_be_timed_out).timed_out);
|
||||
assert!(!pred(should_not_be_timed_out).timed_out);
|
||||
assert!(!pred(now).timed_out);
|
||||
|
||||
// check the threshold is exact.
|
||||
assert!(!pred(would_be_timed_out + 1).timed_out);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_up_on_available_uses_next_scheduled_or_none() {
|
||||
let mut config = default_config();
|
||||
config.scheduler_params.num_cores = 1;
|
||||
let genesis_config = genesis_config(&config);
|
||||
|
||||
let para_a = ParaId::from(1_u32);
|
||||
let para_b = ParaId::from(2_u32);
|
||||
|
||||
new_test_ext(genesis_config).execute_with(|| {
|
||||
register_para(para_a);
|
||||
register_para(para_b);
|
||||
|
||||
// start a new session to activate, 2 validators for 2 cores.
|
||||
run_to_block(1, |number| match number {
|
||||
1 => Some(SessionChangeNotification {
|
||||
new_config: config.clone(),
|
||||
validators: vec![
|
||||
ValidatorId::from(Sr25519Keyring::Alice.public()),
|
||||
ValidatorId::from(Sr25519Keyring::Eve.public()),
|
||||
],
|
||||
..Default::default()
|
||||
}),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
MockAssigner::add_test_assignment(Assignment::Bulk(para_a));
|
||||
|
||||
run_to_block(2, |_| None);
|
||||
|
||||
{
|
||||
// Two assignments for A on core 0, because the claim queue used to be empty.
|
||||
assert_eq!(Scheduler::claim_queue_len(), 2);
|
||||
|
||||
assert!(Scheduler::next_up_on_available(CoreIndex(1)).is_none());
|
||||
|
||||
assert_eq!(
|
||||
Scheduler::next_up_on_available(CoreIndex(0)).unwrap(),
|
||||
ScheduledCore { para_id: para_a, collator: None }
|
||||
);
|
||||
|
||||
Scheduler::advance_claim_queue(&Default::default());
|
||||
assert_eq!(
|
||||
Scheduler::next_up_on_available(CoreIndex(0)).unwrap(),
|
||||
ScheduledCore { para_id: para_a, collator: None }
|
||||
);
|
||||
|
||||
Scheduler::advance_claim_queue(&Default::default());
|
||||
assert!(Scheduler::next_up_on_available(CoreIndex(0)).is_none());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_change_increasing_number_of_cores() {
|
||||
let mut config = default_config();
|
||||
config.scheduler_params.num_cores = 2;
|
||||
let genesis_config = genesis_config(&config);
|
||||
|
||||
let para_a = ParaId::from(3_u32);
|
||||
let para_b = ParaId::from(4_u32);
|
||||
|
||||
let assignment_a = Assignment::Bulk(para_a);
|
||||
let assignment_b = Assignment::Bulk(para_b);
|
||||
|
||||
new_test_ext(genesis_config).execute_with(|| {
|
||||
// Add 2 paras
|
||||
register_para(para_a);
|
||||
register_para(para_b);
|
||||
|
||||
// start a new session to activate, 2 validators for 2 cores.
|
||||
run_to_block(1, |number| match number {
|
||||
1 => Some(SessionChangeNotification {
|
||||
new_config: config.clone(),
|
||||
validators: vec![
|
||||
ValidatorId::from(Sr25519Keyring::Alice.public()),
|
||||
ValidatorId::from(Sr25519Keyring::Bob.public()),
|
||||
],
|
||||
..Default::default()
|
||||
}),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
MockAssigner::add_test_assignment(assignment_a.clone());
|
||||
MockAssigner::add_test_assignment(assignment_b.clone());
|
||||
|
||||
// This will call advance_claim_queue
|
||||
run_to_block(2, |_| None);
|
||||
|
||||
{
|
||||
let mut claim_queue = scheduler::ClaimQueue::<Test>::get();
|
||||
assert_eq!(Scheduler::claim_queue_len(), 4);
|
||||
|
||||
assert_eq!(
|
||||
claim_queue.remove(&CoreIndex(0)).unwrap(),
|
||||
[assignment_a.clone(), assignment_a.clone()]
|
||||
.into_iter()
|
||||
.collect::<VecDeque<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
claim_queue.remove(&CoreIndex(1)).unwrap(),
|
||||
[assignment_b.clone(), assignment_b.clone()]
|
||||
.into_iter()
|
||||
.collect::<VecDeque<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
// Increase number of cores to 4.
|
||||
let old_config = config;
|
||||
let mut new_config = old_config.clone();
|
||||
new_config.scheduler_params.num_cores = 4;
|
||||
|
||||
// add another assignment for para b.
|
||||
MockAssigner::add_test_assignment(assignment_b.clone());
|
||||
|
||||
run_to_block(3, |number| match number {
|
||||
3 => Some(SessionChangeNotification {
|
||||
new_config: new_config.clone(),
|
||||
prev_config: old_config.clone(),
|
||||
validators: vec![
|
||||
ValidatorId::from(Sr25519Keyring::Alice.public()),
|
||||
ValidatorId::from(Sr25519Keyring::Bob.public()),
|
||||
ValidatorId::from(Sr25519Keyring::Charlie.public()),
|
||||
ValidatorId::from(Sr25519Keyring::Dave.public()),
|
||||
],
|
||||
..Default::default()
|
||||
}),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
{
|
||||
let mut claim_queue = scheduler::ClaimQueue::<Test>::get();
|
||||
assert_eq!(Scheduler::claim_queue_len(), 3);
|
||||
|
||||
assert_eq!(
|
||||
claim_queue.remove(&CoreIndex(0)).unwrap(),
|
||||
[assignment_a].into_iter().collect::<VecDeque<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
claim_queue.remove(&CoreIndex(1)).unwrap(),
|
||||
[assignment_b.clone()].into_iter().collect::<VecDeque<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
claim_queue.remove(&CoreIndex(2)).unwrap(),
|
||||
[assignment_b.clone()].into_iter().collect::<VecDeque<_>>()
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_change_decreasing_number_of_cores() {
|
||||
let mut config = default_config();
|
||||
config.scheduler_params.num_cores = 3;
|
||||
let genesis_config = genesis_config(&config);
|
||||
|
||||
let para_a = ParaId::from(3_u32);
|
||||
let para_b = ParaId::from(4_u32);
|
||||
|
||||
let assignment_a = Assignment::Bulk(para_a);
|
||||
let assignment_b = Assignment::Bulk(para_b);
|
||||
|
||||
new_test_ext(genesis_config).execute_with(|| {
|
||||
// Add 2 paras
|
||||
register_para(para_a);
|
||||
register_para(para_b);
|
||||
|
||||
// start a new session to activate, 2 validators for 2 cores.
|
||||
run_to_block(1, |number| match number {
|
||||
1 => Some(SessionChangeNotification {
|
||||
new_config: config.clone(),
|
||||
validators: vec![
|
||||
ValidatorId::from(Sr25519Keyring::Alice.public()),
|
||||
ValidatorId::from(Sr25519Keyring::Bob.public()),
|
||||
],
|
||||
..Default::default()
|
||||
}),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
scheduler::Pallet::<Test>::set_claim_queue(BTreeMap::from([
|
||||
(CoreIndex::from(0), VecDeque::from([assignment_a.clone()])),
|
||||
// Leave a hole for core 1.
|
||||
(CoreIndex::from(2), VecDeque::from([assignment_b.clone(), assignment_b.clone()])),
|
||||
]));
|
||||
|
||||
// Decrease number of cores to 1.
|
||||
let old_config = config;
|
||||
let mut new_config = old_config.clone();
|
||||
new_config.scheduler_params.num_cores = 1;
|
||||
|
||||
// Session change.
|
||||
// Assignment A had its shot already so will be dropped for good.
|
||||
// The two assignments of B will be pushed back to the assignment provider.
|
||||
run_to_block(3, |number| match number {
|
||||
3 => Some(SessionChangeNotification {
|
||||
new_config: new_config.clone(),
|
||||
prev_config: old_config.clone(),
|
||||
validators: vec![ValidatorId::from(Sr25519Keyring::Alice.public())],
|
||||
..Default::default()
|
||||
}),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
let mut claim_queue = scheduler::ClaimQueue::<Test>::get();
|
||||
assert_eq!(Scheduler::claim_queue_len(), 1);
|
||||
|
||||
// There's only one assignment for B because run_to_block also calls advance_claim_queue at
|
||||
// the end.
|
||||
assert_eq!(
|
||||
claim_queue.remove(&CoreIndex(0)).unwrap(),
|
||||
[assignment_b.clone()].into_iter().collect::<VecDeque<_>>()
|
||||
);
|
||||
|
||||
Scheduler::advance_claim_queue(&Default::default());
|
||||
// No more assignments now.
|
||||
assert_eq!(Scheduler::claim_queue_len(), 0);
|
||||
|
||||
// Retain number of cores to 1 but remove all validator groups. The claim queue length
|
||||
// should be the minimum of these two.
|
||||
|
||||
// Add an assignment.
|
||||
MockAssigner::add_test_assignment(assignment_b.clone());
|
||||
|
||||
run_to_block(4, |number| match number {
|
||||
4 => Some(SessionChangeNotification {
|
||||
new_config: new_config.clone(),
|
||||
prev_config: new_config.clone(),
|
||||
validators: vec![],
|
||||
..Default::default()
|
||||
}),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
assert_eq!(Scheduler::claim_queue_len(), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_change_increasing_lookahead() {
|
||||
let mut config = default_config();
|
||||
config.scheduler_params.num_cores = 2;
|
||||
config.scheduler_params.lookahead = 2;
|
||||
let genesis_config = genesis_config(&config);
|
||||
|
||||
let para_a = ParaId::from(3_u32);
|
||||
let para_b = ParaId::from(4_u32);
|
||||
|
||||
let assignment_a = Assignment::Bulk(para_a);
|
||||
let assignment_b = Assignment::Bulk(para_b);
|
||||
|
||||
new_test_ext(genesis_config).execute_with(|| {
|
||||
// Add 2 paras
|
||||
register_para(para_a);
|
||||
register_para(para_b);
|
||||
|
||||
// start a new session to activate, 2 validators for 2 cores.
|
||||
run_to_block(1, |number| match number {
|
||||
1 => Some(SessionChangeNotification {
|
||||
new_config: config.clone(),
|
||||
validators: vec![
|
||||
ValidatorId::from(Sr25519Keyring::Alice.public()),
|
||||
ValidatorId::from(Sr25519Keyring::Bob.public()),
|
||||
],
|
||||
..Default::default()
|
||||
}),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
MockAssigner::add_test_assignment(assignment_a.clone());
|
||||
MockAssigner::add_test_assignment(assignment_a.clone());
|
||||
MockAssigner::add_test_assignment(assignment_a.clone());
|
||||
MockAssigner::add_test_assignment(assignment_b.clone());
|
||||
MockAssigner::add_test_assignment(assignment_b.clone());
|
||||
MockAssigner::add_test_assignment(assignment_b.clone());
|
||||
|
||||
// Lookahead is currently 2.
|
||||
|
||||
run_to_block(2, |_| None);
|
||||
|
||||
{
|
||||
let mut claim_queue = scheduler::ClaimQueue::<Test>::get();
|
||||
assert_eq!(Scheduler::claim_queue_len(), 4);
|
||||
|
||||
assert_eq!(
|
||||
claim_queue.remove(&CoreIndex(0)).unwrap(),
|
||||
[assignment_a.clone(), assignment_a.clone()]
|
||||
.into_iter()
|
||||
.collect::<VecDeque<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
claim_queue.remove(&CoreIndex(1)).unwrap(),
|
||||
[assignment_a.clone(), assignment_a.clone()]
|
||||
.into_iter()
|
||||
.collect::<VecDeque<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
// Increase lookahead to 4.
|
||||
let old_config = config;
|
||||
let mut new_config = old_config.clone();
|
||||
new_config.scheduler_params.lookahead = 4;
|
||||
|
||||
run_to_block(3, |number| match number {
|
||||
3 => Some(SessionChangeNotification {
|
||||
new_config: new_config.clone(),
|
||||
prev_config: old_config.clone(),
|
||||
validators: vec![
|
||||
ValidatorId::from(Sr25519Keyring::Alice.public()),
|
||||
ValidatorId::from(Sr25519Keyring::Bob.public()),
|
||||
],
|
||||
..Default::default()
|
||||
}),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
{
|
||||
let mut claim_queue = scheduler::ClaimQueue::<Test>::get();
|
||||
assert_eq!(Scheduler::claim_queue_len(), 6);
|
||||
|
||||
assert_eq!(
|
||||
claim_queue.remove(&CoreIndex(0)).unwrap(),
|
||||
[assignment_a.clone(), assignment_a.clone(), assignment_b.clone()]
|
||||
.into_iter()
|
||||
.collect::<VecDeque<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
claim_queue.remove(&CoreIndex(1)).unwrap(),
|
||||
[assignment_a.clone(), assignment_b.clone(), assignment_b.clone()]
|
||||
.into_iter()
|
||||
.collect::<VecDeque<_>>()
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
// 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 session info pallet provides information about validator sets
|
||||
//! from prior sessions needed for approvals and disputes.
|
||||
//!
|
||||
//! See the documentation on [session info][session-info-page] in the implementers' guide.
|
||||
//!
|
||||
//! [session-info-page]: https://docs.pezkuwichain.io/sdk/book/runtime/session_info.html
|
||||
use crate::{
|
||||
configuration, paras, scheduler, shared,
|
||||
util::{take_active_subset, take_active_subset_and_inactive},
|
||||
};
|
||||
use alloc::vec::Vec;
|
||||
use frame_support::{
|
||||
pallet_prelude::*,
|
||||
traits::{OneSessionHandler, ValidatorSet, ValidatorSetWithIdentification},
|
||||
};
|
||||
use frame_system::pallet_prelude::BlockNumberFor;
|
||||
use pezkuwi_primitives::{
|
||||
AssignmentId, AuthorityDiscoveryId, ExecutorParams, SessionIndex, SessionInfo,
|
||||
};
|
||||
|
||||
pub use pallet::*;
|
||||
|
||||
pub mod migration;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// A type for representing the validator account id in a session.
|
||||
pub type AccountId<T> = <<T as Config>::ValidatorSet as ValidatorSet<
|
||||
<T as frame_system::Config>::AccountId,
|
||||
>>::ValidatorId;
|
||||
|
||||
/// A tuple of `(AccountId, Identification)` where `Identification`
|
||||
/// is the full identification of `AccountId`.
|
||||
pub type IdentificationTuple<T> = (
|
||||
AccountId<T>,
|
||||
<<T as Config>::ValidatorSet as ValidatorSetWithIdentification<
|
||||
<T as frame_system::Config>::AccountId,
|
||||
>>::Identification,
|
||||
);
|
||||
|
||||
#[frame_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
|
||||
#[pallet::pallet]
|
||||
#[pallet::storage_version(migration::STORAGE_VERSION)]
|
||||
#[pallet::without_storage_info]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config:
|
||||
frame_system::Config
|
||||
+ configuration::Config
|
||||
+ shared::Config
|
||||
+ paras::Config
|
||||
+ scheduler::Config
|
||||
+ AuthorityDiscoveryConfig
|
||||
{
|
||||
/// A type for retrieving `AccountId`s of the validators in the current session.
|
||||
/// These are stash keys of the validators.
|
||||
/// It's used for rewards and slashing. `Identification` is only needed for slashing.
|
||||
type ValidatorSet: ValidatorSetWithIdentification<Self::AccountId>;
|
||||
}
|
||||
|
||||
/// Assignment keys for the current session.
|
||||
/// Note that this API is private due to it being prone to 'off-by-one' at session boundaries.
|
||||
/// When in doubt, use `Sessions` API instead.
|
||||
#[pallet::storage]
|
||||
pub(super) type AssignmentKeysUnsafe<T: Config> =
|
||||
StorageValue<_, Vec<AssignmentId>, ValueQuery>;
|
||||
|
||||
/// The earliest session for which previous session info is stored.
|
||||
#[pallet::storage]
|
||||
pub type EarliestStoredSession<T: Config> = StorageValue<_, SessionIndex, ValueQuery>;
|
||||
|
||||
/// Session information in a rolling window.
|
||||
/// Should have an entry in range `EarliestStoredSession..=CurrentSessionIndex`.
|
||||
/// Does not have any entries before the session index in the first session change notification.
|
||||
#[pallet::storage]
|
||||
pub type Sessions<T: Config> = StorageMap<_, Identity, SessionIndex, SessionInfo>;
|
||||
|
||||
/// The validator account keys of the validators actively participating in teyrchain consensus.
|
||||
// We do not store this in `SessionInfo` to avoid leaking the `AccountId` type to the client,
|
||||
// which would complicate the migration process if we are to change it in the future.
|
||||
#[pallet::storage]
|
||||
pub type AccountKeys<T: Config> = StorageMap<_, Identity, SessionIndex, Vec<AccountId<T>>>;
|
||||
|
||||
/// Executor parameter set for a given session index
|
||||
#[pallet::storage]
|
||||
pub type SessionExecutorParams<T: Config> =
|
||||
StorageMap<_, Identity, SessionIndex, ExecutorParams>;
|
||||
}
|
||||
|
||||
/// An abstraction for the authority discovery pallet
|
||||
/// to help with mock testing.
|
||||
pub trait AuthorityDiscoveryConfig {
|
||||
/// Retrieve authority identifiers of the current authority set in canonical ordering.
|
||||
fn authorities() -> Vec<AuthorityDiscoveryId>;
|
||||
}
|
||||
|
||||
impl<T: pallet_authority_discovery::Config> AuthorityDiscoveryConfig for T {
|
||||
fn authorities() -> Vec<AuthorityDiscoveryId> {
|
||||
pallet_authority_discovery::Pallet::<T>::current_authorities().to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Handle an incoming session change.
|
||||
pub(crate) fn initializer_on_new_session(
|
||||
notification: &crate::initializer::SessionChangeNotification<BlockNumberFor<T>>,
|
||||
) {
|
||||
let config = configuration::ActiveConfig::<T>::get();
|
||||
|
||||
let dispute_period = config.dispute_period;
|
||||
|
||||
let validators = notification.validators.clone().into();
|
||||
let discovery_keys = <T as AuthorityDiscoveryConfig>::authorities();
|
||||
let assignment_keys = AssignmentKeysUnsafe::<T>::get();
|
||||
let active_set = shared::ActiveValidatorIndices::<T>::get();
|
||||
|
||||
let validator_groups = scheduler::ValidatorGroups::<T>::get();
|
||||
let n_cores = validator_groups.len() as u32;
|
||||
let zeroth_delay_tranche_width = config.zeroth_delay_tranche_width;
|
||||
let relay_vrf_modulo_samples = config.relay_vrf_modulo_samples;
|
||||
let n_delay_tranches = config.n_delay_tranches;
|
||||
let no_show_slots = config.no_show_slots;
|
||||
let needed_approvals = config.needed_approvals;
|
||||
|
||||
let new_session_index = notification.session_index;
|
||||
let random_seed = notification.random_seed;
|
||||
let old_earliest_stored_session = EarliestStoredSession::<T>::get();
|
||||
let new_earliest_stored_session = new_session_index.saturating_sub(dispute_period);
|
||||
let new_earliest_stored_session =
|
||||
core::cmp::max(new_earliest_stored_session, old_earliest_stored_session);
|
||||
// remove all entries from `Sessions` from the previous value up to the new value
|
||||
// avoid a potentially heavy loop when introduced on a live chain
|
||||
if old_earliest_stored_session != 0 || Sessions::<T>::get(0).is_some() {
|
||||
for idx in old_earliest_stored_session..new_earliest_stored_session {
|
||||
Sessions::<T>::remove(&idx);
|
||||
// Idx will be missing for a few sessions after the runtime upgrade.
|
||||
// But it shouldn't be a problem.
|
||||
AccountKeys::<T>::remove(&idx);
|
||||
SessionExecutorParams::<T>::remove(&idx);
|
||||
}
|
||||
// update `EarliestStoredSession` based on `config.dispute_period`
|
||||
EarliestStoredSession::<T>::set(new_earliest_stored_session);
|
||||
} else {
|
||||
// just introduced on a live chain
|
||||
EarliestStoredSession::<T>::set(new_session_index);
|
||||
}
|
||||
|
||||
// The validator set is guaranteed to be of the current session
|
||||
// because we delay `on_new_session` till the end of the block.
|
||||
let account_ids = T::ValidatorSet::validators();
|
||||
let active_account_ids = take_active_subset(&active_set, &account_ids);
|
||||
AccountKeys::<T>::insert(&new_session_index, &active_account_ids);
|
||||
|
||||
// create a new entry in `Sessions` with information about the current session
|
||||
let new_session_info = SessionInfo {
|
||||
validators, // these are from the notification and are thus already correct.
|
||||
discovery_keys: take_active_subset_and_inactive(&active_set, &discovery_keys),
|
||||
assignment_keys: take_active_subset(&active_set, &assignment_keys),
|
||||
validator_groups: validator_groups.into(),
|
||||
n_cores,
|
||||
zeroth_delay_tranche_width,
|
||||
relay_vrf_modulo_samples,
|
||||
n_delay_tranches,
|
||||
no_show_slots,
|
||||
needed_approvals,
|
||||
active_validator_indices: active_set,
|
||||
random_seed,
|
||||
dispute_period,
|
||||
};
|
||||
Sessions::<T>::insert(&new_session_index, &new_session_info);
|
||||
|
||||
SessionExecutorParams::<T>::insert(&new_session_index, config.executor_params);
|
||||
}
|
||||
|
||||
/// Called by the initializer to initialize the session info pallet.
|
||||
pub(crate) fn initializer_initialize(_now: BlockNumberFor<T>) -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
|
||||
/// Called by the initializer to finalize the session info pallet.
|
||||
pub(crate) fn initializer_finalize() {}
|
||||
}
|
||||
|
||||
impl<T: Config> sp_runtime::BoundToRuntimeAppPublic for Pallet<T> {
|
||||
type Public = AssignmentId;
|
||||
}
|
||||
|
||||
impl<T: pallet_session::Config + Config> OneSessionHandler<T::AccountId> for Pallet<T> {
|
||||
type Key = AssignmentId;
|
||||
|
||||
fn on_genesis_session<'a, I: 'a>(_validators: I)
|
||||
where
|
||||
I: Iterator<Item = (&'a T::AccountId, Self::Key)>,
|
||||
{
|
||||
}
|
||||
|
||||
fn on_new_session<'a, I: 'a>(_changed: bool, validators: I, _queued: I)
|
||||
where
|
||||
I: Iterator<Item = (&'a T::AccountId, Self::Key)>,
|
||||
{
|
||||
let assignment_keys: Vec<_> = validators.map(|(_, v)| v).collect();
|
||||
AssignmentKeysUnsafe::<T>::set(assignment_keys);
|
||||
}
|
||||
|
||||
fn on_disabled(_i: u32) {}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// 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/>.
|
||||
|
||||
//! A module that is responsible for migration of storage.
|
||||
|
||||
use frame_support::traits::StorageVersion;
|
||||
|
||||
/// The in-code storage version.
|
||||
pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
|
||||
@@ -0,0 +1,211 @@
|
||||
// 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::{
|
||||
configuration::HostConfiguration,
|
||||
initializer::SessionChangeNotification,
|
||||
mock::{
|
||||
new_test_ext, Configuration, MockGenesisConfig, ParasShared, RuntimeOrigin, SessionInfo,
|
||||
System, Test,
|
||||
},
|
||||
util::take_active_subset,
|
||||
};
|
||||
use pezkuwi_primitives::{BlockNumber, SchedulerParams, ValidatorId, ValidatorIndex};
|
||||
use sp_keyring::Sr25519Keyring;
|
||||
|
||||
fn run_to_block(
|
||||
to: BlockNumber,
|
||||
new_session: impl Fn(BlockNumber) -> Option<SessionChangeNotification<BlockNumber>>,
|
||||
) {
|
||||
while System::block_number() < to {
|
||||
let b = System::block_number();
|
||||
|
||||
SessionInfo::initializer_finalize();
|
||||
ParasShared::initializer_finalize();
|
||||
Configuration::initializer_finalize();
|
||||
|
||||
if let Some(notification) = new_session(b + 1) {
|
||||
Configuration::initializer_on_new_session(¬ification.session_index);
|
||||
ParasShared::initializer_on_new_session(
|
||||
notification.session_index,
|
||||
notification.random_seed,
|
||||
¬ification.new_config,
|
||||
notification.validators.clone(),
|
||||
);
|
||||
SessionInfo::initializer_on_new_session(¬ification);
|
||||
}
|
||||
|
||||
System::on_finalize(b);
|
||||
|
||||
System::on_initialize(b + 1);
|
||||
System::set_block_number(b + 1);
|
||||
|
||||
Configuration::initializer_initialize(b + 1);
|
||||
ParasShared::initializer_initialize(b + 1);
|
||||
SessionInfo::initializer_initialize(b + 1);
|
||||
}
|
||||
}
|
||||
|
||||
fn default_config() -> HostConfiguration<BlockNumber> {
|
||||
HostConfiguration {
|
||||
dispute_period: 2,
|
||||
needed_approvals: 3,
|
||||
scheduler_params: SchedulerParams { num_cores: 1, ..Default::default() },
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn genesis_config() -> MockGenesisConfig {
|
||||
MockGenesisConfig {
|
||||
configuration: configuration::GenesisConfig { config: default_config() },
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn session_changes(n: BlockNumber) -> Option<SessionChangeNotification<BlockNumber>> {
|
||||
if n % 10 == 0 {
|
||||
Some(SessionChangeNotification { session_index: n / 10, ..Default::default() })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn new_session_every_block(n: BlockNumber) -> Option<SessionChangeNotification<BlockNumber>> {
|
||||
Some(SessionChangeNotification { session_index: n, ..Default::default() })
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_pruning_is_based_on_dispute_period() {
|
||||
new_test_ext(genesis_config()).execute_with(|| {
|
||||
// Dispute period starts at 2
|
||||
let config = configuration::ActiveConfig::<Test>::get();
|
||||
assert_eq!(config.dispute_period, 2);
|
||||
|
||||
// Move to session 10
|
||||
run_to_block(100, session_changes);
|
||||
// Earliest stored session is 10 - 2 = 8
|
||||
assert_eq!(EarliestStoredSession::<Test>::get(), 8);
|
||||
// Pruning works as expected
|
||||
assert!(Sessions::<Test>::get(7).is_none());
|
||||
assert!(Sessions::<Test>::get(8).is_some());
|
||||
assert!(Sessions::<Test>::get(9).is_some());
|
||||
|
||||
// changing `dispute_period` works
|
||||
let dispute_period = 5;
|
||||
Configuration::set_dispute_period(RuntimeOrigin::root(), dispute_period).unwrap();
|
||||
|
||||
// Dispute period does not automatically change
|
||||
let config = configuration::ActiveConfig::<Test>::get();
|
||||
assert_eq!(config.dispute_period, 2);
|
||||
// Two sessions later it will though
|
||||
run_to_block(120, session_changes);
|
||||
let config = configuration::ActiveConfig::<Test>::get();
|
||||
assert_eq!(config.dispute_period, 5);
|
||||
|
||||
run_to_block(200, session_changes);
|
||||
assert_eq!(EarliestStoredSession::<Test>::get(), 20 - dispute_period);
|
||||
|
||||
// Increase dispute period even more
|
||||
let new_dispute_period = 16;
|
||||
Configuration::set_dispute_period(RuntimeOrigin::root(), new_dispute_period).unwrap();
|
||||
|
||||
run_to_block(210, session_changes);
|
||||
assert_eq!(EarliestStoredSession::<Test>::get(), 21 - dispute_period);
|
||||
|
||||
// Two sessions later it kicks in
|
||||
run_to_block(220, session_changes);
|
||||
let config = configuration::ActiveConfig::<Test>::get();
|
||||
assert_eq!(config.dispute_period, 16);
|
||||
// Earliest session stays the same
|
||||
assert_eq!(EarliestStoredSession::<Test>::get(), 21 - dispute_period);
|
||||
|
||||
// We still don't have enough stored sessions to start pruning
|
||||
run_to_block(300, session_changes);
|
||||
assert_eq!(EarliestStoredSession::<Test>::get(), 21 - dispute_period);
|
||||
|
||||
// now we do
|
||||
run_to_block(420, session_changes);
|
||||
assert_eq!(EarliestStoredSession::<Test>::get(), 42 - new_dispute_period);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_info_is_based_on_config() {
|
||||
new_test_ext(genesis_config()).execute_with(|| {
|
||||
run_to_block(1, new_session_every_block);
|
||||
let session = Sessions::<Test>::get(&1).unwrap();
|
||||
assert_eq!(session.needed_approvals, 3);
|
||||
|
||||
// change some param
|
||||
Configuration::set_needed_approvals(RuntimeOrigin::root(), 42).unwrap();
|
||||
// 2 sessions later
|
||||
run_to_block(3, new_session_every_block);
|
||||
let session = Sessions::<Test>::get(&3).unwrap();
|
||||
assert_eq!(session.needed_approvals, 42);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_info_active_subsets() {
|
||||
let unscrambled = vec![
|
||||
Sr25519Keyring::Alice,
|
||||
Sr25519Keyring::Bob,
|
||||
Sr25519Keyring::Charlie,
|
||||
Sr25519Keyring::Dave,
|
||||
Sr25519Keyring::Eve,
|
||||
];
|
||||
|
||||
let active_set = vec![ValidatorIndex(4), ValidatorIndex(0), ValidatorIndex(2)];
|
||||
|
||||
let unscrambled_validators: Vec<ValidatorId> =
|
||||
unscrambled.iter().map(|v| v.public().into()).collect();
|
||||
let unscrambled_discovery: Vec<AuthorityDiscoveryId> =
|
||||
unscrambled.iter().map(|v| v.public().into()).collect();
|
||||
let unscrambled_assignment: Vec<AssignmentId> =
|
||||
unscrambled.iter().map(|v| v.public().into()).collect();
|
||||
|
||||
let validators = take_active_subset(&active_set, &unscrambled_validators);
|
||||
|
||||
new_test_ext(genesis_config()).execute_with(|| {
|
||||
ParasShared::set_active_validators_with_indices(active_set.clone(), validators.clone());
|
||||
|
||||
assert_eq!(shared::ActiveValidatorIndices::<Test>::get(), active_set);
|
||||
|
||||
AssignmentKeysUnsafe::<Test>::set(unscrambled_assignment.clone());
|
||||
crate::mock::set_discovery_authorities(unscrambled_discovery.clone());
|
||||
assert_eq!(<Test>::authorities(), unscrambled_discovery);
|
||||
|
||||
// invoke directly, because `run_to_block` will invoke `Shared` and clobber our
|
||||
// values.
|
||||
SessionInfo::initializer_on_new_session(&SessionChangeNotification {
|
||||
session_index: 1,
|
||||
validators: validators.clone(),
|
||||
..Default::default()
|
||||
});
|
||||
let session = Sessions::<Test>::get(&1).unwrap();
|
||||
|
||||
assert_eq!(session.validators.to_vec(), validators);
|
||||
assert_eq!(
|
||||
session.discovery_keys,
|
||||
take_active_subset_and_inactive(&active_set, &unscrambled_discovery),
|
||||
);
|
||||
assert_eq!(
|
||||
session.assignment_keys,
|
||||
take_active_subset(&active_set, &unscrambled_assignment),
|
||||
);
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
// 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/>.
|
||||
|
||||
//! A pallet for any shared state that other pallets may want access to.
|
||||
//!
|
||||
//! To avoid cyclic dependencies, it is important that this pallet is not
|
||||
//! dependent on any of the other pallets.
|
||||
|
||||
use alloc::{
|
||||
collections::{btree_map::BTreeMap, btree_set::BTreeSet, vec_deque::VecDeque},
|
||||
vec::Vec,
|
||||
};
|
||||
use frame_support::{pallet_prelude::*, traits::DisabledValidators};
|
||||
use frame_system::pallet_prelude::BlockNumberFor;
|
||||
use pezkuwi_primitives::{
|
||||
transpose_claim_queue, CoreIndex, Id, SessionIndex, ValidatorId, ValidatorIndex,
|
||||
};
|
||||
use sp_runtime::traits::AtLeast32BitUnsigned;
|
||||
|
||||
use rand::{seq::SliceRandom, SeedableRng};
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
|
||||
use crate::configuration::HostConfiguration;
|
||||
|
||||
pub use pallet::*;
|
||||
|
||||
// `SESSION_DELAY` is used to delay any changes to Paras registration or configurations.
|
||||
// Wait until the session index is 2 larger then the current index to apply any changes,
|
||||
// which guarantees that at least one full session has passed before any changes are applied.
|
||||
pub(crate) const SESSION_DELAY: SessionIndex = 2;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub mod migration;
|
||||
|
||||
/// Information about a relay parent.
|
||||
#[derive(Encode, Decode, Default, TypeInfo, Debug)]
|
||||
pub struct RelayParentInfo<Hash> {
|
||||
// Relay parent hash
|
||||
pub relay_parent: Hash,
|
||||
// The state root at this block
|
||||
pub state_root: Hash,
|
||||
// Claim queue snapshot, optimized for accessing the assignments by `ParaId`.
|
||||
// For each para we store the cores assigned per depth.
|
||||
pub claim_queue: BTreeMap<Id, BTreeMap<u8, BTreeSet<CoreIndex>>>,
|
||||
}
|
||||
|
||||
/// Keeps tracks of information about all viable relay parents.
|
||||
#[derive(Encode, Decode, Default, TypeInfo)]
|
||||
pub struct AllowedRelayParentsTracker<Hash, BlockNumber> {
|
||||
// Information about past relay parents that are viable to build upon.
|
||||
//
|
||||
// They are in ascending chronologic order, so the newest relay parents are at
|
||||
// the back of the deque.
|
||||
buffer: VecDeque<RelayParentInfo<Hash>>,
|
||||
|
||||
// The number of the most recent relay-parent, if any.
|
||||
// If the buffer is empty, this value has no meaning and may
|
||||
// be nonsensical.
|
||||
latest_number: BlockNumber,
|
||||
}
|
||||
|
||||
impl<Hash: PartialEq + Copy, BlockNumber: AtLeast32BitUnsigned + Copy>
|
||||
AllowedRelayParentsTracker<Hash, BlockNumber>
|
||||
{
|
||||
/// Add a new relay-parent to the allowed relay parents, along with info about the header.
|
||||
/// Provide a maximum ancestry length for the buffer, which will cause old relay-parents to be
|
||||
/// pruned.
|
||||
/// If the relay parent hash is already present, do nothing.
|
||||
pub(crate) fn update(
|
||||
&mut self,
|
||||
relay_parent: Hash,
|
||||
state_root: Hash,
|
||||
claim_queue: BTreeMap<CoreIndex, VecDeque<Id>>,
|
||||
number: BlockNumber,
|
||||
max_ancestry_len: u32,
|
||||
) {
|
||||
if self.buffer.iter().any(|info| info.relay_parent == relay_parent) {
|
||||
// Already present.
|
||||
return;
|
||||
}
|
||||
|
||||
let claim_queue = transpose_claim_queue(claim_queue);
|
||||
|
||||
self.buffer.push_back(RelayParentInfo { relay_parent, state_root, claim_queue });
|
||||
|
||||
self.latest_number = number;
|
||||
while self.buffer.len() > (max_ancestry_len as usize) {
|
||||
let _ = self.buffer.pop_front();
|
||||
}
|
||||
|
||||
// We only allow relay parents within the same sessions, the buffer
|
||||
// gets cleared on session changes.
|
||||
}
|
||||
|
||||
/// Attempt to acquire the state root and block number to be used when building
|
||||
/// upon the given relay-parent.
|
||||
///
|
||||
/// This only succeeds if the relay-parent is one of the allowed relay-parents.
|
||||
/// If a previous relay-parent number is passed, then this only passes if the new relay-parent
|
||||
/// is more recent than the previous.
|
||||
pub(crate) fn acquire_info(
|
||||
&self,
|
||||
relay_parent: Hash,
|
||||
prev: Option<BlockNumber>,
|
||||
) -> Option<(&RelayParentInfo<Hash>, BlockNumber)> {
|
||||
let pos = self.buffer.iter().position(|info| info.relay_parent == relay_parent)?;
|
||||
let age = (self.buffer.len() - 1) - pos;
|
||||
let number = self.latest_number - BlockNumber::from(age as u32);
|
||||
|
||||
if let Some(prev) = prev {
|
||||
if prev > number {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Some((&self.buffer[pos], number))
|
||||
}
|
||||
|
||||
/// Returns block number of the earliest block the buffer would contain if
|
||||
/// `now` is pushed into it.
|
||||
pub(crate) fn hypothetical_earliest_block_number(
|
||||
&self,
|
||||
now: BlockNumber,
|
||||
max_ancestry_len: u32,
|
||||
) -> BlockNumber {
|
||||
let allowed_ancestry_len = max_ancestry_len.min(self.buffer.len() as u32);
|
||||
|
||||
now - allowed_ancestry_len.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[frame_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: frame_system::Config {
|
||||
type DisabledValidators: frame_support::traits::DisabledValidators;
|
||||
}
|
||||
|
||||
/// The current session index.
|
||||
#[pallet::storage]
|
||||
pub type CurrentSessionIndex<T: Config> = StorageValue<_, SessionIndex, ValueQuery>;
|
||||
|
||||
/// All the validators actively participating in teyrchain consensus.
|
||||
/// Indices are into the broader validator set.
|
||||
#[pallet::storage]
|
||||
pub type ActiveValidatorIndices<T: Config> = StorageValue<_, Vec<ValidatorIndex>, ValueQuery>;
|
||||
|
||||
/// The teyrchain attestation keys of the validators actively participating in teyrchain
|
||||
/// consensus. This should be the same length as `ActiveValidatorIndices`.
|
||||
#[pallet::storage]
|
||||
pub type ActiveValidatorKeys<T: Config> = StorageValue<_, Vec<ValidatorId>, ValueQuery>;
|
||||
|
||||
/// All allowed relay-parents.
|
||||
#[pallet::storage]
|
||||
pub(crate) type AllowedRelayParents<T: Config> =
|
||||
StorageValue<_, AllowedRelayParentsTracker<T::Hash, BlockNumberFor<T>>, ValueQuery>;
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {}
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Called by the initializer to initialize the configuration pallet.
|
||||
pub(crate) fn initializer_initialize(_now: BlockNumberFor<T>) -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
|
||||
/// Called by the initializer to finalize the configuration pallet.
|
||||
pub(crate) fn initializer_finalize() {}
|
||||
|
||||
/// Called by the initializer to note that a new session has started.
|
||||
///
|
||||
/// Returns the list of outgoing paras from the actions queue.
|
||||
pub(crate) fn initializer_on_new_session(
|
||||
session_index: SessionIndex,
|
||||
random_seed: [u8; 32],
|
||||
new_config: &HostConfiguration<BlockNumberFor<T>>,
|
||||
all_validators: Vec<ValidatorId>,
|
||||
) -> Vec<ValidatorId> {
|
||||
// Drop allowed relay parents buffer on a session change.
|
||||
//
|
||||
// During the initialization of the next block we always add its parent
|
||||
// to the tracker.
|
||||
//
|
||||
// With asynchronous backing candidates built on top of relay
|
||||
// parent `R` are still restricted by the runtime to be backed
|
||||
// by the group assigned at `number(R) + 1`, which is guaranteed
|
||||
// to be in the current session.
|
||||
AllowedRelayParents::<T>::mutate(|tracker| tracker.buffer.clear());
|
||||
|
||||
CurrentSessionIndex::<T>::set(session_index);
|
||||
let mut rng: ChaCha20Rng = SeedableRng::from_seed(random_seed);
|
||||
|
||||
let mut shuffled_indices: Vec<_> = (0..all_validators.len())
|
||||
.enumerate()
|
||||
.map(|(i, _)| ValidatorIndex(i as _))
|
||||
.collect();
|
||||
|
||||
shuffled_indices.shuffle(&mut rng);
|
||||
|
||||
if let Some(max) = new_config.max_validators {
|
||||
shuffled_indices.truncate(max as usize);
|
||||
}
|
||||
|
||||
let active_validator_keys =
|
||||
crate::util::take_active_subset(&shuffled_indices, &all_validators);
|
||||
|
||||
ActiveValidatorIndices::<T>::set(shuffled_indices);
|
||||
ActiveValidatorKeys::<T>::set(active_validator_keys.clone());
|
||||
|
||||
active_validator_keys
|
||||
}
|
||||
|
||||
/// Return the session index that should be used for any future scheduled changes.
|
||||
pub fn scheduled_session() -> SessionIndex {
|
||||
CurrentSessionIndex::<T>::get().saturating_add(SESSION_DELAY)
|
||||
}
|
||||
|
||||
/// Fetches disabled validators list from session pallet.
|
||||
/// CAVEAT: this might produce incorrect results on session boundaries
|
||||
pub fn disabled_validators() -> Vec<ValidatorIndex> {
|
||||
let shuffled_indices = ActiveValidatorIndices::<T>::get();
|
||||
// mapping from raw validator index to `ValidatorIndex`
|
||||
// this computation is the same within a session, but should be cheap
|
||||
let reverse_index = shuffled_indices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, v)| (v.0, ValidatorIndex(i as u32)))
|
||||
.collect::<BTreeMap<u32, ValidatorIndex>>();
|
||||
|
||||
// we might have disabled validators who are not teyrchain validators
|
||||
T::DisabledValidators::disabled_validators()
|
||||
.iter()
|
||||
.filter_map(|v| reverse_index.get(v).cloned())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Test function for setting the current session index.
|
||||
#[cfg(any(feature = "std", feature = "runtime-benchmarks", test))]
|
||||
pub fn set_session_index(index: SessionIndex) {
|
||||
CurrentSessionIndex::<T>::set(index);
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "std", feature = "runtime-benchmarks", test))]
|
||||
pub fn set_active_validators_ascending(active: Vec<ValidatorId>) {
|
||||
ActiveValidatorIndices::<T>::set(
|
||||
(0..active.len()).map(|i| ValidatorIndex(i as _)).collect(),
|
||||
);
|
||||
ActiveValidatorKeys::<T>::set(active);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn set_active_validators_with_indices(
|
||||
indices: Vec<ValidatorIndex>,
|
||||
keys: Vec<ValidatorId>,
|
||||
) {
|
||||
assert_eq!(indices.len(), keys.len());
|
||||
ActiveValidatorIndices::<T>::set(indices);
|
||||
ActiveValidatorKeys::<T>::set(keys);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn add_allowed_relay_parent(
|
||||
relay_parent: T::Hash,
|
||||
state_root: T::Hash,
|
||||
claim_queue: BTreeMap<CoreIndex, VecDeque<Id>>,
|
||||
number: BlockNumberFor<T>,
|
||||
max_ancestry_len: u32,
|
||||
) {
|
||||
AllowedRelayParents::<T>::mutate(|tracker| {
|
||||
tracker.update(relay_parent, state_root, claim_queue, number, max_ancestry_len + 1)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
// 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.
|
||||
|
||||
use super::*;
|
||||
use codec::{Decode, Encode};
|
||||
use frame_support::{
|
||||
pallet_prelude::ValueQuery, traits::UncheckedOnRuntimeUpgrade, weights::Weight,
|
||||
};
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
const LOG_TARGET: &str = "runtime::shared";
|
||||
|
||||
pub mod v0 {
|
||||
use super::*;
|
||||
use alloc::collections::vec_deque::VecDeque;
|
||||
|
||||
use frame_support::storage_alias;
|
||||
|
||||
/// All allowed relay-parents storage at version 0.
|
||||
#[storage_alias]
|
||||
pub(crate) type AllowedRelayParents<T: Config> = StorageValue<
|
||||
Pallet<T>,
|
||||
super::v0::AllowedRelayParentsTracker<<T as frame_system::Config>::Hash, BlockNumberFor<T>>,
|
||||
ValueQuery,
|
||||
>;
|
||||
|
||||
#[derive(Encode, Decode, Default, TypeInfo)]
|
||||
pub struct AllowedRelayParentsTracker<Hash, BlockNumber> {
|
||||
// The past relay parents, paired with state roots, that are viable to build upon.
|
||||
//
|
||||
// They are in ascending chronologic order, so the newest relay parents are at
|
||||
// the back of the deque.
|
||||
//
|
||||
// (relay_parent, state_root)
|
||||
pub buffer: VecDeque<(Hash, Hash)>,
|
||||
|
||||
// The number of the most recent relay-parent, if any.
|
||||
// If the buffer is empty, this value has no meaning and may
|
||||
// be nonsensical.
|
||||
pub latest_number: BlockNumber,
|
||||
}
|
||||
|
||||
// Required to workaround #64.
|
||||
impl<Hash: PartialEq + Copy, BlockNumber: AtLeast32BitUnsigned + Copy>
|
||||
AllowedRelayParentsTracker<Hash, BlockNumber>
|
||||
{
|
||||
/// Returns block number of the earliest block the buffer would contain if
|
||||
/// `now` is pushed into it.
|
||||
pub(crate) fn hypothetical_earliest_block_number(
|
||||
&self,
|
||||
now: BlockNumber,
|
||||
max_ancestry_len: u32,
|
||||
) -> BlockNumber {
|
||||
let allowed_ancestry_len = max_ancestry_len.min(self.buffer.len() as u32);
|
||||
|
||||
now - allowed_ancestry_len.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Hash, BlockNumber> From<AllowedRelayParentsTracker<Hash, BlockNumber>>
|
||||
for super::AllowedRelayParentsTracker<Hash, BlockNumber>
|
||||
{
|
||||
fn from(value: AllowedRelayParentsTracker<Hash, BlockNumber>) -> Self {
|
||||
Self {
|
||||
latest_number: value.latest_number,
|
||||
buffer: value
|
||||
.buffer
|
||||
.into_iter()
|
||||
.map(|(relay_parent, state_root)| super::RelayParentInfo {
|
||||
relay_parent,
|
||||
state_root,
|
||||
claim_queue: Default::default(),
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod v1 {
|
||||
use super::*;
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
use frame_support::{
|
||||
ensure,
|
||||
traits::{GetStorageVersion, StorageVersion},
|
||||
};
|
||||
|
||||
pub struct VersionUncheckedMigrateToV1<T>(core::marker::PhantomData<T>);
|
||||
|
||||
impl<T: Config> UncheckedOnRuntimeUpgrade for VersionUncheckedMigrateToV1<T> {
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::TryRuntimeError> {
|
||||
log::trace!(target: LOG_TARGET, "Running pre_upgrade() for shared MigrateToV1");
|
||||
let bytes = u32::to_ne_bytes(v0::AllowedRelayParents::<T>::get().buffer.len() as u32);
|
||||
|
||||
Ok(bytes.to_vec())
|
||||
}
|
||||
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
let mut weight: Weight = Weight::zero();
|
||||
|
||||
// Read old storage.
|
||||
let old_rp_tracker = v0::AllowedRelayParents::<T>::take();
|
||||
|
||||
super::AllowedRelayParents::<T>::set(old_rp_tracker.into());
|
||||
|
||||
weight = weight.saturating_add(T::DbWeight::get().reads_writes(1, 1));
|
||||
|
||||
weight
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(state: Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
|
||||
log::trace!(target: LOG_TARGET, "Running post_upgrade() for shared MigrateToV1");
|
||||
ensure!(
|
||||
Pallet::<T>::on_chain_storage_version() >= StorageVersion::new(1),
|
||||
"Storage version should be >= 1 after the migration"
|
||||
);
|
||||
|
||||
let relay_parent_count = u32::from_ne_bytes(
|
||||
state
|
||||
.try_into()
|
||||
.expect("u32::from_ne_bytes(to_ne_bytes(u32)) always works; qed"),
|
||||
);
|
||||
|
||||
let rp_tracker = AllowedRelayParents::<T>::get();
|
||||
|
||||
ensure!(
|
||||
relay_parent_count as usize == rp_tracker.buffer.len(),
|
||||
"Number of allowed relay parents should be the same as the one before the upgrade."
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Migrate shared module storage to v1.
|
||||
pub type MigrateToV1<T> = frame_support::migrations::VersionedMigration<
|
||||
0,
|
||||
1,
|
||||
v1::VersionUncheckedMigrateToV1<T>,
|
||||
Pallet<T>,
|
||||
<T as frame_system::Config>::DbWeight,
|
||||
>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{v1::VersionUncheckedMigrateToV1, *};
|
||||
use crate::mock::{new_test_ext, MockGenesisConfig, Test};
|
||||
use frame_support::traits::UncheckedOnRuntimeUpgrade;
|
||||
use pezkuwi_primitives::Hash;
|
||||
|
||||
#[test]
|
||||
fn migrate_to_v1() {
|
||||
new_test_ext(MockGenesisConfig::default()).execute_with(|| {
|
||||
let rp_tracker = v0::AllowedRelayParentsTracker {
|
||||
latest_number: 9,
|
||||
buffer: (0..10u64)
|
||||
.into_iter()
|
||||
.map(|idx| (Hash::from_low_u64_ne(idx), Hash::from_low_u64_ne(2 * idx)))
|
||||
.collect::<VecDeque<_>>(),
|
||||
};
|
||||
|
||||
v0::AllowedRelayParents::<Test>::put(rp_tracker);
|
||||
|
||||
<VersionUncheckedMigrateToV1<Test> as UncheckedOnRuntimeUpgrade>::on_runtime_upgrade();
|
||||
|
||||
let rp_tracker = AllowedRelayParents::<Test>::get();
|
||||
|
||||
assert_eq!(rp_tracker.buffer.len(), 10);
|
||||
|
||||
for idx in 0..10u64 {
|
||||
let relay_parent = Hash::from_low_u64_ne(idx);
|
||||
let state_root = Hash::from_low_u64_ne(2 * idx);
|
||||
let (info, block_num) = rp_tracker.acquire_info(relay_parent, None).unwrap();
|
||||
|
||||
assert!(info.claim_queue.is_empty());
|
||||
assert_eq!(info.relay_parent, relay_parent);
|
||||
assert_eq!(info.state_root, state_root);
|
||||
assert_eq!(block_num as u64, idx);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
// 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::{
|
||||
configuration::HostConfiguration,
|
||||
mock::{new_test_ext, MockGenesisConfig, ParasShared, Test},
|
||||
shared,
|
||||
};
|
||||
use assert_matches::assert_matches;
|
||||
use pezkuwi_primitives::Hash;
|
||||
use pezkuwi_primitives_test_helpers::validator_pubkeys;
|
||||
use sp_keyring::Sr25519Keyring;
|
||||
|
||||
#[test]
|
||||
fn tracker_earliest_block_number() {
|
||||
let mut tracker = AllowedRelayParentsTracker::default();
|
||||
|
||||
// Test it on an empty tracker.
|
||||
let now: u32 = 1;
|
||||
let max_ancestry_len = 5;
|
||||
assert_eq!(tracker.hypothetical_earliest_block_number(now, max_ancestry_len), now);
|
||||
|
||||
// Push a single block into the tracker, suppose max capacity is 1.
|
||||
let max_ancestry_len = 0;
|
||||
tracker.update(Hash::zero(), Hash::zero(), Default::default(), 0, max_ancestry_len + 1);
|
||||
assert_eq!(tracker.hypothetical_earliest_block_number(now, max_ancestry_len as _), now);
|
||||
|
||||
// Test a greater capacity.
|
||||
let max_ancestry_len = 4;
|
||||
let now = 4;
|
||||
for i in 1..now {
|
||||
tracker.update(
|
||||
Hash::from([i as u8; 32]),
|
||||
Hash::zero(),
|
||||
Default::default(),
|
||||
i,
|
||||
max_ancestry_len + 1,
|
||||
);
|
||||
assert_eq!(tracker.hypothetical_earliest_block_number(i + 1, max_ancestry_len as _), 0);
|
||||
}
|
||||
|
||||
// Capacity exceeded.
|
||||
tracker.update(Hash::zero(), Hash::zero(), Default::default(), now, max_ancestry_len);
|
||||
assert_eq!(tracker.hypothetical_earliest_block_number(now + 1, max_ancestry_len as _), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tracker_claim_queue_transpose() {
|
||||
let mut tracker = AllowedRelayParentsTracker::<Hash, u32>::default();
|
||||
|
||||
let mut claim_queue = BTreeMap::new();
|
||||
claim_queue.insert(CoreIndex(0), vec![Id::from(0), Id::from(1), Id::from(2)].into());
|
||||
claim_queue.insert(CoreIndex(1), vec![Id::from(0), Id::from(0), Id::from(100)].into());
|
||||
claim_queue.insert(CoreIndex(2), vec![Id::from(1), Id::from(2), Id::from(100)].into());
|
||||
|
||||
tracker.update(Hash::zero(), Hash::zero(), claim_queue, 1u32, 4);
|
||||
|
||||
let (info, _block_num) = tracker.acquire_info(Hash::zero(), None).unwrap();
|
||||
assert_eq!(
|
||||
info.claim_queue.get(&Id::from(0)).unwrap()[&0],
|
||||
vec![CoreIndex(0), CoreIndex(1)].into_iter().collect::<BTreeSet<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
info.claim_queue.get(&Id::from(1)).unwrap()[&0],
|
||||
vec![CoreIndex(2)].into_iter().collect::<BTreeSet<_>>()
|
||||
);
|
||||
assert_eq!(info.claim_queue.get(&Id::from(2)).unwrap().get(&0), None);
|
||||
assert_eq!(info.claim_queue.get(&Id::from(100)).unwrap().get(&0), None);
|
||||
|
||||
assert_eq!(
|
||||
info.claim_queue.get(&Id::from(0)).unwrap()[&1],
|
||||
vec![CoreIndex(1)].into_iter().collect::<BTreeSet<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
info.claim_queue.get(&Id::from(1)).unwrap()[&1],
|
||||
vec![CoreIndex(0)].into_iter().collect::<BTreeSet<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
info.claim_queue.get(&Id::from(2)).unwrap()[&1],
|
||||
vec![CoreIndex(2)].into_iter().collect::<BTreeSet<_>>()
|
||||
);
|
||||
assert_eq!(info.claim_queue.get(&Id::from(100)).unwrap().get(&1), None);
|
||||
|
||||
assert_eq!(info.claim_queue.get(&Id::from(0)).unwrap().get(&2), None);
|
||||
assert_eq!(info.claim_queue.get(&Id::from(1)).unwrap().get(&2), None);
|
||||
assert_eq!(
|
||||
info.claim_queue.get(&Id::from(2)).unwrap()[&2],
|
||||
vec![CoreIndex(0)].into_iter().collect::<BTreeSet<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
info.claim_queue.get(&Id::from(100)).unwrap()[&2],
|
||||
vec![CoreIndex(1), CoreIndex(2)].into_iter().collect::<BTreeSet<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tracker_acquire_info() {
|
||||
let mut tracker = AllowedRelayParentsTracker::<Hash, u32>::default();
|
||||
let max_ancestry_len = 2;
|
||||
|
||||
// (relay_parent, state_root) pairs.
|
||||
let blocks = &[
|
||||
(Hash::repeat_byte(0), Hash::repeat_byte(10)),
|
||||
(Hash::repeat_byte(1), Hash::repeat_byte(11)),
|
||||
(Hash::repeat_byte(2), Hash::repeat_byte(12)),
|
||||
];
|
||||
|
||||
let (relay_parent, state_root) = blocks[0];
|
||||
tracker.update(relay_parent, state_root, Default::default(), 0, max_ancestry_len + 1);
|
||||
assert_matches!(
|
||||
tracker.acquire_info(relay_parent, None),
|
||||
Some((s, b)) if s.state_root == state_root && b == 0
|
||||
);
|
||||
|
||||
// Try to push a duplicate. Should be ignored.
|
||||
tracker.update(
|
||||
relay_parent,
|
||||
Hash::repeat_byte(13),
|
||||
Default::default(),
|
||||
0,
|
||||
max_ancestry_len + 1,
|
||||
);
|
||||
assert_eq!(tracker.buffer.len(), 1);
|
||||
assert_matches!(
|
||||
tracker.acquire_info(relay_parent, None),
|
||||
Some((s, b)) if s.state_root == state_root && b == 0
|
||||
);
|
||||
|
||||
let (relay_parent, state_root) = blocks[1];
|
||||
tracker.update(relay_parent, state_root, Default::default(), 1u32, max_ancestry_len + 1);
|
||||
let (relay_parent, state_root) = blocks[2];
|
||||
tracker.update(relay_parent, state_root, Default::default(), 2u32, max_ancestry_len + 1);
|
||||
for (block_num, (rp, state_root)) in blocks.iter().enumerate().take(2) {
|
||||
assert_matches!(
|
||||
tracker.acquire_info(*rp, None),
|
||||
Some((s, b)) if &s.state_root == state_root && b == block_num as u32
|
||||
);
|
||||
|
||||
assert!(tracker.acquire_info(*rp, Some(2)).is_none());
|
||||
}
|
||||
|
||||
for (block_num, (rp, state_root)) in blocks.iter().enumerate().skip(1) {
|
||||
assert_matches!(
|
||||
tracker.acquire_info(*rp, Some(block_num as u32 - 1)),
|
||||
Some((s, b)) if &s.state_root == state_root && b == block_num as u32
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sets_and_shuffles_validators() {
|
||||
let validators = vec![
|
||||
Sr25519Keyring::Alice,
|
||||
Sr25519Keyring::Bob,
|
||||
Sr25519Keyring::Charlie,
|
||||
Sr25519Keyring::Dave,
|
||||
Sr25519Keyring::Ferdie,
|
||||
];
|
||||
|
||||
let mut config = HostConfiguration::default();
|
||||
config.max_validators = None;
|
||||
|
||||
let pubkeys = validator_pubkeys(&validators);
|
||||
|
||||
new_test_ext(MockGenesisConfig::default()).execute_with(|| {
|
||||
let validators = ParasShared::initializer_on_new_session(1, [1; 32], &config, pubkeys);
|
||||
|
||||
assert_eq!(
|
||||
validators,
|
||||
validator_pubkeys(&[
|
||||
Sr25519Keyring::Ferdie,
|
||||
Sr25519Keyring::Bob,
|
||||
Sr25519Keyring::Charlie,
|
||||
Sr25519Keyring::Dave,
|
||||
Sr25519Keyring::Alice,
|
||||
])
|
||||
);
|
||||
|
||||
assert_eq!(shared::ActiveValidatorKeys::<Test>::get(), validators);
|
||||
|
||||
assert_eq!(
|
||||
shared::ActiveValidatorIndices::<Test>::get(),
|
||||
vec![
|
||||
ValidatorIndex(4),
|
||||
ValidatorIndex(1),
|
||||
ValidatorIndex(2),
|
||||
ValidatorIndex(3),
|
||||
ValidatorIndex(0),
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sets_truncates_and_shuffles_validators() {
|
||||
let validators = vec![
|
||||
Sr25519Keyring::Alice,
|
||||
Sr25519Keyring::Bob,
|
||||
Sr25519Keyring::Charlie,
|
||||
Sr25519Keyring::Dave,
|
||||
Sr25519Keyring::Ferdie,
|
||||
];
|
||||
|
||||
let mut config = HostConfiguration::default();
|
||||
config.max_validators = Some(2);
|
||||
|
||||
let pubkeys = validator_pubkeys(&validators);
|
||||
|
||||
new_test_ext(MockGenesisConfig::default()).execute_with(|| {
|
||||
let validators = ParasShared::initializer_on_new_session(1, [1; 32], &config, pubkeys);
|
||||
|
||||
assert_eq!(validators, validator_pubkeys(&[Sr25519Keyring::Ferdie, Sr25519Keyring::Bob,]));
|
||||
|
||||
assert_eq!(shared::ActiveValidatorKeys::<Test>::get(), validators);
|
||||
|
||||
assert_eq!(
|
||||
shared::ActiveValidatorIndices::<Test>::get(),
|
||||
vec![ValidatorIndex(4), ValidatorIndex(1),]
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,708 @@
|
||||
// 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 crate::{
|
||||
configuration,
|
||||
inclusion::{
|
||||
tests::run_to_block_default_notifications as run_to_block, AggregateMessageOrigin,
|
||||
AggregateMessageOrigin::Ump, UmpAcceptanceCheckErr, UmpQueueId,
|
||||
},
|
||||
mock::{
|
||||
assert_last_event, assert_last_events, new_test_ext, MessageQueue, MessageQueueSize,
|
||||
MockGenesisConfig, ParaInclusion, Processed, System, Test, *,
|
||||
},
|
||||
};
|
||||
use frame_support::{
|
||||
assert_noop, assert_ok,
|
||||
pallet_prelude::*,
|
||||
traits::{ExecuteOverweightError, QueueFootprintQuery, ServiceQueues},
|
||||
weights::Weight,
|
||||
};
|
||||
use pezkuwi_primitives::{
|
||||
well_known_keys, ClaimQueueOffset, CoreSelector, Id as ParaId, UMPSignal, UpwardMessage,
|
||||
UMP_SEPARATOR,
|
||||
};
|
||||
use sp_crypto_hashing::{blake2_256, twox_64};
|
||||
use sp_runtime::traits::Bounded;
|
||||
|
||||
pub(super) struct GenesisConfigBuilder {
|
||||
max_upward_message_size: u32,
|
||||
max_upward_message_num_per_candidate: u32,
|
||||
max_upward_queue_count: u32,
|
||||
max_upward_queue_size: u32,
|
||||
}
|
||||
|
||||
impl Default for GenesisConfigBuilder {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_upward_message_size: 16,
|
||||
max_upward_message_num_per_candidate: 2,
|
||||
max_upward_queue_count: 4,
|
||||
max_upward_queue_size: 64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GenesisConfigBuilder {
|
||||
pub(super) fn large_queue_count() -> Self {
|
||||
Self { max_upward_queue_count: 128, ..Default::default() }
|
||||
}
|
||||
|
||||
pub(super) fn build(self) -> crate::mock::MockGenesisConfig {
|
||||
let mut genesis = default_genesis_config();
|
||||
let config = &mut genesis.configuration.config;
|
||||
|
||||
config.max_upward_message_size = self.max_upward_message_size;
|
||||
config.max_upward_message_num_per_candidate = self.max_upward_message_num_per_candidate;
|
||||
config.max_upward_queue_count = self.max_upward_queue_count;
|
||||
config.max_upward_queue_size = self.max_upward_queue_size;
|
||||
genesis
|
||||
}
|
||||
}
|
||||
|
||||
fn default_genesis_config() -> MockGenesisConfig {
|
||||
MockGenesisConfig {
|
||||
configuration: crate::configuration::GenesisConfig {
|
||||
config: crate::configuration::HostConfiguration {
|
||||
max_downward_message_size: 1024,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn queue_upward_msg(para: ParaId, msg: UpwardMessage) {
|
||||
try_queue_upward_msg(para, msg).unwrap();
|
||||
}
|
||||
|
||||
fn try_queue_upward_msg(para: ParaId, msg: UpwardMessage) -> Result<(), UmpAcceptanceCheckErr> {
|
||||
let msgs = vec![msg];
|
||||
ParaInclusion::check_upward_messages(&configuration::ActiveConfig::<Test>::get(), para, &msgs)?;
|
||||
ParaInclusion::receive_upward_messages(para, msgs.as_slice());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
mod check_upward_messages {
|
||||
use super::*;
|
||||
|
||||
const P_0: ParaId = ParaId::new(0u32);
|
||||
const P_1: ParaId = ParaId::new(1u32);
|
||||
|
||||
// Currently its trivial since unbounded, but this function will be handy when we bound it.
|
||||
fn msg(data: &str) -> UpwardMessage {
|
||||
data.as_bytes().to_vec()
|
||||
}
|
||||
|
||||
/// Check that these messages *could* be queued.
|
||||
fn check(para: ParaId, msgs: Vec<UpwardMessage>, err: Option<UmpAcceptanceCheckErr>) {
|
||||
assert_eq!(
|
||||
ParaInclusion::check_upward_messages(
|
||||
&configuration::ActiveConfig::<Test>::get(),
|
||||
para,
|
||||
&msgs[..]
|
||||
)
|
||||
.err(),
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
/// Enqueue these upward messages.
|
||||
fn queue(para: ParaId, msgs: Vec<UpwardMessage>) {
|
||||
msgs.into_iter().for_each(|msg| super::queue_upward_msg(para, msg));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_works() {
|
||||
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
|
||||
let _g = frame_support::StorageNoopGuard::default();
|
||||
check(P_0, vec![msg("p0m0")], None);
|
||||
check(P_1, vec![msg("p1m0")], None);
|
||||
check(P_0, vec![msg("p0m1")], None);
|
||||
check(P_1, vec![msg("p1m1")], None);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_per_candidate_exceeded_error() {
|
||||
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
|
||||
let _g = frame_support::StorageNoopGuard::default();
|
||||
let permitted =
|
||||
configuration::ActiveConfig::<Test>::get().max_upward_message_num_per_candidate;
|
||||
|
||||
for sent in 0..permitted + 1 {
|
||||
check(P_0, vec![msg("a"); sent as usize], None);
|
||||
}
|
||||
for sent in permitted + 1..permitted + 10 {
|
||||
check(
|
||||
P_0,
|
||||
vec![msg("a"); sent as usize],
|
||||
Some(UmpAcceptanceCheckErr::MoreMessagesThanPermitted { sent, permitted }),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn size_per_message_exceeded_error() {
|
||||
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
|
||||
let _g = frame_support::StorageNoopGuard::default();
|
||||
let max_size = configuration::ActiveConfig::<Test>::get().max_upward_message_size;
|
||||
let max_per_candidate =
|
||||
configuration::ActiveConfig::<Test>::get().max_upward_message_num_per_candidate;
|
||||
|
||||
for msg_size in 1..=max_size {
|
||||
check(P_0, vec![vec![0; msg_size as usize]], None);
|
||||
}
|
||||
for msg_size in max_size + 1..max_size + 10 {
|
||||
for goods in 0..max_per_candidate {
|
||||
let mut msgs = vec![vec![0; max_size as usize]; goods as usize];
|
||||
msgs.push(vec![0; msg_size as usize]);
|
||||
|
||||
check(
|
||||
P_0,
|
||||
msgs,
|
||||
Some(UmpAcceptanceCheckErr::MessageSize { idx: goods, msg_size, max_size }),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queue_count_exceeded_error() {
|
||||
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
|
||||
let limit = configuration::ActiveConfig::<Test>::get().max_upward_queue_count as u64;
|
||||
|
||||
for _ in 0..limit {
|
||||
check(P_0, vec![msg("a")], None);
|
||||
queue(P_0, vec![msg("a")]);
|
||||
}
|
||||
|
||||
check(
|
||||
P_0,
|
||||
vec![msg("a")],
|
||||
Some(UmpAcceptanceCheckErr::CapacityExceeded { count: limit + 1, limit }),
|
||||
);
|
||||
check(
|
||||
P_0,
|
||||
vec![msg("a"); 2],
|
||||
Some(UmpAcceptanceCheckErr::CapacityExceeded { count: limit + 2, limit }),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queue_size_exceeded_error() {
|
||||
new_test_ext(GenesisConfigBuilder::large_queue_count().build()).execute_with(|| {
|
||||
let limit = configuration::ActiveConfig::<Test>::get().max_upward_queue_size as u64;
|
||||
assert_eq!(pallet_message_queue::ItemHeader::<MessageQueueSize>::max_encoded_len(), 5);
|
||||
assert!(
|
||||
configuration::ActiveConfig::<Test>::get().max_upward_queue_size <
|
||||
crate::inclusion::MaxUmpMessageLenOf::<Test>::get(),
|
||||
"Test will not work"
|
||||
);
|
||||
|
||||
for _ in 0..limit {
|
||||
check(P_0, vec![msg("1")], None);
|
||||
queue(P_0, vec![msg("1")]);
|
||||
}
|
||||
|
||||
check(
|
||||
P_0,
|
||||
vec![msg("1")],
|
||||
Some(UmpAcceptanceCheckErr::TotalSizeExceeded { total_size: limit + 1, limit }),
|
||||
);
|
||||
check(
|
||||
P_0,
|
||||
vec![msg("123456")],
|
||||
Some(UmpAcceptanceCheckErr::TotalSizeExceeded { total_size: limit + 6, limit }),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dispatch_empty() {
|
||||
new_test_ext(default_genesis_config()).execute_with(|| {
|
||||
// make sure that the case with empty queues is handled properly
|
||||
MessageQueue::service_queues(Weight::max_value());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dispatch_single_message() {
|
||||
let a = ParaId::from(228);
|
||||
let msg = 1000u32.encode();
|
||||
|
||||
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
|
||||
queue_upward_msg(a, msg.clone());
|
||||
MessageQueue::service_queues(Weight::max_value());
|
||||
assert_eq!(Processed::take(), vec![(a, msg)]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dispatch_resume_after_exceeding_dispatch_stage_weight() {
|
||||
let a = ParaId::from(128);
|
||||
let c = ParaId::from(228);
|
||||
let q = ParaId::from(911);
|
||||
|
||||
let a_msg_1 = (200u32, "a_msg_1").encode();
|
||||
let a_msg_2 = (100u32, "a_msg_2").encode();
|
||||
let c_msg_1 = (300u32, "c_msg_1").encode();
|
||||
let c_msg_2 = (100u32, "c_msg_2").encode();
|
||||
let q_msg = (500u32, "q_msg").encode();
|
||||
|
||||
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
|
||||
queue_upward_msg(q, q_msg.clone());
|
||||
queue_upward_msg(c, c_msg_1.clone());
|
||||
queue_upward_msg(a, a_msg_1.clone());
|
||||
queue_upward_msg(a, a_msg_2.clone());
|
||||
|
||||
// we expect only two first messages to fit in the first iteration.
|
||||
MessageQueue::service_queues(Weight::from_parts(500, 500));
|
||||
assert_eq!(Processed::take(), vec![(q, q_msg)]);
|
||||
queue_upward_msg(c, c_msg_2.clone());
|
||||
// second iteration should process the second message.
|
||||
MessageQueue::service_queues(Weight::from_parts(500, 500));
|
||||
assert_eq!(Processed::take(), vec![(c, c_msg_1), (c, c_msg_2)]);
|
||||
// 3rd iteration.
|
||||
MessageQueue::service_queues(Weight::from_parts(500, 500));
|
||||
assert_eq!(Processed::take(), vec![(a, a_msg_1), (a, a_msg_2)]);
|
||||
// finally, make sure that the queue is empty.
|
||||
MessageQueue::service_queues(Weight::from_parts(500, 500));
|
||||
assert_eq!(Processed::take(), vec![]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dispatch_keeps_message_after_weight_exhausted() {
|
||||
let a = ParaId::from(128);
|
||||
|
||||
let a_msg_1 = (300u32, "a_msg_1").encode();
|
||||
let a_msg_2 = (300u32, "a_msg_2").encode();
|
||||
|
||||
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
|
||||
queue_upward_msg(a, a_msg_1.clone());
|
||||
queue_upward_msg(a, a_msg_2.clone());
|
||||
|
||||
// we expect only one message to fit in the first iteration.
|
||||
MessageQueue::service_queues(Weight::from_parts(500, 500));
|
||||
assert_eq!(Processed::take(), vec![(a, a_msg_1)]);
|
||||
// second iteration should process the remaining message.
|
||||
MessageQueue::service_queues(Weight::from_parts(500, 500));
|
||||
assert_eq!(Processed::take(), vec![(a, a_msg_2)]);
|
||||
// finally, make sure that the queue is empty.
|
||||
MessageQueue::service_queues(Weight::from_parts(500, 500));
|
||||
assert_eq!(Processed::take(), vec![]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dispatch_correctly_handle_remove_of_latest() {
|
||||
let a = ParaId::from(1991);
|
||||
let b = ParaId::from(1999);
|
||||
|
||||
let a_msg_1 = (300u32, "a_msg_1").encode();
|
||||
let a_msg_2 = (300u32, "a_msg_2").encode();
|
||||
let b_msg_1 = (300u32, "b_msg_1").encode();
|
||||
|
||||
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
|
||||
// We want to test here an edge case, where we remove the queue with the highest
|
||||
// para id (i.e. last in the `needs_dispatch` order).
|
||||
//
|
||||
// If the last entry was removed we should proceed execution, assuming we still have
|
||||
// weight available.
|
||||
|
||||
queue_upward_msg(a, a_msg_1.clone());
|
||||
queue_upward_msg(a, a_msg_2.clone());
|
||||
queue_upward_msg(b, b_msg_1.clone());
|
||||
MessageQueue::service_queues(Weight::from_parts(900, 900));
|
||||
assert_eq!(Processed::take(), vec![(a, a_msg_1), (a, a_msg_2), (b, b_msg_1)]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(debug_assertions, should_panic = "Defensive failure has been triggered")]
|
||||
fn queue_enact_too_long_ignored() {
|
||||
const P_0: ParaId = ParaId::new(0u32);
|
||||
|
||||
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
|
||||
let max_enact = crate::inclusion::MaxUmpMessageLenOf::<Test>::get() as usize;
|
||||
let m1 = (300u32, "a_msg_1").encode();
|
||||
let m2 = vec![0u8; max_enact + 1];
|
||||
let m3 = (300u32, "a_msg_3").encode();
|
||||
|
||||
// .. but the enact defensively ignores.
|
||||
ParaInclusion::receive_upward_messages(P_0, &[m1.clone(), m2.clone(), m3.clone()]);
|
||||
// There is one message in the queue now:
|
||||
MessageQueue::service_queues(Weight::from_parts(900, 900));
|
||||
assert_eq!(Processed::take(), vec![(P_0, m1), (P_0, m3)]);
|
||||
});
|
||||
}
|
||||
|
||||
/// Check that the Inclusion pallet correctly updates the well known keys in the MQ handler.
|
||||
///
|
||||
/// Also checks that it works in the presence of overweight messages.
|
||||
#[test]
|
||||
fn relay_dispatch_queue_size_is_updated() {
|
||||
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
|
||||
let cfg = configuration::ActiveConfig::<Test>::get();
|
||||
|
||||
for p in 0..100 {
|
||||
let para = p.into();
|
||||
// Do some tricks with the weight such that the MQ pallet will process in order:
|
||||
// Q0:0, Q1:0 … Q0:1, Q1:1 …
|
||||
let m1 = (300u32 * (100 - p), "m1").encode();
|
||||
let m2 = (300u32 * (100 - p), "m11").encode();
|
||||
|
||||
queue_upward_msg(para, m1);
|
||||
queue_upward_msg(para, m2);
|
||||
|
||||
assert_queue_size(para, 2, 15);
|
||||
assert_queue_remaining(
|
||||
para,
|
||||
cfg.max_upward_queue_count - 2,
|
||||
cfg.max_upward_queue_size - 15,
|
||||
);
|
||||
|
||||
// Now processing one message should also update the queue size.
|
||||
MessageQueue::service_queues(Weight::from_all(300u64 * (100 - p) as u64));
|
||||
assert_queue_remaining(
|
||||
para,
|
||||
cfg.max_upward_queue_count - 1,
|
||||
cfg.max_upward_queue_size - 8,
|
||||
);
|
||||
}
|
||||
|
||||
// The messages of Q0…Q98 are overweight, so `service_queues` wont help.
|
||||
for p in 0..98 {
|
||||
let para = UmpQueueId::Para(p.into());
|
||||
MessageQueue::service_queues(Weight::from_all(u64::MAX));
|
||||
|
||||
let fp = MessageQueue::footprint(AggregateMessageOrigin::Ump(para));
|
||||
let (para_queue_count, para_queue_size) = (fp.storage.count, fp.storage.size);
|
||||
assert_eq!(para_queue_count, 1, "count wrong for para: {}", p);
|
||||
assert_eq!(para_queue_size, 8, "size wrong for para: {}", p);
|
||||
}
|
||||
// All queues are empty after processing overweight messages.
|
||||
for p in 0..100 {
|
||||
let para = UmpQueueId::Para(p.into());
|
||||
let _ = <MessageQueue as ServiceQueues>::execute_overweight(
|
||||
Weight::from_all(u64::MAX),
|
||||
(AggregateMessageOrigin::Ump(para.clone()), 0, 1),
|
||||
);
|
||||
|
||||
assert_queue_remaining(p.into(), cfg.max_upward_queue_count, cfg.max_upward_queue_size);
|
||||
let fp = MessageQueue::footprint(AggregateMessageOrigin::Ump(para));
|
||||
let (para_queue_count, para_queue_size) = (fp.storage.count, fp.storage.size);
|
||||
assert_eq!(para_queue_count, 0, "count wrong for para: {}", p);
|
||||
assert_eq!(para_queue_size, 0, "size wrong for para: {}", p);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Assert that the old and the new way of accessing `relay_dispatch_queue_size` is the same.
|
||||
#[test]
|
||||
fn relay_dispatch_queue_size_key_is_correct() {
|
||||
#![allow(deprecated)]
|
||||
// Storage alias to the old way of accessing the queue size.
|
||||
#[frame_support::storage_alias]
|
||||
type RelayDispatchQueueSize = StorageMap<Ump, Twox64Concat, ParaId, (u32, u32), ValueQuery>;
|
||||
|
||||
for i in 0..1024 {
|
||||
// A "random" para id.
|
||||
let para: ParaId = u32::from_ne_bytes(twox_64(&i.encode())[..4].try_into().unwrap()).into();
|
||||
|
||||
let well_known = pezkuwi_primitives::well_known_keys::relay_dispatch_queue_size(para);
|
||||
let aliased = RelayDispatchQueueSize::hashed_key_for(para);
|
||||
|
||||
assert_eq!(well_known, aliased, "Old and new key must match");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_relay_dispatch_queue_size_is_externally_accessible() {
|
||||
// Make sure that the relay dispatch queue size storage entry is accessible via well known
|
||||
// keys and is decodable into a (u32, u32).
|
||||
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
|
||||
let cfg = configuration::ActiveConfig::<Test>::get();
|
||||
|
||||
for para in 0..10 {
|
||||
let para = para.into();
|
||||
queue_upward_msg(para, vec![0u8; 3]);
|
||||
assert_queue_size(para, 1, 3);
|
||||
assert_queue_remaining(
|
||||
para,
|
||||
cfg.max_upward_queue_count - 1,
|
||||
cfg.max_upward_queue_size - 3,
|
||||
);
|
||||
|
||||
queue_upward_msg(para, vec![0u8; 3]);
|
||||
assert_queue_size(para, 2, 6);
|
||||
assert_queue_remaining(
|
||||
para,
|
||||
cfg.max_upward_queue_count - 2,
|
||||
cfg.max_upward_queue_size - 6,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn assert_queue_size(para: ParaId, count: u32, size: u32) {
|
||||
#[allow(deprecated)]
|
||||
let raw_queue_size = sp_io::storage::get(&well_known_keys::relay_dispatch_queue_size(para))
|
||||
.expect(
|
||||
"enqueuing a message should create the dispatch queue\
|
||||
and it should be accessible via the well known keys",
|
||||
);
|
||||
let (c, s) = <(u32, u32)>::decode(&mut &raw_queue_size[..])
|
||||
.expect("the dispatch queue size should be decodable into (u32, u32)");
|
||||
assert_eq!((c, s), (count, size));
|
||||
|
||||
// Test the deprecated but at least type-safe `relay_dispatch_queue_size_typed`:
|
||||
#[allow(deprecated)]
|
||||
let (c, s) = well_known_keys::relay_dispatch_queue_size_typed(para).get().expect(
|
||||
"enqueuing a message should create the dispatch queue\
|
||||
and it should be accessible via the well known keys",
|
||||
);
|
||||
assert_eq!((c, s), (count, size));
|
||||
}
|
||||
|
||||
fn assert_queue_remaining(para: ParaId, count: u32, size: u32) {
|
||||
let (remaining_cnt, remaining_size) =
|
||||
well_known_keys::relay_dispatch_queue_remaining_capacity(para)
|
||||
.get()
|
||||
.expect("No storage value");
|
||||
assert_eq!(remaining_cnt, count, "Wrong number of remaining messages in Q{}", para);
|
||||
assert_eq!(remaining_size, size, "Wrong remaining size in Q{}", para);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn service_overweight_unknown() {
|
||||
// This test just makes sure that 0 is not a valid index and we can use it not worrying in
|
||||
// the next test.
|
||||
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
|
||||
assert_noop!(
|
||||
<MessageQueue as ServiceQueues>::execute_overweight(
|
||||
Weight::MAX,
|
||||
(Ump(UmpQueueId::Para(0u32.into())), 0, 0)
|
||||
),
|
||||
ExecuteOverweightError::NotFound,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overweight_queue_works() {
|
||||
let para_a = ParaId::from(2021);
|
||||
|
||||
let a_msg_1 = (301u32, "a_msg_1").encode();
|
||||
let a_msg_2 = (501u32, "a_msg_2").encode();
|
||||
let a_msg_3 = (501u32, "a_msg_3").encode();
|
||||
|
||||
let hash_1 = blake2_256(&a_msg_1[..]);
|
||||
let hash_2 = blake2_256(&a_msg_2[..]);
|
||||
let hash_3 = blake2_256(&a_msg_3[..]);
|
||||
|
||||
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
|
||||
// HACK: Start with the block number 1. This is needed because should an event be
|
||||
// emitted during the genesis block they will be implicitly wiped.
|
||||
System::set_block_number(1);
|
||||
|
||||
// This one is overweight. However, the weight is plenty and we can afford to execute
|
||||
// this message, thus expect it.
|
||||
queue_upward_msg(para_a, a_msg_1.clone());
|
||||
queue_upward_msg(para_a, a_msg_2.clone());
|
||||
queue_upward_msg(para_a, a_msg_3.clone());
|
||||
|
||||
MessageQueue::service_queues(Weight::from_parts(500, 500));
|
||||
assert_last_events(
|
||||
[
|
||||
pallet_message_queue::Event::<Test>::Processed {
|
||||
id: hash_1.into(),
|
||||
origin: Ump(UmpQueueId::Para(para_a)),
|
||||
weight_used: Weight::from_parts(301, 301),
|
||||
success: true,
|
||||
}
|
||||
.into(),
|
||||
pallet_message_queue::Event::<Test>::OverweightEnqueued {
|
||||
id: hash_2.into(),
|
||||
origin: Ump(UmpQueueId::Para(para_a)),
|
||||
page_index: 0,
|
||||
message_index: 1,
|
||||
}
|
||||
.into(),
|
||||
pallet_message_queue::Event::<Test>::OverweightEnqueued {
|
||||
id: hash_3.into(),
|
||||
origin: Ump(UmpQueueId::Para(para_a)),
|
||||
page_index: 0,
|
||||
message_index: 2,
|
||||
}
|
||||
.into(),
|
||||
]
|
||||
.into_iter(),
|
||||
);
|
||||
assert_eq!(Processed::take(), vec![(para_a, a_msg_1)]);
|
||||
|
||||
// Now verify that if we wanted to service this overweight message with less than enough
|
||||
// weight it will fail.
|
||||
assert_noop!(
|
||||
<MessageQueue as ServiceQueues>::execute_overweight(
|
||||
Weight::from_parts(500, 500),
|
||||
(Ump(UmpQueueId::Para(para_a)), 0, 2)
|
||||
),
|
||||
ExecuteOverweightError::InsufficientWeight,
|
||||
);
|
||||
|
||||
// ... and if we try to service it with just enough weight it will succeed as well.
|
||||
assert_ok!(<MessageQueue as ServiceQueues>::execute_overweight(
|
||||
Weight::from_parts(501, 501),
|
||||
(Ump(UmpQueueId::Para(para_a)), 0, 2)
|
||||
));
|
||||
assert_last_event(
|
||||
pallet_message_queue::Event::<Test>::Processed {
|
||||
id: hash_3.into(),
|
||||
origin: Ump(UmpQueueId::Para(para_a)),
|
||||
weight_used: Weight::from_parts(501, 501),
|
||||
success: true,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
// But servicing again will not work.
|
||||
assert_noop!(
|
||||
<MessageQueue as ServiceQueues>::execute_overweight(
|
||||
Weight::from_parts(501, 501),
|
||||
(Ump(UmpQueueId::Para(para_a)), 0, 2)
|
||||
),
|
||||
ExecuteOverweightError::AlreadyProcessed,
|
||||
);
|
||||
|
||||
// Using an invalid index does not work.
|
||||
assert_noop!(
|
||||
<MessageQueue as ServiceQueues>::execute_overweight(
|
||||
Weight::from_parts(501, 501),
|
||||
(Ump(UmpQueueId::Para(para_a)), 0, 3)
|
||||
),
|
||||
ExecuteOverweightError::NotFound,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Tests that UMP messages in the dispatch queue of the relay prevents the teyrchain from being
|
||||
/// scheduled for offboarding.
|
||||
#[test]
|
||||
fn cannot_offboard_while_ump_dispatch_queued() {
|
||||
let para = 32.into();
|
||||
let msg = (300u32, "something").encode();
|
||||
|
||||
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
|
||||
register_teyrchain(para);
|
||||
run_to_block(5, vec![4, 5]);
|
||||
|
||||
queue_upward_msg(para, msg.clone());
|
||||
queue_upward_msg(para, msg.clone());
|
||||
// Cannot offboard since there are two UMP messages in the queue.
|
||||
for i in 6..10 {
|
||||
assert!(try_deregister_teyrchain(para).is_err());
|
||||
run_to_block(i, vec![i]);
|
||||
assert!(Paras::is_valid_para(para));
|
||||
}
|
||||
|
||||
// Now let's process the first message.
|
||||
MessageQueue::on_initialize(System::block_number());
|
||||
assert_eq!(Processed::take().len(), 1);
|
||||
// Cannot offboard since there is another one in the queue.
|
||||
assert!(try_deregister_teyrchain(para).is_err());
|
||||
// Now also process the second message ...
|
||||
MessageQueue::on_initialize(System::block_number());
|
||||
assert_eq!(Processed::take().len(), 1);
|
||||
|
||||
// ... and offboard.
|
||||
run_to_block(10, vec![10]);
|
||||
assert!(Paras::is_valid_para(para));
|
||||
assert_ok!(try_deregister_teyrchain(para));
|
||||
assert!(Paras::is_offboarding(para));
|
||||
|
||||
// Offboarding completed.
|
||||
run_to_block(11, vec![11]);
|
||||
assert!(!Paras::is_valid_para(para));
|
||||
});
|
||||
}
|
||||
|
||||
/// Test UMP signals are filtered out and don't consume `max_upward_message_num_per_candidate`.
|
||||
#[test]
|
||||
fn enqueue_ump_signals() {
|
||||
let para = 100.into();
|
||||
|
||||
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
|
||||
register_teyrchain(para);
|
||||
run_to_block(5, vec![4, 5]);
|
||||
|
||||
let config = configuration::ActiveConfig::<Test>::get();
|
||||
let mut messages = (0..config.max_upward_message_num_per_candidate)
|
||||
.into_iter()
|
||||
.map(|_| "msg".encode())
|
||||
.collect::<Vec<_>>();
|
||||
let expected_messages = messages.iter().cloned().map(|msg| (para, msg)).collect::<Vec<_>>();
|
||||
|
||||
// `UMPSignals` and separator do not count as XCM messages. The below check must pass.
|
||||
messages.append(&mut vec![
|
||||
UMP_SEPARATOR,
|
||||
UMPSignal::SelectCore(CoreSelector(0), ClaimQueueOffset(0)).encode(),
|
||||
]);
|
||||
|
||||
ParaInclusion::check_upward_messages(
|
||||
&configuration::ActiveConfig::<Test>::get(),
|
||||
para,
|
||||
&messages,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// We expect that all messages except UMP signal and separator are processed
|
||||
ParaInclusion::receive_upward_messages(para, &messages);
|
||||
MessageQueue::service_queues(Weight::max_value());
|
||||
assert_eq!(Processed::take(), expected_messages);
|
||||
});
|
||||
}
|
||||
|
||||
/// A para-chain cannot send an UMP to the relay chain while it is offboarding.
|
||||
#[test]
|
||||
fn cannot_enqueue_ump_while_offboarding() {
|
||||
let para = 32.into();
|
||||
let msg = (300u32, "something").encode();
|
||||
|
||||
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
|
||||
register_teyrchain(para);
|
||||
run_to_block(5, vec![4, 5]);
|
||||
|
||||
// Start with an offboarding para.
|
||||
assert_ok!(try_deregister_teyrchain(para));
|
||||
assert!(Paras::is_offboarding(para));
|
||||
|
||||
// Cannot enqueue a message.
|
||||
assert!(try_queue_upward_msg(para, msg.clone()).is_err());
|
||||
run_to_block(6, vec![6]);
|
||||
// Para is still there and still cannot enqueue a message.
|
||||
assert!(Paras::is_offboarding(para));
|
||||
assert!(try_queue_upward_msg(para, msg.clone()).is_err());
|
||||
// Now offboarding is completed.
|
||||
run_to_block(7, vec![7]);
|
||||
assert!(!Paras::is_valid_para(para));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
// 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/>.
|
||||
|
||||
//! Utilities that don't belong to any particular module but may draw
|
||||
//! on all modules.
|
||||
|
||||
use alloc::{collections::btree_set::BTreeSet, vec::Vec};
|
||||
use frame_system::pallet_prelude::BlockNumberFor;
|
||||
use pezkuwi_primitives::{HeadData, Id as ParaId, PersistedValidationData, ValidatorIndex};
|
||||
|
||||
use crate::{configuration, hrmp, paras};
|
||||
|
||||
/// Make the persisted validation data for a particular teyrchain, a specified relay-parent and it's
|
||||
/// storage root.
|
||||
///
|
||||
/// This ties together the storage of several modules.
|
||||
pub fn make_persisted_validation_data<T: paras::Config + hrmp::Config>(
|
||||
para_id: ParaId,
|
||||
relay_parent_number: BlockNumberFor<T>,
|
||||
relay_parent_storage_root: T::Hash,
|
||||
) -> Option<PersistedValidationData<T::Hash, BlockNumberFor<T>>> {
|
||||
let config = configuration::ActiveConfig::<T>::get();
|
||||
|
||||
Some(PersistedValidationData {
|
||||
parent_head: paras::Heads::<T>::get(¶_id)?,
|
||||
relay_parent_number,
|
||||
relay_parent_storage_root,
|
||||
max_pov_size: config.max_pov_size,
|
||||
})
|
||||
}
|
||||
|
||||
/// Make the persisted validation data for a particular teyrchain, a specified relay-parent, its
|
||||
/// storage root and parent head data.
|
||||
pub fn make_persisted_validation_data_with_parent<T: configuration::Config>(
|
||||
relay_parent_number: BlockNumberFor<T>,
|
||||
relay_parent_storage_root: T::Hash,
|
||||
parent_head: HeadData,
|
||||
) -> PersistedValidationData<T::Hash, BlockNumberFor<T>> {
|
||||
let config = configuration::ActiveConfig::<T>::get();
|
||||
|
||||
PersistedValidationData {
|
||||
parent_head,
|
||||
relay_parent_number,
|
||||
relay_parent_storage_root,
|
||||
max_pov_size: config.max_pov_size,
|
||||
}
|
||||
}
|
||||
|
||||
/// Take an active subset of a set containing all validators.
|
||||
///
|
||||
/// First item in pair will be all items in set have indices found in the `active` indices set (in
|
||||
/// the order of the `active` vec, the second item will contain the rest, in the original order.
|
||||
///
|
||||
/// ```ignore
|
||||
/// split_active_subset(active, all).0 == take_active_subset(active, all)
|
||||
/// ```
|
||||
pub fn split_active_subset<T: Clone>(active: &[ValidatorIndex], all: &[T]) -> (Vec<T>, Vec<T>) {
|
||||
let active_set: BTreeSet<_> = active.iter().cloned().collect();
|
||||
// active result has ordering of active set.
|
||||
let active_result = take_active_subset(active, all);
|
||||
// inactive result preserves original ordering of `all`.
|
||||
let inactive_result = all
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(i, _)| !active_set.contains(&ValidatorIndex(*i as _)))
|
||||
.map(|(_, v)| v)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
if active_result.len() != active.len() {
|
||||
log::warn!(
|
||||
target: "runtime::teyrchains",
|
||||
"Took active validators from set with wrong size.",
|
||||
);
|
||||
}
|
||||
|
||||
(active_result, inactive_result)
|
||||
}
|
||||
|
||||
/// Uses `split_active_subset` and concatenates the inactive to the active vec.
|
||||
///
|
||||
/// ```ignore
|
||||
/// split_active_subset(active, all)[0..active.len()]) == take_active_subset(active, all)
|
||||
/// ```
|
||||
pub fn take_active_subset_and_inactive<T: Clone>(active: &[ValidatorIndex], all: &[T]) -> Vec<T> {
|
||||
let (mut a, mut i) = split_active_subset(active, all);
|
||||
a.append(&mut i);
|
||||
a
|
||||
}
|
||||
|
||||
/// Take the active subset of a set containing all validators.
|
||||
pub fn take_active_subset<T: Clone>(active: &[ValidatorIndex], set: &[T]) -> Vec<T> {
|
||||
let subset: Vec<_> = active.iter().filter_map(|i| set.get(i.0 as usize)).cloned().collect();
|
||||
|
||||
if subset.len() != active.len() {
|
||||
log::warn!(
|
||||
target: "runtime::teyrchains",
|
||||
"Took active validators from set with wrong size",
|
||||
);
|
||||
}
|
||||
|
||||
subset
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::util::{split_active_subset, take_active_subset};
|
||||
use pezkuwi_primitives::ValidatorIndex;
|
||||
|
||||
#[test]
|
||||
fn take_active_subset_is_compatible_with_split_active_subset() {
|
||||
let active: Vec<_> = vec![ValidatorIndex(1), ValidatorIndex(7), ValidatorIndex(3)];
|
||||
let validators = vec![9, 1, 6, 7, 4, 5, 2, 3, 0, 8];
|
||||
let (selected, unselected) = split_active_subset(&active, &validators);
|
||||
let selected2 = take_active_subset(&active, &validators);
|
||||
assert_eq!(selected, selected2);
|
||||
assert_eq!(unselected, vec![9, 6, 4, 5, 2, 0, 8]);
|
||||
assert_eq!(selected, vec![1, 3, 7]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user