feat: initialize Kurdistan SDK - independent fork of Polkadot SDK

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