Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -92,19 +92,34 @@ pub fn get_amount_from_versioned_assets(assets: VersionedAssets) -> u128 {
amount
}

fn to_mq_processed_id<C: Chain>(event: C::RuntimeEvent) -> Option<H256>
where
<C as Chain>::Runtime: pallet_message_queue::Config,
C::RuntimeEvent: TryInto<pallet_message_queue::Event<<C as Chain>::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<C: Chain>() -> Vec<H256>
where
<C as Chain>::Runtime: pallet_message_queue::Config,
C::RuntimeEvent: TryInto<pallet_message_queue::Event<<C as Chain>::Runtime>>,
{
C::events().into_iter().filter_map(to_mq_processed_id::<C>).collect()
}

/// Helper method to find the ID of the first `Event::Processed` event in the chain's events.
pub fn find_mq_processed_id<C: Chain>() -> Option<H256>
where
<C as Chain>::Runtime: pallet_message_queue::Config,
C::RuntimeEvent: TryInto<pallet_message_queue::Event<<C as Chain>::Runtime>>,
{
C::events().into_iter().find_map(|event| {
if let Ok(pallet_message_queue::Event::Processed { id, .. }) = event.try_into() {
Some(id)
} else {
None
}
})
C::events().into_iter().find_map(to_mq_processed_id::<C>)
}

/// Helper method to find the message ID of the first `Event::Sent` event in the chain's events.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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::<PenpalB>().expect("Missing Sent Event");
find_xcm_sent_message_id::<PenpalB>().expect("Missing Sent Event on PenpalB");
topic_id_tracker.insert("PenpalB", msg_sent_id.into());

result
Expand All @@ -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::<AssetHubWestend>().expect("Missing Processed Event");
topic_id_tracker.insert("AssetHubWestend", mq_prc_id);
let mq_prc_ids = find_all_mq_processed_ids::<AssetHubWestend>();
assert!(!mq_prc_ids.is_empty(), "Missing Processed Event on AssetHubWestend");
topic_id_tracker.insert_all("AssetHubWestend", &mq_prc_ids);
});
});
}
Expand All @@ -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::<AssetHubRococo>().expect("Missing Processed Event");
topic_id_tracker.insert("AssetHubRococo", mq_prc_id);
let mq_prc_ids = find_all_mq_processed_ids::<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_unique();
topic_id_tracker.assert_only_id_seen_on_all_chains("PenpalB");

let sender_rocs_after = PenpalB::execute_with(|| {
type ForeignAssets = <PenpalB as PenpalBPallet>::ForeignAssets;
Expand Down Expand Up @@ -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 = <AssetHubRococo as Chain>::RuntimeEvent;
let mq_prc_id = find_mq_processed_id::<AssetHubRococo>().expect("Missing Processed Event");
topic_id_tracker.insert_and_assert_unique("AssetHubRococo", mq_prc_id);
let mq_prc_ids = find_all_mq_processed_ids::<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,
vec![
Expand All @@ -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(|| {
Expand Down
142 changes: 135 additions & 7 deletions polkadot/xcm/xcm-simulator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -467,20 +467,105 @@ pub mod helpers {
}
}

/// A test utility for tracking XCM topic IDs
#[derive(Clone)]
/// 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<String, H256>,
ids: HashMap<String, HashSet<H256>>,
}
impl TopicIdTracker {
/// Initialises a new, empty topic ID tracker.
pub fn new() -> Self {
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) {
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 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,
Expand All @@ -492,14 +577,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!(
id, *existing_id,
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,
"Topic ID mismatch for chain '{}': expected {:?}, got {:?}",
id, existing_id, chain
);
Expand All @@ -509,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);
}
}
}
9 changes: 9 additions & 0 deletions prdoc/pr_9316.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
title: Enable `TopicIdTracker` to support multiple flows
doc:
- audience: Runtime Dev
description: This PR enables `TopicIdTracker` to support multiple flows.
crates:
- name: emulated-integration-tests-common
bump: minor
- name: xcm-simulator
bump: minor
Loading