Cross-chain messages

Hello,

In this thread, I want to propose a LIP for the roadmap objective “Define cross-chain messaging protocol”. The proposal’s main contribution is to define a uniform message format to be used for cross-chain interactions in the Lisk ecosystem.

I’m looking forward to your feedback.

Here is a complete LIP draft:

LIP: <LIP number>
Title: Cross-chain messages
Author: Maxime Gagnebin <maxime.gagnebin@lightcurve.io>
Type: Standards Track
Created: <YYYY-MM-DD>
Updated: <YYYY-MM-DD>

Abstract

This proposal introduces the cross-chain message schema, the generic message processing and the base error handling. Defining a base cross-chain message allows all chains in the ecosystem to read and understand the base properties of messages.

The proposal also introduces three standard messages used by the interoperability module, the cross-chain update receipt, the channel terminated receipt and the registration message.

Copyright

This LIP is licensed under the Creative Commons Zero 1.0 Universal.

Motivation

To achieve interoperability, chains need to exchange information by sending messages to each other. To this end, we introduce a new message format, which we call a cross-chain message and define in this proposal the base schema of cross-chain messages. Specifying a unified base message schema allows all chains in the Lisk ecosystem to deserialize and read cross-chain messages.

To achieve some of its basic functionalities, the interoperability module uses three standard messages. The first one, the cross-chain update receipt, is sent back to the partner chain (the chain with which the current chain is exchanging messages) whenever a cross-chain update transaction from this chain gets included. The cross-chain update receipt serves to update a few properties of the interoperability store, and in general to acknowledge the inclusion of cross-chain update transactions. The second one, the channel terminated message, is sent to chains which have been terminated. This message also updates a few properties of the interoperability store to keep them synchronized between chains. The last one, the registration message, is used when registering a chain on the Lisk mainchain. The message serves as a guarantee that the correct chain ID and network ID are used when the registration transaction is sent on the sidechain.

Further motivation and rationale behind the Lisk interoperability architecture is given in the general interoperability document Properties, serialization, and initial values of the interoperability module.

Rationale

Messages in the Lisk ecosystem will be included in multiple chains. It is therefore important that all chains can deserialize and process the base part of cross-chain messages in the same way. The properties of the message contain all the information necessary for this purpose while trying to keep a minimal size.

Cross-chain Message Properties

In the following, we list all the properties of a cross-chain message.

Sending Chain ID and Receiving Chain ID

Used to identify the chains exchanging the cross-chain message. On the mainchain the receiving chain ID is read to route the message to the corresponding chain outbox, as specified in LIP “Introduce cross-chain update transactions”. The sending chain ID is used for example if the message triggers an error and has to be sent back.

Index

When a cross-chain message is created and added to the partner chain outbox, the size of the outbox at that point is added to the message in the index property. This allows all messages to be distinct and to be tracked throughout the ecosystem.

Module ID and Asset ID

Once the message has reached the recipient chain, the two properties moduleID and assetID specify which logic should be used to validate and apply the message. The interoperability module handles the message in case the required logic is not available on the chain. This brings the benefit that sending chains do not need to monitor all other chains in the ecosystem and the modules they support. We are following an optimistic approach in which all messages will be delivered and error handling will kick in when it is needed. In that regard, users should be aware that valid messages will usually not trigger a response. A quick API call will guarantee that the message has been properly received and applied.

To avoid any confusion with transaction logic and asset schemas, we extend LIP 0036 to impose assetID uniqueness between all transactions and cross-chain messages with the same moduleID.

Fee

For all cross-chain messages, a fee paid in LSK is used to account for the transaction processing in the recipient chain.This fee must be transferred from the sending chain account to the recipient chain account in order to maintain the correct LSK balances on all chains in the ecosystem. The token ID for this fee is always the token ID of the LSK token and as such is not repeated in the message. The LSK token is the main utility token of the Lisk ecosystem and as such is the only good candidate for paying cross-chain fees.

Status

The basic error handling for routing messages to other chains is done by the mainchain. For example, in the case the recipient chain does not exist, is not active or has been terminated, the mainchain will return the message to the sending chain. The sending chain can then revert the message and potentially refund users. This design choice allows sidechains to send messages to other chains without needing to monitor the status (or even existence) of every other chain. Information about the reason why the message failed and the initial message identifier are stored in the status property.

The constant table lists the different status codes defined by the interoperability module. Other modules may use other status codes.

Asset

The asset property of the messages is defined by each module and can follow any schema, similar to the asset property of a transaction. The asset property is not deserialized or validated by the interoperability module.

In the Lisk ecosystem, all cross-chain messages are routed through the mainchain. This means that messages should always have a sufficiently small size in order to be easily included in mainchain blocks. As the mainchain payload size limit is 15 KiB, and other properties in the cross-chain updates will not be larger than 4 KiB, we limit the message size to 10 KiB. To guarantee that all messages can be included and handled, sidechains in the Lisk ecosystem should have a payload size limit equal to or greater than 15 KiB (15 x 1024 bytes).

Message Tracking

Tracking messages throughout the ecosystem can be done via the unique tuples (sendingChainID, receivingChainID, originalIndex). Each chain should provide an API to allow users to check if a given message was included. When messages are errored, the originalIndex property of the message is never overwritten. Notice that for errored messages, the sending chain and the receiving chain are swapped (the message is returning to the chain that created it) and so the message is now identified by the tuple (receivingChainID, sendingChainID, originalIndex).

Sending Cross-chain Messages

Whenever a message is created, it must be added to the outbox of the corresponding partner chain. On sidechains, this logic always appends to the mainchain outbox, while on the mainchain, this logic can append to any registered sidechain outbox.

The interoperability module exposes the send reducer which is used to check the liveness of the receiving chain, set the messages index and original index properties and append messages to the outbox of the partner chain.

Cross-chain Update Receipt

The main role of the cross-chain update receipt is to inform chains that a cross-chain update transaction has been posted on the partner chain, by whom, the fee that was paid, and the inbox size of the partner chain. This is then used on the mainchain to allow for message recovery, see LIP “Sidechain recovery transactions”. This can also be used in the sidechain to compensate the cross-chain update transaction poster, also called the relayer. The precise choice of the compensation mechanism is left to sidechain developers.
For example, all cross-chain transaction fees could be split between the block forger and a compensation pool. Whenever a cross-chain update transaction is posted on the mainchain, the poster gets compensation from the pool.

Channel Terminated Message

The role of the channel terminated message is to inform chains that their channel has been terminated on the mainchain. The chain receiving this message can then also close the channel to the mainchain. This is helpful in preventing users from sending transactions to a chain whilst the cross-chain update transaction will be invalid. Note that the interoperability module is designed in such a way that no channel should be terminated while the sidechain is respecting the Lisk interoperability protocol. Sending and receiving this message should therefore be a rare occurrence.

The termination message contains the last inbox size of the sidechain. This allows the sidechain to know exactly which messages were applied on the mainchain and which are still pending at the time of termination.

Registration Message

The role of the registration message is to guarantee that a channel was opened on the sidechain with the correct chain ID and network ID. When a sidechain is registered on the mainchain, an ecosystem wide chain ID is attributed to this chain. This chain ID and the corresponding network ID are included in a cross-chain message that is appended to the sidechain outbox. When the first cross-chain update is sent to the sidechain, the equality between the properties in the cross-chain message and the ones in the interoperability store is verified.

Specification

Cross-chain messages (CCM) are used by modules to execute actions on other chains in the ecosystem. They are part of the interoperability module.

Notation and Constants

The following constants are used throughout the document, multiple of those constants are shared with the other LIPs defining the interoperability module and all of the needed constants for the interoperability module are defined in LIP “Properties, serialization, and initial values of the interoperability module”. That LIP should be considered correct if a value stated here would be different.

Name Type Value
Interoperability Constants
MODULE_ID_INTEROPERABILITY uint32 64
MAINCHAIN_ID bytes 1
MIN_RETURN_FEE uint64 1000
MAX_CCM_SIZE uint64 10240
Interoperability Storage
STORE_PREFIX_ACCOUNT bytes 0x8000
Interoperability Asset IDs
ASSET_ID_CCM_REGISTRATION uint32 7
ASSET_ID_CCM_CCU_RECEIPT uint32 8
ASSET_ID_CCM_CHANNEL_TERMINATED uint32 9
Message Status and Errors
CCM_STATUS_OK uint32 0
CCM_STATUS_MODULE_NOT_SUPPORTED uint32 1
CCM_STATUS_ASSET_NOT_SUPPORTED uint32 2
CCM_STATUS_CHANNEL_UNAVAILABLE uint32 3
CCM_STATUS_RECOVERED uint32 4
Chain Status
CHAIN_REGISTERED uint32 0
CHAIN_ACTIVE uint32 1
CHAIN_TERMINATED uint32 2

Chain Account

Let account(chainID) be the entry in the interoperability store with

  • storagePrefix = STORE_PREFIX_ACCOUNT
  • storageKey = chainID, serialized as big endian uint32, this serialization always has 4 bytes.

Use of Asset ID

The logic associated with a cross-chain message is identified by the pair (moduleID,assetID). To this end, we extend the specification of LIP 0036 by:

A pair (moduleID,assetID) must uniquely correspond to

  • one transaction or cross-chain message asset schema,
  • one transaction or cross-chain message validation and verification logic, and
  • one transaction or cross-chain message execution logic

in one blockchain, with uniqueness being among all transactions and cross-chain messages. This means that for any change with respect to the three aspects above, a different pair (moduleID,assetID) must be used for the new message. Typically, new messages are still contained in the same module, i.e., the value of moduleID stays the same, and a new unique value of assetID is used.

Cross-chain Message Schema

All cross-chain messages in the Lisk ecosystem use the following schema.

crossChainMessageSchema = {
    "type": "object",
    "properties": {
        "index": {
            "dataType": uint64,
            "fieldNumber": 1 
        },
        "originalIndex": {
            "dataType": uint64,
            "fieldNumber": 2 
        },
        "moduleID": {
            "dataType": uint32,
            "fieldNumber": 3 
        },
        "assetID": {
            "dataType": uint32,
            "fieldNumber": 4 
        },
        "sendingChainID": {
            "dataType": uint32,
            "fieldNumber": 5 
        },
        "receivingChainID": {
            "dataType": uint32,
            "fieldNumber": 6 
        },
        "fee": {
            "dataType": uint64,
            "fieldNumber": 7
        },
        "status": {
            "dataType": uint32,
            "fieldNumber": 8 
        },
        "asset": {
            "dataType": bytes,
            "fieldNumber": 9 
        }
   },
   "required": [
       "index", 
       "originalIndex", 
       "moduleID", 
       "assetID", 
       "sendingChainID", 
       "receivingChainID", 
       "fee", 
       "status", 
       "asset"
   ]
}

Internal Functions

createCrossChainMessage

The following logic should be used to create new cross chain messages:

createCrossChainMessage(moduleID, assetID, receivingChainID, fee, asset):

    return message = {
               "index": 0,
               "originalIndex": 0,
               "moduleID": moduleID,
               "assetID": assetID,
               "sendingChainID": chainID of the current chain,
               "receivingChainID": receivingChainID,
               "fee": fee,
               "status": CCM_STATUS_OK,
               "asset": asset
           }

validateFormat

All cross-chain messages CCM must have the correct format, which is checked by the following logic:

validateFormat(CCM):
   if size(CCM) > MAX_CCM_SIZE bytes: 
      return False
   if CCM does not follow crossChainMessageSchema: 
      return False
   return True

process

When processing a cross-chain message, with a valid format, CCM, follow the logic below:

process(CCM):
   let ownChainID be the chainID of the current chain 

   if CCM.receivingChainID == ownChainID:
      tryToApply(CCM)
   else: // CCM.receivingChainID != ownChainID
      tryToForward(CCM)
tryToApply(CCM):
   if (CCM.mouleID, CCM.assetID) is supported:
      call the logic associated with (CCM.moduleID,CCM.assetID) on CCM
   else:
      if CCM.status != CCM_STATUS_OK or CCM.fee < MIN_RETURN_FEE*size(CCM):
         CCM is discarded and has no further effect
      elif moduleID is not supported:
         newCCM = CCM except for
             newCCM.fee = 0
             newCCM.status = CCM_STATUS_MODULE_NOT_SUPPORTED
             newCCM.receivingChainID = CCM.sendingChainID
             newCCM.sendingChainID = CCM.receivingChainID
         process(newCCM) 
      elif assetID is not supported:
         newCCM = CCM except for
             newCCM.fee = 0
             newCCM.status = CCM_STATUS_ASSET_NOT_SUPPORTED 
             newCCM.receivingChainID = CCM.sendingChainID
             newCCM.sendingChainID = CCM.receivingChainID
         process(newCCM) 
tryToForward(CCM):
   let partnerChainID = getPartnerChainID(CCM.receivingChainID)

   if account(partnerChainID).status == CHAIN_ACTIVE and isLive(partnerChainID):
      addToOutbox(partnerChainID,CCM) as specified in LIP_INTEROP
   elif CCM.status == CCM_STATUS_OK:
      newCCM = CCM except for
          newCCM.fee = 0
          newCCM.status = CCM_STATUS_CHANNEL_UNAVAILABLE  
          newCCM.receivingChainID = CCM.sendingChainID
          newCCM.sendingChainID = CCM.receivingChainID
      process(newCCM)
   else:
      CCM is discarded and has no further effect

Cross-chain Update Receipt

The cross-chain update receipt (CCU receipt) is a CCM with

  • moduleID = MODULE_ID_INTEROPERABILITY.
  • assetID = ASSET_ID_CCM_CCU_RECEIPT.

Asset Schema

The asset schema for CCU receipts is:

CCUReceiptAsset = {
    "type": "object",
    "properties": {
        "paidFee": {
            "dataType": uint64,
            "fieldNumber": 1 
        },
        "relayerPublicKey": {
            "dataType": bytes,
            "fieldNumber": 2 
        },
        "partnerChainInboxSize": {
            "dataType": uint64,
            "fieldNumber": 3
        }
    },
    "required": [
        "paidFee", 
        "relayerPublicKey", 
        "partnerChainInboxSize"
    ]
}

Creating Cross-chain Update Receipts

A CCU receipt is created by the interoperability module upon acting on a cross-chain update transaction as specified in LIP “Introduce cross-chain update transactions”.

Applying Cross-chain Update Receipts

When a CCU receipt CCUR is received, the following is done:

if CCUR.status != CCM_STATUS_OK 
or account(sendingChainID).partnerChainInboxSize > CCUR.asset.partnerChainInboxSize:
    terminateChain(CCUR.sendingChainID) as specified in LIP_INTEROP and exit the CCUR application

account(sendingChainID).partnerChainInboxSize = CCUR.asset.partnerChainInboxSize

Channel Terminated Message

The channel terminated message is a CCM with

  • moduleID = MODULE_ID_INTEROPERABILITY.
  • assetID = ASSET_ID_CCM_CHANNEL_TERMINATED.

Asset Schema

The asset schema for channel terminated is:

channelTerminatedCCMAsset = {
    "type": "object",
    "properties": {
        "partnerChainInboxSize": {
            "dataType": uint64,
            "fieldNumber": 1
        }
    },
    "required" : ["partnerChainInboxSize"]
}

Creating Channel Terminated Messages

A channel terminated message is created by the interoperability module when terminating a chain account, for example when encountering a malicious cross-chain update transaction or malicious cross-chain message.

Applying Channel Terminated Messages

When a channel terminated message CTM is received, the following is done:

account(CTM.sendingChainID).status = CHAIN_TERMINATED

if account(CTM.sendingChainID).partnerChainInboxSize < CTM.asset.partnerChainInboxSize 
    set account(CTM.sendingChainID).partnerChainInboxSize = CTM.asset.partnerChainInboxSize

Registration Message

The registration message is a CCM with

  • moduleID = MODULE_ID_INTEROPERABILITY.
  • assetID = ASSET_ID_CCM_REGISTRATION.

Asset Schema

The asset schema for the registration CCM is:

registrationCCMAsset = {
"type": "object",
    "properties": {
        "networkID": {
            "dataType": bytes,
            "fieldNumber": 1 
        },
        "name": {
            "dataType": string,
            "fieldNumber": 2 
        }
    },
    "required" : ["networkID", "name"]
}

Creating Registration Message

A registration message is created by the interoperability module when registering a sidechain on the mainchain, as specified in LIP “Chain registration”.

Applying Registration Message

When a registration message RM is applied, the following is done:

Let ownChainID and ownName be the chain ID and name of the current chain 
// interoperability store entry with 
// storagePrefix = STORE_PREFIX_ACCOUNT 
//	storageKey = 0x00000000

if CCM.index != 0 
or ownChainID != CCM.receivingChainID
or ownName != CCM.asset.name
or CCM.asset.networkID does not equal the chain's networkID:
    terminateChain(RM.sendingChainID) as specified in LIP_INTEROP

Backwards Compatibility

This proposal, together with LIP “Chain registration”, LIP “Properties, serialization, and initial values of the interoperability module”, LIP “Introduce cross-chain update transactions”, and LIP “Sidechain recovery transactions”, is part of the interoperability module. Chains adding this module will need to do so with a hard fork.

2 Likes