LIP: 0043
Title: Introduce chain registration mechanism
Author: Iker Alustiza <iker@lightcurve.io>
Discussions-To: https://research.lisk.com/t/chain-registration
Status: Draft
Type: Standards Track
Created: 2021-05-22
Updated: 2021-12-01
Requires: 0038, 0045, 0049, 0056
This LIP introduces the concept of chain registration in the Lisk ecosystem. The chain registration is a necessary step to make a sidechain interoperable with the Lisk mainchain. Specifically, for the Lisk mainchain, this LIP specifies a new command for the sidechain registration. This command creates a sidechain account in the Lisk mainchain with some specific properties given by the user submitting the transaction. Similarly for sidechains, this LIP specifies the mainchain registration command.
This LIP is licensed under the Creative Commons Zero 1.0 Universal.
The Lisk ecosystem is a permissionless network of blockchains where a sidechain following the standard protocol can interoperate with any other Lisk compatible chain. In particular, the Lisk mainchain serves as a central node or relayer for the entire ecosystem and every cross-chain interaction has to be sent through it. This implies there should exist a standardized protocol for the Lisk mainchain to maintain a cross-chain channel to communicate with every sidechain.
The first step for establishing this cross-chain channel protocol is the chain registration process, which can be thought of as the creation/opening of the channel between two chains. This process defines the data structures, and protocol rules that every chain needs to implement in the ecosystem if they want to interoperate with another specific chain.
For a general picture of the Lisk interoperability architecture, please refer to LIP 0045.
In this LIP, the registration process introduced in the previous section is specified from the point of view of both the Lisk mainchain and sidechains. For the Lisk mainchain, this is done by the sidechain registration command, whereas for sidechains, it is done by the mainchain registration command.
As mentioned above, for a sidechain to be interoperable in the Lisk ecosystem, it has to be registered in the Lisk Mainchain via a sidechain registration command. A transaction with this command can be sent by any user account in the Lisk Mainchain with enough funds to pay the required fee. The processing of this command implies the creation of a sidechain account in the mainchain state associated with a unique network identifier and a name. This means that every new sidechain occupies a certain namespace in the ecosystem. Additionally, every newly registered sidechain can increase the size of every cross-chain update command posted on the mainchain (due to the increasing size of the outboxRootWitness
property of the command). For these two reasons, the minimum fee for this command has an added constant similar to the extra fee in a delegate registration command. The value of this extra registration fee is 10 LSK.
Once the sidechain registration command is processed, the sidechain account status is set to registered. In this state, the cross-chain channel is still not active, so the users on the mainchain or other chains cannot send cross-chain messages (CCMs) to this sidechain yet. Moreover, the liveness requirement to maintain the channel is not enforced, this means that there is no specific time requirement for a sidechain to be activated on the mainchain, it can stay in the registered status for any period of time. When a first valid cross-chain update command from this sidechain is processed, the sidechain status is changed to active, making it active in the ecosystem. Now it is possible to send CCMs to the sidechain and the liveness condition is enforced.
When a new sidechain is registered on the mainchain via a sidechain registration command, it is assigned a chain ID, a unique integer that identifies the sidechain in the ecosystem. Furthermore, new data structures are inserted for the sidechain in the Lisk mainchain state. Specifically, a new entry is created in six different substores of the interoperability module store (see Figure 1): outbox root substore, chain data substore, channel data substore, chain validators substore, registered names, and registered network IDs. The values of these entries are initialized as specified in LIP 0045.
Figure 1: A summary of the Interoperability module store: Each box represents a substore, where we indicate the storeKey --> storeValue
relation. For the Lisk mainchain, the 'own chain' substore exists by default in the state whereas there is one entry per registered sidechain for six other substores (outbox root, chain data, channel data, chain validators, registered names, and registered network IDs) created by a sidechain registration command. For sidechains, the 'own chain' and one entry for the mainchain account for four other substores (outbox root, chain data, channel data, and chain validators) are created by the mainchain registration command.
The sidechain registration command contains the following parameters used to connect a new sidechain in the ecosystem.
The name
property sets the name of the sidechain as a string of characters. It has to be unique in the ecosystem.
The ID of the genesis block (as defined in LIP 0034). It is computed from the SHA-256 digest of the serialized bytes of the sidechain genesis block. It can also help future sidechain node operators to identify the sidechain genesis block with respect to its value.
This property defines the set of eligible BLS public keys with their respective BFT weights required to sign the first certificate from the sidechain.
The certificateThreshold
property is an integer setting the required cumulative weight needed for a certificate signature from the sidechain to be valid.
The chain ID uniquely identifies a chain in the Lisk ecosystem. It serves a similar purpose for chains as addresses do for user accounts. The chain ID for a sidechain is deterministically computed when processing the sidechain registration command. Specifically, the chain ID of a new sidechain is assigned as an incremental integer similar to transaction nonces. For example, if there are 41 chains already registered in the Lisk ecosystem (the mainchain and 40 sidechains), the next registered sidechain will have chainID = 42
. The chain ID has to be stated in every cross-chain interaction. For example, it has to be specified in the receivingChainID
property of a CCM to this sidechain and in the sendingChainId
property of a cross-chain update command from this sidechain.
The format of chain IDs aims to provide an efficient and compact way to uniquely identify chains in the ecosystem. This has another advantage in terms of usability: Users can easily remember the integer assigned as chain ID for their favorite blockchain applications.
The network identifier, or network ID, is a byte sequence unique to a chain that has to be prepended to the input of the signing function of every transaction, block, or message of the chain. It is necessary to avoid transaction replays between different chains in the ecosystem.
In the Lisk ecosystem, the network ID for a sidechain is computed as the hash digest of the sidechain genesis block ID given in a transaction with the registration command and the address of the account sending this transaction. This is convenient for two reasons:
- The sidechain developers can pre-compute the network ID of their sidechain as soon as they set the genesis block and an account to send the transaction on the mainchain. The sidechain can be started already from this moment without being registered on the mainchain.
- The network ID is known to the mainchain as soon as the sidechain is registered, thus it can validate cross-chain update commands coming from the sidechain without further context.
This LIP overwrites the network ID definition given in LIP 0009 and solves the open problem given in the Rationale section of that LIP. Note that in the case of a sidechain undergoing a community hard fork, one of the competing forks will need to register their fork of the sidechain again on the Lisk Mainchain. Regardless of the genesis block ID set by this sidechain fork, it has to be registered from a different user account which implies that the chain ID and network ID of the forked chain will be different from the original one.
Once the sidechain has been registered on the mainchain, a similar registration process should happen in the sidechain before the interoperable channel is opened between the two chains. This is done by submitting a transaction with the mainchain registration command in the sidechain, which implies the creation of a mainchain account in the sidechain state associated with the Lisk mainchain and other structures needed for interoperability. This mainchain account has a similar structure as the one depicted in Figure 1. By protocol, the chain ID of the mainchain is a constant equal to 1 in the ecosystem. The network ID of the mainchain is also a constant since it has to be known to validate cross-chain update commands.
This registration process has to happen always after the sidechain registration on the mainchain since the sidechain has no prior knowledge of the current mainchain validators or its own chain ID and name. Similar to the sidechain registration case, the mainchain account status will not change to active until a valid cross-chain update command from the mainchain containing a valid registration CCM is processed.
The mainchain registration command sets certain parameters in the sidechain related to the interoperability module and initializes the corresponding mainchain data structures. This command requires the approval of the sidechain validators. They have to agree on the content of this command and add their aggregated signatures accordingly. It is important that the sidechain validators make sure that they are signing the registration command with the right information from the mainchain, otherwise, the sidechain interoperable functionality may be unusable.
This command has no requirement for a minimum fee since it should be submitted only once in a sidechain and approved by a majority of validators. For this reason, a transaction with this command should be treated differently in terms of priority in case it is included in a sidechain node’s transaction pool. The recommendation is that, once the transaction is properly signed by the validators and ready to be submitted, a validator simply includes it in its next forged block without including it in the transaction pool. The command has the following parameters:
Similar to the initValidators
property in the sidechain registration command, it defines the set of mainchain validators with their respective BFT weight expected to sign the first certificate from the mainchain.
The chain ID assigned to this sidechain on the mainchain after processing the corresponding sidechain registration command.
The ownName
property sets the name of the sidechain in its own state according to the name given in the mainchain.
Name | Type | Value | Description |
---|---|---|---|
Interoperability Constants | |||
MODULE_ID_INTEROPERABILITY |
uint32 | 64 | ID of the interoperability module. |
MAINCHAIN_ID |
bytes | 1 | Chain ID of the Lisk mainchain. |
MAINCHAIN_NAME |
string | "lisk-mainchain" | Name of the Lisk mainchain. |
MAINCHAIN_NETWORK_ID |
bytes | TBD | Network identifier of the Lisk mainchain. |
Interoperability Store | |||
STORE_PREFIX_OUTBOX_ROOT |
bytes | 0x0000 | Store prefix of the outbox root substore. |
STORE_PREFIX_CHAIN_DATA |
bytes | 0x8000 | Store prefix of the chain data substore. |
STORE_PREFIX_CHANNEL_DATA |
bytes | 0xa000 | Store prefix of the channel data substore. |
STORE_PREFIX_CHAIN_VALIDATORS |
bytes | 0xb000 | Store prefix of the chain validators substore. |
STORE_PREFIX_TERMINATED_STATE |
bytes | 0xc000 | Store prefix of the terminated state substore. |
STORE_PREFIX_TERMINATED_OUTBOX |
bytes | 0xd000 | Store prefix of the terminated outbox substore. |
STORE_PREFIX_REGISTERED_NAMES |
bytes | 0xe000 | Store prefix of the chain names substore. |
STORE_PREFIX_REGISTERED_NETWORK_IDS |
bytes | 0xf000 | Store prefix of the chain network IDs substore. |
Interoperability Command IDs | |||
COMMAND_ID_SIDECHAIN_REG |
uint32 | 0 | Command ID of sidechain registration command. |
COMMAND_ID_MAINCHAIN_REG |
uint32 | 1 | Command ID of mainchain registration command. |
CROSS_CHAIN_COMMAND_ID_REGISTRATION |
uint32 | 0 | Cross-chain command ID of chain registration CCM. |
General Constants | |||
REGISTRATION_FEE |
uint64 | 1000000000 | Fee to pay for a sidechain registration command. |
MAX_NUM_VALIDATORS |
uint32 | 199 | Maximum number of validators in a sidechain. |
MAX_LENGTH_NAME |
uint32 | 40 | Maximum allowed name for a sidechain. |
MAX_UINT32 |
uint32 | 4294967295 | Maximum value for a 32-bit unsigned integer. |
MAX_UINT64 |
uint64 | 18446744073709551615 | Maximum value for a 64-bit unsigned integer. |
THRESHOLD_MAINCHAIN |
uint32 | 68 |
The certificate threshold used for Lisk mainchain. |
TAG_CHAIN_REG_MESSAGE |
bytes | ASCII encoded string “LSK_CHAIN_REGISTRATION_” . |
Message tag for chain registration message |
Calling a function fct
from a module module
is represented by module.fct(required inputs)
. In this LIP, functions from the Validators module and the BFT module are used.
The command ID of this transaction is COMMAND_ID_SIDECHAIN_REG
.
This command has an extra fee:
extraCommandFee(MODULE_ID_INTEROPERABILITY, COMMAND_ID_SIDECHAIN_REG) = REGISTRATION_FEE
where REGISTRATION_FEE
is a constant given in the protocol.
sidechainRegParams = {
"type": "object",
"required": [
"name",
"genesisBlockID",
"initValidators",
"certificateThreshold"
],
"properties": {
"name": {
"dataType": "string",
"fieldNumber": 1
},
"genesisBlockID": {
"dataType": "bytes",
"fieldNumber": 2
},
"initValidators": {
"type": "array",
"fieldNumber": 3,
"items": {
"type": "object",
"required": ["blsKey", "bftWeight"],
"properties": {
"blsKey": {
"dataType": "bytes",
"fieldNumber": 1
},
"bftWeight": {
"dataType": "uint64",
"fieldNumber": 2
}
}
}
},
"certificateThreshold": {
"dataType": "uint64",
"fieldNumber": 4
}
}
}
Let trs
be a transaction with module ID MODULE_ID_INTEROPERABILITY
and command ID COMMAND_ID_SIDECHAIN_REG
to be verified. Then the set of validity rules to validate trs.params
are:
- The
trs.params.name
property has to contain only characters from the set[a-z0-9!@$&_.]
and has to be at mostMAX_LENGTH_NAME
characters long. - The
trs.params.name
property has to be unique with respect to the set of already registered sidechain names in the blockchain state, such that the entry with store prefix equal toSTORE_PREFIX_REGISTERED_NAMES
and store key equal totrs.params.name
(serialized as a utf-8 encoded string) does not exist in the store. - Let
netId
be the hash digest ofSHA-256(trs.params.genesisBlockId || senderAddress)
where||
indicates bytes concatenation andsenderAddress
is the address of the user account corresponding to thetrs.senderPublicKey
property:- Then
netId
has to be unique with respect to the set of already registered sidechain network IDs in the blockchain state. That is the entry with store prefix equal toSTORE_PREFIX_NETWORK_ID
and store key equal totrs.params.netID
does not exist in the store.
- Then
- Rules for
trs.params.initValidators
array:- The array must have at least 1 element and at most
MAX_NUM_VALIDATORS
elements. - The array must be ordered lexicographically by
blsKey
property. - The
blsKey
property of each element has a length of 48 bytes. - The
blsKey
property of each element is unique within the array. - The
bftWeight
property of each element is a positive integer. - Let
totalWeight
be the sum of thebftWeight
property of every element in thetrs.params.initValidators
array. ThentotalWeight
has to be less than or equal toMAX_UINT64
.
- The array must have at least 1 element and at most
- The range of valid values of the
trs.params.certificateThreshold
property is given by the total sum of the validators weights:- Minimum value:
floor(1/3 * totalWeight) + 1
. - Maximum value:
totalWeight
.
- Minimum value:
When a sidechain registration command is executed, new entries for the Interoperability module are created in the mainchain state (see Figure 1). In particular, let trs
be a transaction with module ID MODULE_ID_INTEROPERABILITY
and command ID COMMAND_ID_SIDECHAIN_REG
to be executed. Then:
-
The chain ID of the sidechain is assigned as
chainID = currentHighestChainID + 1
, wherecurrentHighestChainID
is the highest chain ID among all registered chains on the Lisk mainchain (included). -
An entry in the chain data substore as specified in LIP 0045 is created as:
storePrefix
:STORE_PREFIX_CHAIN_DATA
storeKey
:uint32be(chainID)
.storeValue
:sidechainAccount
wheresidechainAccount
is the chain account object, serialized according to thechainAccountSchema
schema as defined in LIP 0045, and initialized as:- The
sidechainAccount.name
property is equal to thetrs.params.name
property. - The
sidechainAccount.networkID
property is calculated as the output ofSHA-256(trs.params.genesisBlockId || senderAddress)
where||
indicates bytes concatenation andsenderAddress
is the address of the user account corresponding to thetrs.senderPublicKey
property. - The
sidechainAccount.lastCertificate.validatorsHash
property is set tobft.computeValidatorsHash(trs.params.initValidators, trs.params.certificateThreshold)
. - The rest of the properties are initialized to their default values.
- The
-
An entry in the channel data substore as specified in LIP 0045 is created as:
-
storePrefix
:STORE_PREFIX_CHANNEL_DATA
-
storeKey
:uint32be(chainID)
. -
storeValue
:sidechainChannel
wheresidechainChannel
is the channel object, serialized according to thechannelSchema
schema as defined in LIP 0045, and initialized as:- All properties are initialized to their default values.
- A registration CCM is sent to the sidechain by calling
sendInternal(EMPTY_FEE_ADDRESS, MODULE_ID_INTEROPERABILITY, CROSS_CHAIN_COMMAND_ID_REGISTRATION, chainID, 0, CCM_STATUS_OK, registrationCCMParams)
, where
registrationCCMParams = { "networkID": SHA-256(trs.params.genesisBlockId || senderAddress), "name": trs.params.name, "messageFeeTokenID": sidechainChannel.messageFeeTokenID }
-
-
An entry in the chain validators substore as specified in LIP 0045 is created as:
storePrefix
:STORE_PREFIX_CHAIN_VALIDATORS
storeKey
:uint32be(chainID)
.storeValue
:sidechainValidators
wheresidechainValidators
is the validators object, serialized according to thevalidatorsSchema
schema as defined in LIP 0045, and initialized as:- The
sidechainValidators.activeValidators
property is equal to thetrs.params.initValidators
property. - The
sidechainValidators.certificateThreshold
property is equal to thetrs.params.certificateThreshold
property.
- The
-
An entry in the outbox root substore as specified in LIP 0045 is created as:
storePrefix
:STORE_PREFIX_OUTBOX_ROOT
.storeKey
:uint32be(chainID)
.storeValue
: The propertysidechainChannel.outbox.root
serialized according to theoutboxRootSchema
schema defined in Introduce Interoperability module LIP.
-
An entry in the registered names substore as specified in LIP 0045 is created as:
storePrefix
:STORE_PREFIX_REGISTERED_NAMES
.storeKey
:sidechainAccount.name
serialized as a utf-8 encoded string.storeValue
:chainID
serialized according tochainIDSchema
schema defined in Introduce Interoperability module LIP.
-
An entry in the registered network IDs substore as specified in LIP 0045 is created as:
storePrefix
:STORE_PREFIX_NETWORK_ID
.storeKey
:sidechainAccount.networkID
serialized as bytes.storeValue
:chainID
serialized according tochainIDSchema
schema as defined in LIP 0045.
The command ID is COMMAND_ID_MAINCHAIN_REG
.
This command does not have an extra fee, i.e., extra fee = 0
.
mainchainRegParams = {
"type": "object",
"required": [
"ownChainID",
"ownName",
"mainchainValidators",
"signature",
"aggregationBits"
],
"properties": {
"ownChainID": {
"dataType": "uint32",
"fieldNumber": 1
},
"ownName": {
"dataType": "string",
"fieldNumber": 2
},
"mainchainValidators": {
"type": "array",
"fieldNumber": 3,
"items": {
"type": "object",
"required": ["blsKey", "bftWeight"],
"properties": {
"blsKey": {
"dataType": "bytes",
"fieldNumber": 1
},
"bftWeight": {
"dataType": "uint64",
"fieldNumber": 2
}
}
}
},
"signature": {
"dataType": "bytes",
"fieldNumber": 4
},
"aggregationBits": {
"dataType": "bytes",
"fieldNumber": 5
}
}
}
Let trs
be a transaction with module ID MODULE_ID_INTEROPERABILITY
and command ID COMMAND_ID_MAINCHAIN_REG
to be verified. The set of validity rules to validate trs.params
are:
- The
trs.params.ownChainID
property has to be an integer less than or equal toMAX_UINT32
. - The
trs.params.ownName
property has to contain only characters from the set[a-z0-9!@$&_.]
and has to be at mostMAX_LENGTH_NAME
characters long. - Rules for
trs.params.mainchainValidators
array:- The array must contain 101 elements (exact number of active delegates on the Lisk mainchain).
- The array must be ordered lexicographically by
blsKey
property. - The
blsKey
property of each element has a length of 48 bytes. - The
blsKey
property of each element is unique within the array. - The
bftWeight
property of each element is equal to 1.
- The properties
trs.params.aggregationBits
andtrs.params.signature
are validated as follows:-
The function
verifyWeightedAggSig(keyList, tag, netID, aggregationBits, signature, weights, certificateThreshold, message)
specified in LIP 0038, must returnVALID
where:- Let
b
be the current block andvalidators
be an array of objects equal tobftModule.getBFTParameters(b.header.height).validators
, then:keyList = [ validatorsModule.getValidatorAccount(validator.address).blsKey for validator in validators ]
sorted lexicographically.weights = [ validator.bftWeight for validator in validators ]
sorted accordingkeyList
.
- The
tag
is equal toTAG_CHAIN_REG_MESSAGE
. - The
netID
byte array corresponds to the network ID of the chain. - The
aggregationBits
argument is the byte array given intrs.params.aggregationBits
. - The
signature
argument is the aggregate signature given intrs.params.signature
. - The
certificateThreshold
argument is equal togetBFTParameters(b.header.height).certificateThreshold
whereb
is the current block. - The
message
argument is the output bytes of the serialization, as specified in LIP 0027, oftrs.params.ownChainID
,trs.params.ownName
andtrs.params.mainchainValidators
properties according to the following schema:
registrationSignatureMessage = { "type": "object", "required": ["ownChainID", "ownName", "mainchainValidators"], "properties": { "ownChainID": { "dataType": "uint32", "fieldNumber": 1 }, "ownName": { "dataType": "string", "fieldNumber": 2 }, "mainchainValidators": { "type": "array", "fieldNumber": 3, "items": { "type": "object", "required": ["blsKey", "bftWeight"], "properties": { "blsKey": { "dataType": "bytes", "fieldNumber": 1 }, "bftWeight": { "dataType": "uint64", "fieldNumber": 2 } } } } } }
- Let
-
When a mainchain registration command is applied, new entries of the Interoperability module are created in the sidechain state. In particular, let trs
be a transaction with module ID MODULE_ID_INTEROPERABILITY
and command ID COMMAND_ID_MAINCHAIN_REG
to be executed. Then:
-
An entry in the chain data substore as specified in LIP 0045 is created as:
storePrefix
:STORE_PREFIX_CHAIN_DATA
.storeKey
:uint32be(MAINCHAIN_ID)
.storeValue
:mainchainAccount
wheremainchainAccount
is the chain account object, serialized according to thechainAccountSchema
schema as defined in LIP 0045, and initialized as:mainchainAccount.name
=MAINCHAIN_NAME
.mainchainAccount.networkID
=MAINCHAIN_NETWORK_ID
.- The
mainchainAccount.lastCertificate.validatorsHash
property is set tobft.computeValidatorsHash(trs.params.mainchainValidators, THRESHOLD_MAINCHAIN)
. - The rest of the properties are initialized to their default values.
-
An entry in the channel data substore as specified in LIP 0045 is created as:
-
storePrefix
:STORE_PREFIX_CHANNEL_DATA
-
storeKey
:uint32be(chainID)
. -
storeValue
:mainchainChannel
wheremainchainChannel
is the channel object, serialized according to thechannelSchema
schema as defined in LIP 0045, and initialized as:- All properties are initialized to their default values.
- A registration CCM is sent to the mainchain by calling
sendInternal(EMPTY_FEE_ADDRESS, MODULE_ID_INTEROPERABILITY, CROSS_CHAIN_COMMAND_ID_REGISTRATION, MAINCHAIN_ID, 0, CCM_STATUS_OK, registrationCCMParams)
, where
registrationCCMParams = { "networkID": MAINCHAIN_NETWORK_ID, "name": MAINCHAIN_NAME, "messageFeeTokenID": mainchainChannel.messageFeeTokenID }
-
-
An entry in the chain validators substore as specified in LIP 0045 is created as:
storePrefix
:STORE_PREFIX_CHAIN_VALIDATORS
storeKey
:uint32be(MAINCHAIN_ID)
.storeValue
:mainchainValidators
wheremainchainValidators
is the validators object, serialized according to thevalidatorsSchema
schema as defined in LIP 0045, and initialized as:- The
mainchainValidators.activeValidators
property is equal to thetrs.params.mainchainValidators
property. - The
mainchainValidators.certificateThreshold
property is equal to theTHRESHOLD_MAINCHAIN
.
- The
-
An entry in the outbox root substore as specified in LIP 0045 is created as:
storePrefix
:STORE_PREFIX_OUTBOX_ROOT
.storeKey
:uint32be(MAINCHAIN_ID)
.storeValue
: The propertymainchainChannel.outbox.root
serialized according to theoutboxRootSchema
schema defined in Introduce Interoperability module LIP.
-
An entry in the own chain account data substore as specified in LIP 0045 is created as:
storePrefix
:STORE_PREFIX_CHAIN_DATA
storeKey
:uint32be(0)
.storeValue
:ownChainAccount
whereownChainAccount
is the own chain account object, serialized according to theownChainAccountSchema
schema defined in Introduce Interoperability module LIP, and initialized as:ownChainAccount.ID
=trs.params.ownChainID
.ownChainAccount.name
=trs.params.ownName
.ownChainAccount.nonce
= 0.
This proposal, together with LIP 0045, LIP 0053, LIP 0049, and LIP 0054, is part of the Interoperability module. Chains adding this module will need to do so with a hardfork.
TBA