Hello everyone,
I would like to propose another LIP for the roadmap objective "Define sidechain registration and lifecycle”. This LIP specifies the message, token and NFT recovery mechanisms.
I’m looking forward to your feedback.
Here is the complete LIP draft:
LIP:
Title: Introduce sidechain recovery mechanism
Author: Iker Alustiza <iker@lightcurve.io>
Type: Standards Track
Created: <YYYY-MM-DD>
Updated: <YYYY-MM-DD>
Requires: Introduce an interoperable token modulee LIP, Introduce a non-fungible token module module LIP,
Properties, serialization, and initial values of the interoperability module LIP,
Cross-chain messages LIP, State model and state root LIP
Abstract
This LIP introduces three new commands to the Lisk ecosystem: the message recovery command, the state recovery command, and the recovery initialization command. The message recovery command allows users to recover cross-chain messages that are pending in the outbox of an inactive or terminated sidechain. The state recovery command is used to recover entries from the module store of a terminated sidechain, whereas the recovery initialization command is used to initialize this state recovery process on sidechains.
Copyright
This LIP is licensed under the Creative Commons Zero 1.0 Universal.
Motivation
In the Lisk ecosystem, the ability of a sidechain to interoperate with other chains can be revoked, i.e., terminated, permanently. Specifically, this occurs when the sidechain has been inactive for too long, i.e., not posting a transaction with a cross-chain update (CCU) command for more than 30 days, or if it posted one with a malicious CCU command on the mainchain. Once a sidechain is terminated in the ecosystem, the users of said chain cannot have any cross-chain interaction with it. This means they will no longer be able to send or receive any (fungible or non-fungible) token, message or custom information from or to the sidechain. Therefore, it is useful to provide a trustless on-chain mechanism to recover tokens, messages and information from terminated sidechains. This mechanism will noticeably improve the user experience of the Lisk ecosystem without affecting the security guarantees of the general interoperability solution.
Rationale
This LIP introduces new commands to the Lisk ecosystem to provide a recovery mechanism for sidechain users in the scenario stated in the previous section.These commands are part of the Lisk interoperability module, thus they make use of the information provided in the interoperability store of the terminated sidechain. The main use cases provided by this recovery mechanism are:
-
On the Lisk mainchain:
- The users can recover a pending cross-chain message (CCM) from the sidechain account outbox by submitting a transaction with a message recovery command on the Lisk mainchain.
- The users can recover the balance of LSK they had on a terminated sidechain by submitting a transaction with a state recovery command.
-
On sidechains:
- The users can recover the balance of any custom token they had on a terminated sidechain by submitting a transaction with a state recovery command.
- The users can recover any NFT they had on a terminated sidechain by submitting a transaction with a state recovery command.
- The stored data of certain custom modules can be recovered from a terminated sidechain by submitting a transaction with a state recovery command.
In the next subsections, these mechanisms are explained together with their conditions, sidechain data availability requirements, effects, and their potential usage by certain network participants.
Message Recovery from the Sidechain Account Outbox
This mechanism allows to recover any CCM pending in the sidechain outbox. That is, those CCMs that have not been included in the receiving sidechain yet. Specifically, this includes all the CCMs whose indices are larger than the last message index that the receiving sidechain reported to have included in its inbox on the mainchain. This recovery mechanism requires these conditions to work:
- The pending CCMs to be recovered have to be available to the sender of the recovery command.
- The indices of the pending CCMs to be recovered have to be larger than the value of the
partnerChainInboxSize
property of the interoperability account of the sidechain. This implies that these CCMs are still pending or that their processing has not been certified to the mainchain. - The proof of inclusion for the pending CCMs into the current
outbox.root
has to be available. When a message recovery command is processed, theoutbox.root
property of the interoperability account of the corresponding sidechain is updated to account for the recovered CCMs (see Figure 1). This implies that the future potential message recovery commands have to include a proof of inclusion into the updated Merkle tree against the newoutbox.root
.
Figure 1: (top) The message recovery command recovers CCM32 and CCM34 by providing the sibling hashes to compute the outbox root (the proof of inclusion for these CCMs in the tree). The data provided by the command is highlighted in red in the tree.
(bottom) The outbox root is then updated by recomputing the Merkle tree root with the recovered CCMs. In the second Merkle tree the updated nodes are highlighted in green. A new recovery command will need to provide a proof of inclusion for this updated Merkle tree.
Assuming these conditions are fulfilled, any user can submit a message recovery command to recover several CCMs at the same time. When the command is processed, the corresponding CCMs are recovered differently depending on their sending chain:
- If the sending chain of the pending CCM is the Lisk mainchain, it must be a cross-chain LSK token transfer CCM. The amount of LSK transferred in the CCM will be added back to the sender of the original transaction.
- If the sending chain of the pending CCM is any other sidechain, the CCM will be sent back to this chain. The sending sidechain will act on the CCM as usual, i,e., with respect to the specific logic associated with the cross-chain command.
Bear in mind that users are not guaranteed to recover their CCMs in every situation. Certain state information of the terminated sidechain might have been modified before termination and this would make the recovered CCM application fail. For example, the escrowed LSK in the sidechain account on the mainchain could have been subtracted by prior malicious behavior in the terminated sidechain.
State Recovery from the Sidechain State Root
This mechanism allows to recover a specific entry from a substore (i.e. the collection of key-value pairs with a common store prefix) of a module store of a terminated sidechain. Here “recover” means triggering a specific state transition defined as part of the relevant module protocol logic.
In particular, it is based on the sidechain state root, stateRoot
, set in the last certificate before sidechain termination. In the context of the mainchain, a valid state recovery command can recover the LSK token balance that users had in the terminated sidechain. In the context of a sidechain, it can recover an entry in a recoverable module store from a terminated sidechain.
A recoverable module is any module that exposes a recover function. This includes the token module (for any custom token) and the NFT module.
This recovery mechanism requires these conditions to be valid:
- The state of the sidechain has to be consistent with respect to the value of the
stateRoot
property in the interoperability account of the sidechain in the mainchain. - The specific entry of the substore of the recoverable module store to be recovered has to be available.
- The proof of inclusion for the specific entry to be recovered into the current
stateRoot
has to be available. - When one of these commands is processed, the
stateRoot
property of the terminated sidechain is updated to account for the recovered tokens. - This implies that the future potential state recovery commands on a specific chain have to include a proof of inclusion into the updated sparse Merkle tree against the new
stateRoot
.
There is an extra requirement in the case of recoveries in the sidechain context: The sidechain in which the recovery will happen needs to be aware of the stateRoot
of the terminated sidechain. In general, this information is only available on mainchain (in the interoperability account of the terminated sidechain). A way to make sidechains aware of this specific information for state recoveries is needed. This recovery initialization process on sidechains can happen in two ways:
-
Recovery initialization command: This command is used to prove on a sidechain the value that the
stateRoot
of the terminated sidechain has on mainchain. Any user on the corresponding sidechain can send a transaction with this command and initiate the state recoveries with respect to the terminated sidechain. - Sidechain terminated message: As specified in the cross-chain message LIP, when a CCM reaches a receiving chain that has been terminated, a sidechain terminated message is created and sent back to the sending chain carrying the stateRoot of the terminated sidechain. The application of this CCM on the sidechain will effectively initiate the recovery process.
Assuming these conditions are fulfilled, the entries of substores of any recoverable module in a terminated sidechain can be recovered back to the chain in which the transaction with this command was submitted. In particular, users can recover their LSK tokens back to their user account on mainchain. What is more, sidechain developers may implement any custom logic for the recover
function in their custom modules, so that recoveries may have different functionalities depending on the module and the sidechain where the process happens.
Similar to the case for message recovery commands, it is not guaranteed to recover from the expected state in every situation. Certain state information of the terminated sidechain might have been modified and certified to the mainchain before termination.
In summary, the functionality provided by these recovery commands applies for sidechains that were terminated for inactivity or other violations of the interoperability protocol. If the validators of the terminated sidechain were byzantine in the past, i.e., the security guarantees of the sidechain were broken, it is likely that these recovery mechanisms would not work.
Recovery Commands as an Off-chain Service
As explained in the previous subsections, these recovery commands require specific information from the terminated sidechain to be available. In particular, for the message recovery mechanism, all the pending CCMs and the state of the sidechain outbox (root, size, and append path) up to the last non-pending CCM have to be available. For the state recovery mechanism, the sidechain state up to the last valid cross-chain update has to be available. Moreover, this information has to be updated for future recovery commands every time a recovery command is successfully processed.
In particular, message recovery commands are better suited to be submitted by accounts with access to a Lisk mainchain full node. The complete Merkle tree with root equal to the last value of the outbox.root
property of the sidechain account can be computed from the history of the Lisk mainchain. In the case of submitting transactions with a state recovery command, a full node of the sidechain should also keep a snapshot of the sidechain state tree corresponding to the last certified stateRoot
on mainchain. As explained above, these commands require the state of the sidechain to be consistent with the last value of stateRoot
. However, the state of the sidechain may have evolved since the last CCU. That is why if a sidechain node intends to be ready to eventually submit these commands, they need to store this extra state information.
Since these technical requirements are not straightforward, the recovery commands are better suited to be offered as an off-chain service to sidechain users. When a sidechain is terminated, these recovery-service providers can recover the CCMs, tokens, NFTs or in general, any cross-chain information for the interested users.
Specification
Constants
Name | Type | Value |
---|---|---|
Module | ||
MODULE_ID_INTEROPERABILITY |
uint32 | 64 |
MODULE_ID_TOKEN |
uint32 | TBD |
Store Prefix | ||
STORE_PREFIX_CHAIN_DATA |
bytes | 0x8000 |
STORE_PREFIX_TERMINATED_CHAIN |
bytes | 0xc000 |
Status | ||
CHAIN_TERMINATED |
uint32 | 2 |
CCM_STATUS_OK |
uint32 | 0 |
CCM_STATUS_RECOVERED |
uint32 | 4 |
Command ID | ||
COMMAND_ID_MESSAGE_RECOVERY |
uint32 | 4 |
COMMAND_ID_STATE_RECOVERY |
uint32 | 5 |
COMMAND_ID_INITIATE_RECOVERY |
uint32 | 6 |
Other | ||
MAINCHAIN_ID |
uint32 | 1 |
LIVENESS_LIMIT | uint32 | 30*24*3600 |
This LIP specifies three commands for Lisk mainchain.
These commands are part of the interoperability module, with moduleID = MODULE_ID_INTEROPERABILITY
.
General Notation
In the rest of the section:
- Let
account(chainID)
be the object of the interoperability account store withstorePrefix = STORE_PREFIX_CHAIN_DATA
andstoreKey = chainID
. - Let
terminatedAccount(chainID)
be the object of the terminated account store withstorePrefix = STORE_PREFIX_TERMINATED_CHAIN
andstoreKey = chainID
. - Let
isLive
andterminateChain
be the interoperability module internal functions defined in the Properties, serialization, and initial values of the interoperability module LIP. - Let
uint32be
be the function for the big endian uint32 serialization of integers defined in the Properties, serialization, and initial values of the interoperability module LIP. - Let
process
be the internal function with the same name defined in Cross-chain messages LIP. - Let
unescrow
be the function with the same name defined in Introduce an interoperable token module LIP. - Let
RMTVerify
be theverifyDataBlock
function specified in the appendix C of LIP 0031. - Let
RMTCalculateRoot
be thecalculateRootFromUpdateData
function specified in the appendix E of LIP 0031. - Let
SMTVerify
be theverify
function specified in the Introduce sparse Merkle trees LIP. - Let
SMTCalculateRoot
be thecalculateRoot
function specified in the Introduce sparse Merkle trees LIP.
Message Recovery Command
The command ID is COMMAND_ID_MESSAGE_RECOVERY
.
-
params
property:-
chainID:
An integer representing the chain ID of the terminated sidechain. -
crossChainMessages:
An array of serialized CCMs, according to the schema specified in Cross-chain messages LIP, to be recovered. -
siblingHashes:
Array of bytes with the paths in the Merkle tree for the proofs of inclusion ofcrossChainMessages
in the outbox root of the sidechain as specified in LIP 0031.
-
Message Recovery Command Schema
messageRecoveryParams = {
"type":"object",
"properties":{
"chainID":{
"dataType":"uint32",
"fieldNumber":1
},
"crossChainMessages":{
"type":"array",
"items":{
"dataType":"bytes"
},
"fieldNumber":2
},
"siblingHashes":{
"type":"array",
"items":{
"dataType":"bytes"
},
"fieldNumber":3
}
},
"required":[
"chainID",
"crossChainMessages",
"siblingHashes"
]
}
Message Recovery Command Verification
Let trs
be a transaction with module ID MODULE_ID_INTEROPERABILITY
and command ID COMMAND_ID_MESSAGE_RECOVERY
to be verified. Also, let deserializedCCMs
be an array with the deserialization of every element in trs.params.crossChainMessages
according to the schema specified in Cross-chain messages LIP. Then the set of validity rules to validate trs.params
are:
if trs.params.chainID does not correspond to a registered sidechain:
return false
sidechainAccount = account(trs.params.chainID)
# chain has to be either terminated or inactive
if sidechainAccount.status != CHAIN_TERMINATED and isLive(trs.params.chainID, timestamp):
return false
# check the validity of the CCMs to be recovered
for CCM in deserializedCCMs:
if CCM.index < sidechainAccount.partnerChainInboxSize:
return false
if CCM.status != CCM_STATUS_OK:
return false
# check the inclusion proof against the sidechain outbox root
proof = { size: sidechainAccount.outbox.size,
idxs: [CCM.index for CCM in deserializedCCMs],
siblingHashes: trs.params.siblingHashes}
return RMTVerify( [SHA-256(CCMData) for CCMData in trs.params.crossChainMessages],
proof, sidechainAccount.outbox.root)
Message Recovery Command Execution
Processing a transaction trs
with module ID MODULE_ID_INTEROPERABILITY
and command ID COMMAND_ID_MESSAGE_RECOVERY
implies the following logic:
let trsSenderAddress be the address of the trs.senderPublicKey
sidechainAccount = account(trs.params.chainID)
# terminate chain if necessary
if sidechainAccount.status != CHAIN_TERMINATED:
terminateChain(trs.params.chainID)
# set CCM status to recovered and assign fee to trs sender
updatedCCMs = []
for CCM in deserializedCCMs:
unescrow(trs.params.chainID, trsSenderAddress, 0, CCM.fee)
CCM.fee = 0
CCM.status = CCM_STATUS_RECOVERED
push serialized(CCM) to updatedCCMs # CCM is serialized again
# update sidechain outbox root
proof = { size: sidechainAccount.outbox.size,
idxs: [CCM.index for CCM in deserializedCCMs],
siblingHashes: trs.params.siblingHashes}
sidechainAccount.outbox.root = RMTCalculateRoot([SHA-256(CCMData) for CCMData in updatedCCMs], proof)
# process recovery
for CCM in deserializedCCMs:
if CCM.sendingChainID == MAINCHAIN_ID:
# this is a LSK transfer CCM
unescrow(trs.params.chainID, CCM.params.senderAddress, 0, CCM.params.amount)
else:
swap CCM.sendingChainID and CCM.receivingChainID
process(CCM)
State Recovery Command
The command ID is COMMAND_ID_STATE_RECOVERY
.
-
params
property:-
chainID:
An integer representing the chain ID of the terminated sidechain. -
moduleID
: An integer representing the ID of the recoverable module. -
storeEntries
: An array of objects containing:-
storePrefix
: An integer representing the store prefix to be recovered.
-
-
storeKey
: Array of bytes with the store key to be recovered.-
storeValue
: Array of bytes with the store value to be recovered. -
bitmap
: The bitmap corresponding tostoreValue
in the sparse Merkle tree as specified in Introduce sparse Merkle trees LIP.
-
-
siblingHashes
: Array of bytes with the sibling hashes in the sparse Merkle tree for the inclusion proofs ofstoreEntries
in the state of the sidechain as specified in Introduce sparse Merkle trees LIP.
-
State Recovery Command Schema
stateRecoveryParams = {
"type":"object",
"properties":{
"chainID":{
"dataType":"uint32",
"fieldNumber":1
},
"moduleID":{
"dataType":"uint32",
"fieldNumber":2
},
"storeEntries":{
"type":"array",
"fieldNumber":3,
"items":{
"type":"object",
"properties":{
"storePrefix":{
"dataType":"uint32",
"fieldNumber":1
},
"storeKey":{
"dataType":"bytes",
"fieldNumber":2
},
"storeValue":{
"dataType":"bytes",
"fieldNumber":3
},
"bitmap":{
"dataType":"bytes",
"fieldNumber":4
}
},
"required":[
"storePrefix",
"storeKey",
"storeValue",
"bitmap"
]
}
},
"siblingHashes":{
"type":"array",
"items":{
"dataType":"bytes"
},
"fieldNumber":4
}
},
"required":[
"chainID",
"moduleID"
"storeEntries",
"siblingHashes"
]
}
State Recovery Command Verification
Let trs
be a transaction with module ID MODULE_ID_INTEROPERABILITY
and command ID COMMAND_ID_STATE_RECOVERY
to be verified.
Then trs
is valid if the following logic returns true
:
sidechainAccount = terminatedAccount(trs.params.chainID)
# the terminated account has to exist for this sidechain
if sidechainAccount is empty:
return false
let queryKeys and storeQueries be empty arrays
for each entry in trs.params.storeEntries:
# the recover function corresponding to the module ID has to pass
route processing logic to the module given by trs.params.moduleID
if recover(trs.params.chainID, trs.params.moduleID, entry.storePrefix, entry.storekey, entry.storeValue) fails:
return false
push entry.storeKey to queryKeys
query = { key: entry.storeKey,
value: SHA-256(entry.storeValue),
bitmap: entry.bitmap}
push query to storeQueries
proofOfInclusionStores = { siblingHashes: trs.params.siblingHashes, queries : storeQueries}
return SMTVerify(queryKeys, proofOfInclusionStores, sidechainAccount.stateRoot)
State Recovery Command Execution
Processing a transaction trs
with module ID MODULE_ID_INTEROPERABILITY
and command ID COMMAND_ID_STATE_RECOVERY
implies the following logic:
sidechainAccount = terminatedAccount(trs.params.chainID)
let storeQueries be an empty array
for each entry in trs.params.storeEntries:
# the recover function corresponding to the module ID applies the recovery logic
route processing logic to the module given by trs.params.moduleID
recover(trs.params.chainID, trs.params.moduleID, entry.storePrefix, entry.storeKey, entry.storeValue)
emptyStore = empty bytes // define an empty store entry
query = { key: entry.storekey,
value: SHA-256(emptyStore),
bitmap: entry.bitmap}
push query to storeQueries
sidechainAccount.stateRoot = SMTCalculateRoot(trs.params.siblingHashes, storeQueries)
Recover Function
For the verification and application of this command it is assumed that the module given by trs.params.moduleID
exposes a recover
function, with the following interface:
recover(terminatedChainID, moduleID, storePrefix, storeKey, storeValue),
where:
-
terminatedChainID
: The ID of the terminated chain. -
moduleID
: The ID of the recoverable module. -
storePrefix
: The store prefix of the store entry in the recoverable module state. -
storeKey
: The store key of the store entry in the recoverable module state. -
storeValue
: The store value of the store entry in the recoverable module state.
The recover function is specified for the token module and in the NFT module.
Recovery Initialization Command
The command ID is COMMAND_ID_INITIATE_RECOVERY
.
-
params
property:-
chainID:
An integer representing the chain ID of the terminated sidechain. -
sidechainInteropAccount
: A byte array containing the serialization of the interoperability account of the terminated sidechain according to theinteroperabilityAccount
schema specified in the Interoperability LIP. -
bitmap
: The bitmap corresponding tostateRoot
in the sparse Merkle tree as specified in Introduce sparse Merkle trees LIP. -
siblingHashes
: Array of bytes with the sibling hashes in the sparse Merkle tree for the inclusion proofs ofstateRoot
in the state of the mainchain as specified in Introduce sparse Merkle trees LIP.
-
Recovery Initialization Command Schema
recoveryInitializationParams = {
"type":"object",
"properties":{
"chainID":{
"dataType":"uint32",
"fieldNumber":1
},
"sidechainInteropAccount":{
"dataType":"bytes",
"fieldNumber":2
},
"bitmap":{
"dataType":"bytes",
"fieldNumber":3
},
"siblingHashes":{
"type":"array",
"items":{
"dataType":"bytes"
},
"fieldNumber":4
}
},
"required":[
"chainID",
"sidechainInteropAccount"
"bitmap",
"siblingHashes"
]
}
Recovery Initialization Command Validation
Let trs
be a transaction with module ID MODULE_ID_INTEROPERABILITY
and command ID COMMAND_ID_INITIATE_RECOVERY
to be verified.
if trs.params.chainID == 1 or trs.params.chainID == ownChainID:
return false
mainchainAccount = account(MAINCHAIN_ID)
sidechainAccount = terminatedAccount(trs.params.chainID)
let deserializedInteropAccount be the deserialization of trs.params.sidechainInteropAccount
# the commands fails if the sidechain is already terminated on this chain
if sidechainAccount exists:
return false
if (deserializedInteropAccount.status != CHAIN_TERMINATED
and mainchainAccount.lastCertifiedTimestamp - deserializedInteropAccount.lastCertifiedTimestamp <= LIVENESS_LIMIT):
return false
interopAccKey = uint32be(MODULE_ID_INTEROPERABILITY) || STORE_PREFIX_CHAIN_DATA || uint32be(trs.params.chainID)
query = { key: interopAccKey,
value: SHA-256(trs.params.sidechainInteropAccount),
bitmap: trs.params.bitmap }
proofOfInclusionInteropAccount = { siblingHashes: trs.params.siblingHashes, queries : [query]}
return SMTVerify(queryKeys, proofOfInclusionInteropAccount, mainchainAccount.stateRoot)
Recovery Initialization Command Execution
Processing a transaction trs
with module ID MODULE_ID_INTEROPERABILITY
and command ID COMMAND_ID_INITIATE_RECOVERY
implies the following logic:
let deserializedInteropAccount be the deserialization of trs.params.sidechainInteropAccount
create a terminatedAccount entry in the terminatedChain substore with
storePrefix = STORE_PREFIX_TERMINATED_CHAIN
storeKey = uint32be(trs.params.chainID)
storeValue = object serialized according to terminatedChain schema
let sidechainAccount = terminatedAccount(trs.params.chainID)
sidechainAccount.stateRoot = deserializedInteropAccount.lastCertifiedStateRoot
Backwards Compatibility
This LIP introduces new commands with new effects to the Lisk mainchain state, thus it will imply a hardfork. It also implies that sidechains implement the interoperability module, recoverable modules and follow the standard state model structure.