How to register a sidechain

Registration overview

To complete the sidechain registration process, perform the following steps:

Please read Sidechain Registration & Recovery to get more information about the lifecycle of a sidechain.

1. How to register a sidechain on the mainchain

A sidechain is registered on the mainchain by posting a Sidechain Registration transaction.

A Sidechain Registration transaction can be sent by any user account on the Lisk Mainchain that has adequate funds to pay the required fee.

The transaction requires specific parameters. It is recommended to prepare the transaction and all required parameters in a dedicated script.

The process of registering a sidechain on the mainchain is explained by following the existing example sidechain_registration.ts, which can be found in the interoperability example in the Lisk SDK repository.

1.1. Sidechain registration fee

When the "Sidechain Registration" command is processed, it creates a sidechain account in the mainchain state which is associated with a unique chain identifier and a name. Hence, every new sidechain occupies a certain namespace within the ecosystem. Additionally, every newly registered sidechain can increase the size of every cross-chain update command posted on the mainchain, because of the longer Merkle proofs.

For these two reasons, the minimum fee for this command has an added constant similar to the extra fee in a Register Validator transaction.

The extra registration fee for a sidechain registration is 10 LSK tokens.

1.2. Connect to a mainchain and a sidechain node

sidechain_registration.ts
import { apiClient, codec, sidechainRegParams, cryptography, Transaction } from 'lisk-sdk';
// Replace this with the path to a file storing the public and private keys of a mainchain account that will send the sidechain registration transaction.
// (Can be any account with enough tokens).
import { keys } from '../default/dev-validators.json';

(async () => {
	const { address } = cryptography;

	// Replace this with the alias of the sidechain node(s)
	const SIDECHAIN_ARRAY = ['pos-sidechain-example-one', 'pos-sidechain-example-two'];
	// Replace this with the alias of the mainchain node(s), e.g. lisk-core
	// Note: The number of mainchain nodes should be equal to sidechain nodes, for this script to work properly.
	const MAINCHAIN_ARRAY = ['mainchain-node-one', 'mainchain-node-two'];
	let i = 0;
	for (const nodeAlias of SIDECHAIN_ARRAY) {
        // Connect to the sidechain node
		const sidechainClient = await apiClient.createIPCClient(`~/.lisk/${nodeAlias}`);
		// Connect to the mainchain node
		const mainchainClient = await apiClient.createIPCClient(`~/.lisk/${MAINCHAIN_ARRAY[i]}`);

        // ...
    }
    process.exit(0);
})();

Next, get the basic node information from the mainchain and sidechain nodes.

The node information is requested to receive the following values:

  • CHAIN_ID (Mainchain): The chain ID of the mainchain is required to sign the transaction.

  • CHAIN_ID (Sidechain): The chain ID of the sidechain is required as a parameter of the transaction.

  • height (Sidechain): The latest height of the sidechain is used to get the data about the currently active sidechain validators.

sidechain_registration.ts
// Get node info data from sidechain and mainchain
const mainchainNodeInfo = await mainchainClient.invoke('system_getNodeInfo');
const sidechainNodeInfo = await sidechainClient.invoke('system_getNodeInfo');

1.3. Get info about the active sidechain validators

To retrieve the BFT parameters of all sidechain validators, invoke the consensus_getBFTParameters endpoint on the sidechain node.

Set the height to the latest block height on the sidechain in the request parameters.

The validator list needs to be lexicographically sorted after the blsKey, to be a valid parameter.

sidechain_registration.ts
// Get info about the active sidechain validators and the certificate threshold
const { validators: sidechainActiveValidators, certificateThreshold } =
    await sidechainClient.invoke('consensus_getBFTParameters', {
        height: sidechainNodeInfo.height,
});

// Sort the validators lexicographically after their BLS keys
(sidechainActiveValidators as { blsKey: string; bftWeight: string }[]).sort((a, b) =>
    Buffer.from(a.blsKey, 'hex').compare(Buffer.from(b.blsKey, 'hex')),
);

1.4. Prepare transaction with parameters

To create a Sidechain Registration transaction, the following information is required:

All these parameters can be prepared as a JSON object.

Create the Register Sidechain transaction using the prepared parameters, and sign it with a mainchain account, that has enough funds to send the transaction and pay the required transaction fees.

sidechain_registration.ts
// Define parameters for the sidechain registration
const params = {
    sidechainCertificateThreshold: certificateThreshold,
    sidechainValidators: sidechainActiveValidators,
    chainID: sidechainNodeInfo.chainID,
    name: nodeAlias.replace(/-/g, '_'),
};

// Get public key and nonce of the sender account
const relayerKeyInfo = keys[2];
const { nonce } = await mainchainClient.invoke<{ nonce: string }>('auth_getAuthAccount', {
    address: address.getLisk32AddressFromPublicKey(Buffer.from(relayerKeyInfo.publicKey, 'hex')),
});

// Create registerSidechain transaction
const tx = new Transaction({
    module: 'interoperability',
    command: 'registerSidechain',
    fee: BigInt(2000000000),
    params: codec.encodeJSON(sidechainRegParams, params),
    nonce: BigInt(nonce),
    senderPublicKey: Buffer.from(relayerKeyInfo.publicKey, 'hex'),
    signatures: [],
});

// Sign the transaction
tx.sign(
    Buffer.from(mainchainNodeInfo.chainID as string, 'hex'),
    Buffer.from(relayerKeyInfo.privateKey, 'hex'),
);

1.4.1. chainID

The chainID of the sidechain. Has to be unique within the Lisk ecosystem.

If the given value is already taken by another sidechain, the "Sidechain Registration" transaction will fail.

In this case, the sidechain has to be restarted completely, or it needs to perform a hardfork to change the chain ID, and resubmit the "Sidechain Registration" transaction with a new value for the chain ID.

To check if a certain chain ID is still available in the network, call RPC endpoint interoperability_isChainIDAvailable with the chain ID as a parameter. If the chain ID is still available, it should return true.

1.4.2. Name

The name of the sidechain is a string that has to be unique within the Lisk ecosystem and should only contain characters from the set [a-z0-9!@$&_.].

If the given value is already taken by another sidechain, the "Sidechain Registration" transaction will fail.

In this case, the sidechain has to be restarted completely, or it needs to perform a hardfork to change the name, and resubmit the "Sidechain Registration" transaction with a new value for the name.

To check if a certain name is still available in the network, call RPC endpoint interoperability_isNameAvailable with the name as parameter. If the name is still available, it should return true.

1.4.3. sidechainValidators

Defines the set of sidechain validators expected to sign the first certificate from the sidechain. Each item contains the following properties:

  1. blsKey: The public BLS key of the validator.

  2. bftWeight: The BFT weight is the weight attributed to the pre-votes and pre-commits cast by a validator, and therefore determines to what extent the validator contributes to finalizing blocks.

The length of the sidechainValidators is equal to the amount of active validators on the sidechain (101 by default).

1.4.4. sidechainCertificateThreshold

An integer defining the minimum BFT weight threshold required for the first sidechain certificate to be valid.

A valid value for the threshold can be obtained by invoking the consensus_getBFTParameters endpoint.

Minimum and maximum values for the certificate threshold are calculated using the Aggregated BFT weight.

How to calculate minimum and maximum certificate thresholds for sidechain validators
const { apiClient } = require('@liskhq/lisk-client');

(async () => {
	// Update the path to point to your sidechain client data folder
	const sidechainClient = await apiClient.createIPCClient('~/.lisk/hello_client');
	const sidechainNodeInfo = await sidechainClient.invoke('system_getNodeInfo');
	// Get active validators from sidechain
	const bftParams = await sidechainClient.invoke('consensus_getBFTParameters', { height: sidechainNodeInfo.height });

	// Calculate the aggregated BFT weight
	let aggregateBFTWeight = BigInt(0);
	for (const validator of bftParams.validators) {
		aggregateBFTWeight += BigInt(validator.bftWeight);
	}

	console.log("certificateThreshold:");
	console.log("min:");
	console.log(aggregateBFTWeight/BigInt(3) + BigInt(1));
	console.log("max:");
	console.log(aggregateBFTWeight);
	process.exit(0);
})();
Aggregated BFT weight

The aggregated BFT weight is the sum of BFT weights of all active validators at a specific block height.

The aggregated BFT weight is used to calculate the minimum and maximum values of :

Minimum threshold:

min = floor( 1/3 * Aggregated BFT weight ) + 1

Maximum threshold:

max = Aggregated BFT weight

1.5. Send the transaction to the mainchain

Send the registerSidechain transaction to a node that is connected to the mainchain.

sidechain_registration.ts
// Post the transaction to a mainchain node
const result = await mainchainClient.invoke<{
    transactionId: string;
}>('txpool_postTransaction', {
    transaction: tx.getBytes().toString('hex'),
});

console.log(
    `Sent sidechain registration transaction on mainchain node ${MAINCHAIN_ARRAY[1]}. Result from transaction pool is: `,
    result,
);

The node will respond with the transaction ID if it accepts the transaction.

2. How to verify the sidechain registration

2.1. Check the sidechain account

Once the Sidechain Registration command is processed, the sidechain account status is set to registered.

To verify that the account was created successfully, request the interoperability_getChainData endpoint from a mainchain node.

Parameters:

  • chainID: The chain ID of the registered sidechain.

  • Mainchain node CLI

  • cURL

lisk-core endpoint:invoke interoperability_getChainData '{"chainID": "04000001"}'
curl --location --request POST 'http://localhost:7887/rpc' \
--header 'Content-Type: application/json' \
--data-raw '{
    "jsonrpc": "2.0",
    "id": "1",
    "method": "interoperability_getChainData",
    "params": {
         "chainID": "04000001"
    }
}'

This will return the respective sidechain account stored in the Chain substore of the mainchain.

A newly created sidechain account on the mainchain
{
    "id": "1",
    "jsonrpc": "2.0",
    "result": {
        "lastCertificate": {
            "height": 0,
            "timestamp": 0,
            "stateRoot": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
            "validatorsHash": "58fa1be3fca7aef9952a7640397124229837079b14a144907a7e3373685daceb"
        },
        "name": "pos-sidechain-example-one",
        "status": 0
    }
}

2.2. Check the sidechain channel

To verify that the channel to the sidechain was created successfully, request the interoperability_getChannelData endpoint from a mainchain node.

Parameters:

  • chainID: The chain ID of the registered sidechain.

  • Mainchain node CLI

  • cURL

lisk-core endpoint:invoke interoperability_getChannelData '{"chainID": "04000001"}'
curl --location --request POST 'http://localhost:7887/rpc' \
--header 'Content-Type: application/json' \
--data-raw '{
    "jsonrpc": "2.0",
    "id": "1",
    "method": "interoperability_getChannelData",
    "params": {
         "chainID": "04000001"
    }
}'

This will return the respective sidechain account stored in the Channel substore of the mainchain.

A newly created sidechain channel on the mainchain
{
    "id": "1",
    "jsonrpc": "2.0",
    "result": {
        "messageFeeTokenID": "0000000000000000",
        "outbox": {
            "appendPath": [
                "16dfaad8458dd4ae56ba2787593c2c5a55b14ba734c0431f177212226cf8328b"
            ],
            "root": "16dfaad8458dd4ae56ba2787593c2c5a55b14ba734c0431f177212226cf8328b",
            "size": 1
        },
        "inbox": {
            "appendPath": [],
            "root": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
            "size": 0
        },
        "partnerChainOutboxRoot": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
    }
}

2.3. Check the sidechain validators

To verify that the sidechain validators list was created successfully, request the interoperability_getChainValidators endpoint from a mainchain node.

Parameters:

  • chainID: The chain ID of the registered sidechain.

  • Mainchain node CLI

  • cURL

lisk-core endpoint:invoke interoperability_getChainValidators '{"chainID": "04000001"}'
curl --location --request POST 'http://localhost:7887/rpc' \
--header 'Content-Type: application/json' \
--data-raw '{
    "jsonrpc": "2.0",
    "id": "1",
    "method": "interoperability_getChainValidators",
    "params": {
         "chainID": "04000001"
    }
}'

This will return the respective sidechain account stored in the Chain Validators substore of the mainchain.

A newly created chain validators list on the mainchain
{
    "id": "1",
    "jsonrpc": "2.0",
    "result": {
        "activeValidators": [
            {
                "blsKey": "8012798d2ac6b93df3bfa931192d9b2d496c3c947958a7408232e08872895557c06c1b94f5cc2555e28addbaadf3e0bd",
                "bftWeight": "1"
            },
            // ...
        ],
        "certificateThreshold": "65"
    }
}

3. How to register the mainchain on the sidechain

The mainchain is registered on a sidechain by posting a Register Mainchain transaction. A "Register Mainchain" transaction can be sent by any user account in the sidechain that has adequate funds to pay the required fee.

The process of registering the mainchain on a sidechain is explained by following the existing example mainchain_registration.ts, which can be found in the Lisk SDK repository.
  • The mainchain registration process always has to occur after the sidechain registration on the mainchain, since the sidechain has no prior knowledge of its name and must be certain that the correct chain ID has been registered.

  • It is of key importance that the sidechain validators ensure that they are signing the registration command with the correct information, otherwise, the sidechain interoperable functionality may be unusable.

  • If the mainchain and/or sidechain registration command was submitted with the wrong data, then the whole sidechain needs to be restarted completely with the correct values, or it needs to hardfork in order to connect to the mainchain.

To create a Register Mainchain transaction, the following information is required:

3.1. Preparing the Register Mainchain parameters

The parameters for the Register Mainchain transaction are created in an analog way as the Sidechain Registration parameters have been created in the previous step.

common/mainchain_registration.ts
import {
	codec,
	cryptography,
	apiClient,
	Transaction,
	registrationSignatureMessageSchema,
	mainchainRegParams as mainchainRegParamsSchema,
	MESSAGE_TAG_CHAIN_REG,
} from 'lisk-sdk';

/**
 * Registers the mainchain to a specific sidechain node.
 *
 * @example
 * ```js
 * // Update path to point to the dev-validators.json file of the sidechain which shall be registered on the mainchain
import { keys as sidechainDevValidators } from '../default/dev-validators.json';

 * (async () => {
 *	await registerMainchain("lisk-core","my-lisk-app",sidechainDevValidators);
 *})();
 * ```
 *
 * @param mc mainchain alias of the mainchain to be registered.
 * @param sc sidechain alias of the sidechain, where the mainchain shall be registered.
 * @param sidechainDevValidators the `key` property of the `dev-validators.json` file.
 * Includes all keys of the sidechain validators to create the aggregated signature.
 */

export const registerMainchain = async (mc: string, sc: string, sidechainDevValidators: any[]) => {
	const { bls, address } = cryptography;

	// Connect to the mainchain node
	const mainchainClient = await apiClient.createIPCClient(`~/.lisk/${mc}`);
	// Connect to the sidechain node
	const sidechainClient = await apiClient.createIPCClient(`~/.lisk/${sc}`);

	// Get node info from sidechain and mainchain
	const mainchainNodeInfo = await mainchainClient.invoke('system_getNodeInfo');
	const sidechainNodeInfo = await sidechainClient.invoke('system_getNodeInfo');

	// Get active validators from mainchain
	const {
		validators: mainchainActiveValidators,
		certificateThreshold: mainchainCertificateThreshold,
	} = await mainchainClient.invoke('consensus_getBFTParameters', {
		height: mainchainNodeInfo.height,
	});

	// Sort validator list lexicographically after their BLS key
	const paramsJSON = {
		ownChainID: sidechainNodeInfo.chainID,
		ownName: sc.replace(/-/g, '_'),
		mainchainValidators: (mainchainActiveValidators as { blsKey: string; bftWeight: string }[])
			.map(v => ({ blsKey: v.blsKey, bftWeight: v.bftWeight }))
			.sort((a, b) => Buffer.from(a.blsKey, 'hex').compare(Buffer.from(b.blsKey, 'hex'))),
		mainchainCertificateThreshold,
	};

	// Define parameters for the mainchain registration
	const params = {
		ownChainID: Buffer.from(paramsJSON.ownChainID as string, 'hex'),
		ownName: paramsJSON.ownName,
		mainchainValidators: paramsJSON.mainchainValidators.map(v => ({
			blsKey: Buffer.from(v.blsKey, 'hex'),
			bftWeight: BigInt(v.bftWeight),
		})),
		mainchainCertificateThreshold: paramsJSON.mainchainCertificateThreshold,
	};

	// Encode parameters
	const message = codec.encode(registrationSignatureMessageSchema, params);
}

The next step is to collect and aggregate the corresponding signatures from the sidechain validators, see Preparing the aggregated signature.

3.1.1. ownChainID

The chain ID of the sidechain.

Should be identical to chainID.

3.1.2. ownName

Sets the name of the sidechain in its own state according to the name given in the mainchain.

Should be identical to the Name.

3.1.3. mainchainValidators

Defines the set of mainchain validators expected to sign the first certificate from the mainchain. Each item contains the following properties:

  1. blsKey: The public BLS key of the validator.

  2. bftWeight: The BFT weight of the validator.

To retrieve the BFT parameters of all mainchain validators, invoke the consensus_getBFTParameters endpoint on the mainchain node.

3.1.4. mainchainCertificateThreshold

mainchainCertificateThreshold is an integer that defines the minimum BFT weight threshold required for the first mainchain certificate to be valid.

Minimum and maximum values for the certificate threshold are calculated with the Aggregated BFT weight.

3.2. Preparing the aggregated signature

To create a valid signature, enough sidechain validators need to individually sign the mainchain registration message, so that the total weight is equal or greater to the sidechainCertificateThreshold. By signing the mainchain registration message, they verify the correctness of the following values: ownChainID, ownChainName, mainchainCertificateThreshold, and mainchainValidators. All individual signatures are then aggregated into one signature and the corresponding aggregationBits, which are appended to the Register Mainchain parameters.

The individual signing of the mainchain registration by the sidechain validators is executed off-chain, similar to the process of creating multi-signature transactions.

3.2.1. Collecting the signatures of the sidechain validators

The process of signing the registration parameters needs to be coordinated off-chain by the sidechain validators.

The most important parts of the script are the following:

common/mainchain_registration.ts
// Get active validators from sidechain
const { validators: sidechainActiveValidators } = await sidechainClient.invoke(
    'consensus_getBFTParameters',
    { height: sidechainNodeInfo.height },
);

// Add validator private keys to the sidechain validator list
const activeValidatorsBLSKeys: { blsPublicKey: Buffer; blsPrivateKey: Buffer }[] = [];
for (const activeValidator of sidechainActiveValidators as {
    blsKey: string;
    bftWeight: string;
}[]) {
    const sidechainDevValidator = sidechainDevValidators.find(
        devValidator => devValidator.plain.blsKey === activeValidator.blsKey,
    );
    if (sidechainDevValidator) {
        activeValidatorsBLSKeys.push({
            blsPublicKey: Buffer.from(activeValidator.blsKey, 'hex'),
            blsPrivateKey: Buffer.from(sidechainDevValidator.plain.blsPrivateKey, 'hex'),
        });
    }
}

console.log('Total activeValidatorsBLSKeys:', activeValidatorsBLSKeys.length);

// Sort active validators from the sidechain lexicographically after their BLS public key
activeValidatorsBLSKeys.sort((a, b) => a.blsPublicKey.compare(b.blsPublicKey));

const sidechainValidatorsSignatures: { publicKey: Buffer; signature: Buffer }[] = [];

// Sign parameters with each active sidechain validator
for (const validator of activeValidatorsBLSKeys) {
    const signature = bls.signData(
        MESSAGE_TAG_CHAIN_REG,
        params.ownChainID,
        message,
        validator.blsPrivateKey,
    );
    sidechainValidatorsSignatures.push({ publicKey: validator.blsPublicKey, signature });
}

const publicKeysList = activeValidatorsBLSKeys.map(v => v.blsPublicKey);
console.log('Total active sidechain validators:', sidechainValidatorsSignatures.length);

The validator signatures need to be collected by the person intending to send the Register Mainchain transaction.

Once the aggregated BFT weight of the validators who signed is equal to or above the sidechainCertificateThreshold, enough validators have signed the registration parameters.

3.2.2. Aggregating the validator signatures

Once enough validators add their signatures, the list of signatures is aggregated into one single signature, which is then appended to the registration params that we created in step Preparing the Register Mainchain parameters.

signature

The signature property is an aggregate signature of the sidechain validators. It ensures that the sidechain validators agree on registering the mainchain in the sidechain.

aggregationBits

The aggregationBits property is a bit vector used to validate the aggregate signature.

common/mainchain_registration.ts
// Create an aggregated signature
const { aggregationBits, signature } = bls.createAggSig(
    publicKeysList,
    sidechainValidatorsSignatures,
);

// Get public key and nonce of the sender account
const relayerKeyInfo = sidechainDevValidators[0];
const { nonce } = await sidechainClient.invoke<{ nonce: string }>('auth_getAuthAccount', {
    address: address.getLisk32AddressFromPublicKey(Buffer.from(relayerKeyInfo.publicKey, 'hex')),
});

// Add aggregated signature to the parameters of the mainchain registration
const mainchainRegParams = {
    ...paramsJSON,
    signature: signature.toString('hex'),
    aggregationBits: aggregationBits.toString('hex'),
};

// Create registerMainchain transaction
const tx = new Transaction({
    module: 'interoperability',
    command: 'registerMainchain',
    fee: BigInt(2000000000),
    params: codec.encodeJSON(mainchainRegParamsSchema, mainchainRegParams),
    nonce: BigInt(nonce),
    senderPublicKey: Buffer.from(relayerKeyInfo.publicKey, 'hex'),
    signatures: [],
});

// Sign the transaction
tx.sign(
    Buffer.from(sidechainNodeInfo.chainID as string, 'hex'),
    Buffer.from(relayerKeyInfo.privateKey, 'hex'),
);

3.3. Posting a Register Mainchain transaction

Send the registerMainchain transaction to a node that is connected to the mainchain.

common/mainchain_registration.ts
// Post the transaction to a sidechain node
const result = await sidechainClient.invoke<{
    transactionId: string;
}>('txpool_postTransaction', {
    transaction: tx.getBytes().toString('hex'),
});

console.log('Sent mainchain registration transaction. Result from transaction pool is: ', result);

The node will respond with the transaction ID if it accepts the transaction.

4. How to verify the mainchain registration

4.1. Check the mainchain account

Once the "Register Mainchain" command is processed, the mainchain account is initialized and its status is set to registered.

To verify that the account was created successfully, request the interoperability_getChainData endpoint from a sidechain node.

Parameters:

  • chainID: The chain ID of the registered mainchain.

  • Node CLI

  • cURL

If you maintain your own instance of a sidechain node, it is possible to invoke endpoints directly via the node CLI:

./bin/run endpoint:invoke interoperability_getChainData '{"chainID": "04000000"}'

Replace localhost:7887 with the IP and port to a node that is connected to the sidechain.

curl --location --request POST 'http://localhost:7887/rpc' \
--header 'Content-Type: application/json' \
--data-raw '{
    "jsonrpc": "2.0",
    "id": "1",
    "method": "interoperability_getChainData",
    "params": {
         "chainID": "04000000"
    }
}'

This should return the mainchain account stored in the Chain substore of the sidechain.

A newly registered mainchain account on the sidechain
{
  "lastCertificate": {
    "height": 0,
    "timestamp": 0,
    "stateRoot": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
    "validatorsHash": "36d3f20ea724a9708ead8b00e52c68d9188f1655e057bb523b19db35271d9073"
  },
  "name": "lisk_mainchain",
  "status": 0
}

4.2. Check the mainchain channel

  • Node CLI

  • cURL

If you maintain your own instance of a sidechain node, it is possible to invoke endpoints directly via the node CLI:

./bin/run endpoint:invoke interoperability_getChannelData  '{"chainID": "04000000"}' --pretty

Replace localhost:7887 with the IP and port to a node that is connected to the sidechain.

curl --location --request POST 'http://localhost:7887/rpc' \
--header 'Content-Type: application/json' \
--data-raw '{
    "jsonrpc": "2.0",
    "id": "1",
    "method": "interoperability_getChannelData",
    "params": {
         "chainID": "04000000"
    }
}'

This should return the mainchain channel data stored in the Channel substore of the sidechain.

Example response
{
  "messageFeeTokenID": "0400000000000000",
  "outbox": {
    "appendPath": [
      "4d48ae83b249d1b409d2d7f1ae18792e7aeb15f647bd8a607c6639723a76a487"
    ],
    "root": "4d48ae83b249d1b409d2d7f1ae18792e7aeb15f647bd8a607c6639723a76a487",
    "size": 1
  },
  "inbox": {
    "appendPath": [],
    "root": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
    "size": 0
  },
  "partnerChainOutboxRoot": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
  "minReturnFeePerByte": "1000"
}

4.3. Check the mainchain validators

  • Node CLI

  • cURL

If you maintain your own instance of a sidechain node, it is possible to invoke endpoints directly via the node CLI:

./bin/run endpoint:invoke interoperability_getChainValidators '{"chainID": "04000000"}' --pretty

Replace localhost:7887 with the IP and port to a node that is connected to the sidechain.

curl --location --request POST 'http://localhost:7887/rpc' \
--header 'Content-Type: application/json' \
--data-raw '{
    "jsonrpc": "2.0",
    "id": "1",
    "method": "interoperability_getChainValidators",
    "params": {
         "chainID": "04000000"
    }
}'

This should return the mainchain validators data stored in the Chain Validators substore of the sidechain.

Example response
{
  "activeValidators": [
    {
      "blsKey": "817cdb74e2136473f579ec7c5ff341e98a36a7ebfd1baadded8fbb315fcda6e77fd73f2132be39a018fcc3ae10b4a0e6",
      "bftWeight": "1"
    },
    {
      "blsKey": "830b156d85886dc0ea00cf76643030b82b4b80660a99e6b38386cd3774b8f1252f243a5ae2b38df915b1f71d5f5dfda3",
      "bftWeight": "1"
    },
    {
      "blsKey": "832d2cdd741b0df3f80311ed90deec394c13ed05da115d3c1fc135c96556911c6b00592451de0ab37296417559852080",
      "bftWeight": "1"
    },
    {
      "blsKey": "8394e9cfa7c81ff453ea93e50aca110dc1bb823b2681d8b7900787abd207a6268b34ff7d95e0f60530e05e45607a924e",
      "bftWeight": "1"
    },
    {
      "blsKey": "851d06def623bafccf24911d7baac97dbbdfdd3d9ebef92a912bbb335223e9f73e111ed181fb46bd5fd31044820246de",
      "bftWeight": "1"
    },
    {
      "blsKey": "85657021983a5019c8bd7461bbe04b830291d1094e26534131770b14dd3a88768a6ee57a6324bb8fa726f7c24d3f4507",
      "bftWeight": "1"
    },
    {
      "blsKey": "859a15723bc6fbef835069e843cc6f8a8221ffded4e9d2455b5520f63538775a5de66501e514a1c37d85df1c5092c418",
      "bftWeight": "1"
    },
    {
      "blsKey": "86896756d01d6c5b03f23647d7bce1cdab601ae8adb20d86614d13aaaa0e40a6bb8e524a4637d47cfc4bb1cece538862",
      "bftWeight": "1"
    },
    {
      "blsKey": "877a80b466c2090b917726b305c6f3861aea2c7e8ae9d9653fe5b871b2873d8a045643e202c007c927f9c90d65946d68",
      "bftWeight": "1"
    },
    {
      "blsKey": "87e74f2352400144080e849248c668e16da593c1d661a11ee20d5c31f149ec546729f93950b673b66dea6a33c9260d78",
      "bftWeight": "1"
    },
    {
      "blsKey": "8824e9c41ead5ee18e5fe71709f100351a9724320b7c444b0f14fa5e8f877470872edb58533442c933ee97fae8dba785",
      "bftWeight": "1"
    },
    {
      "blsKey": "897597d4dc645d4aad51246ed1d9d6b0c8f81bd1809fa9106ce367a791f0b98151bfa4b49b1251d6b307240ba1dad106",
      "bftWeight": "1"
    },
    {
      "blsKey": "89ffe19409b06f1e7854a9a5512b0d045a4ec696dc74b7d7d33117c9a8e1cfd68e185b09ae276d1a1bc3942395eb4dce",
      "bftWeight": "1"
    },
    {
      "blsKey": "8ad40d292e82ffaf9f75517b03df7b5693adba8b0f1654791de0a92a3ab0bfb26bf7d90b4cfed334d5480c329ad81614",
      "bftWeight": "1"
    },
    {
      "blsKey": "8adb937e0050d9fcf66321e2b55bc89f76592f359a07559e76c2e0cebc4c1611feb852d9f9172b71dc970a4766f9c935",
      "bftWeight": "1"
    },
    {
      "blsKey": "8aeba1cc038ad2cf1ba6ae1479f293f1e3c074369c3afe623e6921ac4cd6c959647ca85fe197228c38dda1df18812d32",
      "bftWeight": "1"
    },
    {
      "blsKey": "8b517672ecb3b38a766baedd0cedd7a0f2f2a568c067ac7b75087452c7f879c68951661e82cc2e2d1224f57b24fd0874",
      "bftWeight": "1"
    },
    {
      "blsKey": "8c43fcdeed68cefc70e02af16b5d18938f12d6a994d0ceb1519a0bfdba07ebaa3b92cd64204f411f501defaa5bce0a98",
      "bftWeight": "1"
    },
    {
      "blsKey": "8c6a9cc624212b23d5e4afc935db990b99b2f6a46390b9819ec4c34e4da73ceb49ce247eadacddb39a874b5404ca8982",
      "bftWeight": "1"
    },
    {
      "blsKey": "8d4298f04a38dd65f2b8ba59276d8da350aaf08f2d5f8101f1d23001394f74389a8c7339b1ba4a9167ca663de4b36c28",
      "bftWeight": "1"
    },
    {
      "blsKey": "8e6b900d97becb167b6182f815a4c50797538d221b5fef9d0eaeba0771560f8ec5b78e1bd9f466a823c02cead6b00006",
      "bftWeight": "1"
    },
    {
      "blsKey": "8ed44b95d761d3534ea609c7a015f465f9829a0d68c5efb04d7a2043b6be5ebc175c52277b4bb064c727018164b3e41e",
      "bftWeight": "1"
    },
    {
      "blsKey": "8edcba4c5f1c12f2d4834146cc3685d63d0ddceac3eae8aa55bfe72fc3adfae9aad0fffdc244a6d84125929add786206",
      "bftWeight": "1"
    },
    {
      "blsKey": "8f1861579e9344ed88748206a2fce1a040f974c40733fafd2a98bef813250c602bc2d3e4dd97836c638f1e8522d9304f",
      "bftWeight": "1"
    },
    {
      "blsKey": "8f3d6b429550ba9e9cab338cccb803b8553f1ab8c393089a772f8d40580683bbdef82b6972f8cafb1a6e3703f186f731",
      "bftWeight": "1"
    },
    {
      "blsKey": "8f4c1f0421fc4e84a9aaff71b15ea5387f2c2023de5764018c6260ac756ea4bcc1fa1e1b2adca51c41f33788f977202e",
      "bftWeight": "1"
    },
    {
      "blsKey": "8f558a92ea2ddb97aaf1950b2ad8cacf3bbb6df0ba6f3bb2af7e7dd2bb8c8f695af1d4cf89148f36bc7cc1b6c3c9b3e6",
      "bftWeight": "1"
    },
    {
      "blsKey": "91de8f0b3e01dcf01973a8a7ba0c787fad98e9e485b71b78ea5f4d6d684f3f8f74f25120818729bdcf19dec78beac82b",
      "bftWeight": "1"
    },
    {
      "blsKey": "921fab12f32b83338acd846c4f2e22e78d5657a9ad6e26a1b953235b837c97c1994b410fb98ed9d60043046bc49cf04b",
      "bftWeight": "1"
    },
    {
      "blsKey": "945159215901b8094983b7c457d47350031a6894ca2168bd9902de0248e11f7a7b3849820e7bbf8d12c9111df5dd0c59",
      "bftWeight": "1"
    },
    {
      "blsKey": "946f53073e217c65c31f135950827706bffc53a1714ef6476751f88e499cb2405fdbc68bffb715b94774a37c6c49b695",
      "bftWeight": "1"
    },
    {
      "blsKey": "94db4e95e0bdf2a2f2dad6fe1758d598cf39e5f5013d30812d89983560fd630e730d7f8443f8f8f7dedb2817d7199dd3",
      "bftWeight": "1"
    },
    {
      "blsKey": "94eab1c0dd071eb08efffdfc9fa4a1a084f18b155c265cea14b897f9ef8da838f061d072aec8b1fddf64d982db5383f2",
      "bftWeight": "1"
    },
    {
      "blsKey": "94f3abffbb40e97d2b024c614377a49bc02ccb96f5c39d5f5240cabed3abf71a79dfd1415819c945bd7bfbd6ce974e29",
      "bftWeight": "1"
    },
    {
      "blsKey": "962ad95f9eb8de66f40ae8650e4a1b9715db8b93082f94a999c5513b59b34ae765bbdf52525348eb2c3bfae9b74be5d0",
      "bftWeight": "1"
    },
    {
      "blsKey": "9649a2666c4397e558ac70522551fa2282bc12cb9f0efaaaeb723916050115eb6a06944047e27b2765c48a8fdc54723b",
      "bftWeight": "1"
    },
    {
      "blsKey": "964bb701671ac2ff8c74479ac55edd73425552873f62b0f0c7e806d31b97b04b3c3fce13746b9758c577fabc35569484",
      "bftWeight": "1"
    },
    {
      "blsKey": "974e9ad5b6dc8589d13f6ea588e13e2f911d274bdca25a0eaf95e78070ada24ebf055e7e96f4bc5d54d1e1ecbb88891b",
      "bftWeight": "1"
    },
    {
      "blsKey": "97ec41664e973d6c47a6eb42a71cdab2b32cab143ebbff4987ca1f21b70d2a4a31865fa63e498172ddfcdc66c375df3c",
      "bftWeight": "1"
    },
    {
      "blsKey": "981ed9e0ecefede2baabe2190da98bcda364e9cd2cdc84294104f76baa96357b208b8a46b35815ff70a2782321c7a997",
      "bftWeight": "1"
    },
    {
      "blsKey": "988b6193525235d5c772e65b777d699b5a6a2c78b9588aba6ba791cc90f8ad64d811b60db840d6335af8c1b56e64a6a8",
      "bftWeight": "1"
    },
    {
      "blsKey": "98c1650f3954150884b93926f0b9bb2895a86b7f53548b55c54a8baaf6298011dda1d4c59b0de58ff0b7049c84d4bb91",
      "bftWeight": "1"
    },
    {
      "blsKey": "a07a39359e00f2dc70586643b0192b72bf5cac12664c9b3e86196b9ff8ab42d2de0c99feaabe150a2f158bc5655af28a",
      "bftWeight": "1"
    },
    {
      "blsKey": "a13d891ddf147982e0c9a5f131e06e9839fbfbc1c2e45a0e36047420cf096c7befdb2368999f9127ae1c1351cd006360",
      "bftWeight": "1"
    },
    {
      "blsKey": "a227caf7eaccffa90c894e8338f1bf792936d3e70392970f87246c88ba8e37ac48242c6b9d94f0f34f45cca988d723a4",
      "bftWeight": "1"
    },
    {
      "blsKey": "a377f60b451ee1824e4bcef98ee59ed2d9e9e79fda80e9f9cb4d5142ec6e34c3cb2ba0b58fa9cda79ccfecf9be1944e1",
      "bftWeight": "1"
    },
    {
      "blsKey": "a3d818681e97be169c51414f692f743ffeb5fbf60478710a8dd4ab883044d2bf8b318bbf931316a30755061338cbf857",
      "bftWeight": "1"
    },
    {
      "blsKey": "a4ae34b8c356805c5a71cc7b9344eb190004733dc6ec97f2cb19249595ac2ccfd11a25ddbf455be0b17573e306c71301",
      "bftWeight": "1"
    },
    {
      "blsKey": "a4f8a7ab03605906f89a6b3e2a6091c53feae76f3a9e48593732fc027babbbfd6cf6fa5c98251f8453cb6b57fda99ced",
      "bftWeight": "1"
    },
    {
      "blsKey": "a5966288b2fcc72df1a9ec5434dd3a4120bfad14b96939591ddc3312f5d4645ac9715fc9f261ea969a96122c94e71c8e",
      "bftWeight": "1"
    },
    {
      "blsKey": "a5c327e70f2d173b3bbb1dc69e57e03bd4b32f91a8874309656cf9a364e0ab6f1b2fe76e3690981c53e32223530f9009",
      "bftWeight": "1"
    },
    {
      "blsKey": "a67a75058d23b7b9ef52c7de10f1faddb7ffdd53dd9daa0d77455bd6d202b6e5056c37b5fd09a336baaaa3e31fd73875",
      "bftWeight": "1"
    },
    {
      "blsKey": "a6f7e55f71ae7fdcaa2bcbda040c091e76862d4cadcea2d483c0af7af337e2bfd53d120ffa1c0b445f04ac3a74a1f369",
      "bftWeight": "1"
    },
    {
      "blsKey": "a7252bc36c9fdaf4310c65727558545882683a08d4c46e3f278de2ecc3bd76e66e9aa2917f8707d11893a0911b39f3c0",
      "bftWeight": "1"
    },
    {
      "blsKey": "a74d911e73ccf66c0d60b622fe507608aeb5af06b5a4258a4d1e83168688217eaf05ef350342c186c593cdc834fb1593",
      "bftWeight": "1"
    },
    {
      "blsKey": "abbc6a25a487fb9de634713c7c5ab755dacb33a39c84d711c7b2934ddc529c8f953044fe85e97cbebf9ab517ce0c36d6",
      "bftWeight": "1"
    },
    {
      "blsKey": "acc6b671c8e4843ac5c7db7625a5f4c4d6823dd93bf06e2eb16c7263543a58cee5243715efb41b982a02f0bdcca5e412",
      "bftWeight": "1"
    },
    {
      "blsKey": "af3f9f14bf7db60f3283f21f20516d1d82caece0ef1d4ee88ab6f8f0be46af594105114ae2eff847ace826b1a163cadf",
      "bftWeight": "1"
    },
    {
      "blsKey": "b0508dc694713f5fcd4a60d134d8254d18d70559981864f594f74150aaf49724e32ee6bad8933107609d807d9ea7942b",
      "bftWeight": "1"
    },
    {
      "blsKey": "b092e4bb3eb9c291d94cfac62f44f889b0727981e81115d69573a97766845e1b17f553bdd9208462605cfda44c058411",
      "bftWeight": "1"
    },
    {
      "blsKey": "b0dd52e91344ee8d961b01fda84f6bfd2c71bde2317e0bb01a6821b9dec36e10cfa01153fa5f427f598c04ab50681518",
      "bftWeight": "1"
    },
    {
      "blsKey": "b178489edeb8b41d987b5a898d9dc084e23f6793f6318d5aa2073e021830d74f1b49fb663cebcb2d549e354f0c6fe320",
      "bftWeight": "1"
    },
    {
      "blsKey": "b2be3c23c2ee5a80ee46ee6685cc759baec4d6e5fcb553ed09160ca0c2dc4e6e0ebc361ab48faec07acb08f1f9f43e8a",
      "bftWeight": "1"
    },
    {
      "blsKey": "b31f1cdafe5ae69ea79cb01e38b377d489f7ea6374ad92a6eeb1c599dc791f91711901c53dd19f584873f2eb95a4a0c0",
      "bftWeight": "1"
    },
    {
      "blsKey": "b346b18b84af84321cd00979443560b8d50615940fa6430f3f187db5971324761aaa50fb08b86480b1b175f696125ad6",
      "bftWeight": "1"
    },
    {
      "blsKey": "b462fb1caa2f877f2e5a23742a2e4aa52b285fcf8413191232b4c78f3a1f545d8531b103867cdff0e7a65ceb05ddbdae",
      "bftWeight": "1"
    },
    {
      "blsKey": "b47b918f6488ec7e77233e01fcf469ac4c3322add127fe5bf4598a8c8e8d2dfe27fa94b1bd46201883a0e578c32454d4",
      "bftWeight": "1"
    },
    {
      "blsKey": "b48052c2a26c2853e048b0d3781b4401e57e4628e2557ac4845d7d980ef03f997c4c71687370c63c044aa51244af1875",
      "bftWeight": "1"
    },
    {
      "blsKey": "b48b57d9ae6b0658430050bc0336005eba5a4f318589d8cb2641a6e813c6ee4e6a3158a503b18e06702591337c4adfc7",
      "bftWeight": "1"
    },
    {
      "blsKey": "b57ded217776a9bbd06939f6465300e922284e12e3bf584565fcc0526297a4234fc1f47829f9242491bb42c20b46f9e3",
      "bftWeight": "1"
    },
    {
      "blsKey": "b59d42bc9c72ec01ca4cb668953d25b150f936b39556fea44b59ab58a7696dda1ee48fa71947f8b6937b642d844cf7a2",
      "bftWeight": "1"
    },
    {
      "blsKey": "b5d6cc8adeff2f0f40d15dd9c8854810951915a2c5b02b562385e4f67ee979c17c4f1888d8c878a4346f42b963a5cfc6",
      "bftWeight": "1"
    },
    {
      "blsKey": "b6056ba058859f1f0e8985550ae76214301802a3149727e233edf7a63d9dac9317f116adc8fde387adc207a81cd57d9a",
      "bftWeight": "1"
    },
    {
      "blsKey": "b6fed2994e0796ddcad19dec93bcaabf2bdb458a57847e23cff3e5bd70183cd89946bc4f6289494da369cc3e64e7726d",
      "bftWeight": "1"
    },
    {
      "blsKey": "b79172fc4333d255ee3a1884b78ffde5901638e60ed674ccaadd57921448c4dcbc2cd5d9faf1cf71c5a48d27b89c482b",
      "bftWeight": "1"
    },
    {
      "blsKey": "b7ca4bc931e95a39d9b04d4f03ffd50c2bf1de9261901e5b3e9783b953b3171f877733d8db2189317ed2bf865199fcf6",
      "bftWeight": "1"
    },
    {
      "blsKey": "b809509d27fc57d0bcb96eec7aa0ae0a5fc44f31bf22c288ccd7745969148e6a559ee374c183154b6f3fa0a5269e1206",
      "bftWeight": "1"
    },
    {
      "blsKey": "b87c1c8c3f07f5bcf067eea76ca9328da862bbfffd9af24ec63271307b3e2490aec28d8c8ffb00ed6873357a3859f22c",
      "bftWeight": "1"
    },
    {
      "blsKey": "b8f904f1e724e1dfc6075fa763eb60987a2e51e9863a45cf7691deaeb698e66769919d19d3be85daed169bd3266ffc20",
      "bftWeight": "1"
    },
    {
      "blsKey": "b9a15fb236e20f5857e062579e8deb7384153e4b0655014964a5501f25fc43d7268f97c45343b71bf7ac3423c45b401b",
      "bftWeight": "1"
    },
    {
      "blsKey": "b9b285aa8e0f5811f19250ab6893d7a9a502d1b0430302487a226eb905e2e42413c15945f2cda5cc472298856b9f9fa8",
      "bftWeight": "1"
    }
  ],
  "certificateThreshold": "65"
}

5. Relayer nodes and app-registry

Relayer nodes are required to send cross-chain messages from the sidechain to the mainchain and vice-versa.

To facilitate cross-chain communication, at least one relayer node is required on the mainchain and the sidechain.

5.1. Setup relayer nodes on mainchain and sidechain

Please check the guide Setting up a relayer node for a step-by-step explanation of how to turn a node into a relayer.

5.2. Register off-chain data in the app-registry

As a last step, it is necessary to register the sidechain related off-chain metadata to the app registry.

The data can be provided as a pull request to the repository, which adds a new folder under the respective network with the following structure:

.
├── app-registry/betanet/<APP>/
│   └── app.json
│   ├── nativetokens.json
│   └── images/
│   │   ├── application
│   │   │   ├── app.png
│   │   │   └── app.svg
│   │   └── tokens
│   │   │   ├── token.png
│   │   │   └── token.svg
tsconfig.json

where <APP> is the name of the sidechain app.

Please refer to the README.md for all important details about the app registration process, like the required image formats and sizes.
app.json

Off-chain metadata of a sidechain, to be used by client applications such as Lisk Desktop, Mobile and other explorers.

Example: app.json
{
    "title": "Lisk - Betanet",
    "description": "Metadata configuration for the Lisk blockchain (mainchain) in betanet",
    "chainName": "Lisk",
    "chainID": "02000000",
    "networkType": "betanet",
    "genesisURL": "https://downloads.lisk.com/lisk/betanet/genesis_block.json.tar.gz",
    "projectPage": "https://lisk.com",
    "logo": {
        "png": "https://raw.githubusercontent.com/LiskHQ/app-registry/main/betanet/Lisk/images/application/lisk.png",
        "svg": "https://raw.githubusercontent.com/LiskHQ/app-registry/main/betanet/Lisk/images/application/lisk.svg"
    },
    "backgroundColor": "#f7f9fb",
    "serviceURLs": [
        {
            "http": "https://betanet-service.lisk.com",
            "ws": "wss://betanet-service.lisk.com"
        }
    ],
    "explorers": [
        {
            "url": "https://betanet.liskscan.com",
            "txnPage": "https://betanet.liskscan.com/transactions"
        }
    ],
    "appNodes": [
        {
            "url": "https://betanet.lisk.com",
            "maintainer": "Lightcurve GmbH"
        },
        {
            "url": "wss://betanet.lisk.com",
            "maintainer": "Lightcurve GmbH"
        }
    ]
}
nativetokens.json

Off-chain metadata about the native tokens of a sidechain.

Example: nativetokens.json
{
    "title": "Lisk - Betanet - Native tokens",
    "tokens": [
        {
            "tokenID": "0200000000000000",
            "tokenName": "Lisk",
            "description": "Default token for the entire Lisk ecosystem",
            "denomUnits": [
                {
                    "denom": "beddows",
                    "decimals": 0,
                    "aliases": [
                        "Beddows"
                    ]
                },
                {
                    "denom": "lsk",
                    "decimals": 8,
                    "aliases": [
                        "Lisk"
                    ]
                }
            ],
            "baseDenom": "beddows",
            "displayDenom": "lsk",
            "symbol": "LSK",
            "logo": {
                "png": "https://raw.githubusercontent.com/LiskHQ/app-registry/main/betanet/Lisk/images/tokens/lisk.png",
                "svg": "https://raw.githubusercontent.com/LiskHQ/app-registry/main/betanet/Lisk/images/tokens/lisk.svg"
            }
        }
    ]
}
images/application/

Icons to be used for the sidechain application.

images/tokens/

Icons to be used for the native tokens of the sidechain.

5.3. Move LSK to sidechain

This step is not necessary, if the default token for the CCM fees (LSK) is changed to a native sidechain token.

To be able to pay the CCM transaction fees, it is necessary to move a sufficient amount of LSK tokens from the mainchain to the sidechain via a cross-chain transfer transaction.

Sending CCMs from the mainchain to the sidechain does not Activate the sidechain, yet. Only a CCM from the sidechain to the mainchain will enable the liveness condition.

Create the following transaction with your mainchain node:

Example: Creating a cross-chain transfer transaction
$ lisk-core transaction:create token transferCrossChain 10000000
? Please enter passphrase:  [hidden]
? Please enter: tokenID:  0100000000000000
? Please enter: amount:  1000000000
? Please enter: receivingChainID:  01000001
? Please enter: recipientAddress:  lsk24cd35u4jdq8szo3pnsqe5dsxwrnazyqqqg5eu
? Please enter: data:  cross-chain transfer
? Please enter: messageFee:  1
? Please enter: messageFeeTokenID:  0100000000000000
  • tokenID: The token ID of the LSK token.

  • receivingChainID: Enter the chain ID of the respective sidechain where you intend to transfer your LSK tokens.

  • recipientAddress: Enter the recipient address for the account on the sidechain.

  • messageFee: Enter the fee for the CCM.

  • messageFeeTokenID: Enter the token ID of the token that should be used to pay the fee. Enter here the tokenID of the LSK token.

To verify that the cross-chain transfer was executed successfully, request the token_getBalance endpoint with the recipient address as a parameter.

Alternatively, the following script can be used to perform the cross-chain transfer: transfer_lsk_sidechain_one.ts

To make it work for your sidechain, please adjust the script as explained in the code comments of the script.

The script is executed like so:

ts-node pos-mainchain-fast/config/scripts/transfer_lsk_sidechain_one.ts

6. Activate the sidechain

After registering the sidechain and setting up the required relayer nodes, the sidechain is ready to be activated.

To activate the sidechain, send a first CCU from sidechain to mainchain.

  • The first cross-chain update (CCU) to be submitted on the mainchain must contain a certificate less than 15 days old.

  • Once the sidechain is active, it needs to fulfill the liveness condition, otherwise, it will be terminated.

Once the first CCU is sent successfully from the sidechain to the mainchain, the sidechain is activated and is interoperable with the mainchain and other sidechains within the Lisk ecosystem.