referrerpolicy=no-referrer-when-downgrade

xcm_simulator/
lib.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Polkadot.
3
4// Polkadot is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Polkadot is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
16
17//! Test kit to simulate cross-chain message passing and XCM execution.
18
19/// Implementation of a simple message queue.
20/// Used for sending messages.
21pub mod mock_message_queue;
22
23extern crate alloc;
24
25pub use codec::Encode;
26pub use paste;
27
28pub use alloc::collections::vec_deque::VecDeque;
29pub use core::{cell::RefCell, marker::PhantomData};
30pub use frame_support::{
31	traits::{EnqueueMessage, Get, ProcessMessage, ProcessMessageError, ServiceQueues},
32	weights::{Weight, WeightMeter},
33};
34pub use sp_io::{hashing::blake2_256, TestExternalities};
35
36pub use polkadot_core_primitives::BlockNumber as RelayBlockNumber;
37pub use polkadot_parachain_primitives::primitives::{
38	DmpMessageHandler as DmpMessageHandlerT, Id as ParaId, XcmpMessageFormat,
39	XcmpMessageHandler as XcmpMessageHandlerT,
40};
41pub use polkadot_runtime_parachains::{
42	dmp,
43	inclusion::{AggregateMessageOrigin, UmpQueueId},
44};
45pub use xcm::{latest::prelude::*, VersionedXcm};
46pub use xcm_builder::ProcessXcmMessage;
47pub use xcm_executor::XcmExecutor;
48
49pub trait TestExt {
50	/// Initialize the test environment.
51	fn new_ext() -> sp_io::TestExternalities;
52	/// Resets the state of the test environment.
53	fn reset_ext();
54	/// Execute code in the context of the test externalities, without automatic
55	/// message processing. All messages in the message buses can be processed
56	/// by calling `Self::dispatch_xcm_buses()`.
57	fn execute_without_dispatch<R>(execute: impl FnOnce() -> R) -> R;
58	/// Process all messages in the message buses
59	fn dispatch_xcm_buses();
60	/// Execute some code in the context of the test externalities, with
61	/// automatic message processing.
62	/// Messages are dispatched once the passed closure completes.
63	fn execute_with<R>(execute: impl FnOnce() -> R) -> R {
64		let result = Self::execute_without_dispatch(execute);
65		Self::dispatch_xcm_buses();
66		result
67	}
68}
69
70pub enum MessageKind {
71	Ump,
72	Dmp,
73	Xcmp,
74}
75
76/// Encodes the provided XCM message based on the `message_kind`.
77pub fn encode_xcm(message: Xcm<()>, message_kind: MessageKind) -> Vec<u8> {
78	match message_kind {
79		MessageKind::Ump | MessageKind::Dmp => VersionedXcm::<()>::from(message).encode(),
80		MessageKind::Xcmp => {
81			let fmt = XcmpMessageFormat::ConcatenatedVersionedXcm;
82			let mut outbound = fmt.encode();
83
84			let encoded = VersionedXcm::<()>::from(message).encode();
85			outbound.extend_from_slice(&encoded[..]);
86			outbound
87		},
88	}
89}
90
91pub fn fake_message_hash<T>(message: &Xcm<T>) -> XcmHash {
92	message.using_encoded(blake2_256)
93}
94
95/// The macro is implementing upward message passing(UMP) for the provided relay
96/// chain struct. The struct has to provide the XCM configuration for the relay
97/// chain.
98///
99/// ```ignore
100/// decl_test_relay_chain! {
101///	    pub struct Relay {
102///	        Runtime = relay_chain::Runtime,
103///	        XcmConfig = relay_chain::XcmConfig,
104///	        new_ext = relay_ext(),
105///	    }
106///	}
107/// ```
108#[macro_export]
109#[rustfmt::skip]
110macro_rules! decl_test_relay_chain {
111	(
112		pub struct $name:ident {
113			Runtime = $runtime:path,
114			RuntimeCall = $runtime_call:path,
115			RuntimeEvent = $runtime_event:path,
116			XcmConfig = $xcm_config:path,
117			MessageQueue = $mq:path,
118			System = $system:path,
119			new_ext = $new_ext:expr,
120		}
121	) => {
122		pub struct $name;
123
124		$crate::__impl_ext!($name, $new_ext);
125
126		impl $crate::ProcessMessage for $name {
127			type Origin = $crate::ParaId;
128
129			fn process_message(
130				msg: &[u8],
131				para: Self::Origin,
132				meter: &mut $crate::WeightMeter,
133				id: &mut [u8; 32],
134			) -> Result<bool, $crate::ProcessMessageError> {
135				use $crate::{Weight, AggregateMessageOrigin, UmpQueueId, ServiceQueues, EnqueueMessage};
136				use $mq as message_queue;
137				use $runtime_event as runtime_event;
138
139				Self::execute_with(|| {
140					<$mq as EnqueueMessage<AggregateMessageOrigin>>::enqueue_message(
141						msg.try_into().expect("Message too long"),
142						AggregateMessageOrigin::Ump(UmpQueueId::Para(para.clone()))
143					);
144
145					<$system>::reset_events();
146					<$mq as ServiceQueues>::service_queues(Weight::MAX);
147					let events = <$system>::events();
148					let event = events.last().expect("There must be at least one event");
149
150					match &event.event {
151						runtime_event::MessageQueue(
152								pallet_message_queue::Event::Processed {origin, ..}) => {
153							assert_eq!(origin, &AggregateMessageOrigin::Ump(UmpQueueId::Para(para)));
154						},
155						event => panic!("Unexpected event: {:#?}", event),
156					}
157					Ok(true)
158				})
159			}
160		}
161	};
162}
163
164/// The macro is implementing the `XcmMessageHandlerT` and `DmpMessageHandlerT`
165/// traits for the provided parachain struct. Expects the provided parachain
166/// struct to define the XcmpMessageHandler and DmpMessageHandler pallets that
167/// contain the message handling logic.
168///
169/// ```ignore
170/// decl_test_parachain! {
171/// 	    pub struct ParaA {
172/// 	        Runtime = parachain::Runtime,
173/// 	        XcmpMessageHandler = parachain::MsgQueue,
174/// 	        DmpMessageHandler = parachain::MsgQueue,
175/// 	        new_ext = para_ext(),
176/// 	    }
177/// }
178/// ```
179#[macro_export]
180macro_rules! decl_test_parachain {
181	(
182		pub struct $name:ident {
183			Runtime = $runtime:path,
184			XcmpMessageHandler = $xcmp_message_handler:path,
185			DmpMessageHandler = $dmp_message_handler:path,
186			new_ext = $new_ext:expr,
187		}
188	) => {
189		pub struct $name;
190
191		$crate::__impl_ext!($name, $new_ext);
192
193		impl $crate::XcmpMessageHandlerT for $name {
194			fn handle_xcmp_messages<
195				'a,
196				I: Iterator<Item = ($crate::ParaId, $crate::RelayBlockNumber, &'a [u8])>,
197			>(
198				iter: I,
199				max_weight: $crate::Weight,
200			) -> $crate::Weight {
201				use $crate::{TestExt, XcmpMessageHandlerT};
202
203				$name::execute_with(|| {
204					<$xcmp_message_handler>::handle_xcmp_messages(iter, max_weight)
205				})
206			}
207		}
208
209		impl $crate::DmpMessageHandlerT for $name {
210			fn handle_dmp_messages(
211				iter: impl Iterator<Item = ($crate::RelayBlockNumber, Vec<u8>)>,
212				max_weight: $crate::Weight,
213			) -> $crate::Weight {
214				use $crate::{DmpMessageHandlerT, TestExt};
215
216				$name::execute_with(|| {
217					<$dmp_message_handler>::handle_dmp_messages(iter, max_weight)
218				})
219			}
220		}
221	};
222}
223
224/// Implements the `TestExt` trait for a specified struct.
225#[macro_export]
226macro_rules! __impl_ext {
227	// entry point: generate ext name
228	($name:ident, $new_ext:expr) => {
229		$crate::paste::paste! {
230			$crate::__impl_ext!(@impl $name, $new_ext, [<EXT_ $name:upper>]);
231		}
232	};
233	// impl
234	(@impl $name:ident, $new_ext:expr, $ext_name:ident) => {
235		thread_local! {
236			pub static $ext_name: $crate::RefCell<$crate::TestExternalities>
237				= $crate::RefCell::new($new_ext);
238		}
239
240		impl $crate::TestExt for $name {
241			fn new_ext() -> $crate::TestExternalities {
242				$new_ext
243			}
244
245			fn reset_ext() {
246				$ext_name.with(|v| *v.borrow_mut() = $new_ext);
247			}
248
249			fn execute_without_dispatch<R>(execute: impl FnOnce() -> R) -> R {
250				$ext_name.with(|v| v.borrow_mut().execute_with(execute))
251			}
252
253			fn dispatch_xcm_buses() {
254				while exists_messages_in_any_bus() {
255					if let Err(xcm_error) = process_relay_messages() {
256						panic!("Relay chain XCM execution failure: {:?}", xcm_error);
257					}
258					if let Err(xcm_error) = process_para_messages() {
259						panic!("Parachain XCM execution failure: {:?}", xcm_error);
260					}
261				}
262			}
263		}
264	};
265}
266
267thread_local! {
268	pub static PARA_MESSAGE_BUS: RefCell<VecDeque<(ParaId, Location, Xcm<()>)>>
269		= RefCell::new(VecDeque::new());
270	pub static RELAY_MESSAGE_BUS: RefCell<VecDeque<(Location, Xcm<()>)>>
271		= RefCell::new(VecDeque::new());
272}
273
274/// Declares a test network that consists of a relay chain and multiple
275/// parachains. Expects a network struct as an argument and implements testing
276/// functionality, `ParachainXcmRouter` and the `RelayChainXcmRouter`. The
277/// struct needs to contain the relay chain struct and an indexed list of
278/// parachains that are going to be in the network.
279///
280/// ```ignore
281/// decl_test_network! {
282/// 	    pub struct ExampleNet {
283/// 	        relay_chain = Relay,
284/// 	        parachains = vec![
285/// 	            (1, ParaA),
286/// 	            (2, ParaB),
287/// 	        ],
288/// 	    }
289/// }
290/// ```
291#[macro_export]
292macro_rules! decl_test_network {
293	(
294		pub struct $name:ident {
295			relay_chain = $relay_chain:ty,
296			parachains = vec![ $( ($para_id:expr, $parachain:ty), )* ],
297		}
298	) => {
299		use $crate::Encode;
300		pub struct $name;
301
302		impl $name {
303			pub fn reset() {
304				use $crate::{TestExt, VecDeque};
305				// Reset relay chain message bus.
306				$crate::RELAY_MESSAGE_BUS.with(|b| b.replace(VecDeque::new()));
307				// Reset parachain message bus.
308				$crate::PARA_MESSAGE_BUS.with(|b| b.replace(VecDeque::new()));
309				<$relay_chain>::reset_ext();
310				$( <$parachain>::reset_ext(); )*
311			}
312		}
313
314		/// Check if any messages exist in either message bus.
315		fn exists_messages_in_any_bus() -> bool {
316			use $crate::{RELAY_MESSAGE_BUS, PARA_MESSAGE_BUS};
317			let no_relay_messages_left = RELAY_MESSAGE_BUS.with(|b| b.borrow().is_empty());
318			let no_parachain_messages_left = PARA_MESSAGE_BUS.with(|b| b.borrow().is_empty());
319			!(no_relay_messages_left && no_parachain_messages_left)
320		}
321
322		/// Process all messages originating from parachains.
323		fn process_para_messages() -> $crate::XcmResult {
324			use $crate::{ProcessMessage, XcmpMessageHandlerT};
325
326			while let Some((para_id, destination, message)) = $crate::PARA_MESSAGE_BUS.with(
327				|b| b.borrow_mut().pop_front()) {
328				match destination.unpack() {
329					(1, []) => {
330						let encoded = $crate::encode_xcm(message, $crate::MessageKind::Ump);
331						let mut _id = [0; 32];
332						let r = <$relay_chain>::process_message(
333							encoded.as_slice(), para_id,
334							&mut $crate::WeightMeter::new(),
335							&mut _id,
336						);
337						match r {
338							Err($crate::ProcessMessageError::Overweight(required)) =>
339								return Err($crate::XcmError::WeightLimitReached(required)),
340							// Not really the correct error, but there is no "undecodable".
341							Err(_) => return Err($crate::XcmError::Unimplemented),
342							Ok(_) => (),
343						}
344					},
345					$(
346						(1, [$crate::Parachain(id)]) if *id == $para_id => {
347							let encoded = $crate::encode_xcm(message, $crate::MessageKind::Xcmp);
348							let messages = vec![(para_id, 1, &encoded[..])];
349							let _weight = <$parachain>::handle_xcmp_messages(
350								messages.into_iter(),
351								$crate::Weight::MAX,
352							);
353						},
354					)*
355					_ => {
356						return Err($crate::XcmError::Unroutable);
357					}
358				}
359			}
360
361			Ok(())
362		}
363
364		/// Process all messages originating from the relay chain.
365		fn process_relay_messages() -> $crate::XcmResult {
366			use $crate::DmpMessageHandlerT;
367
368			while let Some((destination, message)) = $crate::RELAY_MESSAGE_BUS.with(
369				|b| b.borrow_mut().pop_front()) {
370				match destination.unpack() {
371					$(
372						(0, [$crate::Parachain(id)]) if *id == $para_id => {
373							let encoded = $crate::encode_xcm(message, $crate::MessageKind::Dmp);
374							// NOTE: RelayChainBlockNumber is hard-coded to 1
375							let messages = vec![(1, encoded)];
376							let _weight = <$parachain>::handle_dmp_messages(
377								messages.into_iter(), $crate::Weight::MAX,
378							);
379						},
380					)*
381					_ => return Err($crate::XcmError::Transport("Only sends to children parachain.")),
382				}
383			}
384
385			Ok(())
386		}
387
388		/// XCM router for parachain.
389		pub struct ParachainXcmRouter<T>($crate::PhantomData<T>);
390
391		impl<T: $crate::Get<$crate::ParaId>> $crate::SendXcm for ParachainXcmRouter<T> {
392			type Ticket = ($crate::ParaId, $crate::Location, $crate::Xcm<()>);
393			fn validate(
394				destination: &mut Option<$crate::Location>,
395				message: &mut Option<$crate::Xcm<()>>,
396			) -> $crate::SendResult<($crate::ParaId, $crate::Location, $crate::Xcm<()>)> {
397				use $crate::XcmpMessageHandlerT;
398
399				let d = destination.take().ok_or($crate::SendError::MissingArgument)?;
400				match d.unpack() {
401					(1, []) => {},
402					$(
403						(1, [$crate::Parachain(id)]) if id == &$para_id => {}
404					)*
405					_ => {
406						*destination = Some(d);
407						return Err($crate::SendError::NotApplicable)
408					},
409				}
410				let m = message.take().ok_or($crate::SendError::MissingArgument)?;
411				Ok(((T::get(), d, m), $crate::Assets::new()))
412			}
413			fn deliver(
414				triple: ($crate::ParaId, $crate::Location, $crate::Xcm<()>),
415			) -> Result<$crate::XcmHash, $crate::SendError> {
416				let hash = $crate::helpers::derive_topic_id(&triple.2);
417				$crate::PARA_MESSAGE_BUS.with(|b| b.borrow_mut().push_back(triple));
418				Ok(hash)
419			}
420		}
421
422		/// XCM router for relay chain.
423		pub struct RelayChainXcmRouter;
424		impl $crate::SendXcm for RelayChainXcmRouter {
425			type Ticket = ($crate::Location, $crate::Xcm<()>);
426			fn validate(
427				destination: &mut Option<$crate::Location>,
428				message: &mut Option<$crate::Xcm<()>>,
429			) -> $crate::SendResult<($crate::Location, $crate::Xcm<()>)> {
430				use $crate::DmpMessageHandlerT;
431
432				let d = destination.take().ok_or($crate::SendError::MissingArgument)?;
433				match d.unpack() {
434					$(
435						(0, [$crate::Parachain(id)]) if id == &$para_id => {},
436					)*
437					_ => {
438						*destination = Some(d);
439						return Err($crate::SendError::NotApplicable)
440					},
441				}
442				let m = message.take().ok_or($crate::SendError::MissingArgument)?;
443				Ok(((d, m), $crate::Assets::new()))
444			}
445			fn deliver(
446				pair: ($crate::Location, $crate::Xcm<()>),
447			) -> Result<$crate::XcmHash, $crate::SendError> {
448				let hash = $crate::helpers::derive_topic_id(&pair.1);
449				$crate::RELAY_MESSAGE_BUS.with(|b| b.borrow_mut().push_back(pair));
450				Ok(hash)
451			}
452		}
453	};
454}
455
456pub mod helpers {
457	use super::*;
458	use sp_runtime::testing::H256;
459	use std::collections::{HashMap, HashSet};
460
461	/// Derives a topic ID for an XCM in tests.
462	pub fn derive_topic_id<T>(message: &Xcm<T>) -> XcmHash {
463		if let Some(SetTopic(topic_id)) = message.last() {
464			*topic_id
465		} else {
466			fake_message_hash(message)
467		}
468	}
469
470	/// A test utility for tracking XCM topic IDs.
471	///
472	/// # Examples
473	///
474	/// ```
475	/// use sp_runtime::testing::H256;
476	/// use xcm_simulator::helpers::TopicIdTracker;
477	///
478	/// // Dummy topic IDs
479	/// let topic_id = H256::repeat_byte(0x42);
480	///
481	/// // Create a new tracker
482	/// let mut tracker = TopicIdTracker::new();
483	///
484	/// // Insert the same topic ID for three chains
485	/// tracker.insert("ChainA", topic_id);
486	/// tracker.insert_all("ChainB", &[topic_id]);
487	/// tracker.insert_and_assert_unique("ChainC", topic_id);
488	///
489	/// // Assert the topic ID exists everywhere
490	/// tracker.assert_contains("ChainA", &topic_id);
491	/// tracker.assert_id_seen_on_all_chains(&topic_id);
492	/// tracker.assert_only_id_seen_on_all_chains("ChainB");
493	/// tracker.assert_unique();
494	///
495	/// // You can also test that inserting inconsistent topic IDs fails:
496	/// let another_id = H256::repeat_byte(0x43);
497	/// let result = std::panic::catch_unwind(|| {
498	///     let mut tracker = TopicIdTracker::new();
499	///     tracker.insert("ChainA", topic_id);
500	///     tracker.insert_and_assert_unique("ChainB", another_id);
501	/// });
502	/// assert!(result.is_err());
503	///
504	/// let result = std::panic::catch_unwind(|| {
505	///     let mut tracker = TopicIdTracker::new();
506	///     tracker.insert("ChainA", topic_id);
507	///     tracker.insert("ChainB", another_id);
508	///     tracker.assert_unique();
509	/// });
510	/// assert!(result.is_err());
511	/// ```
512	#[derive(Clone, Debug)]
513	pub struct TopicIdTracker {
514		ids: HashMap<String, HashSet<H256>>,
515	}
516	impl TopicIdTracker {
517		/// Initialises a new, empty topic ID tracker.
518		pub fn new() -> Self {
519			TopicIdTracker { ids: HashMap::new() }
520		}
521
522		/// Asserts that the given topic ID has been recorded for the specified chain.
523		pub fn assert_contains(&self, chain: &str, id: &H256) {
524			let ids = self
525				.ids
526				.get(chain)
527				.expect(&format!("No topic IDs recorded for chain '{}'", chain));
528
529			assert!(
530				ids.contains(id),
531				"Expected topic ID {:?} not found for chain '{}'. Found topic IDs: {:?}",
532				id,
533				chain,
534				ids
535			);
536		}
537
538		/// Asserts that the given topic ID has been recorded on all chains.
539		pub fn assert_id_seen_on_all_chains(&self, id: &H256) {
540			self.ids.keys().for_each(|chain| {
541				self.assert_contains(chain, id);
542			});
543		}
544
545		/// Asserts that exactly one topic ID is recorded on the given chain, and that the same ID
546		/// is present on all other chains.
547		pub fn assert_only_id_seen_on_all_chains(&self, chain: &str) {
548			let ids = self
549				.ids
550				.get(chain)
551				.expect(&format!("No topic IDs recorded for chain '{}'", chain));
552
553			assert_eq!(
554				ids.len(),
555				1,
556				"Expected exactly one topic ID for chain '{}', but found {}: {:?}",
557				chain,
558				ids.len(),
559				ids
560			);
561
562			let id = *ids.iter().next().unwrap();
563			self.assert_id_seen_on_all_chains(&id);
564		}
565
566		/// Asserts that exactly one unique topic ID is present across all captured entries.
567		pub fn assert_unique(&self) {
568			let unique_ids: HashSet<_> = self.ids.values().flatten().collect();
569			assert_eq!(
570				unique_ids.len(),
571				1,
572				"Expected exactly one topic ID, found {}: {:?}",
573				unique_ids.len(),
574				unique_ids
575			);
576		}
577
578		/// Inserts a topic ID with the given chain name in the captor.
579		pub fn insert(&mut self, chain: &str, id: H256) {
580			self.ids.entry(chain.to_string()).or_default().insert(id);
581		}
582
583		/// Inserts all topic IDs associated with the given chain name.
584		pub fn insert_all(&mut self, chain: &str, ids: &[H256]) {
585			ids.iter().for_each(|&id| self.insert(chain, id));
586		}
587
588		/// Inserts a topic ID for a given chain and then asserts global uniqueness.
589		pub fn insert_and_assert_unique(&mut self, chain: &str, id: H256) {
590			if let Some(existing_ids) = self.ids.get(chain) {
591				assert_eq!(
592					existing_ids.len(),
593					1,
594					"Expected exactly one topic ID for chain '{}', but found: {:?}",
595					chain,
596					existing_ids
597				);
598				let existing_id =
599					*existing_ids.iter().next().expect(&format!("Topic ID for chain '{}'", chain));
600				assert_eq!(
601					id, existing_id,
602					"Topic ID mismatch for chain '{}': expected {:?}, got {:?}",
603					id, existing_id, chain
604				);
605			} else {
606				self.insert(chain, id);
607			}
608			self.assert_unique();
609		}
610	}
611
612	#[cfg(test)]
613	mod tests {
614		use super::*;
615		use sp_runtime::testing::H256;
616
617		#[test]
618		#[should_panic(expected = "Expected exactly one topic ID")]
619		fn test_assert_unique_fails_with_multiple_ids() {
620			let mut tracker = TopicIdTracker::new();
621			let id1 = H256::repeat_byte(0x42);
622			let id2 = H256::repeat_byte(0x43);
623
624			tracker.insert("ChainA", id1);
625			tracker.insert("ChainB", id2);
626			tracker.assert_unique();
627		}
628
629		#[test]
630		#[should_panic(expected = "Topic ID mismatch")]
631		fn test_insert_and_assert_unique_mismatch() {
632			let mut tracker = TopicIdTracker::new();
633			let id1 = H256::repeat_byte(0x42);
634			let id2 = H256::repeat_byte(0x43);
635
636			tracker.insert_and_assert_unique("ChainA", id1);
637			tracker.insert_and_assert_unique("ChainA", id2);
638		}
639	}
640}