Cross-chain Message in a Bottle

  • By Alessandro Ricottone Ph.D. in Research
  • 21 Sep 2021
  • 14 min read

In this next edition of the ongoing series covering the Lisk interoperability solution, we discuss the salt and pepper of interoperability: cross-chain messages. In the previous blog post, Maxime Gagnebin presented the cross-chain updates, the transactions used to exchange information between chains in the Lisk ecosystem. A brief summary of this is given below, however it is highly recommended to read the entire blog post, before diving into cross-chain messages. Furthermore, Maxime also discussed cross-chain messages at Lisk.js.

 

If cross-chain updates are the envelopes used to deliver information from one chain to another, cross-chain messages are the actual content inside the envelope. Users can send messages from one chain to another to move tokens, send information, or in general implement any kind of cross-chain custom logic.

 

Overview

To understand why we need cross-chain messages, it is better to start from the very basics.

Stripping it down to the essentials, a blockchain is a distributed database. At any given time, the database is in one of finitely many precise states (also called finite-state machine). For example, the state of the blockchain could correspond to Alice having 5 LSK and Bob having no LSK. The blockchain can undergo state transitions and move from one database state to another. Users can trigger a state transition by sending a transaction. Each transaction contains a command that, once executed, changes the state of the database. For instance, Alice can send 2 LSK to Bob by issuing a LSK transfer command. Batches of transactions are regularly collected into blocks which are sent across the network to keep the distributed database in sync.

 

However, not all state transitions are allowed. In general, the set of valid state transitions depends on the current state of the blockchain. In the previous example, a command sending 10 LSK from Alice to Bob would be invalid, as Alice does not have enough LSK. The rules defining the valid state transitions are called the blockchain protocol (again, we are simplifying things somewhat here, as the consensus rules are also part of the protocol).

Figure 1

Figure 1: In the initial state of the blockchain, Alice has 5 LSK and Bob has none. Alice sends 2 LSK to Bob with a token transfer transaction. After the transaction has been processed, the blockchain transitions to a state where Alice has 3 LSK and Bob 2 LSK.

 

 

But what happens if we have two blockchains running independently from each other? How can users of blockchain A trigger a state transition on blockchain B? An obvious way would be to just send a transaction to blockchain B directly. This transaction however can only be validated against the state of blockchain B. What we really want instead is to trigger a state transition on blockchain B from blockchain A (validating it against the state of blockchain A).

 

Here is where cross-chain transactions come into play. A cross-chain transaction is a transaction that, upon its execution, creates one or more cross-chain messages. Cross-chain messages are a new data structure introduced with interoperability. They play a similar role to transactions, in the sense that they contain a cross-chain command. In analogy with commands, cross-chain commands induce a state transition in the receiving chain. Several cross-chain messages are collected together and posted to another chain as part of a cross-chain update (again, more information can be found in the previous blog post).

 

Hence, cross-chain commands are validated in the sending chain. In the receiving chain, the validity of the cross-chain messages can be assumed (there are certain checks that are performed to the cross-chain update that guarantee the validity of all cross-chain messages contained in it). If the cross-chain message is also valid with respect to the state of the receiving chain, the state transition can be finally processed.

 

Alice can now send 2 LSK she has on blockchain A to Bob on blockchain B. To do that, she issues a cross-chain LSK transfer command on blockchain A. The command creates a cross-chain message which is included in a cross-chain update command included in blockchain B. The cross-chain update is processed along with all cross-chain messages contained in it, including Alice’s LSK transfer. Bob finally gets his 2 LSK directly on blockchain B. Hurray!

Figure 2

Figure 2: Alice sends 2 LSK to Bob from chain A to chain B. The cross-chain transaction T generates a message M which is included in the cross-chain update CCU. The cross-chain update is included in chain B to induce the state transition that credits Bob with 2 LSK.

 

Terminology Summary

  • Transaction: An envelope object for a command.
  • Command: Trigger of a state transition in the same chain.
  • Cross-chain transaction: Transaction generating one or more cross-chain messages.
  • Cross-chain message: An envelope object for a cross-chain command.
  • Cross-chain command: Trigger of a state transition coming from another chain.
  • Cross-chain updates: A special command containing cross-chain messages.

 

Format of Cross-chain Messages

In this section, we briefly describe the properties common to all cross-chain messages (see LIP "Introduce cross-chain messages" for more information).

Sending Chain ID and Receiving Chain ID

As previously explained, cross-chain messages are spawned in one blockchain and processed in another. The blockchain where the cross-chain message is created is called the sending chain and it is identified by the sending chain ID. The blockchain where the message is processed is called the receiving chain and is identified by the receiving chain ID. The mainchain uses the receiving chain ID to route the message to the correct chain.

Nonce

This property counts the total number of messages that were sent from the sending chain. When a cross-chain message is created, it is automatically assigned the correct nonce. The mainchain does not update this value when a message is routed.

Module ID and Cross-chain Command ID

Similar to transactions, cross-chain messages have a module ID and a cross-chain command ID. The module ID and the cross-chain command ID are used to identify the logic that should be processed in the receiving chain.

Fee

Transaction fees pay for the processing on the sending chain. Similarly, message fees pay for the processing on the receiving chain. Message fees are paid in LSK in the whole ecosystem. When routing a message, the mainchain also transfers LSK from the sending to the receiving chain to account for the transferred fees.

Status

The status property is used for error handling. If it is not possible to deliver a cross-chain message (for example if the receiving chain does not exist), the status of the message is updated and the message is routed back to the sending chain. The sending chain can then process the failed message and potentially refund users.

 

Currently, interoperability supports 5 status codes. The status used for error handling describes the reason for the message failure as shown below:

 

  1. OK: the default status of a cross-chain message.

  2. MODULE_NOT_SUPPORTED: status assigned on the receiving chain if it does not implement any module with ID equal to the message module ID.

  3. CROSS_CHAIN_COMMAND_NOT_SUPPORTED: status assigned on the receiving chain if it does not implement any command with ID equal to the message command ID.

  4. CHANNEL_UNAVAILABLE: status assigned on the mainchain if the receiving chain is not available (it does not exist, it is not active, or it has been terminated).

  5. RECOVERED: status assigned on the mainchain to a cross-chain message after it has been recovered from the outbox of a terminated sidechain (more information will be provided in a future blog post).

 

Modules can introduce new values for the status property, which can be used for custom error handling. Notice that the first 64 error codes are reserved for the Interoperability module.

 

For instance, the new Token module defines the TOKEN_NOT_SUPPORTED status to indicate that a cross-chain token transfer is invalid because the receiving chain does not support that specific token.

Params

Similar to transactions, the params property contains the specific parameters used to process the cross-chain message. This property is defined by the relevant module and can follow any schema. Notice that the mainchain does not deserialize nor validate this property when routing a message.

 

Cross-chain Messages of the Interoperability Module

There are 4 special cross-chain messages that are used to facilitate interoperability. A brief description of these messages is covered below in this section.

 

Cross-chain Update Receipt

The cross-chain update receipt acknowledges the inclusion of a cross-chain update transaction. The receiving chain automatically generates this message, which is then sent back to the sending chain.   

 

The cross-chain update receipt contains the following properties:

  • paidFee: the fee of the cross-chain update. This property can be used back in the sending chain to reimburse the relayer, for instance (recall that the relayer is the user that prepared and sent the cross-chain update).
  • relayerAddress: the address of the relayer.
  • partnerChainInboxSize: the size of the inbox in the receiving chain. This property informs the sending chain on the number of messages that have been processed on the receiving chain. In general, the size of the outbox on the sending chain is not equal to the size of the inbox in the receiving chain (as, for example, a cross-chain update has not been posted on the receiving chain yet). This property allows users to recover unprocessed messages from a terminated sidechain outbox.

 

Channel Terminated Message

When a sidechain is terminated, the mainchain automatically generates a channel terminated message and sends it to the terminated sidechain. This message also contains the partnerChainInboxSize property.

Registration Message

When a sidechain registers on the mainchain, a registration message is generated to acknowledge the successful registration. This message contains the network ID and the name of the sidechain. The network ID and name included in the mainchain registration transaction (see "Mainchain registration process" in the blog post "The Lifecycle of a Sidechain in the Lisk Ecosystem") must match these values, guaranteeing their correctness.

Sidechain Terminated Message

The sidechain terminated message is created on the mainchain when a message should be routed to a terminated or inactive chain. This message is sent to the original sending chain, allowing it to create a terminated sidechain account which can then be used for the recovery commands. It contains the chain ID and the last certified state root of the terminated sidechain.

 

Life Cycle of a Cross-chain Message

In this section, we summarize again the main features of Lisk interoperability from the point of view of a cross-chain message.

 

Mainchain routing: The Lisk ecosystem is organized in a star topology: Each sidechain is connected to the Lisk mainchain. Sidechain-to-sidechain cross-chain messages are always first posted on the mainchain. The mainchain is then responsible for delivering the message to the correct receiving chain. Notice that in doing so, the mainchain does not process the message, but merely moves it from the inbox of the sending chain to the outbox of the receiving chain.

 

Mainchain error handling: While delivering messages, the mainchain also provides a base level of error handling: If the receiving chain does not exist, is not active, or has been terminated, the message is sent back to the sending chain with status set to CHANNEL_UNAVAILABLE and sending chain ID and receiving chain ID swapped. The original sending sidechain can process this, and, for instance, revert it. In general, mainchain error handling allows sidechains to send messages without monitoring the status of other chains.

Figure 3

Figure 3: An example of mainchain error handling. A cross-chain transaction T on sidechain A creates a cross-chain message M. The cross-chain message is included in a cross-chain update CCU1 which is posted on the mainchain. Here it is processed, however, for instance, the receiving chain has been terminated. Hence, a new error message Merr is created and sent back to sidechain A. It is included in another cross-chain update CCU2 which is posted on sidechain A where it can be reverted.

 

 

Sidechain error handling: On the other hand, sometimes a cross-chain message can be errored only once it reaches the receiving chain. As mentioned above, the Interoperability module provides the predefined error codes MODULE_NOT_SUPPORTED and CROSS_CHAIN_COMMAND_NOT_SUPPORTED. In addition, custom modules can introduce new error codes.

Figure 4

Figure 4: An example of sidechain error handling. A cross-chain transaction T on sidechain A creates a cross-chain message M. The cross-chain message is included in a cross-chain update CCU1 which is posted on the mainchain and then forwarded to sidechain B. It is then included in a second cross-chain update CCU2 which is posted on sidechain B. On sidechain B it is processed but, for instance, the module ID corresponds to a non-supported module. Hence, a new error message Merr is created and sent back to sidechain A. This is included in yet another cross-chain update CCU3 which is posted on the mainchain and then routed towards sidechain B. Finally, it gets included in the final cross-chain update CCU4 and posted on sidechain B where it can be reverted.

 

Message tracking: All messages are uniquely identified in the ecosystem by the (sendingChainID, nonce) tuple. Messages that have been sent back from the mainchain with CHANNEL_UNAVAILABLE status are instead identified by the (receivingChainID, nonce) tuple, since the sending and receiving chain IDs have been swapped. This property is very useful to track messages throughout the ecosystem, for instance using a UI tool. Furthermore, just as for transactions, we define the cross-chain message ID to be the hash of the serialized message object.

 

It is easier to follow the path of a cross-chain message in the ecosystem with two concrete examples. Notice that in both these examples, the user starting the process, Alice, only has to care about sending the initial cross-chain command. From her point of view, everything else happens automagically.

 

Mainchain-to-sidechain LSK transfer

Arguably the most important cross-chain message, and one that many users will actually use, is the cross-chain LSK transfer.

 

LSK are the native tokens of the Lisk mainchain. This means that originally all LSK are stored on the mainchain. LSK tokens are used to power interoperability: All cross-chain messages fees are paid in LSK, and cross-chain updates posted on the mainchain pay a fee in LSK (just as any other mainchain transaction). Therefore, users need to move their LSK to a sidechain to start sending cross-chain commands from there. The first step is to send a cross-chain LSK transfer command on the mainchain, targeting the sidechain.

 

Alice has 10 LSK on the Lisk mainchain and she wants to send 5 LSK to her account on sidechain A. The complete process of transferring LSK from the mainchain to a sidechain looks like this:

 

1. Alice sends a cross-chain LSK transfer command with the following parameters:

{

  tokenID = {"chainID": 1, "localID": 0} : the ID of the LSK token

  amount = 5 : the amount of LSK to transfer

  receivingChainID = 16 : the ID of sidechain A

  recipientAddress = 0x0a11ce : Alice’s address

  data = "" : Alice could add a custom message in this field

  messageFee = 0.01 : the fee associated to the message

}

2. Alice is debited the 5 LSK on the mainchain.

3. The command spawns a cross-chain message with the following parameters:

{

  nonce = 21 : in this example, the current mainchain nonce

  moduleID = MODULE_ID_TOKEN : ID of the token module

  crossChainCommandID = 0 : ID of the token transfer cross-chain command

  sendingChainID = 1 : ID of the Lisk mainchain

  receivingChainID = 16 : ID of sidechain A

  fee = 0.01 : message fee specified in the command

  status = OK : default status of a cross-chain message

  params = {

              tokenID = {"chainID": 1, "localID": 0} =

              amount = 5

              senderAddress = 0x0a11ce

              recipientAddress = 0x0a11ce

              data = ""

           } : these parameters correspond to the command parameters

}

4. The cross-chain message is added to the sidechain A outbox.

5. The cross-chain message is included in a cross-chain update posted on sidechain A.

6. The cross-chain update is processed on sidechain A. The cross-chain message is added to the mainchain inbox and has its effect. Alice is credited with 5 LSK on sidechain A. 🎉

 

Sidechain-to-sidechain Custom Message

Sidechain-to-sidechain messages are the humus on which a rich interoperability ecosystem can flourish. The Lisk interoperability provides the general protocol for exchanging messages, but the real fun happens with the custom cross-chain commands and messages developers create for the blockchain applications. As previously mentioned, the mainchain does not process custom cross-chain messages but only routes them without even looking at them.

 

Alice decides to send a cross-chain message from sidechain A to sidechain B. The effect of this message on sidechain B is part of its protocol, and here we do not care about the specific functionality, only about the general procedure.

 

1. Alice sends a custom cross-chain transaction with some custom parameters.

2. The command is processed on sidechain A.

3. The command spawns a cross-chain message with the following parameters:

{

  nonce = 22 : in this example, the current sidechain A nonce

  moduleID = 1034 : ID of the custom module

  crossChainCommandID = 2 : ID of the custom cross-chain command

  sendingChainID = 16 : ID of sidechain A

  receivingChainID = 42 : ID of sidechain B

  fee = messageFee : message fee specified in the command

  status = OK : default status of a cross-chain message

  params : custom parameters of the cross-chain message

}

4. The cross-chain message is added to the mainchain outbox.

5. The cross-chain message is included in a cross-chain update posted on the mainchain.

6. The cross-chain update is processed on the mainchain. The cross-chain message is added to the sidechain A inbox and immediately added to the sidechain B outbox.

7. The cross-chain message is included in a cross-chain update posted on sidechain B.

8. The cross-chain update is processed on sidechain B. The cross-chain message is added to the mainchain inbox and has its effect on sidechain B.

9. Something went wrong! An error occurred during the processing of the message, for instance, sidechain B does not support the cross-chain command ID. A new message is then created:

{

  nonce = 31 : in this example, the current sidechain B nonce

  moduleID = 34 : ID of the custom module

  crossChainCommandID = 2 : ID of the custom cross-chain command

  sendingChainID = 42 : ID of sidechain B

  receivingChainID = 16 : ID of sidechain A

  fee = 0 : message fee is set to 0, no one is paying for the error

  status = CROSS_CHAIN_COMMAND_NOT_SUPPORTED : error status for invalid cross-chain command

  params : custom parameters of the cross-chain message

}

10. The error message is added to the mainchain outbox.

11. The cross-chain message is included in a cross-chain update posted on the mainchain.

12. The cross-chain update is processed on the mainchain. The cross-chain message is added to the sidechain B inbox and immediately added to the sidechain A outbox.

13. The cross-chain message is included in a cross-chain update posted on sidechain A.

14. The cross-chain update is processed on sidechain A. The cross-chain message is added to the mainchain inbox and has its effect on sidechain A. In particular, the error code is used to trigger the correct processing logic (for instance, the message could be simply reverted).

 

Conclusion and Next Topic

This blog post has presented the cross-chain messages in detail. We discussed the general procedure to send a cross-chain message between two interoperable chains and described the format of a cross-chain message. The Interoperability module will contain 4 standard messages by default, while the Token module allows the transfer of standard tokens between chains with the cross-chain token transfer command. Custom modules can implement new cross-chain commands in a similar manner to normal custom commands. The details of the Token module will be presented in the next blog post.

 

If you wish to dive further into the technical specifications, you can view our Research forum, in particular at the “Introduce cross-chain messages” LIP. We are looking forward to your feedback on the Lisk interoperability solution in the Lisk Research forum.

Alessandro Ricottone Ph.D.

Research Scientist

Alessandro grew up in Viareggio, a coastal town in northern Tuscany. He discovered his love for computers while playing "Aldo's adventure" on a 286 as a kid. As a natural consequence, he got passionate into quantum computers and received a PhD in physics from McGill University, specializing in quantum information. He got interested in blockchains back in 2012, but missing the entrepreneurialism to get rich, he just participated in various hackathons. Beside computers, he likes math, nerds, and baking pizza.