Polkadot HRMP protocol (implementation guide)


Implementation-oriented notes from the Polkadot SDK: relay pallet polkadot/runtime/parachains/src/hrmp.rs, inclusion wiring, and Cumulus parachain-system. See also validate_block and PVF.

What HRMP is

HRMP (Horizontally Relay-routed Message Passing) lets parachains send bytes to each other using the same conceptual model as XCMP (channels, queues), but full message payloads live in relay-chain storage. That keeps semantics simpler and makes relay resource use higher. It is described as an interim mechanism until XCMP is fully available.

From the implementers guide (polkadot/roadmap/implementers-guide/src/messaging.md):

Data model (relay chain)

Main implementation: polkadot/runtime/parachains/src/hrmp.rs

Important storage:

Storage Role
HrmpChannels Per-channel metadata + MQC head
HrmpChannelContents Queued InboundHrmpMessage per channel
HrmpChannelDigests Per recipient: relay block numbers where inbound HRMP arrived (watermark rules)
HrmpWatermarks Last relay block the recipient committed to having processed up to
HrmpOpenChannelRequests* / HrmpCloseChannelRequests* Handshake + teardown
HrmpIngressChannelsIndex / HrmpEgressChannelsIndex Neighbor index per para

Message types (from types/messages.md):

Channel lifecycle

  1. Open: sender hrmp_init_open_channel, recipient hrmp_accept_open_channel (parachain origin).
  2. Activate / close on session change: initializer_on_new_session runs process_hrmp_open_channel_requests, process_hrmp_close_channel_requests, and cleanup for outgoing paras (polkadot/runtime/parachains/src/initializer.rs → HRMP).

Per-block flow: candidate → relay state

In inclusion::enact_candidate (polkadot/runtime/parachains/src/inclusion/mod.rs):

  1. prune_hrmp(recipient_para, hrmp_watermark) — recipient candidate says it processed inbound HRMP up to that relay block; relay drops stored messages with sent_at ≤ watermark and updates digests / watermark.
  2. queue_outbound_hrmp(sender_para, horizontal_messages) — appends InboundHrmpMessage { sent_at: current_block, data } to HrmpChannelContents, updates channel counts, MQC head, and recipient digest for that block.

Acceptance checks (invalid candidates fail before enact)

check_validation_outputs calls:

Parachain side (Cumulus)

cumulus/pallets/parachain-system: enqueue_inbound_horizontal_messages

Relay inclusion: both roles per included para

For each enacted candidate, the relay runs both:

So inclusion is not “sender-only” or “receiver-only”; it applies both effects for the same para_id in one inclusion.

Sending OUT → horizontal_messages → relay storage

How outbound bytes become CandidateCommitments.horizontal_messages and then HRMP queues on the relay.

1. Source of payloads on the parachain

Outbound HRMP is opaque bytes to the relay. In typical runtimes they come from XCMP / XCM egress buffered in pallet-xcmp-queue, wired as type OutboundXcmpMessageSource = XcmpQueue.

Trait (cumulus/primitives/core/src/lib.rs): XcmpMessageSource::take_outbound_messages(maximum_channels) -> Vec<(ParaId, Vec<u8>)> — up to N messages as (recipient para, payload).

XcmpQueue::take_outbound_messages drains OutboundXcmpMessages per recipient and respects ChannelInfo (channel closed / full / ready).

2. parachain-system on_finalize: XCMP queue → HrmpOutboundMessages

At on_finalize, after the block body has executed (XCM may have enqueued outbound messages):

Constraints enforced in code comments: channel must exist; at most one message per channel in that send batch; strictly ascending recipient ParaId; capacity / size limits via RelevantMessagingState / GetChannelInfo.

collect_collation_info (runtime API used when building collations) exposes the same horizontal_messages: HrmpOutboundMessages::get() plus hrmp_watermark, UMP, DMP counts, etc.

3. validate_block: storage → ValidationResult.horizontal_messages

Validators re-execute the block; they do not trust the collator’s commitments blindly.

After execute_verified_block, validate_block/implementation.rs reads parachain-system storage and builds ValidationResult, including:

That result is what becomes CandidateCommitments (hashed with the rest). Backing validators run the same PVF / validate_block; mismatched horizontal_messages ⇒ invalid candidate.

The reads run under run_with_externalities_and_recorder: production PVF code sets Substrate externalities over the PoV-backed trie + overlay so pallet storage reads see the state after block execution — not test-only.

4. Relay inclusion

One-liner

App logic enqueues XCMP egress → on_finalize drains into HrmpOutboundMessagesvalidate_block copies that into commitments’ horizontal_messages → relay checks and queue_outbound_hrmp writes relay HRMP storage for recipients.

Deeper follow-up: cumulus/pallets/xcmp-queue/src/lib.rstake_outbound_messages (signals vs data pages, closed channels swallowing pages).

End-to-end sequence

sequenceDiagram
	participant A as Para A (sender)
	participant RC as Relay chain HRMP pallet
	participant B as Para B (recipient)

	A->>RC: Candidate: horizontal_messages to B
	RC->>RC: check_outbound_hrmp
	RC->>RC: queue_outbound_hrmp: append messages, update mqc_head + digests
	B->>RC: Candidate: hrmp_watermark
	RC->>RC: check_hrmp_watermark
	RC->>RC: prune_hrmp
	B->>B: parachain-system: apply messages, verify MQC vs relay proof

Files to read

Topic Path
Relay pallet polkadot/runtime/parachains/src/hrmp.rs
Inclusion polkadot/runtime/parachains/src/inclusion/mod.rs
Session hooks polkadot/runtime/parachains/src/initializer.rs
Para ingest + outbound finalize cumulus/pallets/parachain-system/src/lib.rs (on_finalize, collect_collation_info)
validate_block / commitments cumulus/pallets/parachain-system/src/validate_block/implementation.rs
XCMP egress queue cumulus/pallets/xcmp-queue/src/lib.rs (take_outbound_messages)
XcmpMessageSource trait cumulus/primitives/core/src/lib.rs
Concepts polkadot/roadmap/implementers-guide/src/messaging.md, types/messages.md
register_validate_block! macro cumulus/pallets/parachain-system/proc-macro/src/lib.rs

validate_block / PVF — relay validators vs relay runtime

cumulus/pallets/parachain-system/src/validate_block/implementation.rs is the main body of parachain block validation for Cumulus: build a trie from the PoV storage proof, replace storage host calls so execution stays inside the validation artifact, execute the block, return ValidationResult. run_with_externalities_and_recorder is not test-only — it is how executed state is read back after execute_verified_block.

Relationship to the PVF

“Run on validators” — precise wording

Correct Misleading
Relay validators (Polkadot node, PVF / candidate-validation worker) execute this Wasm in an isolated VM (e.g. Wasmtime; PolkaVM where applicable). It runs as on-chain relay-chain logic (relay pallets executing inside a relay block).

So: same network and security model as the relay chain, but PVF execution is off-chain validation work on the node, not a relay extrinsic.

One-liner: implementation.rs is the validate-block logic inside the PVF; validators run it when checking parachain candidates; it is not part of relay-chain state transitions, is part of parachain candidate validation.