From 55644dc00e79a21434f6ef1c7285292ff18630b6 Mon Sep 17 00:00:00 2001 From: Raymond Cheung <178801527+raymondkfcheung@users.noreply.github.com> Date: Thu, 24 Jul 2025 13:26:53 +0100 Subject: [PATCH 1/8] Enable `TopicIdTracker` to support multiple flows --- .../emulated/common/src/xcm_helpers.rs | 26 +++++--- .../src/tests/asset_transfers.rs | 26 ++++---- polkadot/xcm/xcm-simulator/src/lib.rs | 60 +++++++++++++++++-- 3 files changed, 87 insertions(+), 25 deletions(-) diff --git a/cumulus/parachains/integration-tests/emulated/common/src/xcm_helpers.rs b/cumulus/parachains/integration-tests/emulated/common/src/xcm_helpers.rs index a990e70434b1e..477fc8ef0b0ff 100644 --- a/cumulus/parachains/integration-tests/emulated/common/src/xcm_helpers.rs +++ b/cumulus/parachains/integration-tests/emulated/common/src/xcm_helpers.rs @@ -92,19 +92,31 @@ pub fn get_amount_from_versioned_assets(assets: VersionedAssets) -> u128 { amount } +/// Helper method to find all `Event::Processed` IDs from the chain's events. +pub fn find_all_mq_processed_ids() -> Vec +where + ::Runtime: pallet_message_queue::Config, + C::RuntimeEvent: TryInto::Runtime>>, +{ + C::events() + .into_iter() + .filter_map(|event| { + if let Ok(pallet_message_queue::Event::Processed { id, .. }) = event.try_into() { + Some(id) + } else { + None + } + }) + .collect() +} + /// Helper method to find the ID of the first `Event::Processed` event in the chain's events. pub fn find_mq_processed_id() -> Option where ::Runtime: pallet_message_queue::Config, C::RuntimeEvent: TryInto::Runtime>>, { - C::events().into_iter().find_map(|event| { - if let Ok(pallet_message_queue::Event::Processed { id, .. }) = event.try_into() { - Some(id) - } else { - None - } - }) + find_all_mq_processed_ids::().first().cloned() } /// Helper method to find the message ID of the first `Event::Sent` event in the chain's events. diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/asset_transfers.rs b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/asset_transfers.rs index 27bab30a0cb9f..98ac93b8d0ba5 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/asset_transfers.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/asset_transfers.rs @@ -16,7 +16,7 @@ use crate::tests::{snowbridge_common::snowbridge_sovereign, *}; use emulated_integration_tests_common::{ macros::Dmp, - xcm_helpers::{find_mq_processed_id, find_xcm_sent_message_id}, + xcm_helpers::{find_all_mq_processed_ids, find_mq_processed_id, find_xcm_sent_message_id}, xcm_simulator::helpers::TopicIdTracker, }; use xcm::latest::AssetTransferFilter; @@ -1145,7 +1145,7 @@ fn send_back_rocs_from_penpal_westend_through_asset_hub_westend_to_asset_hub_roc ); let msg_sent_id = - find_xcm_sent_message_id::().expect("Missing Sent Event"); + find_xcm_sent_message_id::().expect("Missing Sent Event on PenpalB"); topic_id_tracker.insert("PenpalB", msg_sent_id.into()); result @@ -1171,9 +1171,9 @@ fn send_back_rocs_from_penpal_westend_through_asset_hub_westend_to_asset_hub_roc ) => {}, ] ); - let mq_prc_id = - find_mq_processed_id::().expect("Missing Processed Event"); - topic_id_tracker.insert("AssetHubWestend", mq_prc_id); + let mq_prc_ids = find_all_mq_processed_ids::(); + assert!(mq_prc_ids.len() >= 1, "Missing Processed Event on AssetHubWestend"); + topic_id_tracker.insert_all("AssetHubWestend", &mq_prc_ids); }); }); } @@ -1200,10 +1200,11 @@ fn send_back_rocs_from_penpal_westend_through_asset_hub_westend_to_asset_hub_roc ) => {}, ] ); - let mq_prc_id = find_mq_processed_id::().expect("Missing Processed Event"); - topic_id_tracker.insert("AssetHubRococo", mq_prc_id); + let mq_prc_ids = find_all_mq_processed_ids::(); + assert!(mq_prc_ids.len() >= 1, "Missing Processed Event on AssetHubRococo"); + topic_id_tracker.insert_all("AssetHubRococo", &mq_prc_ids); }); - topic_id_tracker.assert_unique(); + topic_id_tracker.assert_only_id_seen_on_all_chains("PenpalB"); let sender_rocs_after = PenpalB::execute_with(|| { type ForeignAssets = ::ForeignAssets; @@ -1482,8 +1483,9 @@ fn send_pens_and_wnds_from_penpal_westend_via_ahw_to_ahr() { let wnd = Location::new(2, [GlobalConsensus(ByGenesis(WESTEND_GENESIS_HASH))]); AssetHubRococo::execute_with(|| { type RuntimeEvent = ::RuntimeEvent; - let mq_prc_id = find_mq_processed_id::().expect("Missing Processed Event"); - topic_id_tracker.insert_and_assert_unique("AssetHubRococo", mq_prc_id); + let mq_prc_ids = find_all_mq_processed_ids::(); + assert!(mq_prc_ids.len() >= 1, "Missing Processed Event on AssetHubRococo"); + topic_id_tracker.insert_all("AssetHubRococo", &mq_prc_ids); assert_expected_events!( AssetHubRococo, vec![ @@ -1500,8 +1502,8 @@ fn send_pens_and_wnds_from_penpal_westend_via_ahw_to_ahr() { ); }); - // assert unique topic across all chains - topic_id_tracker.assert_unique(); + // assert that the only topic ID on 'PenpalB' exists on all chains + topic_id_tracker.assert_only_id_seen_on_all_chains("PenpalB"); // account balances after let sender_wnds_after = PenpalB::execute_with(|| { diff --git a/polkadot/xcm/xcm-simulator/src/lib.rs b/polkadot/xcm/xcm-simulator/src/lib.rs index d494c64daacec..17e32822f8416 100644 --- a/polkadot/xcm/xcm-simulator/src/lib.rs +++ b/polkadot/xcm/xcm-simulator/src/lib.rs @@ -468,9 +468,9 @@ pub mod helpers { } /// A test utility for tracking XCM topic IDs - #[derive(Clone)] + #[derive(Clone, Debug)] pub struct TopicIdTracker { - ids: HashMap, + ids: HashMap>, } impl TopicIdTracker { /// Initialises a new, empty topic ID tracker. @@ -478,9 +478,43 @@ pub mod helpers { TopicIdTracker { ids: HashMap::new() } } + /// Asserts that exactly one topic ID is recorded on the given chain, and that the same ID + /// is present on all other chains. + pub fn assert_only_id_seen_on_all_chains(&self, chain: &str) { + let ids = self + .ids + .get(chain) + .expect(&format!("No topic IDs recorded for chain '{}'", chain)); + + assert_eq!( + ids.len(), + 1, + "Expected exactly one topic ID for chain '{}', but found {}: {:?}", + chain, + ids.len(), + ids + ); + + let id = *ids.iter().next().unwrap(); + self.assert_id_seen_on_all_chains(&id); + } + + /// Asserts that the given topic ID has been recorded on all chains. + pub fn assert_id_seen_on_all_chains(&self, id: &H256) { + self.ids.iter().for_each(|(chain, values)| { + assert!( + values.contains(id), + "Topic ID {:?} not found on chain '{}'. Found topic IDs: {:?}", + id, + chain, + values + ) + }); + } + /// Asserts that exactly one unique topic ID is present across all captured entries. pub fn assert_unique(&self) { - let unique_ids: HashSet<_> = self.ids.values().collect(); + let unique_ids: HashSet<_> = self.ids.values().flatten().collect(); assert_eq!( unique_ids.len(), 1, @@ -492,14 +526,28 @@ pub mod helpers { /// Inserts a topic ID with the given chain name in the captor. pub fn insert(&mut self, chain: &str, id: H256) { - self.ids.insert(chain.to_string(), id); + self.ids.entry(chain.to_string()).or_default().insert(id); + } + + /// Inserts all topic IDs associated with the given chain name. + pub fn insert_all(&mut self, chain: &str, ids: &[H256]) { + ids.iter().for_each(|&id| self.insert(chain, id)); } /// Inserts a topic ID for a given chain and then asserts global uniqueness. pub fn insert_and_assert_unique(&mut self, chain: &str, id: H256) { - if let Some(existing_id) = self.ids.get(chain) { + if let Some(existing_ids) = self.ids.get(chain) { + assert_eq!( + existing_ids.len(), + 1, + "Expected exactly one topic ID for chain '{}', but found: {:?}", + chain, + existing_ids + ); + let existing_id = + *existing_ids.iter().next().expect(&format!("Topic ID for chain '{}'", chain)); assert_eq!( - id, *existing_id, + id, existing_id, "Topic ID mismatch for chain '{}': expected {:?}, got {:?}", id, existing_id, chain ); From 407797803d515d0f61fd1574f98ddd934d4a2944 Mon Sep 17 00:00:00 2001 From: "cmd[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 24 Jul 2025 13:31:44 +0000 Subject: [PATCH 2/8] Update from github-actions[bot] running command 'prdoc --audience runtime_dev --bump patch' --- prdoc/pr_9316.prdoc | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 prdoc/pr_9316.prdoc diff --git a/prdoc/pr_9316.prdoc b/prdoc/pr_9316.prdoc new file mode 100644 index 0000000000000..8fb021ad7597a --- /dev/null +++ b/prdoc/pr_9316.prdoc @@ -0,0 +1,9 @@ +title: Enable `TopicIdTracker` to support multiple flows +doc: +- audience: Runtime Dev + description: This PR enable `TopicIdTracker` to support multiple flows. +crates: +- name: emulated-integration-tests-common + bump: patch +- name: xcm-simulator + bump: patch From f6ea8f0a5d80129d656854e31207fa5de98b85e4 Mon Sep 17 00:00:00 2001 From: Raymond Cheung <178801527+raymondkfcheung@users.noreply.github.com> Date: Thu, 24 Jul 2025 14:32:25 +0100 Subject: [PATCH 3/8] Add assert_contains --- polkadot/xcm/xcm-simulator/src/lib.rs | 36 +++++++++++++++++---------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/polkadot/xcm/xcm-simulator/src/lib.rs b/polkadot/xcm/xcm-simulator/src/lib.rs index 17e32822f8416..fd6f9b26628e5 100644 --- a/polkadot/xcm/xcm-simulator/src/lib.rs +++ b/polkadot/xcm/xcm-simulator/src/lib.rs @@ -478,6 +478,29 @@ pub mod helpers { TopicIdTracker { ids: HashMap::new() } } + /// Asserts that the given topic ID has been recorded for the specified chain. + pub fn assert_contains(&self, chain: &str, id: &H256) { + let ids = self + .ids + .get(chain) + .expect(&format!("No topic IDs recorded for chain '{}'", chain)); + + assert!( + ids.contains(id), + "Expected topic ID {:?} not found for chain '{}'. Found topic IDs: {:?}", + id, + chain, + ids + ); + } + + /// Asserts that the given topic ID has been recorded on all chains. + pub fn assert_id_seen_on_all_chains(&self, id: &H256) { + self.ids.keys().for_each(|chain| { + self.assert_contains(chain, id); + }); + } + /// Asserts that exactly one topic ID is recorded on the given chain, and that the same ID /// is present on all other chains. pub fn assert_only_id_seen_on_all_chains(&self, chain: &str) { @@ -499,19 +522,6 @@ pub mod helpers { self.assert_id_seen_on_all_chains(&id); } - /// Asserts that the given topic ID has been recorded on all chains. - pub fn assert_id_seen_on_all_chains(&self, id: &H256) { - self.ids.iter().for_each(|(chain, values)| { - assert!( - values.contains(id), - "Topic ID {:?} not found on chain '{}'. Found topic IDs: {:?}", - id, - chain, - values - ) - }); - } - /// Asserts that exactly one unique topic ID is present across all captured entries. pub fn assert_unique(&self) { let unique_ids: HashSet<_> = self.ids.values().flatten().collect(); From 178e9dfa67bdb604fa01558823660f6e341d0e01 Mon Sep 17 00:00:00 2001 From: Raymond Cheung <178801527+raymondkfcheung@users.noreply.github.com> Date: Thu, 24 Jul 2025 15:27:13 +0100 Subject: [PATCH 4/8] Update PRDoc --- prdoc/pr_9316.prdoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/prdoc/pr_9316.prdoc b/prdoc/pr_9316.prdoc index 8fb021ad7597a..dcd1712fa1e84 100644 --- a/prdoc/pr_9316.prdoc +++ b/prdoc/pr_9316.prdoc @@ -1,9 +1,9 @@ title: Enable `TopicIdTracker` to support multiple flows doc: - audience: Runtime Dev - description: This PR enable `TopicIdTracker` to support multiple flows. + description: This PR enables `TopicIdTracker` to support multiple flows. crates: - name: emulated-integration-tests-common - bump: patch + bump: minor - name: xcm-simulator - bump: patch + bump: minor From 5e0fed3b53f2623548788e4ad4603bf36a5bd5ae Mon Sep 17 00:00:00 2001 From: Raymond Cheung <178801527+raymondkfcheung@users.noreply.github.com> Date: Fri, 25 Jul 2025 09:56:56 +0100 Subject: [PATCH 5/8] Add doc tests --- polkadot/xcm/xcm-simulator/src/lib.rs | 72 ++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/polkadot/xcm/xcm-simulator/src/lib.rs b/polkadot/xcm/xcm-simulator/src/lib.rs index fd6f9b26628e5..160240a6e12d9 100644 --- a/polkadot/xcm/xcm-simulator/src/lib.rs +++ b/polkadot/xcm/xcm-simulator/src/lib.rs @@ -467,7 +467,48 @@ pub mod helpers { } } - /// A test utility for tracking XCM topic IDs + /// A test utility for tracking XCM topic IDs. + /// + /// # Examples + /// + /// ``` + /// use sp_runtime::testing::H256; + /// use xcm_simulator::helpers::TopicIdTracker; + /// + /// // Dummy topic IDs + /// let topic_id = H256::repeat_byte(0x42); + /// + /// // Create a new tracker + /// let mut tracker = TopicIdTracker::new(); + /// + /// // Insert the same topic ID for three chains + /// tracker.insert("ChainA", topic_id); + /// tracker.insert_all("ChainB", &[topic_id]); + /// tracker.insert_and_assert_unique("ChainC", topic_id); + /// + /// // Assert the topic ID exists everywhere + /// tracker.assert_contains("ChainA", &topic_id); + /// tracker.assert_id_seen_on_all_chains(&topic_id); + /// tracker.assert_only_id_seen_on_all_chains("ChainB"); + /// tracker.assert_unique(); + /// + /// // You can also test that inserting inconsistent topic IDs fails: + /// let another_id = H256::repeat_byte(0x43); + /// let result = std::panic::catch_unwind(|| { + /// let mut tracker = TopicIdTracker::new(); + /// tracker.insert("ChainA", topic_id); + /// tracker.insert_and_assert_unique("ChainB", another_id); + /// }); + /// assert!(result.is_err()); + /// + /// let result = std::panic::catch_unwind(|| { + /// let mut tracker = TopicIdTracker::new(); + /// tracker.insert("ChainA", topic_id); + /// tracker.insert("ChainB", another_id); + /// tracker.assert_unique(); + /// }); + /// assert!(result.is_err()); + /// ``` #[derive(Clone, Debug)] pub struct TopicIdTracker { ids: HashMap>, @@ -567,4 +608,33 @@ pub mod helpers { self.assert_unique(); } } + + #[cfg(test)] + mod tests { + use super::*; + use sp_runtime::testing::H256; + + #[test] + #[should_panic(expected = "Expected exactly one topic ID")] + fn test_assert_unique_fails_with_multiple_ids() { + let mut tracker = TopicIdTracker::new(); + let id1 = H256::repeat_byte(0x42); + let id2 = H256::repeat_byte(0x43); + + tracker.insert("ChainA", id1); + tracker.insert("ChainB", id2); + tracker.assert_unique(); + } + + #[test] + #[should_panic(expected = "Topic ID mismatch")] + fn test_insert_and_assert_unique_mismatch() { + let mut tracker = TopicIdTracker::new(); + let id1 = H256::repeat_byte(0x42); + let id2 = H256::repeat_byte(0x43); + + tracker.insert_and_assert_unique("ChainA", id1); + tracker.insert_and_assert_unique("ChainA", id2); + } + } } From 29104b17dba27cfa02b95917441cf5e4f7152fa7 Mon Sep 17 00:00:00 2001 From: Raymond Cheung <178801527+raymondkfcheung@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:31:40 +0100 Subject: [PATCH 6/8] Use is_empty --- .../bridges/bridge-hub-westend/src/tests/asset_transfers.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/asset_transfers.rs b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/asset_transfers.rs index a8fe67f56e993..fa641e13c09fa 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/asset_transfers.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/asset_transfers.rs @@ -1172,7 +1172,7 @@ fn send_back_rocs_from_penpal_westend_through_asset_hub_westend_to_asset_hub_roc ] ); let mq_prc_ids = find_all_mq_processed_ids::(); - assert!(mq_prc_ids.len() >= 1, "Missing Processed Event on AssetHubWestend"); + assert!(!mq_prc_ids.is_empty(), "Missing Processed Event on AssetHubWestend"); topic_id_tracker.insert_all("AssetHubWestend", &mq_prc_ids); }); }); @@ -1201,7 +1201,7 @@ fn send_back_rocs_from_penpal_westend_through_asset_hub_westend_to_asset_hub_roc ] ); let mq_prc_ids = find_all_mq_processed_ids::(); - assert!(mq_prc_ids.len() >= 1, "Missing Processed Event on AssetHubRococo"); + assert!(!mq_prc_ids.is_empty(), "Missing Processed Event on AssetHubRococo"); topic_id_tracker.insert_all("AssetHubRococo", &mq_prc_ids); }); topic_id_tracker.assert_only_id_seen_on_all_chains("PenpalB"); @@ -1484,7 +1484,7 @@ fn send_pens_and_wnds_from_penpal_westend_via_ahw_to_ahr() { AssetHubRococo::execute_with(|| { type RuntimeEvent = ::RuntimeEvent; let mq_prc_ids = find_all_mq_processed_ids::(); - assert!(mq_prc_ids.len() >= 1, "Missing Processed Event on AssetHubRococo"); + assert!(!mq_prc_ids.is_empty(), "Missing Processed Event on AssetHubRococo"); topic_id_tracker.insert_all("AssetHubRococo", &mq_prc_ids); assert_expected_events!( AssetHubRococo, From 9e60b72d6674e54654340d79e71667ee496a7a00 Mon Sep 17 00:00:00 2001 From: Raymond Cheung <178801527+raymondkfcheung@users.noreply.github.com> Date: Wed, 20 Aug 2025 08:45:59 +0100 Subject: [PATCH 7/8] Use find_map --- .../emulated/common/src/xcm_helpers.rs | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/cumulus/parachains/integration-tests/emulated/common/src/xcm_helpers.rs b/cumulus/parachains/integration-tests/emulated/common/src/xcm_helpers.rs index 477fc8ef0b0ff..1b37a85525d5c 100644 --- a/cumulus/parachains/integration-tests/emulated/common/src/xcm_helpers.rs +++ b/cumulus/parachains/integration-tests/emulated/common/src/xcm_helpers.rs @@ -13,6 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::{iter::FilterMap, vec::IntoIter}; // Cumulus use parachains_common::AccountId; @@ -92,22 +93,25 @@ pub fn get_amount_from_versioned_assets(assets: VersionedAssets) -> u128 { amount } +fn to_mq_processed_id(event: C::RuntimeEvent) -> Option +where + ::Runtime: pallet_message_queue::Config, + C::RuntimeEvent: TryInto::Runtime>>, +{ + if let Ok(pallet_message_queue::Event::Processed { id, .. }) = event.try_into() { + Some(id) + } else { + None + } +} + /// Helper method to find all `Event::Processed` IDs from the chain's events. pub fn find_all_mq_processed_ids() -> Vec where ::Runtime: pallet_message_queue::Config, C::RuntimeEvent: TryInto::Runtime>>, { - C::events() - .into_iter() - .filter_map(|event| { - if let Ok(pallet_message_queue::Event::Processed { id, .. }) = event.try_into() { - Some(id) - } else { - None - } - }) - .collect() + C::events().into_iter().filter_map(to_mq_processed_id::).collect() } /// Helper method to find the ID of the first `Event::Processed` event in the chain's events. @@ -116,7 +120,7 @@ where ::Runtime: pallet_message_queue::Config, C::RuntimeEvent: TryInto::Runtime>>, { - find_all_mq_processed_ids::().first().cloned() + C::events().into_iter().find_map(to_mq_processed_id::) } /// Helper method to find the message ID of the first `Event::Sent` event in the chain's events. From c089fec31d4e9ad876f6409fdde9099d49a78fdc Mon Sep 17 00:00:00 2001 From: Raymond Cheung <178801527+raymondkfcheung@users.noreply.github.com> Date: Wed, 20 Aug 2025 08:47:52 +0100 Subject: [PATCH 8/8] Fix imports --- .../integration-tests/emulated/common/src/xcm_helpers.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/cumulus/parachains/integration-tests/emulated/common/src/xcm_helpers.rs b/cumulus/parachains/integration-tests/emulated/common/src/xcm_helpers.rs index 1b37a85525d5c..7a355337f11d1 100644 --- a/cumulus/parachains/integration-tests/emulated/common/src/xcm_helpers.rs +++ b/cumulus/parachains/integration-tests/emulated/common/src/xcm_helpers.rs @@ -13,7 +13,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{iter::FilterMap, vec::IntoIter}; // Cumulus use parachains_common::AccountId;