Cross-Domain Messaging (XDM)
This document describes the current messaging protocol between domains in a trusted code environment (permissioned runtime instantiation). This protocol describes messaging between the consensus chain and any domain and between two domains.
Primitives
Chain
A chain is a blockchain within the Subspace Network. A chain is identified as the Consensus chain or a Domain with a DomainID
pub enum ChainId {
Consensus,
Domain(DomainId),
}
Domain
A Domain is a blockchain with some application modules. These applications act as senders and receivers of the messages using messaging protocol. A unique identifier identifies each application. Domain operators execute transactions bundled by other operators of the same domain when a new block is available.
Trusted third party
A trusted third party from the point of view of the domains is the consensus chain, the farmer network for block production. Domains use consensus chain to submit transaction bundles, verify the Message proofs of domain_a
on domain_b
and submit fraud proofs.
Channel
A Channel is a bi-directional connection between two domains. Channel connection is established when the src_chain_id
initiates the channel connection open message, and dst_chain_id
responds with either approval or rejection. Once a connection is open, sending messages back and forth is possible. A Channel would be open until a maximum number of messages are sent. This could be configured or defaulted to the maximum possible value of the Nonce
type.
There is a deposit to open a channel between domains. Deposit should be high enough to discourage and make it economically inefficient to DDOS channel initiation connections between domains.
A channel can be closed on either end by the root user. Once closed, the channel will stop sending and receiving any further messages. The Relayer will communicate to the other domain to close the channel and clean up.
A channel can be in one of the following states (State
):
Initiated
: When a channel is initiated but has not received acknowledgment from the other domain.Open
: When a bi-directional channel is open to relay messages between domains.Closed
: When the channel is closed between the domains and stops receiving and sending new messages to the other domain.
A Channel is defined as follows
type Channel {
// Unique channel identifier within DomainID namespace
channel_id: ChannelID
// State of the channel
state: State
// Next valid Inbox nonce
next_inbox_nonce: Nonce
// Next valid Outbox nonce
next_outbox_nonce: Nonce
// Latest outbox message nonce for which response was received from dst_chain.
latest_response_received_message_nonce: Nonce
// Fee Model for this channel
fee: FeeModel
// Max number of messages to be in outbox at a given time on both domains.
max_outgoing_messages: u32
/// Owner of the channel
/// Owner maybe None if the channel was initiated on the other chain.
maybe_owner: Option<AccountId>,
}
Channel Inbox
All the incoming messages to the domain are validated and added to a pool before processing. If specific message arrived earlier than a previous message, it is stored until the previous message(s) is processed in the order of Nonce
.
Channel Outbox
All messages originated from src_chain_id
to a dst_chain_id
will be added to this queue in the runtime state.
Messages stay in the outbox of src_chain_id
until the domain block of src_chain_id
containing the originating extrinsic is out of the challenge period or reached archiving depth (if src_chain_id
is consensus chain).
There is also a notion of back pressure by limiting maximum number of messages queued max_outgoing_messages
to outbox of src_chain_id
. So when dst_chain_id
doesn’t send any message responses, this should throttle the outbox until normal operation. Message is removed from the outbox once the message response is received from the dst_chain_id
.
Channel Response Queue
All the message responses to the messages in the outbox are validated and added to this queue. The message responses are passed to the application units within the domain. If a response for message arrived earlier that previous message responses, then this response is stored until the previous message responses are delivered.
Channel Nonce
Channel nonce is used to order messages with in the channel and to avoid replay attacks.
A domain maintains 2 nonces for each domain and channel:
Incoming nonce
: This nonce is used to order the incoming messages to the domain through the channel from other domain. The nonce starts at 0 and is incremented after each received message.Outgoing nonce
: This nonce is used to order the outgoing messages from this domain to the other domains. The nonce starts at 0 and is incremented after every sent message.
Message Proof
Message proof that can verify the validity of the message from the point-of-view of the consensus chain. Proof combines the storage proofs to validate messages.
The proof consists of the following components:
- MMR proof for the state root of the parent consensus block
pub struct MMRProof {
// consensus block number below archiving depth at which this MMR proof was generated
consensus_block_number
// Leaf data that contains consensus storage root
// storage root is used to verify the `ConfirmedDomainBlocks` storage
leaf_data
// merkle proof for this MMR
proof
}
- Proof of the source domain state root and XDM inclusion in runtime
pub struct Proof<BlockNumber, BlockHash, StateRoot> {
// MMR proof, which provides the state root of consensus hash
MMRProof
/// Storage proof that src chain state_root is confirmed on Consensus chain.
/// This is optional when the src_chain is Consensus.
pub domain_confirmed_proof: Option<StorageProof>,
// Storage proof that message is processed on src_chain.
pub message_proof: StorageProof,
}
Message
Message encompasses the actual message being sent and metadata about the message itself. MessageID is a unique tuple of (ChannelID
, Nonce
). There are two types of Message payloads:
-
Protocol
payload, used by the protocol to open or close, acknowledge channel connection with other domain. -
Endpoint
payload, used by the protocol to pass messages between endpoint on thesrc
anddst
domains.
pub struct Message<Balance> {
/// Chain (consensus or domain id) that initiated this message.
pub src_chain_id: ChainId,
/// Chain (consensus or domain id) this message is intended for.
pub dst_chain_id: ChainId,
/// ChannelId the message was sent through.
pub channel_id: ChannelId,
/// Message nonce within the channel.
pub nonce: Nonce,
/// Payload of the message
pub payload: VersionedPayload<Balance>,
/// Last delivered message response nonce on src_chain.
pub last_delivered_message_response_nonce: Option<Nonce>,
}
The response message follows the same structure except the payload
contains either Protocol(Response)
or Endpoint(Response)
Message Lifecycle
Conceptually, a message can be in one of the following states during its lifecycle:
From the POV of the sender src_chain_id
:
- Outboxed: A message-request is added to the outbox for the relayers to relay message to
dst_chain_id
. It stays in the outbox until the message-response is received. - Delivered: After message-response is received and executed, the message request is cleared from the outbox.
From the POV of the receiver dst_chain_id
:
- Inboxed: A message-request is added to the inbox for the execution and response. It is not executed until the domain block of
src_chain_id
containing the originating extrinsic is out of the challenge period or reached archiving depth (ifsrc_chain_id
is consensus chain). - Cleared: After message-request is executed and message-response is constructed, the message-request is cleared from the inbox and message-response is added to the outbox.
Relayer Component
A relayer component relays message from src_chain_id
to dst_chain_id
. Domain operators have builtin relayer to relay messages from the domain to other domains and the consensus chain.
Operators on domain_a
relay messages originating in domain_a
to the consensus network and listen for messages destined to domain_a
from any other domain. Messages are sent through the consensus network where all operators of all domains are present.
The payload for the extrinsic could be a message-request or a message-response.
Fees
Fees are collected from the sender of the message on src_chain_id
to pay for relay and execution of their message on both src_chain_id
and dst_chain_id
respectively.
Compute fees are computed based on weights of the exact calls performed on both src_chain_id
and dst_chain_id
in total. Collected compute fees for the portion of execution happening on src_chain_id
is paid to operators of src_chain_id
and the compute fees for the portion of execution happening dst_chain_id
. The portion of fees that is to be distributed on dst_chain_id
is burned on src_chain_id
when message is added to outbox.
The burnt fees are subtracted from src_chain_id
bookkeeping balance (if it’s a domain).
The relay fee is split equally among operators on src_chain_id
and dst_chain_id
who have submitted the ER that includes the message.
On the source chain, this reward is distributed when the message gets the response from the dst_chain_id
. On the dst_chain_id
, when it receives the next message, it will collect all the messages that are marked delivered on src_chain_id
, mints the funds, and, distributes the rewards to the relayer pool on dst_chain_id
for each message.
The minted fees are added to dst_chain_id
bookkeeping balance (if it’s a domain).
Outbox Message Fees
- User
sender
sends a message fromsrc_chain_id
src_chain_id
collectsfees
to be paid bysender
as follows:- Compute fee for message execution on
dst_chain_id
. This amount is burnt onsrc_chain_id
and minted ondst_chain_id
later. - Relay fee for the relayers on
dst_chain_id
. This amount is burnt onsrc_chain_id
and minted ondst_chain_id
later. - Compute fee for message response execution on
src_chain_id
. - Relay fee for the relayers on
src_chain_id
for relaying the response.
- Compute fee for message execution on
- Message is sent to
dst_chain_id
. - Once the response is received from
dst_chain_id
,src_chain_id
distributes the rewards fromsender
to operators. - This message nonce is sent to
dst_chain_id
aslast_delivered_message_response_nonce
as an acknowledgement so that it can rewards its operators.
Inbox Message Fees
dst_chain_id
receives a message fromsrc_chain_id
dst_chain_id
mints received fees after message validation- Message is processed and response is sent
- After the delivery acknowledgement from
src_chain_id
, thedst_chain_id
distributes the fees fromsender
equally to operators who have submitted the ER containing the message extrinsic
High-Level Workflow
The following describes the generic message from one domain to another. This message could be a protocol message to initiate or close channel connection or an endpoint specific message through an established Channel. In either case, the base message passing remains same:
- User submits a transaction with the message and the required fees. The funds are locked in users account.
- Message with an assigned
nonce
is added to theoutbox
ofsrc_chain_id
with a runtime event issued. - The operator of
src_chain_id
extracts the message and prepares it to be relayed it to the transaction pool of domaindst_chain_id
. The relayer needs to construct a storage proof to prove that this message was accepted by domainsrc_chain_id
(by proving the message is included in domainsrc_chain_id
runtime state), and a state root of domainsrc_chain_id
(with respect to a consensus chain state root) so that domaindst_chain_id
can verify the storage proof. - Operator of
src_chain_id
waits until the domain block with the ER containing the state root used to construct the storage proof is cleared from the challenge period before gossiping the message. - Operator of
src_chain_id
gossips the message to the consensus network where all other operators are connected. - Operators of
dst_chain_id
listen to gossip on consensus and takes the message bound fordst_chain_id
into its domain transaction pool while ignoring other messages. - Message proof is validated and if valid added to the inbox of
dst_chain_id
. - Next message nonce is taken from the inbox of
dst_chain_id
. - Message is executed and response is stored.
- Operator of
dst_chain_id
waits until the domain block with the message transaction is cleared from the challenge period before gossiping the message response. - Message response on
src_chain_id
is validated using storage proofs on the consensus chain. - Next Message response nonce is submitted to the endpoint and message is removed from the outbox of
src_chain_id
. - When the
src_chain_id
prepares the next message, it will include the latest message nonce that was successful as part of the payload to notifydst_chain_id
of message response acknowledgement.
Networking
The messaging protocol uses the consensus network to relay messages, since all the operators of all the domains must also be connected to the consensus network.
This model assumes that there is at least one dst_chain_id
domain operator to pick the transaction and include in the bundle. If there are no operators to pick the transaction, message could be undelivered until its resubmitted in the network.
Type definitions
ChainId
: enum that identifies whether the chain is Consensus or a Domain withDomainId
DomainID
: uniquely identifies a given Domain,U32
.ChannelId
: uniquely identifies a channel,U256
.AccountId
: is the public key of the User accountNonce
: is an incrementing value used for ordering message and avoid replay attacks. We also use nonce as the message ID for a given domain for a given channel,U256
.MessageId
: uniquely identifies a message, a tuple(ChannelId, Nonce)
EndpointId
: represents a unique id of the endpoint on a domain,U64
.
Functions
Detailed description of each function required to be present in the protocol.
Chain Allow List
Consensus chain maintains a ChainAllowList
to keep track of the authorized domain chains that can establish channels with the Consensus chain. ChainAllowList
can be defined at Genesis and can be updated later by the sudo account adding or removing chains.
When a Consensus chain receives an initiate_channel
XDM to open the channel, if the src_chain
is in the allowlist, then channel is opened else XDM is rejected.
Practically, this means that a newly initialized domain chain needs to be approved by governance and added to the Consensus chain's ChainAllowList
before it can initiate a channel with the Consensus chain.
Similarly, each domain chain maintains its own DomainChainAllowList
to keep track of the authorized domains it can establish channels with. This allows the domains to control and restrict which other domains they want to interact with. Updating domain-specific lists is done within a domain by the domain's sudo or governance, without consensus chain approval.
Initiate Channel
- Channel
initiate_channel
transaction is sent by the root user of the domain. - If the domain is in the allow list, the next available
ChannelID
is assigned to the new channel. - If no Channel exits, Channel is created and set to
Initiated
status and cannot accept or receive any messages yet. Protocol
payload message to open the channel is added to thesrc_chain_id
domain outbox with nonce0
Open Channel
Before sending any messages, domain needs to have an channel open with the dst_chain_id
:
- Channel is initiated by
src_chain_id
as described in Initiate Channel - Operator on
dst_chain_id
receives a message with the correspondingProtocol
ChannelOpen
payload andnonce=0
. - Channel status is set to
Open
and a corresponding event is issued - Operators on
dst_domain
submits the transaction with message response tosrc_chain_id
src_domain
moves the channel state toOpen
and starts accepting messages to be sent over channel
Close Channel
Any domain of either end of the open channel can close the channel:
- Channel close transaction is sent by the root user.
- Channel state is set to
Closed
Protocol
payload message to close channel is added to thesrc_chain_id
outbox- Operator on
src_chain_id
gossips the message - Operator on
dst_chain_id
receives a message with the correspondingProtocol
ChannelClose
payload. - Channel close response is submitted to
src_chain_id
Send message
When user wants to send message from endpoint on src_chain_id
to an endpoint on dst_chain_id
with open channel to dst_chain_id
.
- User sends a transaction that results in a message to an endpoint on
dst_chain_id
. - Transaction is included in the runtime state of
src_chain_id
. - A next incrementing
nonce
> 0 (0 is always reserved for Channel open message) is assigned to the message bound todst_chain_id
. - Next message nonce storage is updated.
- Execution layer stores the message in the
Outbox
and emitsnew_message
event. - Operator of
src_chain_id
waits until the domain block with the ER containing the state root used to construct the storage proof is cleared from the challenge period before gossiping the message (or below archiving depth ifsrc_chain_id
is consensus chain). - The operator of
src_chain_id
extracts the message and prepares it to be relayed it to the transaction pool of domaindst_chain_id
. The relayer needs to construct a storage proof to prove that this message was accepted by domainsrc_chain_id
(by proving the message is included in domainsrc_chain_id
runtime state), and a state root of domainsrc_chain_id
(with respect to a parent consensus chain state root via MMR) so that domaindst_chain_id
can verify the storage proof. - Operator of
src_chain_id
gossips the message to the consensus network where all other operators are connected.
Receive message
When a relayer from src_chain_id
submits the message to the inbox of the dst_chain_id
:
dst_chain_id
verifies the message by verifying the storage proof from the point of view of the consensus chain as follows:- Check if the MMR proof is constructed at a finalized consensus block to ensure the
MMR::verify_proof
result is deterministic regardless of the consensus chain fork. - Verifies MMR proof using consensus chain
MMR::verify_proof
function to extract the MMR leaf data and the corresponding state root of consensus chain. - Using
consensus_chain_state_root
,domain_confirmed_proof
is verified and associated domain’sstate_root
is extracted fromDomainBlockInfo
- Using
domain_state_root
,message_proof
is verified and actual XDM is extracted from the storage proof.
- Check if the MMR proof is constructed at a finalized consensus block to ensure the
dst_chain_id
adds the message to its inbox.dst_chain_id
listens for next message to process from the inbox in the nonce order.- Message is passed to the endpoint and eventually executed and response is stored for that message.
dst_chain_id
takes the latest delivered message nonce onsrc_chain_id
and distributes the rewards to the operators and deletes any stored state pertaining to any message with nonce below the confirmed nonce.
Receive message response
Relayer from the dst_chain_id
will submit the message response to the src_chain_id
:
src_chain_id
verifies the message response and adds it to the message response queue.src_chain_id
listens for next message response and submits the response to the caller module on thesrc_domain
src_chain_id
marks the message nonce as the last confirmed message which is included in the next message bound todst_chain_id
src_chain_id
then deletes the state pertaining to the original message from the runtime.
XDM delays
Since XDM is inherently a request response protocol it may require a response in some cases.
The use cases such as transfer or sending some payload where response is just an acknowledgement, it is generally ignored. With that in mind, the following table will give the time to send a message from Any chain -> Any chain
Where K = Archiving depth
(currently 100 consensus blocks) and D = Domain challenge period
(currently 14400 domain blocks)
Domain → Any chain | Consensus → Any domain |
---|---|
K + D | K |
For XDM where response is required from, the following table captures the time in blocks
Domain → Consensus → Domain | Consensus → Domain → Consensus | Domain A → Domain B → Domain A |
---|---|---|
2 * K + D | D + 2 * K | 2 * K + 2 * D |