Skip to content

Commit e097355

Browse files
Add forwardIdFor
1 parent 8912721 commit e097355

File tree

3 files changed

+161
-48
lines changed

3 files changed

+161
-48
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import {blake2AsU8a} from "@polkadot/util-crypto";
2+
import {hexToU8a, stringToU8a, u8aConcat, u8aToHex} from "@polkadot/util";
3+
4+
function forwardIdFor(originalMessageId: string): string {
5+
// Decode the hex original_id into bytes
6+
const messageIdBytes = hexToU8a(originalMessageId);
7+
8+
// Create prefixed input: b"forward_id_for" + original_id
9+
const prefix = stringToU8a("forward_id_for");
10+
const input = u8aConcat(prefix, messageIdBytes);
11+
12+
// Hash it using blake2_256
13+
const forwardedIdBytes = blake2AsU8a(input);
14+
15+
// Convert to hex
16+
return u8aToHex(forwardedIdBytes);
17+
}
18+
19+
// Example: Forwarded ID from a original_id
20+
const originalMessageId = "0x5c082b4750ee8c34986eb22ce6e345bad2360f3682cda3e99de94b0d9970cb3e";
21+
22+
// Create the forwarded ID
23+
const forwardedIdHex = forwardIdFor(originalMessageId);
24+
25+
console.log("🔄 Forwarded ID:", forwardedIdHex);
26+
27+
const expectedForwardedId = "0xb3ae32fd2e2f798e8215865a8950d19df8330843608d4ee44e9f86849029724a";
28+
if (forwardedIdHex === expectedForwardedId) {
29+
console.log("✅ Forwarded ID matches expected value.");
30+
} else {
31+
console.error("❌ Forwarded ID does not match expected value.");
32+
}

llms.txt

Lines changed: 80 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27852,26 +27852,37 @@ If you haven't replayed or dry-run XCMs before, see the [Replay and Dry Run XCMs
2785227852

2785327853
## Understanding the Basics
2785427854

27855-
When sending XCMs using `limited_reserve_transfer_assets` or other extrinsics from the `PolkadotXcm` pallet, two key observability features enable you to trace and correlate messages across chains:
27855+
When sending XCMs using `limited_reserve_transfer_assets` or other extrinsics from the `PolkadotXcm` pallet, two key observability features enable developers to trace and correlate messages across chains:
2785627856

27857-
- [`SetTopic([u8; 32])`](https://github.com/polkadot-fellows/xcm-format?#settopic)
27858-
An XCM instruction that sets the Topic Register. This 32-byte array becomes the `message_id`, which is recorded in both the `PolkadotXcm.Sent` and `MessageQueue.Processed` events. It allows logical grouping or filtering of related messages across multiple hops.
27857+
- [`SetTopic([u8; 32])`](https://github.com/polkadot-fellows/xcm-format#settopic){target=\_blank}: An XCM instruction that sets the **Topic Register**. This 32-byte value becomes the `message_id`, which appears in both the `PolkadotXcm.Sent` and `MessageQueue.Processed` events. It allows logical grouping and filtering of related messages across one or more hops.
2785927858

27860-
> ⚠️ **Note**: The topic is **not guaranteed to be unique**. If uniqueness is required (for example, for deduplication or traceability), it must be enforced by the message creator.
27859+
> ⚠️ **Note**: The topic is **not guaranteed to be unique**. If uniqueness is required (e.g. for deduplication or message tracking), it must be enforced by the message creator.
2786127860

27862-
- **`message_id`**
27863-
A hash emitted in both the [`PolkadotXcm.Sent`](https://paritytech.github.io/polkadot-sdk/master/pallet_xcm/pallet/enum.Event.html#variant.Sent){target=\_blank} event on the origin chain and the [`MessageQueue.Processed`](https://paritytech.github.io/polkadot-sdk/master/pallet_message_queue/pallet/enum.Event.html#variant.Processed){target=\_blank} event on the destination chain. While this identifier is not globally unique, it is sufficient to match a `Sent` message with its corresponding `Processed` result.
27861+
- `message_id`: A hash emitted in both the [`PolkadotXcm.Sent`](https://paritytech.github.io/polkadot-sdk/master/pallet_xcm/pallet/enum.Event.html#variant.Sent){target=\_blank} event (on the origin chain) and the [`MessageQueue.Processed`](https://paritytech.github.io/polkadot-sdk/master/pallet_message_queue/pallet/enum.Event.html#variant.Processed){target=\_blank} event (on the destination chain). While not globally unique, it is sufficient to match a `Sent` message with its corresponding `Processed` result.
2786427862

27865-
These observability features are available in runtimes built from **`stable2503-5` or later**.
27863+
These features are available in runtimes built from **`stable2503-5` or later**.
27864+
27865+
### Key Behaviours
27866+
27867+
- The runtime **automatically appends** a `SetTopic` instruction at the end of the XCM, if it is not already included. See: [`WithUniqueTopic`](https://paritytech.github.io/polkadot-sdk/master/staging_xcm_builder/struct.WithUniqueTopic.html){target=\_blank}
27868+
27869+
- When using high-level extrinsics such as `limited_reserve_transfer_assets`, you do **not** need to include `SetTopic` manually. The runtime will insert it for you.
27870+
27871+
- If you are constructing an XCM manually using the `execute` call, you **can provide your own** `SetTopic([u8; 32])`:
27872+
27873+
- If a topic is already set, the runtime will **not override** it.
27874+
- `SetTopic` **must be the final instruction** in the list. Placing it elsewhere will result in it being ignored during execution.
27875+
27876+
- On newer runtimes, the same topic is preserved throughout a multi-hop transfer. This ensures consistent correlation of the `message_id` between origin and destination, even across multiple chains.
2786627877

2786727878
## Define a Scenario: DOT to Acala Transfer
2786827879

2786927880
We will examine the full lifecycle of a cross-chain message from Polkadot Asset Hub to Acala, using the `limited_reserve_transfer_assets` extrinsic.
2787027881

27871-
* **Origin chain**: Polkadot Asset Hub
27872-
* **Destination chain**: Acala
27873-
* **Extrinsic**: `limited_reserve_transfer_assets`
27874-
* **Goal**: Transfer DOT and trace the XCM using its emitted `message_id`
27882+
- **Origin chain**: Polkadot Asset Hub
27883+
- **Destination chain**: Acala
27884+
- **Extrinsic**: `limited_reserve_transfer_assets`
27885+
- **Goal**: Transfer DOT and trace the XCM using its emitted `message_id`
2787527886

2787627887
This scenario demonstrates how a `SetTopic` is included or generated, how to identify matching `Sent` and `Processed` events, and how to interpret failures using runtime logs.
2787727888

@@ -28157,7 +28168,7 @@ The runtime automatically inserts a `SetTopic` instruction (if not manually prov
2815728168
| Origin (e.g. Asset Hub) | `PolkadotXcm.Sent` | `message_id` | Message ID from `SetTopic`. Appended automatically if missing. |
2815828169
| Destination (e.g. Acala, Hydration) | `MessageQueue.Processed` | `id` | Matches `message_id` from the origin chain, enabling reliable correlation. |
2815928170

28160-
**These two fields now match** on new runtimes (`stable2503-5` or later).
28171+
These two fields now match on new runtimes (`stable2503-5` or later).
2816128172

2816228173
> ⚠️ Do not rely on [`XcmpQueue.XcmpMessageSent`](https://paritytech.github.io/polkadot-sdk/master/cumulus_pallet_xcmp_queue/pallet/enum.Event.html#variant.XcmpMessageSent){target=\_blank}. Its `message_hash` is not derived from `SetTopic` and is not suitable for cross-chain tracking.
2816328174

@@ -28207,15 +28218,15 @@ This output is available on runtimes from **`stable2506` or later**, and is ofte
2820728218

2820828219
For deeper analysis:
2820928220

28210-
* Use Chopsticks to **replay the message with logging enabled**
28211-
* See exactly **which instruction failed**, and why
28212-
* View full error chains like `FailedToTransactAsset`, or `AssetNotFound`
28221+
- Use Chopsticks to **replay the message with logging enabled**
28222+
- See exactly **which instruction failed**, and why
28223+
- View full error chains like `FailedToTransactAsset`, or `AssetNotFound`
2821328224

2821428225
This is especially useful when dealing with:
2821528226

28216-
* Multi-hop XCMs
28217-
* Custom asset locations
28218-
* Execution mismatches or weight issues
28227+
- Multi-hop XCMs
28228+
- Custom asset locations
28229+
- Execution mismatches or weight issues
2821928230

2822028231
→ For replay setup, see [Replay and Dry Run XCMs Using Chopsticks](/tutorials/interoperability/replay-and-dry-run-xcms/){target=\_blank}.
2822128232

@@ -28226,17 +28237,62 @@ This is especially useful when dealing with:
2822628237
3. **Inspect logs** to pinpoint the failing instruction and error.
2822728238
4. Adjust asset location, weight, or execution logic accordingly.
2822828239

28229-
## Step 5: Handle Older Runtimes
28240+
## Workaround for Older Runtimes
28241+
28242+
* On **older runtimes** (prior to `stable2503-5`), the `message_id` seen in downstream `Processed` events is **not the original topic hash**, but rather a **derived `forwarded_id`**.
28243+
* This `forwarded_id` is computed as:
28244+
28245+
```rust
28246+
use sp_core::H256;
28247+
use std::str::FromStr;
28248+
28249+
fn forward_id_for(original_id: &XcmHash) -> XcmHash {
28250+
(b"forward_id_for", original_id).using_encoded(sp_io::hashing::blake2_256)
28251+
}
28252+
```
2823028253

28231-
Older runtimes use a **derived `forwarded_id`** in `Processed` events instead of the original topic hash.
28254+
To reliably trace messages across **mixed-version chains**, indexers and tools should **check for both `original_id` and its forwarded form**.
2823228255

2823328256
```ts
28234-
// Calculate forwarded ID for legacy chains
28235-
const input = u8aConcat(stringToU8a("forward_id_for"), messageIdBytes);
28236-
const forwardedId = blake2AsU8a(input);
28257+
import {blake2AsU8a} from "@polkadot/util-crypto";
28258+
import {hexToU8a, stringToU8a, u8aConcat, u8aToHex} from "@polkadot/util";
28259+
28260+
function forwardIdFor(originalMessageId: string): string {
28261+
// Decode the hex original_id into bytes
28262+
const messageIdBytes = hexToU8a(originalMessageId);
28263+
28264+
// Create prefixed input: b"forward_id_for" + original_id
28265+
const prefix = stringToU8a("forward_id_for");
28266+
const input = u8aConcat(prefix, messageIdBytes);
28267+
28268+
// Hash it using blake2_256
28269+
const forwardedIdBytes = blake2AsU8a(input);
28270+
28271+
// Convert to hex
28272+
return u8aToHex(forwardedIdBytes);
28273+
}
28274+
28275+
// Example: Forwarded ID from a original_id
28276+
const originalMessageId = "0x5c082b4750ee8c34986eb22ce6e345bad2360f3682cda3e99de94b0d9970cb3e";
28277+
28278+
// Create the forwarded ID
28279+
const forwardedIdHex = forwardIdFor(originalMessageId);
28280+
28281+
console.log("🔄 Forwarded ID:", forwardedIdHex);
28282+
28283+
const expectedForwardedId = "0xb3ae32fd2e2f798e8215865a8950d19df8330843608d4ee44e9f86849029724a";
28284+
if (forwardedIdHex === expectedForwardedId) {
28285+
console.log("✅ Forwarded ID matches expected value.");
28286+
} else {
28287+
console.error("❌ Forwarded ID does not match expected value.");
28288+
}
2823728289
```
2823828290

28239-
→ Tools must check both the `original_id` and `forwarded_id` when indexing older chains.
28291+
* ✅ **New runtimes**:
28292+
`message_id == original_id`
28293+
28294+
* ⚠️ **Old runtimes**:
28295+
`message_id == blake2_256("forward_id_for" + original_id)`
2824028296

2824128297
## Additional Samples
2824228298

tutorials/interoperability/xcm-observability.md

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -32,26 +32,37 @@ If you haven't replayed or dry-run XCMs before, see the [Replay and Dry Run XCMs
3232

3333
## Understanding the Basics
3434

35-
When sending XCMs using `limited_reserve_transfer_assets` or other extrinsics from the `PolkadotXcm` pallet, two key observability features enable you to trace and correlate messages across chains:
35+
When sending XCMs using `limited_reserve_transfer_assets` or other extrinsics from the `PolkadotXcm` pallet, two key observability features enable developers to trace and correlate messages across chains:
3636

37-
- [`SetTopic([u8; 32])`](https://github.com/polkadot-fellows/xcm-format?#settopic)
38-
An XCM instruction that sets the Topic Register. This 32-byte array becomes the `message_id`, which is recorded in both the `PolkadotXcm.Sent` and `MessageQueue.Processed` events. It allows logical grouping or filtering of related messages across multiple hops.
37+
- [`SetTopic([u8; 32])`](https://github.com/polkadot-fellows/xcm-format#settopic){target=\_blank}: An XCM instruction that sets the **Topic Register**. This 32-byte value becomes the `message_id`, which appears in both the `PolkadotXcm.Sent` and `MessageQueue.Processed` events. It allows logical grouping and filtering of related messages across one or more hops.
3938

40-
> ⚠️ **Note**: The topic is **not guaranteed to be unique**. If uniqueness is required (for example, for deduplication or traceability), it must be enforced by the message creator.
39+
> ⚠️ **Note**: The topic is **not guaranteed to be unique**. If uniqueness is required (e.g. for deduplication or message tracking), it must be enforced by the message creator.
4140
42-
- **`message_id`**
43-
A hash emitted in both the [`PolkadotXcm.Sent`](https://paritytech.github.io/polkadot-sdk/master/pallet_xcm/pallet/enum.Event.html#variant.Sent){target=\_blank} event on the origin chain and the [`MessageQueue.Processed`](https://paritytech.github.io/polkadot-sdk/master/pallet_message_queue/pallet/enum.Event.html#variant.Processed){target=\_blank} event on the destination chain. While this identifier is not globally unique, it is sufficient to match a `Sent` message with its corresponding `Processed` result.
41+
- `message_id`: A hash emitted in both the [`PolkadotXcm.Sent`](https://paritytech.github.io/polkadot-sdk/master/pallet_xcm/pallet/enum.Event.html#variant.Sent){target=\_blank} event (on the origin chain) and the [`MessageQueue.Processed`](https://paritytech.github.io/polkadot-sdk/master/pallet_message_queue/pallet/enum.Event.html#variant.Processed){target=\_blank} event (on the destination chain). While not globally unique, it is sufficient to match a `Sent` message with its corresponding `Processed` result.
4442

45-
These observability features are available in runtimes built from **`stable2503-5` or later**.
43+
These features are available in runtimes built from **`stable2503-5` or later**.
44+
45+
### Key Behaviours
46+
47+
- The runtime **automatically appends** a `SetTopic` instruction at the end of the XCM, if it is not already included. See: [`WithUniqueTopic`](https://paritytech.github.io/polkadot-sdk/master/staging_xcm_builder/struct.WithUniqueTopic.html){target=\_blank}
48+
49+
- When using high-level extrinsics such as `limited_reserve_transfer_assets`, you do **not** need to include `SetTopic` manually. The runtime will insert it for you.
50+
51+
- If you are constructing an XCM manually using the `execute` call, you **can provide your own** `SetTopic([u8; 32])`:
52+
53+
- If a topic is already set, the runtime will **not override** it.
54+
- `SetTopic` **must be the final instruction** in the list. Placing it elsewhere will result in it being ignored during execution.
55+
56+
- On newer runtimes, the same topic is preserved throughout a multi-hop transfer. This ensures consistent correlation of the `message_id` between origin and destination, even across multiple chains.
4657

4758
## Define a Scenario: DOT to Acala Transfer
4859

4960
We will examine the full lifecycle of a cross-chain message from Polkadot Asset Hub to Acala, using the `limited_reserve_transfer_assets` extrinsic.
5061

51-
* **Origin chain**: Polkadot Asset Hub
52-
* **Destination chain**: Acala
53-
* **Extrinsic**: `limited_reserve_transfer_assets`
54-
* **Goal**: Transfer DOT and trace the XCM using its emitted `message_id`
62+
- **Origin chain**: Polkadot Asset Hub
63+
- **Destination chain**: Acala
64+
- **Extrinsic**: `limited_reserve_transfer_assets`
65+
- **Goal**: Transfer DOT and trace the XCM using its emitted `message_id`
5566

5667
This scenario demonstrates how a `SetTopic` is included or generated, how to identify matching `Sent` and `Processed` events, and how to interpret failures using runtime logs.
5768

@@ -119,7 +130,7 @@ The runtime automatically inserts a `SetTopic` instruction (if not manually prov
119130
| Origin (e.g. Asset Hub) | `PolkadotXcm.Sent` | `message_id` | Message ID from `SetTopic`. Appended automatically if missing. |
120131
| Destination (e.g. Acala, Hydration) | `MessageQueue.Processed` | `id` | Matches `message_id` from the origin chain, enabling reliable correlation. |
121132

122-
**These two fields now match** on new runtimes (`stable2503-5` or later).
133+
These two fields now match on new runtimes (`stable2503-5` or later).
123134

124135
> ⚠️ Do not rely on [`XcmpQueue.XcmpMessageSent`](https://paritytech.github.io/polkadot-sdk/master/cumulus_pallet_xcmp_queue/pallet/enum.Event.html#variant.XcmpMessageSent){target=\_blank}. Its `message_hash` is not derived from `SetTopic` and is not suitable for cross-chain tracking.
125136
@@ -143,15 +154,15 @@ This output is available on runtimes from **`stable2506` or later**, and is ofte
143154

144155
For deeper analysis:
145156

146-
* Use Chopsticks to **replay the message with logging enabled**
147-
* See exactly **which instruction failed**, and why
148-
* View full error chains like `FailedToTransactAsset`, or `AssetNotFound`
157+
- Use Chopsticks to **replay the message with logging enabled**
158+
- See exactly **which instruction failed**, and why
159+
- View full error chains like `FailedToTransactAsset`, or `AssetNotFound`
149160

150161
This is especially useful when dealing with:
151162

152-
* Multi-hop XCMs
153-
* Custom asset locations
154-
* Execution mismatches or weight issues
163+
- Multi-hop XCMs
164+
- Custom asset locations
165+
- Execution mismatches or weight issues
155166

156167
→ For replay setup, see [Replay and Dry Run XCMs Using Chopsticks](/tutorials/interoperability/replay-and-dry-run-xcms/){target=\_blank}.
157168

@@ -162,17 +173,31 @@ This is especially useful when dealing with:
162173
3. **Inspect logs** to pinpoint the failing instruction and error.
163174
4. Adjust asset location, weight, or execution logic accordingly.
164175

165-
## Step 5: Handle Older Runtimes
176+
## Workaround for Older Runtimes
166177

167-
Older runtimes use a **derived `forwarded_id`** in `Processed` events instead of the original topic hash.
178+
* On **older runtimes** (prior to `stable2503-5`), the `message_id` seen in downstream `Processed` events is **not the original topic hash**, but rather a **derived `forwarded_id`**.
179+
* This `forwarded_id` is computed as:
180+
181+
```rust
182+
use sp_core::H256;
183+
use std::str::FromStr;
184+
185+
fn forward_id_for(original_id: &XcmHash) -> XcmHash {
186+
(b"forward_id_for", original_id).using_encoded(sp_io::hashing::blake2_256)
187+
}
188+
```
189+
190+
To reliably trace messages across **mixed-version chains**, indexers and tools should **check for both `original_id` and its forwarded form**.
168191

169192
```ts
170-
// Calculate forwarded ID for legacy chains
171-
const input = u8aConcat(stringToU8a("forward_id_for"), messageIdBytes);
172-
const forwardedId = blake2AsU8a(input);
193+
--8<-- 'code/tutorials/interoperability/xcm-observability/forward-id-for.ts'
173194
```
174195

175-
→ Tools must check both the `original_id` and `forwarded_id` when indexing older chains.
196+
***New runtimes**:
197+
`message_id == original_id`
198+
199+
* ⚠️ **Old runtimes**:
200+
`message_id == blake2_256("forward_id_for" + original_id)`
176201

177202
## Additional Samples
178203

0 commit comments

Comments
 (0)