Setting up a relayer node

On this page, you’ll learn:

  • How to set up a relayer node on a sidechain?

  • How to set up a relayer node on a mainchain?

The process of setting up a relayer node on a mainchain and sidechain are almost identical. The subsequent sections describe the steps to set up a relayer node.

Prerequisites

To use this guide, it is assumed that the following criteria have been met:

  • An up-and-running node connected to the mainchain exists, see How to set up a Lisk Mainnet node.

  • An up-and-running node connected to the sidechain exists.

  • The sidechain must be registered to the mainchain as described in the How to register a sidechain guide.

  • The sidechain account on the mainchain must have either the registered or active status.

1. Installing the Chain Connector plugin

The chain connector plugin comes pre-installed with any chain that has been initialized with Lisk SDK v6. However, in case it is required to install it manually, run the following command in the root folder of the client.

npm install @liskhq/lisk-framework-chain-connector-plugin

2. Configuration

To set up a relayer node, one needs to set up the node according to the following configurations:

2.1. Configuring the Chain Connector plugin

Once the plugin is installed, various configurations are available for the node operator to set. With these configurations, the details such as the receiving chain’s chainID and its IPC path or WS URL, CCU frequency, encrypted private key, etc. can be set.

The following table describes the available configuration options.

  • Configuration options

  • Sample configuration

Name Type Description Sample

receivingChainIPCPath

string

The IPC path of a receiving node

~/.lisk/lisk-core

receivingChainWsURL

string

The WS URL of a receiving node

ws://142.93.230.246:4002/rpc-ws

ccuFrequency

integer

Number of blocks after which a CCU should be created

1

encryptedPrivateKey

string

Encrypted privateKey of the relayer

"kdf=argon2id&cipher=aes-256-gcm&version=1&ciphertext=da36ef64cbf2687d62b014dafdfa8ef8c823b2b1562ae78819599080e4500529b75e80093fba066879f0767e0de83abe285efb259dd9be5109b8a4ef66cfc52ec613314586c1aa1da3a6737c0f8b7f0de7fb4d1b85860cd23915bbcee774e1d85b357e342816a917e517f7c702e1a1deb28dd69a4b69ae2ac67a5c4c4236101c&mac=e253decce05dd50758400d5c7408532a162fedf583ff9cafcb7ad3e12f6b8011&salt=40ab2dcdd387e4372ed1dbb948a9ef84&iv=4559dcaee67eb2c1a0957ecf&tag=81c2c332d915454bed4be26018c598c5&iterations=1&parallelism=4&memorySize=2024"

ccuFee

string

The fee to be paid for each CCU transaction. It’s an optional value and is only used if the user wants to pay a fee higher than the minimum calculated fee. In that case, the minimum calculated fee is overridden with the value passed with the ccuFee property. If the user-provided fee is lower than the calculated minimum fee then the calculated minimum fee will be used in that case.

100000000

isSaveCCU

boolean

Flag for the user to either save or send a CCU on creation. Sending a CCU is the default option.

false

ccuSaveLimit

integer

Number of CCUs to save.

300

maxCCUSize

integer

Maximum size of CCU to be allowed

50

registrationHeight

integer

Height at the time of registration on the receiving chain.

100

receivingChainID

string

Chain ID of the receiving chain.

04000000

{
    // [...]
    "plugins": {
        "chainConnector": {
            "encryptedPrivateKey": "kdf=argon2id&cipher=aes-256-gcm&version=1&ciphertext=da36ef64cbf2687d62b014dafdfa8ef8c823b2b1562ae78819599080e4500529b75e80093fba066879f0767e0de83abe285efb259dd9be5109b8a4ef66cfc52ec613314586c1aa1da3a6737c0f8b7f0de7fb4d1b85860cd23915bbcee774e1d85b357e342816a917e517f7c702e1a1deb28dd69a4b69ae2ac67a5c4c4236101c&mac=e253decce05dd50758400d5c7408532a162fedf583ff9cafcb7ad3e12f6b8011&salt=40ab2dcdd387e4372ed1dbb948a9ef84&iv=4559dcaee67eb2c1a0957ecf&tag=81c2c332d915454bed4be26018c598c5&iterations=1&parallelism=4&memorySize=2024",
            "ccuFee": "800000",
            "receivingChainIPCPath": "~/.lisk/lisk-core",
            "receivingChainID": "04000000"
        }
    }
}
A node operator must add either the value of receivingChainIPCPath or receivingChainWsURL in the chain connector’s config.

2.2. Configuring the system

Before we discuss how to set up a node to act as a relayer node, we need to understand the concept of prefixes, how to retrieve them, and how to calculate inclusionProofKeys.

2.2.1. What are Prefixes and how to get them?

We can get the MODULE_PREFIX and the SUBSTORE_PREFIX from the system_getMetadata endpoint. For more information, see LIP 40. Each module has a set of stores and each store has a unique key. After invoking the aforementioned endpoint, search for the "stores" property of the interoperability module.

Then, look for the outbox substore of the interoperability module and note down the value of the key property. The following details have been retrieved by invoking the system_getMetadata endpoint.

Example of store info retrieved via the "system_getMetadata" endpoint.
"stores": [
    {
        "key": "83ed0d250000",
        "data": {
            "$id": "/modules/interoperability/outbox",
            "type": "object",
            "required": [
                "root"
            ],
            "properties": {
                "root": {
                    "dataType": "bytes",
                    "minLength": 32,
                    "maxLength": 32,
                    "fieldNumber": 1
                }
            }
        }
    },
]
Table 1. Properties required to create "inclusionProofKeys"
Prefix Description

MODULE_PREFIX

The first 4 bytes of the key value is the MODULE_PREFIX. For example, in the above-mentioned snippet, the MODULE_PREFIX is: 83ed0d25.

SUBSTORE_PREFIX

The last 2 bytes of the key value is the SUBSTORE_PREFIX. For example, in the above-mentioned snippet, the SUBSTORE_PREFIX is: 0000.

STORE_KEY

The STORE_KEY is a hexadecimal string, calculated based on the Chain ID of the sidechain. For example, the Chain ID used in the following example is: 04000001.

The MODULE_PREFIX+SUBSTORE_PREFIX is already available in the system’s metadata and it can be found in the response of system_getMetadata. For the example above, the "key": "83ed0d250000" is equal to MODULE_PREFIX+SUBSTORE_PREFIX.

2.2.2. Calculating inclusionProofKeys

To save inclusion proof of outboxRoot for a sidechain on the mainchain on the outbox substore, do the following:

Calculating "inclusionProofKeys"
// 1. Calculate the MODULE_PREFIX using the Interoperability module.
MODULE_PREFIX = Buffer.from('83ed0d25', 'hex')

// 2. Calculate the SUBSTORE_PREFIX using the Outbox Substore.
SUBSTORE_PREFIX = Buffer.from('0000', 'hex') ()

// 3. Calculate the STORE_KEY using the Chain ID of a sidechain.
STORE_KEY = Buffer.from('04000001', 'hex') ()

// 4. Finally, calculate the inclusionProofKeys as described below:
inclusionProofKeys = Buffer.concat([MODULE_PREFIX, SUBSTORE_PREFIX, cryptography.utils.hash(STORE_KEY)]);

Once the inclusionProofKeys value(s) is retrieved, it should be added to the system configuration available inside the config.json file of a node. To save inclusion proof with every new block we need to pass the following configurations:

  • Configuration options

  • Sample configuration

Name Type Description Sample

keepInclusionProofsForHeights

number

Number of blocks for which the inclusion proofs are to be kept. By default, inclusion proofs are kept for the latest 300 blocks. If -1 is passed to this property, then it will keep all the inclusion proofs forever.

-1

inclusionProofKeys

string[]

Contains the keys for which we need to store inclusion proofs against the state tree. This can be created or deduced by the user by concatenating the following: MODULE_PREFIX + SUBSTORE_PREFIX + HASH(STORE_KEY)

"inclusionProofKeys": [
			"83ed0d250000fb5e512425fc9449316ec95969ebe71e2d576dbab833d61e2a5b9330fd70ee02"
		]
{
    // Default configurations
    "system": {
		"dataPath": "~/.lisk/pos-sidechain-example-one",
		"keepEventsForHeights": 300,
		"logLevel": "info",

        // Properties relevant to setting up a relayer node
		"keepInclusionProofsForHeights": -1,
		"inclusionProofKeys": [
			"83ed0d250000fb5e512425fc9449316ec95969ebe71e2d576dbab833d61e2a5b9330fd70ee02"
		]
	},
}

The above approach should be used to configure the Chain connector plugin as the plugin requires the inclusion proof of the outbox root of the receiving chain to calculate outboxRootWitness in CCU transaction.

3. Enabling the Chain Connector plugin

A node operator can perform the following steps to enable the chain connector plugin and turn a node into a relayer node.

3.1. Creating an encrypted private key

  1. The first step is to create an encrypted private key. A node operator can use a REPL session to call the Lisk cryptography libraries.

    • Sidechain

    • Mainchain

    ./bin/run console
    Entering Lisk REPL: type `Ctrl+C` or `.exit` to exit
    lisk-core console
    Entering Lisk REPL: type `Ctrl+C` or `.exit` to exit
  2. The encryptedPrivateKey can be created by calling the encryptMessageWithPassword function. It accepts two arguments: a private key of the account which is supposed to be used as a relayer address and a password.

    The account should have sufficient balance so that the encrypted private key can be used for signing and sending the transaction.

    • Sidechain

    • Mainchain

    Creating an encrypted key on a sidechain node
    sidechain_client> lisk.cryptography.encrypt.stringifyEncryptedMessage(await lisk.cryptography.encrypt.encryptMessageWithPassword('0d7501d3d5c9accaefb3c0b6a569473b59391ae406f6324f98fa6dd70e119368a6454f898d3b82c41b158206c72ecfe917a1071c8084b496a0c5867afc10830b', 'lisk'))
    Encrypted key
    'kdf=argon2id&cipher=aes-256-gcm&version=1&ciphertext=57db80457db93a1abeceee5c6f951ca04579c447a06f45cf5e8b5398e207a26da53a6b191a02c479ede455950eacb48f32d6609f2cd4b5a1a51e895b210b587ef046e6c3151ef2212efd0808b45328742d09a279e7d667f1670ff02a2fd5c91f4afd0a08efb8e6e90b0b11e93b15da8daaeea543a0ff54f3dd51c66cac3b04c6&mac=7822258b12e0c787f5bd622c562914438a9d74ca1e11e11b840f3001a678b04f&salt=d4d051a123326ad2b82c022603e790b6&iv=0bb9e76cd5163d6c5af9d89d&tag=fbcdb355b5135d48df948841de5fcdf5&iterations=1&parallelism=4&memorySize=2024'
    Creating an encrypted key on a mainchain node
    lisk-core> lisk.cryptography.encrypt.stringifyEncryptedMessage(await lisk.cryptography.encrypt.encryptMessageWithPassword('0d7501d3d5c9accaefb3c0b6a569473b59391ae406f6324f98fa6dd70e119368a6454f898d3b82c41b158206c72ecfe917a1071c8084b496a0c5867afc10830b', 'lisk'))
    Encrypted key
    'kdf=argon2id&cipher=aes-256-gcm&version=1&ciphertext=f4dd49061a128d06184308a235311dc487737b7c4a688409224ed39b7d8e76a6cdd814500dd7221297ed122d277af8ba46d42ebd340d228fe6c77132543b303c97ab89e151ecd9f2739284c60c66ab68c0f3531ffc6cbdedad2acc431e8d8e48dffd7c7eda3dfe5f404e00ef7ae825d34da7787bf792b6ecb84ea1bfe10e9ca6&mac=363141e645d5564a150a2634060bd273276b0c987a65cf64513a7871565c3f2a&salt=93213d2d1c11e91d64771c173f8bf3c1&iv=0132fa14a4ed289deb07ee11&tag=7b64ed4a0453302d54bba29d4f7a68ea&iterations=1&parallelism=4&memorySize=2024'

    The encryptMessageWithPassword function will return an encrypted key, which should be added to the config of the blockchain.

3.2. Prepare config for chain connector plugin

Each node whether a mainchain or a sidechain expects mandatory configurations as shown in the following snippets.

  • Sidechain

  • Mainchain

"chainConnector": {
    "encryptedPrivateKey": "kdf=argon2id&cipher=aes-256-gcm&version=1&ciphertext=57db80457db93a1abeceee5c6f951ca04579c447a06f45cf5e8b5398e207a26da53a6b191a02c479ede455950eacb48f32d6609f2cd4b5a1a51e895b210b587ef046e6c3151ef2212efd0808b45328742d09a279e7d667f1670ff02a2fd5c91f4afd0a08efb8e6e90b0b11e93b15da8daaeea543a0ff54f3dd51c66cac3b04c6&mac=7822258b12e0c787f5bd622c562914438a9d74ca1e11e11b840f3001a678b04f&salt=d4d051a123326ad2b82c022603e790b6&iv=0bb9e76cd5163d6c5af9d89d&tag=fbcdb355b5135d48df948841de5fcdf5&iterations=1&parallelism=4&memorySize=2024",
    "ccuFee": "800000",
    "receivingChainIPCPath": "~/.lisk/lisk-core"
    "receivingChainID": "04000000"
}
"chainConnector": {
    "encryptedPrivateKey": "kdf=argon2id&cipher=aes-256-gcm&version=1&ciphertext=f4dd49061a128d06184308a235311dc487737b7c4a688409224ed39b7d8e76a6cdd814500dd7221297ed122d277af8ba46d42ebd340d228fe6c77132543b303c97ab89e151ecd9f2739284c60c66ab68c0f3531ffc6cbdedad2acc431e8d8e48dffd7c7eda3dfe5f404e00ef7ae825d34da7787bf792b6ecb84ea1bfe10e9ca6&mac=363141e645d5564a150a2634060bd273276b0c987a65cf64513a7871565c3f2a&salt=93213d2d1c11e91d64771c173f8bf3c1&iv=0132fa14a4ed289deb07ee11&tag=7b64ed4a0453302d54bba29d4f7a68ea&iterations=1&parallelism=4&memorySize=2024",
    "ccuFee": "800000",
    "receivingChainIPCPath": "~/.lisk/relayer",
    "receivingChainID": "04000002"
}

Once the configuration is ready, update the config.json file of the respective client such as mainchain or sidechain.

3.3. Register plugin

Once the config.json has been updated, it is required to register the plugin with the client. The process differs for both sidechain and mainchain.

  • Sidechain

  • Mainchain

On a sidechain, the plugin can be enabled using the --enable-chain-connector-plugin flag whilst starting the blockchain client.

./bin/run start --enable-chain-connector-plugin --overwrite-config

Alternatively, the plugins.ts file of the client can be updated to have the following options:

sidechain_client/src/app/plugins.ts
import { Application } from 'lisk-sdk';
// Import the 'ChainConnectorPlugin'
import { ChainConnectorPlugin } from '@liskhq/lisk-framework-chain-connector-plugin';

export const registerPlugins = (app: Application): void => {
    // Register the ChainConnectorPlugin with the app
    app.registerPlugin(new ChainConnectorPlugin());
};

The client must be rebuilt to entertain the changes to the code.

npm run build

The client can then be started with the following command:

./bin/run start --overwrite-config

On a mainchain, the plugin can be enabled using the --enable-chain-connector-plugin flag whilst starting the blockchain client.

lisk-core start --network alphanet --enable-chain-connector-plugin --overwrite-config

Since the config of the chain is updated during the process, the node operator must update the existing config with the --overwrite-config flag.

3.4. Result

Once the client is running, the node operators should see the following log messages, depending on the type of node.

  • Sidechain

  • Mainchain

2023-03-17T14:42:30.426Z INFO XYZ.local application 96733 No valid CCU can be generated for the height: 58
2023-03-17T14:42:30.426Z INFO XYZ.local plugin_chainConnector 96899 No valid CCU can be generated for the height: 58

Since we just set up a relayer node and haven’t sent a CCU/CCM, the aforementioned log messages are expected.

The messages suggest that the blockchain doesn’t have any finalized block height for which we can create a certificate, or there are no pending CCUs/CCMs to send across the chain.

The relayer node will start relaying CCUs to the receiving chain, once the finalized height is reached.

Once the plugin is enabled, it is essential to invoke the chainConnector_authorize endpoint to authorize the signing and sending of CCUs.

A node can also output various warnings or error messages as log output; the details of which can be seen in the following tables:

Table 2. Various log messages and their descriptions
Log message Description

Warnings messages: The following log messages can be considered as warnings and they don’t require restarting a node or plugin to fix the underlying issue as such messages usually suggest problems with chain registration or apiClient.

Sending chain is not registered to the receiving chain yet and has no chain data.

As the log suggests, the sending chain is not registered yet on the receiving chain but at the same time, all the data related to CCU creation is saved such as blockheaders, aggregateCommits, events containing CCM creation, proof of outboxRootWitness and validatorsInfo, etc. For more information read LIP 53.

No CCU generation is possible as the node is syncing.

In this scenario, the node is syncing with the network, hence, it cannot generate a CCU.

No attempt to create CCU either due to provided ccuFrequency

In this scenario, the data required for CCU creation is saved and only CCU creation is delayed due to the provided ccuFrequency.

Receiving chain is not registered yet on the sending chain.

In this case, the plugin is running fine and saving data for CCU creation, however, it cannot yet create or send CCU as the user has not yet registered the receiving chain on their own chain. For more information, read LIP 43.

CCU cant be created as there are no pending CCMs for the last certificate

If no new certificate has been created since the last certificate and no pending CCMs are left then, no CCU can be created and the user needs to wait for the next certificate height on its sending chain.

Not able to connect to receivingChainAPIClient. Trying again on next new block.

In this case, the plugin will try again on the next new block if the API issue is resolved to create/send a CCU.

Error occurred while submitting CCU for the blockHeader at height:

The log message suggests that the CCU was created successfully but its submission failed for some reason. In this case, the user needs to check the error attached to the log to debug further.

Error occurred while accessing receivingChainAPIClient but all data is saved on newBlock.

This error occurs when receivingChainAPIClient is not working, the user needs to check the receivingChainAPIClient or receivingChain node if they are working properly or not once they see this error within the logs. For example, receivingChainWsURL or receivingChainIPCPath provided by a user could be incorrect. In this case, all the data is saved required for CCU creation which means when the user fixes apiClient, the plugin would still be able to create/send CCUs.

Error messages: The following error-related logs should be fixed by the user and require a node restart. In these cases, the plugin is usually missing the data required to create CCUs, and that usually happens when something goes wrong while saving data on the plugin. Such errors also occur when the user enables the config.registrationHeight or the height after the height from where the plugin started looking for certificates.

No block header found for the last certified height ${lastCertificate.height}.

In this case, the plugin is looking at a different height and when it is unable to find a certificate, it logs out the aforementioned error.

No validatorsHash preimage data present for the given validatorsHash

As the error suggests, the node is unable to retrieve the validatorHash preimage at the given height.

No validators data found for the given validatorsHash

If the node is unable to find data based on the given validatorsHash on the given height, it will log the aforementioned error.

No validators data found for the given last certificate height

If the node is unable to find data based on the given last certificate height, it will log the aforementioned error.

Possible ways to debug the case when the first CCU is not sent and the user sees the aforementioned errors are described below:

  • Reset the node and start syncing from the height before config.registrationHeight.

  • Try to give a higher value to the config.registrationHeight property from where the plugin started storing data correctly, assuming that the chain of trust is maintained.

Sometimes if the node is very busy with syncing over a long period of time, the plugin misses out on storing information from the new block event, which might lead to the aforementioned errors. This should not usually happen, however, if it does then, the user must re-sync their node.