// Copyright (C) Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use sp_std::marker::PhantomData; use codec::{Decode, DecodeLimit}; use cumulus_primitives_core::{ relay_chain::Slot, AbridgedHrmpChannel, ParaId, PersistedValidationData, }; use cumulus_primitives_parachain_inherent::ParachainInherentData; use cumulus_test_relay_sproof_builder::RelayStateSproofBuilder; use frame_support::{ dispatch::{DispatchResult, RawOrigin}, inherent::{InherentData, ProvideInherent}, pallet_prelude::Get, traits::{OnFinalize, OnInitialize, OriginTrait, UnfilteredDispatchable}, weights::Weight, }; use frame_system::pallet_prelude::{BlockNumberFor, HeaderFor}; use polkadot_parachain_primitives::primitives::{ HeadData, HrmpChannelId, RelayChainBlockNumber, XcmpMessageFormat, }; use sp_consensus_aura::{SlotDuration, AURA_ENGINE_ID}; use sp_core::{Encode, U256}; use sp_runtime::{traits::Header, BuildStorage, Digest, DigestItem, SaturatedConversion}; use xcm::{ latest::{Asset, Location, XcmContext, XcmHash}, prelude::*, VersionedXcm, MAX_XCM_DECODE_DEPTH, }; use xcm_executor::{traits::TransactAsset, AssetsInHolding}; pub mod test_cases; pub type BalanceOf = ::Balance; pub type AccountIdOf = ::AccountId; pub type RuntimeCallOf = ::RuntimeCall; pub type ValidatorIdOf = ::ValidatorId; pub type SessionKeysOf = ::Keys; pub struct CollatorSessionKey< Runtime: frame_system::Config + pallet_balances::Config + pallet_session::Config, > { collator: AccountIdOf, validator: ValidatorIdOf, key: SessionKeysOf, } pub struct CollatorSessionKeys< Runtime: frame_system::Config + pallet_balances::Config + pallet_session::Config, > { items: Vec>, } impl CollatorSessionKey { pub fn new( collator: AccountIdOf, validator: ValidatorIdOf, key: SessionKeysOf, ) -> Self { Self { collator, validator, key } } } impl Default for CollatorSessionKeys { fn default() -> Self { Self { items: vec![] } } } impl CollatorSessionKeys { pub fn new( collator: AccountIdOf, validator: ValidatorIdOf, key: SessionKeysOf, ) -> Self { Self { items: vec![CollatorSessionKey::new(collator, validator, key)] } } pub fn add(mut self, item: CollatorSessionKey) -> Self { self.items.push(item); self } pub fn collators(&self) -> Vec> { self.items.iter().map(|item| item.collator.clone()).collect::>() } pub fn session_keys( &self, ) -> Vec<(AccountIdOf, ValidatorIdOf, SessionKeysOf)> { self.items .iter() .map(|item| (item.collator.clone(), item.validator.clone(), item.key.clone())) .collect::>() } } pub struct SlotDurations { pub relay: SlotDuration, pub para: SlotDuration, } /// A set of traits for a minimal parachain runtime, that may be used in conjunction with the /// `ExtBuilder` and the `RuntimeHelper`. pub trait BasicParachainRuntime: frame_system::Config + pallet_balances::Config + pallet_session::Config + pallet_xcm::Config + parachain_info::Config + pallet_collator_selection::Config + cumulus_pallet_parachain_system::Config + pallet_timestamp::Config { } impl BasicParachainRuntime for T where T: frame_system::Config + pallet_balances::Config + pallet_session::Config + pallet_xcm::Config + parachain_info::Config + pallet_collator_selection::Config + cumulus_pallet_parachain_system::Config + pallet_timestamp::Config, ValidatorIdOf: From>, { } /// Basic builder based on balances, collators and pallet_session. pub struct ExtBuilder { // endowed accounts with balances balances: Vec<(AccountIdOf, BalanceOf)>, // collators to test block prod collators: Vec>, // keys added to pallet session keys: Vec<(AccountIdOf, ValidatorIdOf, SessionKeysOf)>, // safe XCM version for pallet_xcm safe_xcm_version: Option, // para id para_id: Option, _runtime: PhantomData, } impl Default for ExtBuilder { fn default() -> ExtBuilder { ExtBuilder { balances: vec![], collators: vec![], keys: vec![], safe_xcm_version: None, para_id: None, _runtime: PhantomData, } } } impl ExtBuilder { pub fn with_balances( mut self, balances: Vec<(AccountIdOf, BalanceOf)>, ) -> Self { self.balances = balances; self } pub fn with_collators(mut self, collators: Vec>) -> Self { self.collators = collators; self } pub fn with_session_keys( mut self, keys: Vec<(AccountIdOf, ValidatorIdOf, SessionKeysOf)>, ) -> Self { self.keys = keys; self } pub fn with_tracing(self) -> Self { sp_tracing::try_init_simple(); self } pub fn with_safe_xcm_version(mut self, safe_xcm_version: XcmVersion) -> Self { self.safe_xcm_version = Some(safe_xcm_version); self } pub fn with_para_id(mut self, para_id: ParaId) -> Self { self.para_id = Some(para_id); self } pub fn build(self) -> sp_io::TestExternalities { let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); pallet_xcm::GenesisConfig:: { safe_xcm_version: self.safe_xcm_version, ..Default::default() } .assimilate_storage(&mut t) .unwrap(); if let Some(para_id) = self.para_id { parachain_info::GenesisConfig:: { parachain_id: para_id, ..Default::default() } .assimilate_storage(&mut t) .unwrap(); } pallet_balances::GenesisConfig:: { balances: self.balances } .assimilate_storage(&mut t) .unwrap(); pallet_collator_selection::GenesisConfig:: { invulnerables: self.collators.clone(), candidacy_bond: Default::default(), desired_candidates: Default::default(), } .assimilate_storage(&mut t) .unwrap(); pallet_session::GenesisConfig:: { keys: self.keys } .assimilate_storage(&mut t) .unwrap(); let mut ext = sp_io::TestExternalities::new(t); ext.execute_with(|| { frame_system::Pallet::::set_block_number(1u32.into()); }); ext } } pub struct RuntimeHelper( PhantomData<(Runtime, AllPalletsWithoutSystem)>, ); /// Utility function that advances the chain to the desired block number. /// If an author is provided, that author information is injected to all the blocks in the meantime. impl< Runtime: frame_system::Config + cumulus_pallet_parachain_system::Config + pallet_timestamp::Config, AllPalletsWithoutSystem, > RuntimeHelper where AccountIdOf: Into<<::RuntimeOrigin as OriginTrait>::AccountId>, AllPalletsWithoutSystem: OnInitialize> + OnFinalize>, { pub fn run_to_block(n: u32, author: AccountIdOf) -> HeaderFor { let mut last_header = None; loop { let block_number = frame_system::Pallet::::block_number(); if block_number >= n.into() { break } // Set the new block number and author // Inherent is not created at every block, don't finalize parachain // system to avoid panicking. let header = frame_system::Pallet::::finalize(); let pre_digest = Digest { logs: vec![DigestItem::PreRuntime(AURA_ENGINE_ID, author.encode())] }; frame_system::Pallet::::reset_events(); let next_block_number = block_number + 1u32.into(); frame_system::Pallet::::initialize( &next_block_number, &header.hash(), &pre_digest, ); AllPalletsWithoutSystem::on_initialize(next_block_number); last_header = Some(header); } last_header.expect("run_to_block empty block range") } pub fn run_to_block_with_finalize(n: u32) -> HeaderFor { let mut last_header = None; loop { let block_number = frame_system::Pallet::::block_number(); if block_number >= n.into() { break } // Set the new block number and author let header = frame_system::Pallet::::finalize(); let pre_digest = Digest { logs: vec![DigestItem::PreRuntime(AURA_ENGINE_ID, block_number.encode())], }; frame_system::Pallet::::reset_events(); let next_block_number = block_number + 1u32.into(); frame_system::Pallet::::initialize( &next_block_number, &header.hash(), &pre_digest, ); AllPalletsWithoutSystem::on_initialize(next_block_number); let parent_head = HeadData(header.encode()); let sproof_builder = RelayStateSproofBuilder { para_id: ::SelfParaId::get(), included_para_head: parent_head.clone().into(), ..Default::default() }; let (relay_parent_storage_root, relay_chain_state) = sproof_builder.into_state_root_and_proof(); let inherent_data = ParachainInherentData { validation_data: PersistedValidationData { parent_head, relay_parent_number: (block_number.saturated_into::() * 2 + 1).into(), relay_parent_storage_root, max_pov_size: 100_000_000, }, relay_chain_state, downward_messages: Default::default(), horizontal_messages: Default::default(), }; let _ = cumulus_pallet_parachain_system::Pallet::::set_validation_data( Runtime::RuntimeOrigin::none(), inherent_data, ); let _ = pallet_timestamp::Pallet::::set( Runtime::RuntimeOrigin::none(), 300_u32.into(), ); AllPalletsWithoutSystem::on_finalize(next_block_number); let header = frame_system::Pallet::::finalize(); last_header = Some(header); } last_header.expect("run_to_block empty block range") } pub fn root_origin() -> ::RuntimeOrigin { ::RuntimeOrigin::root() } pub fn block_number() -> U256 { frame_system::Pallet::::block_number().into() } pub fn origin_of( account_id: AccountIdOf, ) -> ::RuntimeOrigin { ::RuntimeOrigin::signed(account_id.into()) } } impl RuntimeHelper { pub fn do_transfer( from: Location, to: Location, (asset, amount): (Location, u128), ) -> Result { ::transfer_asset( &Asset { id: AssetId(asset), fun: Fungible(amount) }, &from, &to, // We aren't able to track the XCM that initiated the fee deposit, so we create a // fake message hash here &XcmContext::with_message_id([0; 32]), ) } } impl< Runtime: pallet_xcm::Config + cumulus_pallet_parachain_system::Config, AllPalletsWithoutSystem, > RuntimeHelper { pub fn do_teleport_assets( origin: ::RuntimeOrigin, dest: Location, beneficiary: Location, (asset, amount): (Location, u128), open_hrmp_channel: Option<(u32, u32)>, included_head: HeaderFor, slot_digest: &[u8], slot_durations: &SlotDurations, ) -> DispatchResult where HrmpChannelOpener: frame_support::inherent::ProvideInherent< Call = cumulus_pallet_parachain_system::Call, >, { // open hrmp (if needed) if let Some((source_para_id, target_para_id)) = open_hrmp_channel { mock_open_hrmp_channel::( source_para_id.into(), target_para_id.into(), included_head, slot_digest, slot_durations, ); } // do teleport >::teleport_assets( origin, Box::new(dest.into()), Box::new(beneficiary.into()), Box::new((AssetId(asset), amount).into()), 0, ) } } impl< Runtime: cumulus_pallet_parachain_system::Config + pallet_xcm::Config, AllPalletsWithoutSystem, > RuntimeHelper { pub fn execute_as_governance(call: Vec, require_weight_at_most: Weight) -> Outcome { // prepare xcm as governance will do let xcm = Xcm(vec![ UnpaidExecution { weight_limit: Unlimited, check_origin: None }, Transact { origin_kind: OriginKind::Superuser, require_weight_at_most, call: call.into(), }, ]); // execute xcm as parent origin let mut hash = xcm.using_encoded(sp_io::hashing::blake2_256); <::XcmExecutor>::prepare_and_execute( Location::parent(), xcm, &mut hash, Self::xcm_max_weight(XcmReceivedFrom::Parent), Weight::zero(), ) } } pub enum XcmReceivedFrom { Parent, Sibling, } impl RuntimeHelper { pub fn xcm_max_weight(from: XcmReceivedFrom) -> Weight { match from { XcmReceivedFrom::Parent => ParachainSystem::ReservedDmpWeight::get(), XcmReceivedFrom::Sibling => ParachainSystem::ReservedXcmpWeight::get(), } } } impl RuntimeHelper { pub fn assert_pallet_xcm_event_outcome( unwrap_pallet_xcm_event: &Box) -> Option>>, assert_outcome: fn(Outcome), ) { assert_outcome(Self::get_pallet_xcm_event_outcome(unwrap_pallet_xcm_event)); } pub fn get_pallet_xcm_event_outcome( unwrap_pallet_xcm_event: &Box) -> Option>>, ) -> Outcome { >::events() .into_iter() .filter_map(|e| unwrap_pallet_xcm_event(e.event.encode())) .find_map(|e| match e { pallet_xcm::Event::Attempted { outcome } => Some(outcome), _ => None, }) .expect("No `pallet_xcm::Event::Attempted(outcome)` event found!") } } impl< Runtime: frame_system::Config + cumulus_pallet_xcmp_queue::Config, AllPalletsWithoutSystem, > RuntimeHelper { pub fn xcmp_queue_message_sent( unwrap_xcmp_queue_event: Box< dyn Fn(Vec) -> Option>, >, ) -> Option { >::events() .into_iter() .filter_map(|e| unwrap_xcmp_queue_event(e.event.encode())) .find_map(|e| match e { cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { message_hash } => Some(message_hash), _ => None, }) } } pub fn assert_metadata( asset_id: impl Into + Clone, expected_name: &str, expected_symbol: &str, expected_decimals: u8, ) where Fungibles: frame_support::traits::fungibles::metadata::Inspect + frame_support::traits::fungibles::Inspect, { assert_eq!(Fungibles::name(asset_id.clone().into()), Vec::from(expected_name),); assert_eq!(Fungibles::symbol(asset_id.clone().into()), Vec::from(expected_symbol),); assert_eq!(Fungibles::decimals(asset_id.into()), expected_decimals); } pub fn assert_total( asset_id: impl Into + Clone, expected_total_issuance: impl Into, expected_active_issuance: impl Into, ) where Fungibles: frame_support::traits::fungibles::metadata::Inspect + frame_support::traits::fungibles::Inspect, { assert_eq!(Fungibles::total_issuance(asset_id.clone().into()), expected_total_issuance.into()); assert_eq!(Fungibles::active_issuance(asset_id.into()), expected_active_issuance.into()); } /// Helper function which emulates opening HRMP channel which is needed for `XcmpQueue` to pass. /// /// Calls parachain-system's `create_inherent` in case the channel hasn't been opened before, and /// thus requires additional parameters for validating it: latest included parachain head and /// parachain AuRa-slot. /// /// AuRa consensus hook expects pallets to be initialized, before calling this function make sure to /// `run_to_block` at least once. pub fn mock_open_hrmp_channel< C: cumulus_pallet_parachain_system::Config, T: ProvideInherent>, >( sender: ParaId, recipient: ParaId, included_head: HeaderFor, mut slot_digest: &[u8], slot_durations: &SlotDurations, ) { let slot = Slot::decode(&mut slot_digest).expect("failed to decode digest"); // Convert para slot to relay chain. let timestamp = slot.saturating_mul(slot_durations.para.as_millis()); let relay_slot = Slot::from_timestamp(timestamp.into(), slot_durations.relay); let n = 1_u32; let mut sproof_builder = RelayStateSproofBuilder { para_id: sender, included_para_head: Some(HeadData(included_head.encode())), hrmp_egress_channel_index: Some(vec![recipient]), current_slot: relay_slot, ..Default::default() }; sproof_builder.hrmp_channels.insert( HrmpChannelId { sender, recipient }, AbridgedHrmpChannel { max_capacity: 10, max_total_size: 10_000_000_u32, max_message_size: 10_000_000_u32, msg_count: 0, total_size: 0_u32, mqc_head: None, }, ); let (relay_parent_storage_root, relay_chain_state) = sproof_builder.into_state_root_and_proof(); let vfp = PersistedValidationData { relay_parent_number: n as RelayChainBlockNumber, relay_parent_storage_root, ..Default::default() }; // It is insufficient to push the validation function params // to storage; they must also be included in the inherent data. let inherent_data = { let mut inherent_data = InherentData::default(); let system_inherent_data = ParachainInherentData { validation_data: vfp, relay_chain_state, downward_messages: Default::default(), horizontal_messages: Default::default(), }; inherent_data .put_data( cumulus_primitives_parachain_inherent::INHERENT_IDENTIFIER, &system_inherent_data, ) .expect("failed to put VFP inherent"); inherent_data }; // execute the block T::create_inherent(&inherent_data) .expect("got an inherent") .dispatch_bypass_filter(RawOrigin::None.into()) .expect("dispatch succeeded"); } impl RuntimeHelper { pub fn take_xcm(sent_to_para_id: ParaId) -> Option> { match HrmpChannelSource::take_outbound_messages(10)[..] { [(para_id, ref mut xcm_message_data)] if para_id.eq(&sent_to_para_id.into()) => { let mut xcm_message_data = &xcm_message_data[..]; // decode let _ = XcmpMessageFormat::decode_with_depth_limit( MAX_XCM_DECODE_DEPTH, &mut xcm_message_data, ) .expect("valid format"); VersionedXcm::<()>::decode_with_depth_limit( MAX_XCM_DECODE_DEPTH, &mut xcm_message_data, ) .map(|x| Some(x)) .expect("result with xcm") }, _ => return None, } } }