pub mod migration;
use alloc::{vec, vec::Vec};
use core::result;
use frame_support::{
dispatch::DispatchResult,
ensure,
pallet_prelude::Weight,
traits::{Currency, Get, ReservableCurrency},
};
use frame_system::{self, ensure_root, ensure_signed};
use polkadot_primitives::{
HeadData, Id as ParaId, ValidationCode, LOWEST_PUBLIC_ID, MIN_CODE_SIZE,
};
use polkadot_runtime_parachains::{
configuration, ensure_parachain,
paras::{self, ParaGenesisArgs, UpgradeStrategy},
Origin, ParaLifecycle,
};
use crate::traits::{OnSwap, Registrar};
use codec::{Decode, Encode};
pub use pallet::*;
use polkadot_runtime_parachains::paras::{OnNewHead, ParaKind};
use scale_info::TypeInfo;
use sp_runtime::{
traits::{CheckedSub, Saturating},
RuntimeDebug,
};
#[derive(Encode, Decode, Clone, PartialEq, Eq, Default, RuntimeDebug, TypeInfo)]
pub struct ParaInfo<Account, Balance> {
pub(crate) manager: Account,
deposit: Balance,
locked: Option<bool>,
}
impl<Account, Balance> ParaInfo<Account, Balance> {
pub fn is_locked(&self) -> bool {
self.locked.unwrap_or(false)
}
}
type BalanceOf<T> =
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
pub trait WeightInfo {
fn reserve() -> Weight;
fn register() -> Weight;
fn force_register() -> Weight;
fn deregister() -> Weight;
fn swap() -> Weight;
fn schedule_code_upgrade(b: u32) -> Weight;
fn set_current_head(b: u32) -> Weight;
}
pub struct TestWeightInfo;
impl WeightInfo for TestWeightInfo {
fn reserve() -> Weight {
Weight::zero()
}
fn register() -> Weight {
Weight::zero()
}
fn force_register() -> Weight {
Weight::zero()
}
fn deregister() -> Weight {
Weight::zero()
}
fn swap() -> Weight {
Weight::zero()
}
fn schedule_code_upgrade(_b: u32) -> Weight {
Weight::zero()
}
fn set_current_head(_b: u32) -> Weight {
Weight::zero()
}
}
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
#[pallet::pallet]
#[pallet::without_storage_info]
#[pallet::storage_version(STORAGE_VERSION)]
pub struct Pallet<T>(_);
#[pallet::config]
#[pallet::disable_frame_system_supertrait_check]
pub trait Config: configuration::Config + paras::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type RuntimeOrigin: From<<Self as frame_system::Config>::RuntimeOrigin>
+ Into<result::Result<Origin, <Self as Config>::RuntimeOrigin>>;
type Currency: ReservableCurrency<Self::AccountId>;
type OnSwap: crate::traits::OnSwap;
#[pallet::constant]
type ParaDeposit: Get<BalanceOf<Self>>;
#[pallet::constant]
type DataDepositPerByte: Get<BalanceOf<Self>>;
type WeightInfo: WeightInfo;
}
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
Registered { para_id: ParaId, manager: T::AccountId },
Deregistered { para_id: ParaId },
Reserved { para_id: ParaId, who: T::AccountId },
Swapped { para_id: ParaId, other_id: ParaId },
}
#[pallet::error]
pub enum Error<T> {
NotRegistered,
AlreadyRegistered,
NotOwner,
CodeTooLarge,
HeadDataTooLarge,
NotParachain,
NotParathread,
CannotDeregister,
CannotDowngrade,
CannotUpgrade,
ParaLocked,
NotReserved,
InvalidCode,
CannotSwap,
}
#[pallet::storage]
pub(super) type PendingSwap<T> = StorageMap<_, Twox64Concat, ParaId, ParaId>;
#[pallet::storage]
pub type Paras<T: Config> =
StorageMap<_, Twox64Concat, ParaId, ParaInfo<T::AccountId, BalanceOf<T>>>;
#[pallet::storage]
pub type NextFreeParaId<T> = StorageValue<_, ParaId, ValueQuery>;
#[pallet::genesis_config]
pub struct GenesisConfig<T: Config> {
#[serde(skip)]
pub _config: core::marker::PhantomData<T>,
pub next_free_para_id: ParaId,
}
impl<T: Config> Default for GenesisConfig<T> {
fn default() -> Self {
GenesisConfig { next_free_para_id: LOWEST_PUBLIC_ID, _config: Default::default() }
}
}
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
NextFreeParaId::<T>::put(self.next_free_para_id);
}
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(<T as Config>::WeightInfo::register())]
pub fn register(
origin: OriginFor<T>,
id: ParaId,
genesis_head: HeadData,
validation_code: ValidationCode,
) -> DispatchResult {
let who = ensure_signed(origin)?;
Self::do_register(who, None, id, genesis_head, validation_code, true)?;
Ok(())
}
#[pallet::call_index(1)]
#[pallet::weight(<T as Config>::WeightInfo::force_register())]
pub fn force_register(
origin: OriginFor<T>,
who: T::AccountId,
deposit: BalanceOf<T>,
id: ParaId,
genesis_head: HeadData,
validation_code: ValidationCode,
) -> DispatchResult {
ensure_root(origin)?;
Self::do_register(who, Some(deposit), id, genesis_head, validation_code, false)
}
#[pallet::call_index(2)]
#[pallet::weight(<T as Config>::WeightInfo::deregister())]
pub fn deregister(origin: OriginFor<T>, id: ParaId) -> DispatchResult {
Self::ensure_root_para_or_owner(origin, id)?;
Self::do_deregister(id)
}
#[pallet::call_index(3)]
#[pallet::weight(<T as Config>::WeightInfo::swap())]
pub fn swap(origin: OriginFor<T>, id: ParaId, other: ParaId) -> DispatchResult {
Self::ensure_root_para_or_owner(origin, id)?;
if id == other {
PendingSwap::<T>::remove(id);
return Ok(())
}
let id_lifecycle =
paras::Pallet::<T>::lifecycle(id).ok_or(Error::<T>::NotRegistered)?;
if PendingSwap::<T>::get(other) == Some(id) {
let other_lifecycle =
paras::Pallet::<T>::lifecycle(other).ok_or(Error::<T>::NotRegistered)?;
if id_lifecycle == ParaLifecycle::Parachain &&
other_lifecycle == ParaLifecycle::Parathread
{
Self::do_thread_and_chain_swap(id, other);
} else if id_lifecycle == ParaLifecycle::Parathread &&
other_lifecycle == ParaLifecycle::Parachain
{
Self::do_thread_and_chain_swap(other, id);
} else if id_lifecycle == ParaLifecycle::Parachain &&
other_lifecycle == ParaLifecycle::Parachain
{
T::OnSwap::on_swap(id, other);
} else {
return Err(Error::<T>::CannotSwap.into())
}
Self::deposit_event(Event::<T>::Swapped { para_id: id, other_id: other });
PendingSwap::<T>::remove(other);
} else {
PendingSwap::<T>::insert(id, other);
}
Ok(())
}
#[pallet::call_index(4)]
#[pallet::weight(T::DbWeight::get().reads_writes(1, 1))]
pub fn remove_lock(origin: OriginFor<T>, para: ParaId) -> DispatchResult {
Self::ensure_root_or_para(origin, para)?;
<Self as Registrar>::remove_lock(para);
Ok(())
}
#[pallet::call_index(5)]
#[pallet::weight(<T as Config>::WeightInfo::reserve())]
pub fn reserve(origin: OriginFor<T>) -> DispatchResult {
let who = ensure_signed(origin)?;
let id = NextFreeParaId::<T>::get().max(LOWEST_PUBLIC_ID);
Self::do_reserve(who, None, id)?;
NextFreeParaId::<T>::set(id + 1);
Ok(())
}
#[pallet::call_index(6)]
#[pallet::weight(T::DbWeight::get().reads_writes(1, 1))]
pub fn add_lock(origin: OriginFor<T>, para: ParaId) -> DispatchResult {
Self::ensure_root_para_or_owner(origin, para)?;
<Self as Registrar>::apply_lock(para);
Ok(())
}
#[pallet::call_index(7)]
#[pallet::weight(<T as Config>::WeightInfo::schedule_code_upgrade(new_code.0.len() as u32))]
pub fn schedule_code_upgrade(
origin: OriginFor<T>,
para: ParaId,
new_code: ValidationCode,
) -> DispatchResult {
Self::ensure_root_para_or_owner(origin, para)?;
polkadot_runtime_parachains::schedule_code_upgrade::<T>(
para,
new_code,
UpgradeStrategy::ApplyAtExpectedBlock,
)?;
Ok(())
}
#[pallet::call_index(8)]
#[pallet::weight(<T as Config>::WeightInfo::set_current_head(new_head.0.len() as u32))]
pub fn set_current_head(
origin: OriginFor<T>,
para: ParaId,
new_head: HeadData,
) -> DispatchResult {
Self::ensure_root_para_or_owner(origin, para)?;
polkadot_runtime_parachains::set_current_head::<T>(para, new_head);
Ok(())
}
}
}
impl<T: Config> Registrar for Pallet<T> {
type AccountId = T::AccountId;
fn manager_of(id: ParaId) -> Option<T::AccountId> {
Some(Paras::<T>::get(id)?.manager)
}
fn parachains() -> Vec<ParaId> {
paras::Parachains::<T>::get()
}
fn is_parathread(id: ParaId) -> bool {
paras::Pallet::<T>::is_parathread(id)
}
fn is_parachain(id: ParaId) -> bool {
paras::Pallet::<T>::is_parachain(id)
}
fn apply_lock(id: ParaId) {
Paras::<T>::mutate(id, |x| x.as_mut().map(|info| info.locked = Some(true)));
}
fn remove_lock(id: ParaId) {
Paras::<T>::mutate(id, |x| x.as_mut().map(|info| info.locked = Some(false)));
}
fn register(
manager: T::AccountId,
id: ParaId,
genesis_head: HeadData,
validation_code: ValidationCode,
) -> DispatchResult {
Self::do_register(manager, None, id, genesis_head, validation_code, false)
}
fn deregister(id: ParaId) -> DispatchResult {
Self::do_deregister(id)
}
fn make_parachain(id: ParaId) -> DispatchResult {
ensure!(
paras::Pallet::<T>::lifecycle(id) == Some(ParaLifecycle::Parathread),
Error::<T>::NotParathread
);
polkadot_runtime_parachains::schedule_parathread_upgrade::<T>(id)
.map_err(|_| Error::<T>::CannotUpgrade)?;
Ok(())
}
fn make_parathread(id: ParaId) -> DispatchResult {
ensure!(
paras::Pallet::<T>::lifecycle(id) == Some(ParaLifecycle::Parachain),
Error::<T>::NotParachain
);
polkadot_runtime_parachains::schedule_parachain_downgrade::<T>(id)
.map_err(|_| Error::<T>::CannotDowngrade)?;
Ok(())
}
#[cfg(any(feature = "runtime-benchmarks", test))]
fn worst_head_data() -> HeadData {
let max_head_size = configuration::ActiveConfig::<T>::get().max_head_data_size;
assert!(max_head_size > 0, "max_head_data can't be zero for generating worst head data.");
vec![0u8; max_head_size as usize].into()
}
#[cfg(any(feature = "runtime-benchmarks", test))]
fn worst_validation_code() -> ValidationCode {
let max_code_size = configuration::ActiveConfig::<T>::get().max_code_size;
assert!(max_code_size > 0, "max_code_size can't be zero for generating worst code data.");
let validation_code = vec![0u8; max_code_size as usize];
validation_code.into()
}
#[cfg(any(feature = "runtime-benchmarks", test))]
fn execute_pending_transitions() {
use polkadot_runtime_parachains::shared;
shared::Pallet::<T>::set_session_index(shared::Pallet::<T>::scheduled_session());
paras::Pallet::<T>::test_on_new_session();
}
}
impl<T: Config> Pallet<T> {
fn ensure_root_para_or_owner(
origin: <T as frame_system::Config>::RuntimeOrigin,
id: ParaId,
) -> DispatchResult {
ensure_signed(origin.clone())
.map_err(|e| e.into())
.and_then(|who| -> DispatchResult {
let para_info = Paras::<T>::get(id).ok_or(Error::<T>::NotRegistered)?;
ensure!(!para_info.is_locked(), Error::<T>::ParaLocked);
ensure!(para_info.manager == who, Error::<T>::NotOwner);
Ok(())
})
.or_else(|_| -> DispatchResult { Self::ensure_root_or_para(origin, id) })
}
fn ensure_root_or_para(
origin: <T as frame_system::Config>::RuntimeOrigin,
id: ParaId,
) -> DispatchResult {
if let Ok(caller_id) = ensure_parachain(<T as Config>::RuntimeOrigin::from(origin.clone()))
{
ensure!(caller_id == id, Error::<T>::NotOwner);
} else {
ensure_root(origin.clone())?;
}
Ok(())
}
fn do_reserve(
who: T::AccountId,
deposit_override: Option<BalanceOf<T>>,
id: ParaId,
) -> DispatchResult {
ensure!(!Paras::<T>::contains_key(id), Error::<T>::AlreadyRegistered);
ensure!(paras::Pallet::<T>::lifecycle(id).is_none(), Error::<T>::AlreadyRegistered);
let deposit = deposit_override.unwrap_or_else(T::ParaDeposit::get);
<T as Config>::Currency::reserve(&who, deposit)?;
let info = ParaInfo { manager: who.clone(), deposit, locked: None };
Paras::<T>::insert(id, info);
Self::deposit_event(Event::<T>::Reserved { para_id: id, who });
Ok(())
}
fn do_register(
who: T::AccountId,
deposit_override: Option<BalanceOf<T>>,
id: ParaId,
genesis_head: HeadData,
validation_code: ValidationCode,
ensure_reserved: bool,
) -> DispatchResult {
let deposited = if let Some(para_data) = Paras::<T>::get(id) {
ensure!(para_data.manager == who, Error::<T>::NotOwner);
ensure!(!para_data.is_locked(), Error::<T>::ParaLocked);
para_data.deposit
} else {
ensure!(!ensure_reserved, Error::<T>::NotReserved);
Default::default()
};
ensure!(paras::Pallet::<T>::lifecycle(id).is_none(), Error::<T>::AlreadyRegistered);
let (genesis, deposit) =
Self::validate_onboarding_data(genesis_head, validation_code, ParaKind::Parathread)?;
let deposit = deposit_override.unwrap_or(deposit);
if let Some(additional) = deposit.checked_sub(&deposited) {
<T as Config>::Currency::reserve(&who, additional)?;
} else if let Some(rebate) = deposited.checked_sub(&deposit) {
<T as Config>::Currency::unreserve(&who, rebate);
};
let info = ParaInfo { manager: who.clone(), deposit, locked: None };
Paras::<T>::insert(id, info);
let res = polkadot_runtime_parachains::schedule_para_initialize::<T>(id, genesis);
debug_assert!(res.is_ok());
Self::deposit_event(Event::<T>::Registered { para_id: id, manager: who });
Ok(())
}
fn do_deregister(id: ParaId) -> DispatchResult {
match paras::Pallet::<T>::lifecycle(id) {
Some(ParaLifecycle::Parathread) | None => {},
_ => return Err(Error::<T>::NotParathread.into()),
}
polkadot_runtime_parachains::schedule_para_cleanup::<T>(id)
.map_err(|_| Error::<T>::CannotDeregister)?;
if let Some(info) = Paras::<T>::take(&id) {
<T as Config>::Currency::unreserve(&info.manager, info.deposit);
}
PendingSwap::<T>::remove(id);
Self::deposit_event(Event::<T>::Deregistered { para_id: id });
Ok(())
}
fn validate_onboarding_data(
genesis_head: HeadData,
validation_code: ValidationCode,
para_kind: ParaKind,
) -> Result<(ParaGenesisArgs, BalanceOf<T>), sp_runtime::DispatchError> {
let config = configuration::ActiveConfig::<T>::get();
ensure!(validation_code.0.len() >= MIN_CODE_SIZE as usize, Error::<T>::InvalidCode);
ensure!(validation_code.0.len() <= config.max_code_size as usize, Error::<T>::CodeTooLarge);
ensure!(
genesis_head.0.len() <= config.max_head_data_size as usize,
Error::<T>::HeadDataTooLarge
);
let per_byte_fee = T::DataDepositPerByte::get();
let deposit = T::ParaDeposit::get()
.saturating_add(per_byte_fee.saturating_mul((genesis_head.0.len() as u32).into()))
.saturating_add(per_byte_fee.saturating_mul(config.max_code_size.into()));
Ok((ParaGenesisArgs { genesis_head, validation_code, para_kind }, deposit))
}
fn do_thread_and_chain_swap(to_downgrade: ParaId, to_upgrade: ParaId) {
let res1 = polkadot_runtime_parachains::schedule_parachain_downgrade::<T>(to_downgrade);
debug_assert!(res1.is_ok());
let res2 = polkadot_runtime_parachains::schedule_parathread_upgrade::<T>(to_upgrade);
debug_assert!(res2.is_ok());
T::OnSwap::on_swap(to_upgrade, to_downgrade);
}
}
impl<T: Config> OnNewHead for Pallet<T> {
fn on_new_head(id: ParaId, _head: &HeadData) -> Weight {
let mut writes = 0;
if let Some(mut info) = Paras::<T>::get(id) {
if info.locked.is_none() {
info.locked = Some(true);
Paras::<T>::insert(id, info);
writes += 1;
}
}
T::DbWeight::get().reads_writes(1, writes)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
mock::conclude_pvf_checking, paras_registrar, traits::Registrar as RegistrarTrait,
};
use alloc::collections::btree_map::BTreeMap;
use frame_support::{
assert_noop, assert_ok, derive_impl, parameter_types,
traits::{OnFinalize, OnInitialize},
};
use frame_system::limits;
use pallet_balances::Error as BalancesError;
use polkadot_primitives::{Balance, BlockNumber, SessionIndex, MAX_CODE_SIZE};
use polkadot_runtime_parachains::{configuration, origin, shared};
use sp_core::H256;
use sp_io::TestExternalities;
use sp_keyring::Sr25519Keyring;
use sp_runtime::{
traits::{BadOrigin, BlakeTwo256, IdentityLookup},
transaction_validity::TransactionPriority,
BuildStorage, Perbill,
};
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,
Configuration: configuration,
Parachains: paras,
ParasShared: shared,
Registrar: paras_registrar,
ParachainsOrigin: origin,
}
);
impl<C> frame_system::offchain::SendTransactionTypes<C> for Test
where
RuntimeCall: From<C>,
{
type Extrinsic = UncheckedExtrinsic;
type OverarchingCall = RuntimeCall;
}
const NORMAL_RATIO: Perbill = Perbill::from_percent(75);
parameter_types! {
pub BlockWeights: limits::BlockWeights =
frame_system::limits::BlockWeights::simple_max(Weight::from_parts(1024, u64::MAX));
pub BlockLength: limits::BlockLength =
limits::BlockLength::max_with_normal_ratio(4 * 1024 * 1024, NORMAL_RATIO);
}
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
impl frame_system::Config for Test {
type BaseCallFilter = frame_support::traits::Everything;
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 DbWeight = ();
type BlockWeights = BlockWeights;
type BlockLength = BlockLength;
type Version = ();
type PalletInfo = PalletInfo;
type AccountData = pallet_balances::AccountData<u128>;
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
type SS58Prefix = ();
type OnSetCode = ();
type MaxConsumers = frame_support::traits::ConstU32<16>;
}
parameter_types! {
pub const ExistentialDeposit: Balance = 1;
}
#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)]
impl pallet_balances::Config for Test {
type Balance = Balance;
type ExistentialDeposit = ExistentialDeposit;
type AccountStore = System;
}
impl shared::Config for Test {
type DisabledValidators = ();
}
impl origin::Config for Test {}
parameter_types! {
pub const ParasUnsignedPriority: TransactionPriority = TransactionPriority::max_value();
}
impl paras::Config for Test {
type RuntimeEvent = RuntimeEvent;
type WeightInfo = paras::TestWeightInfo;
type UnsignedPriority = ParasUnsignedPriority;
type QueueFootprinter = ();
type NextSessionRotation = crate::mock::TestNextSessionRotation;
type OnNewHead = ();
type AssignCoretime = ();
}
impl configuration::Config for Test {
type WeightInfo = configuration::TestWeightInfo;
}
parameter_types! {
pub const ParaDeposit: Balance = 10;
pub const DataDepositPerByte: Balance = 1;
pub const MaxRetries: u32 = 3;
}
impl Config for Test {
type RuntimeOrigin = RuntimeOrigin;
type RuntimeEvent = RuntimeEvent;
type Currency = Balances;
type OnSwap = MockSwap;
type ParaDeposit = ParaDeposit;
type DataDepositPerByte = DataDepositPerByte;
type WeightInfo = TestWeightInfo;
}
pub fn new_test_ext() -> TestExternalities {
let mut t = frame_system::GenesisConfig::<Test>::default().build_storage().unwrap();
configuration::GenesisConfig::<Test> {
config: configuration::HostConfiguration {
max_code_size: MAX_CODE_SIZE,
max_head_data_size: 1 * 1024 * 1024, ..Default::default()
},
}
.assimilate_storage(&mut t)
.unwrap();
pallet_balances::GenesisConfig::<Test> {
balances: vec![(1, 10_000_000), (2, 10_000_000), (3, 10_000_000)],
}
.assimilate_storage(&mut t)
.unwrap();
t.into()
}
parameter_types! {
pub static SwapData: BTreeMap<ParaId, u64> = BTreeMap::new();
}
pub struct MockSwap;
impl OnSwap for MockSwap {
fn on_swap(one: ParaId, other: ParaId) {
let mut swap_data = SwapData::get();
let one_data = swap_data.remove(&one).unwrap_or_default();
let other_data = swap_data.remove(&other).unwrap_or_default();
swap_data.insert(one, other_data);
swap_data.insert(other, one_data);
SwapData::set(swap_data);
}
}
const BLOCKS_PER_SESSION: u32 = 3;
const VALIDATORS: &[Sr25519Keyring] = &[
Sr25519Keyring::Alice,
Sr25519Keyring::Bob,
Sr25519Keyring::Charlie,
Sr25519Keyring::Dave,
Sr25519Keyring::Ferdie,
];
fn run_to_block(n: BlockNumber) {
assert!(System::block_number() < n);
while System::block_number() < n {
let b = System::block_number();
if System::block_number() > 1 {
System::on_finalize(System::block_number());
}
if (b + 1) % BLOCKS_PER_SESSION == 0 {
let session_index = shared::CurrentSessionIndex::<Test>::get() + 1;
let validators_pub_keys = VALIDATORS.iter().map(|v| v.public().into()).collect();
shared::Pallet::<Test>::set_session_index(session_index);
shared::Pallet::<Test>::set_active_validators_ascending(validators_pub_keys);
Parachains::test_on_new_session();
}
System::set_block_number(b + 1);
System::on_initialize(System::block_number());
}
}
fn run_to_session(n: BlockNumber) {
let block_number = n * BLOCKS_PER_SESSION;
run_to_block(block_number);
}
fn test_genesis_head(size: usize) -> HeadData {
HeadData(vec![0u8; size])
}
fn test_validation_code(size: usize) -> ValidationCode {
let validation_code = vec![0u8; size as usize];
ValidationCode(validation_code)
}
fn para_origin(id: ParaId) -> RuntimeOrigin {
polkadot_runtime_parachains::Origin::Parachain(id).into()
}
fn max_code_size() -> u32 {
configuration::ActiveConfig::<Test>::get().max_code_size
}
fn max_head_size() -> u32 {
configuration::ActiveConfig::<Test>::get().max_head_data_size
}
#[test]
fn basic_setup_works() {
new_test_ext().execute_with(|| {
assert_eq!(PendingSwap::<Test>::get(&ParaId::from(0u32)), None);
assert_eq!(Paras::<Test>::get(&ParaId::from(0u32)), None);
});
}
#[test]
fn end_to_end_scenario_works() {
new_test_ext().execute_with(|| {
let para_id = LOWEST_PUBLIC_ID;
const START_SESSION_INDEX: SessionIndex = 1;
run_to_session(START_SESSION_INDEX);
assert!(!Parachains::is_parathread(para_id));
let validation_code = test_validation_code(32);
assert_ok!(Registrar::reserve(RuntimeOrigin::signed(1)));
assert_ok!(Registrar::register(
RuntimeOrigin::signed(1),
para_id,
test_genesis_head(32),
validation_code.clone(),
));
conclude_pvf_checking::<Test>(&validation_code, VALIDATORS, START_SESSION_INDEX);
run_to_session(START_SESSION_INDEX + 2);
assert!(Parachains::is_parathread(para_id));
assert!(!Parachains::is_parachain(para_id));
assert_ok!(Registrar::make_parachain(para_id));
run_to_session(START_SESSION_INDEX + 4);
assert!(!Parachains::is_parathread(para_id));
assert!(Parachains::is_parachain(para_id));
assert_ok!(Registrar::make_parathread(para_id));
run_to_session(START_SESSION_INDEX + 6);
assert!(Parachains::is_parathread(para_id));
assert!(!Parachains::is_parachain(para_id));
assert_ok!(Registrar::deregister(RuntimeOrigin::root(), para_id,));
run_to_session(START_SESSION_INDEX + 8);
assert!(!Parachains::is_parathread(para_id));
assert!(!Parachains::is_parachain(para_id));
});
}
#[test]
fn register_works() {
new_test_ext().execute_with(|| {
const START_SESSION_INDEX: SessionIndex = 1;
run_to_session(START_SESSION_INDEX);
let para_id = LOWEST_PUBLIC_ID;
assert!(!Parachains::is_parathread(para_id));
let validation_code = test_validation_code(32);
assert_ok!(Registrar::reserve(RuntimeOrigin::signed(1)));
assert_eq!(Balances::reserved_balance(&1), <Test as Config>::ParaDeposit::get());
assert_ok!(Registrar::register(
RuntimeOrigin::signed(1),
para_id,
test_genesis_head(32),
validation_code.clone(),
));
conclude_pvf_checking::<Test>(&validation_code, VALIDATORS, START_SESSION_INDEX);
run_to_session(START_SESSION_INDEX + 2);
assert!(Parachains::is_parathread(para_id));
let validation_code_deposit =
max_code_size() as BalanceOf<Test> * <Test as Config>::DataDepositPerByte::get();
let head_deposit = 32 * <Test as Config>::DataDepositPerByte::get();
assert_eq!(
Balances::reserved_balance(&1),
<Test as Config>::ParaDeposit::get() + head_deposit + validation_code_deposit
);
});
}
#[test]
fn schedule_code_upgrade_validates_code() {
new_test_ext().execute_with(|| {
const START_SESSION_INDEX: SessionIndex = 1;
run_to_session(START_SESSION_INDEX);
let para_id = LOWEST_PUBLIC_ID;
assert!(!Parachains::is_parathread(para_id));
let validation_code = test_validation_code(32);
assert_ok!(Registrar::reserve(RuntimeOrigin::signed(1)));
assert_eq!(Balances::reserved_balance(&1), <Test as Config>::ParaDeposit::get());
assert_ok!(Registrar::register(
RuntimeOrigin::signed(1),
para_id,
test_genesis_head(32),
validation_code.clone(),
));
conclude_pvf_checking::<Test>(&validation_code, VALIDATORS, START_SESSION_INDEX);
run_to_session(START_SESSION_INDEX + 2);
assert!(Parachains::is_parathread(para_id));
let new_code = test_validation_code(0);
assert_noop!(
Registrar::schedule_code_upgrade(
RuntimeOrigin::signed(1),
para_id,
new_code.clone(),
),
paras::Error::<Test>::InvalidCode
);
let new_code = test_validation_code(max_code_size() as usize + 1);
assert_noop!(
Registrar::schedule_code_upgrade(
RuntimeOrigin::signed(1),
para_id,
new_code.clone(),
),
paras::Error::<Test>::InvalidCode
);
});
}
#[test]
fn register_handles_basic_errors() {
new_test_ext().execute_with(|| {
let para_id = LOWEST_PUBLIC_ID;
assert_noop!(
Registrar::register(
RuntimeOrigin::signed(1),
para_id,
test_genesis_head(max_head_size() as usize),
test_validation_code(max_code_size() as usize),
),
Error::<Test>::NotReserved
);
assert_ok!(Registrar::reserve(RuntimeOrigin::signed(1)));
assert_noop!(
Registrar::register(
RuntimeOrigin::signed(2),
para_id,
test_genesis_head(max_head_size() as usize),
test_validation_code(max_code_size() as usize),
),
Error::<Test>::NotOwner
);
assert_ok!(Registrar::register(
RuntimeOrigin::signed(1),
para_id,
test_genesis_head(max_head_size() as usize),
test_validation_code(max_code_size() as usize),
));
run_to_session(2);
assert_ok!(Registrar::deregister(RuntimeOrigin::root(), para_id));
assert_noop!(
Registrar::register(
RuntimeOrigin::signed(1),
para_id,
test_genesis_head(max_head_size() as usize),
test_validation_code(max_code_size() as usize),
),
Error::<Test>::NotReserved
);
assert_ok!(Registrar::reserve(RuntimeOrigin::signed(2)));
assert_noop!(
Registrar::register(
RuntimeOrigin::signed(2),
para_id + 1,
test_genesis_head((max_head_size() + 1) as usize),
test_validation_code(max_code_size() as usize),
),
Error::<Test>::HeadDataTooLarge
);
assert_noop!(
Registrar::register(
RuntimeOrigin::signed(2),
para_id + 1,
test_genesis_head(max_head_size() as usize),
test_validation_code((max_code_size() + 1) as usize),
),
Error::<Test>::CodeTooLarge
);
assert_noop!(
Registrar::reserve(RuntimeOrigin::signed(1337)),
BalancesError::<Test, _>::InsufficientBalance
);
});
}
#[test]
fn deregister_works() {
new_test_ext().execute_with(|| {
const START_SESSION_INDEX: SessionIndex = 1;
run_to_session(START_SESSION_INDEX);
let para_id = LOWEST_PUBLIC_ID;
assert!(!Parachains::is_parathread(para_id));
let validation_code = test_validation_code(32);
assert_ok!(Registrar::reserve(RuntimeOrigin::signed(1)));
assert_ok!(Registrar::register(
RuntimeOrigin::signed(1),
para_id,
test_genesis_head(32),
validation_code.clone(),
));
conclude_pvf_checking::<Test>(&validation_code, VALIDATORS, START_SESSION_INDEX);
run_to_session(START_SESSION_INDEX + 2);
assert!(Parachains::is_parathread(para_id));
assert_ok!(Registrar::deregister(RuntimeOrigin::root(), para_id,));
run_to_session(START_SESSION_INDEX + 4);
assert!(paras::Pallet::<Test>::lifecycle(para_id).is_none());
assert_eq!(Balances::reserved_balance(&1), 0);
});
}
#[test]
fn deregister_handles_basic_errors() {
new_test_ext().execute_with(|| {
const START_SESSION_INDEX: SessionIndex = 1;
run_to_session(START_SESSION_INDEX);
let para_id = LOWEST_PUBLIC_ID;
assert!(!Parachains::is_parathread(para_id));
let validation_code = test_validation_code(32);
assert_ok!(Registrar::reserve(RuntimeOrigin::signed(1)));
assert_ok!(Registrar::register(
RuntimeOrigin::signed(1),
para_id,
test_genesis_head(32),
validation_code.clone(),
));
conclude_pvf_checking::<Test>(&validation_code, VALIDATORS, START_SESSION_INDEX);
run_to_session(START_SESSION_INDEX + 2);
assert!(Parachains::is_parathread(para_id));
assert_noop!(Registrar::deregister(RuntimeOrigin::signed(2), para_id,), BadOrigin);
assert_ok!(Registrar::make_parachain(para_id));
run_to_session(START_SESSION_INDEX + 4);
assert_noop!(
Registrar::deregister(RuntimeOrigin::root(), para_id,),
Error::<Test>::NotParathread
);
});
}
#[test]
fn swap_works() {
new_test_ext().execute_with(|| {
const START_SESSION_INDEX: SessionIndex = 1;
run_to_session(START_SESSION_INDEX);
let para_1 = LOWEST_PUBLIC_ID;
let para_2 = LOWEST_PUBLIC_ID + 1;
let validation_code = test_validation_code(max_code_size() as usize);
assert_ok!(Registrar::reserve(RuntimeOrigin::signed(1)));
assert_ok!(Registrar::register(
RuntimeOrigin::signed(1),
para_1,
test_genesis_head(max_head_size() as usize),
validation_code.clone(),
));
assert_ok!(Registrar::reserve(RuntimeOrigin::signed(2)));
assert_ok!(Registrar::register(
RuntimeOrigin::signed(2),
para_2,
test_genesis_head(max_head_size() as usize),
validation_code.clone(),
));
conclude_pvf_checking::<Test>(&validation_code, VALIDATORS, START_SESSION_INDEX);
run_to_session(START_SESSION_INDEX + 2);
assert_ok!(Registrar::make_parachain(para_1));
let mut swap_data = SwapData::get();
swap_data.insert(para_1, 69);
swap_data.insert(para_2, 1337);
SwapData::set(swap_data);
run_to_session(START_SESSION_INDEX + 4);
assert!(Parachains::is_parachain(para_1));
assert!(!Parachains::is_parathread(para_1));
assert!(!Parachains::is_parachain(para_2));
assert!(Parachains::is_parathread(para_2));
assert_ok!(Registrar::swap(para_origin(para_1), para_1, para_2,));
assert_ok!(Registrar::swap(para_origin(para_2), para_2, para_1,));
System::assert_last_event(RuntimeEvent::Registrar(paras_registrar::Event::Swapped {
para_id: para_2,
other_id: para_1,
}));
run_to_session(START_SESSION_INDEX + 6);
assert!(!Parachains::is_parachain(para_1));
assert!(Parachains::is_parathread(para_1));
assert!(Parachains::is_parachain(para_2));
assert!(!Parachains::is_parathread(para_2));
assert_eq!(SwapData::get().get(¶_1).unwrap(), &1337);
assert_eq!(SwapData::get().get(¶_2).unwrap(), &69);
assert_ok!(Registrar::swap(para_origin(para_1), para_1, para_2,));
assert_ok!(Registrar::swap(para_origin(para_2), para_2, para_1,));
System::assert_last_event(RuntimeEvent::Registrar(paras_registrar::Event::Swapped {
para_id: para_2,
other_id: para_1,
}));
assert_eq!(SwapData::get().get(¶_1).unwrap(), &69);
assert_eq!(SwapData::get().get(¶_2).unwrap(), &1337);
let para_3 = LOWEST_PUBLIC_ID + 2;
let validation_code = test_validation_code(max_code_size() as usize);
assert_ok!(Registrar::reserve(RuntimeOrigin::signed(3)));
assert_ok!(Registrar::register(
RuntimeOrigin::signed(3),
para_3,
test_genesis_head(max_head_size() as usize),
validation_code.clone(),
));
conclude_pvf_checking::<Test>(&validation_code, VALIDATORS, START_SESSION_INDEX + 6);
run_to_session(START_SESSION_INDEX + 8);
assert_ok!(Registrar::make_parachain(para_3));
let mut swap_data = SwapData::get();
swap_data.insert(para_3, 777);
SwapData::set(swap_data);
run_to_session(START_SESSION_INDEX + 10);
assert!(Parachains::is_parachain(para_3));
assert!(!Parachains::is_parathread(para_3));
assert!(Parachains::is_parachain(para_1));
assert!(!Parachains::is_parathread(para_1));
assert_ok!(Registrar::swap(para_origin(para_1), para_1, para_3,));
assert_ok!(Registrar::swap(para_origin(para_3), para_3, para_1,));
System::assert_last_event(RuntimeEvent::Registrar(paras_registrar::Event::Swapped {
para_id: para_3,
other_id: para_1,
}));
assert_eq!(SwapData::get().get(¶_3).unwrap(), &69);
assert_eq!(SwapData::get().get(¶_1).unwrap(), &777);
});
}
#[test]
fn para_lock_works() {
new_test_ext().execute_with(|| {
run_to_block(1);
assert_ok!(Registrar::reserve(RuntimeOrigin::signed(1)));
let para_id = LOWEST_PUBLIC_ID;
assert_ok!(Registrar::register(
RuntimeOrigin::signed(1),
para_id,
vec![1; 3].into(),
test_validation_code(32)
));
assert_noop!(Registrar::add_lock(RuntimeOrigin::signed(2), para_id), BadOrigin);
Registrar::on_new_head(para_id, &Default::default());
assert_noop!(
Registrar::ensure_root_para_or_owner(RuntimeOrigin::signed(1), para_id),
BadOrigin
);
assert_noop!(Registrar::remove_lock(RuntimeOrigin::signed(1), para_id), BadOrigin);
assert_ok!(Registrar::remove_lock(para_origin(para_id), para_id));
assert_ok!(Registrar::ensure_root_para_or_owner(RuntimeOrigin::signed(1), para_id));
Registrar::on_new_head(para_id, &Default::default());
assert_ok!(Registrar::ensure_root_para_or_owner(RuntimeOrigin::signed(1), para_id));
});
}
#[test]
fn swap_handles_bad_states() {
new_test_ext().execute_with(|| {
const START_SESSION_INDEX: SessionIndex = 1;
run_to_session(START_SESSION_INDEX);
let para_1 = LOWEST_PUBLIC_ID;
let para_2 = LOWEST_PUBLIC_ID + 1;
assert!(!Parachains::is_parathread(para_1));
assert!(!Parachains::is_parathread(para_2));
assert_noop!(
Registrar::swap(RuntimeOrigin::root(), para_1, para_2),
Error::<Test>::NotRegistered
);
let validation_code = test_validation_code(32);
assert_ok!(Registrar::reserve(RuntimeOrigin::signed(1)));
assert_ok!(Registrar::reserve(RuntimeOrigin::signed(2)));
assert_ok!(Registrar::register(
RuntimeOrigin::signed(1),
para_1,
test_genesis_head(32),
validation_code.clone(),
));
assert_ok!(Registrar::register(
RuntimeOrigin::signed(2),
para_2,
test_genesis_head(32),
validation_code.clone(),
));
conclude_pvf_checking::<Test>(&validation_code, VALIDATORS, START_SESSION_INDEX);
assert_ok!(Registrar::swap(RuntimeOrigin::root(), para_1, para_2));
assert_noop!(
Registrar::swap(RuntimeOrigin::root(), para_2, para_1),
Error::<Test>::CannotSwap
);
run_to_session(START_SESSION_INDEX + 2);
assert!(Parachains::is_parathread(para_1));
assert!(Parachains::is_parathread(para_2));
assert_ok!(Registrar::swap(RuntimeOrigin::root(), para_1, para_2));
assert_noop!(
Registrar::swap(RuntimeOrigin::root(), para_2, para_1),
Error::<Test>::CannotSwap
);
assert_ok!(Registrar::make_parachain(para_1));
assert_ok!(Registrar::swap(RuntimeOrigin::root(), para_1, para_2));
assert_noop!(
Registrar::swap(RuntimeOrigin::root(), para_2, para_1),
Error::<Test>::CannotSwap
);
run_to_session(START_SESSION_INDEX + 3);
assert_ok!(Registrar::swap(RuntimeOrigin::root(), para_1, para_2));
assert_noop!(
Registrar::swap(RuntimeOrigin::root(), para_2, para_1),
Error::<Test>::CannotSwap
);
run_to_session(START_SESSION_INDEX + 4);
assert!(Parachains::is_parachain(para_1));
assert!(Parachains::is_parathread(para_2));
assert_ok!(Registrar::swap(RuntimeOrigin::root(), para_1, para_2));
assert_ok!(Registrar::swap(RuntimeOrigin::root(), para_2, para_1));
assert!(System::events().iter().any(|r| matches!(
r.event,
RuntimeEvent::Registrar(paras_registrar::Event::Swapped { .. })
)));
run_to_session(START_SESSION_INDEX + 5);
assert_ok!(Registrar::swap(RuntimeOrigin::root(), para_1, para_2));
assert_noop!(
Registrar::swap(RuntimeOrigin::root(), para_2, para_1),
Error::<Test>::CannotSwap
);
run_to_session(START_SESSION_INDEX + 6);
assert!(Parachains::is_parachain(para_2));
assert!(Parachains::is_parathread(para_1));
assert!(System::events().iter().any(|r| matches!(
r.event,
RuntimeEvent::Registrar(paras_registrar::Event::Swapped { .. })
)));
assert_ok!(Registrar::make_parathread(para_2));
run_to_session(START_SESSION_INDEX + 7);
assert_ok!(Registrar::swap(RuntimeOrigin::root(), para_1, para_2));
assert_noop!(
Registrar::swap(RuntimeOrigin::root(), para_2, para_1),
Error::<Test>::CannotSwap
);
run_to_session(START_SESSION_INDEX + 8);
assert!(Parachains::is_parathread(para_1));
assert!(Parachains::is_parathread(para_2));
});
}
}
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking {
use super::{Pallet as Registrar, *};
use crate::traits::Registrar as RegistrarT;
use frame_support::assert_ok;
use frame_system::RawOrigin;
use polkadot_primitives::{MAX_CODE_SIZE, MAX_HEAD_DATA_SIZE, MIN_CODE_SIZE};
use polkadot_runtime_parachains::{paras, shared, Origin as ParaOrigin};
use sp_runtime::traits::Bounded;
use frame_benchmarking::{account, benchmarks, whitelisted_caller};
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();
let frame_system::EventRecord { event, .. } = &events[events.len() - 1];
assert_eq!(event, &system_event);
}
fn register_para<T: Config>(id: u32) -> ParaId {
let para = ParaId::from(id);
let genesis_head = Registrar::<T>::worst_head_data();
let validation_code = Registrar::<T>::worst_validation_code();
let caller: T::AccountId = whitelisted_caller();
T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
assert_ok!(Registrar::<T>::reserve(RawOrigin::Signed(caller.clone()).into()));
assert_ok!(Registrar::<T>::register(
RawOrigin::Signed(caller).into(),
para,
genesis_head,
validation_code.clone()
));
assert_ok!(polkadot_runtime_parachains::paras::Pallet::<T>::add_trusted_validation_code(
frame_system::Origin::<T>::Root.into(),
validation_code,
));
return para
}
fn para_origin(id: u32) -> ParaOrigin {
ParaOrigin::Parachain(id.into())
}
fn next_scheduled_session<T: Config>() {
shared::Pallet::<T>::set_session_index(shared::Pallet::<T>::scheduled_session());
paras::Pallet::<T>::test_on_new_session();
}
benchmarks! {
where_clause { where ParaOrigin: Into<<T as frame_system::Config>::RuntimeOrigin> }
reserve {
let caller: T::AccountId = whitelisted_caller();
T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
}: _(RawOrigin::Signed(caller.clone()))
verify {
assert_last_event::<T>(Event::<T>::Reserved { para_id: LOWEST_PUBLIC_ID, who: caller }.into());
assert!(Paras::<T>::get(LOWEST_PUBLIC_ID).is_some());
assert_eq!(paras::Pallet::<T>::lifecycle(LOWEST_PUBLIC_ID), None);
}
register {
let para = LOWEST_PUBLIC_ID;
let genesis_head = Registrar::<T>::worst_head_data();
let validation_code = Registrar::<T>::worst_validation_code();
let caller: T::AccountId = whitelisted_caller();
T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
assert_ok!(Registrar::<T>::reserve(RawOrigin::Signed(caller.clone()).into()));
}: _(RawOrigin::Signed(caller.clone()), para, genesis_head, validation_code.clone())
verify {
assert_last_event::<T>(Event::<T>::Registered{ para_id: para, manager: caller }.into());
assert_eq!(paras::Pallet::<T>::lifecycle(para), Some(ParaLifecycle::Onboarding));
assert_ok!(polkadot_runtime_parachains::paras::Pallet::<T>::add_trusted_validation_code(
frame_system::Origin::<T>::Root.into(),
validation_code,
));
next_scheduled_session::<T>();
assert_eq!(paras::Pallet::<T>::lifecycle(para), Some(ParaLifecycle::Parathread));
}
force_register {
let manager: T::AccountId = account("manager", 0, 0);
let deposit = 0u32.into();
let para = ParaId::from(69);
let genesis_head = Registrar::<T>::worst_head_data();
let validation_code = Registrar::<T>::worst_validation_code();
}: _(RawOrigin::Root, manager.clone(), deposit, para, genesis_head, validation_code.clone())
verify {
assert_last_event::<T>(Event::<T>::Registered { para_id: para, manager }.into());
assert_eq!(paras::Pallet::<T>::lifecycle(para), Some(ParaLifecycle::Onboarding));
assert_ok!(polkadot_runtime_parachains::paras::Pallet::<T>::add_trusted_validation_code(
frame_system::Origin::<T>::Root.into(),
validation_code,
));
next_scheduled_session::<T>();
assert_eq!(paras::Pallet::<T>::lifecycle(para), Some(ParaLifecycle::Parathread));
}
deregister {
let para = register_para::<T>(LOWEST_PUBLIC_ID.into());
next_scheduled_session::<T>();
let caller: T::AccountId = whitelisted_caller();
}: _(RawOrigin::Signed(caller), para)
verify {
assert_last_event::<T>(Event::<T>::Deregistered { para_id: para }.into());
}
swap {
let parathread = register_para::<T>(LOWEST_PUBLIC_ID.into());
let parachain = register_para::<T>((LOWEST_PUBLIC_ID + 1).into());
let parachain_origin = para_origin(parachain.into());
next_scheduled_session::<T>();
Registrar::<T>::make_parachain(parachain)?;
next_scheduled_session::<T>();
assert_eq!(paras::Pallet::<T>::lifecycle(parachain), Some(ParaLifecycle::Parachain));
assert_eq!(paras::Pallet::<T>::lifecycle(parathread), Some(ParaLifecycle::Parathread));
let caller: T::AccountId = whitelisted_caller();
Registrar::<T>::swap(parachain_origin.into(), parachain, parathread)?;
}: _(RawOrigin::Signed(caller.clone()), parathread, parachain)
verify {
next_scheduled_session::<T>();
assert_eq!(paras::Pallet::<T>::lifecycle(parachain), Some(ParaLifecycle::Parathread));
assert_eq!(paras::Pallet::<T>::lifecycle(parathread), Some(ParaLifecycle::Parachain));
}
schedule_code_upgrade {
let b in MIN_CODE_SIZE .. MAX_CODE_SIZE;
let new_code = ValidationCode(vec![0; b as usize]);
let para_id = ParaId::from(1000);
}: _(RawOrigin::Root, para_id, new_code)
set_current_head {
let b in 1 .. MAX_HEAD_DATA_SIZE;
let new_head = HeadData(vec![0; b as usize]);
let para_id = ParaId::from(1000);
}: _(RawOrigin::Root, para_id, new_head)
impl_benchmark_test_suite!(
Registrar,
crate::integration_tests::new_test_ext(),
crate::integration_tests::Test,
);
}
}