Skip to content
This repository has been archived by the owner on Jun 11, 2024. It is now read-only.

Latest commit

Β 

History

History
558 lines (455 loc) Β· 31 KB

lip-0072.md

File metadata and controls

558 lines (455 loc) Β· 31 KB
LIP: 0072
Title: Introduce direct sidechain channels
Author: Alessandro Ricottone <alessandro.ricottone@lightcurve.io>
        Mitsuaki Uchimoto <mitsuaki.uchimoto@lightcurve.io>
Discussions-To: https://research.lisk.com/t/introduce-direct-sidechain-channels/400
Status: Draft
Type: Standards Track
Created: 2023-06-22
Updated: 2023-12-15
Requires: 0043, 0045, 0049, 0053

Abstract

This LIP introduces one new command that extends the standard interoperability protocol and allows sidechains to register direct channels, enabling them to exchange cross-chain messages directly without routing them via the mainchain.

Copyright

This LIP is licensed under the Creative Commons Zero 1.0 Universal.

Motivation

The Lisk interoperability solution allows chains that are part of the Lisk ecosystem to communicate by exchanging cross-chain messages. Messages originating from a sidechain and targeting another sidechain are first posted on the mainchain, which acts as a router and delivers the messages to the receiving chain. This is a two-step process, requiring two separate cross-chain update transactions (from the sending chain to the mainchain and from the mainchain to the receiving chain). This architecture is very convenient in that a sidechain needs only to "follow" the mainchain, without requiring direct communication with every other sidechain.

In some cases, however, this direct communication can be beneficial, as it would allow faster sidechain-to-sidechain message exchange (since they need only one cross-chain update), while removing some transactions from the mainchain, which in turn could result in lower fees. For example, this is the case if the two sidechains involved need to communicate regularly.

In this LIP, we introduce the register direct channel command, a new command that allows sidechains to open a direct channel.

This command extends the standard interoperability protocol, i.e. the specifications that are part of the Interoperability module. Sidechains can decide never to open direct channels, by simply not using it.

We also amend the rules of the sidechain cross-chain update command, which is used to post cross-chain updates on a sidechain. We simply modify the verifyRoutingRules function to allow cross-chain messages coming from a direct channel.

Finally, we modify the verification of the Interoperability genesis-block assets to allow the initialization of direct channels from the genesis block.

Rationale

Chain and Channel Data

The Interoperability module stores information about connected chains in different substores. As a general rule, "global" information about the chain, i.e. the information that is the same across the whole ecosystem such as the chain name, is stored in the chain data substore. On the other hand, information that is "relative" to the specific connection between the two chains, such as inbox and outbox, is stored in the channel data substore.

Creating a direct channel involves proving that a sidechain has been registered on the mainchain, by showing an inclusion proof for the entry in the chain data substore. A new entry is created in the channel data substore, which is then used only for the direct channel communication.

Direct Channels Registration Process

Direct communication between sidechains should be treated as a special case, while the standard interoperability protocol should remain the default solution. In particular, the mainchain remains the central point of the ecosystem, and the usual sidechain registration process is maintained. Therefore, direct channels between sidechains can be opened only after both sidechains have completed the registration on mainchain, and in particular only after the mainchain registration commands have been cast on both sidechains, creating in their state, among other data structures, the own chain account.

The registration of the direct channel then follows a pattern analogous to the registration of the sidechain-mainchain channel. First, a register direct channel command is cast on a sidechain, setting up on it the data structures necessary for the direct communication. Then, the same command can be cast on the second sidechain. There is no particular order in which the commands have to be cast, and in practice they could be processed in a very short time window. Only after the registration happened on both chains, they can start exchanging direct cross-chain updates.

There is however a cost associated with opening a direct channel: The size of cross-chain updates posted from the sidechain increases, as the outboxRootWitness size scales as the logarithm of the number of chains registered in the sidechain. Therefore, to prevent the opening of unwanted channels, the register direct channel command must contain a signature from the majority of the sidechain current active validators, similarly to what is necessary for a mainchain registration command. This is to ensure that direct channels are not opened without the consensus of the sidechain validators.

The signed message contains:

  • The partner chain ID, used to verify the inclusion proof of the partner chain account.
  • The partner chain account, otherwise the command could contain an old version of it (modifying, for instance, the liveness of the partner chain).
  • The validators and certificate threshold, to ensure that they correspond to the validators hash in the chain account.
  • The message fee token, because validators have to agree on the token used to pay fees in the direct channel.

Notice that the inclusion proof is not signed. This allows, for instance, collecting signatures and then including in the command a more recent version of the mainchain state (which of course does not update the partner chain account), without needing to re-sign the message.

Direct Channels Cross-chain Updates

Once the direct channel has been opened, cross-chain messages are appended to the outbox of the direct channel, rather than the mainchain outbox. This means in particular that the two sidechains cannot exchange anymore cross-chain messages routed via the mainchain.

Cross-chain messages exchanged via the direct channel are posted using standard sidechain cross-chain update commands. The only modification required is in the verification of the cross-chain messages routing rules. In particular, we add additional checks to ensure that if the sending chain of the sidechain cross-chain update is not the mainchain (i.e. it is another sidechain for which a direct channel exists), the cross-chain messages must all come from the sending chain.

Recoveries from the Direct Channel

If one of the sidechains participating in the direct channel is terminated, the other one can recover from the state of the terminated chain (state recovery) and pending messages in the channel outbox (message recovery). Notice that a sidechain can only be terminated for not respecting the liveness requirement on the mainchain. Violations of the protocol of a custom module can be punished by termination, but this is outside of the scope of this LIP.

State recovery from a direct channel follows the same pattern as in the standard interoperability protocol.

Message recoveries follow a similar pattern from the standard procedure on the mainchain. In particular, the usual process of initializing the message recovery with a message recovery initialization command still holds, and the command will contain the channel state of the terminated sidechain. Once the message recovery has been initialized, CCMs can be recovered (and thus processed) directly on the sidechain (messages from the same terminated sidechain that are recovered on the mainchain and sent back to the sidechain are simply discarded).

Specification

This LIP introduces the register direct channel command and modifies the function verifyRoutingRules and the genesis state initialization. Notice that these changes are pertinent to the Interoperability module registered on sidechains, and hence do not change the Lisk mainchain protocol.

Notation and Constants

All interoperability constants are defined in LIP 0045. In this LIP, we introduce the following new constant.

Name Type Value Description
Interoperability Commands
COMMAND_REGISTER_DIRECT_CHANNEL string "registerDirectChannel" Name of direct channel registration command.

Commands

Direct Channel Registration Command

This command is used to create a direct channel.

Transactions executing this command have:

  • module = MODULE_NAME_INTEROPERABILITY,
  • command = COMMAND_REGISTER_DIRECT_CHANNEL.
Parameters
directChannelRegistrationParams = {
  "type": "object",
  "required": [
    "chainID",
    "partnerchainAccount",
    "partnerchainValidators",
    "partnerchainCertificateThreshold",
    "bitmap",
    "siblingHashes",
    "messageFeeTokenID",
    "minReturnFeePerByte",
    "signature",
    "aggregationBits"
  ],
  "properties": {
    "chainID": {
      "dataType": "bytes",
      "length": CHAIN_ID_LENGTH,
      "fieldNumber": 1
    },
    "partnerchainAccount": {
      "dataType": "bytes",
      "fieldNumber": 2
    },
    "partnerchainValidators": {
      "type": "array",
      "fieldNumber": 3,
      "items": {
        "type": "object",
        "required": [
          "blsKey",
          "bftWeight"
        ],
        "properties": {
          "blsKey": {
            "dataType": "bytes",
            "length": BLS_PUBLIC_KEY_LENGTH,
            "fieldNumber": 1
          },
          "bftWeight": {
            "dataType": "uint64",
            "fieldNumber": 2
          }
        }
      }
    },
    "partnerchainCertificateThreshold": {
      "dataType": "uint64",
      "fieldNumber": 4
    },
    "bitmap": {
      "dataType": "bytes",
      "fieldNumber": 5
    },
    "siblingHashes": {
      "type": "array",
      "fieldNumber": 6,
      "items": {
        "dataType": "bytes",
        "length": HASH_LENGTH
      }
    },
    "messageFeeTokenID": {
        "dataType": "bytes",
        "length": TOKEN_ID_LENGTH,
        "fieldNumber": 7
    },
    "minReturnFeePerByte": {
        "dataType": "uint64",
        "fieldNumber": 8
    },
    "signature": {
      "dataType": "bytes",
      "length": BLS_SIGNATURE_LENGTH,
      "fieldNumber": 9
    },
    "aggregationBits": {
      "dataType": "bytes",
      "fieldNumber": 10
    }
  }
}
  • chainID: The ID of the partner chain to be registered.
  • partnerchainAccount: The account on the partner chain stored on the mainchain. It will be checked with an inclusion proof with respect to the last certified mainchain state root.
  • partnerchainValidators: The validators of the partner chain. For each validator we indicate:
    • blsKey: The BLS key,
    • bftWeight: The corresponding BFT weight.
  • partnerchainCertificateThreshold: The certificate threshold of the partner chain. It is used together with the partnerchainValidators to check the validatorsHash property.
  • bitmap: The bitmap of the inclusion proof of the partnerchainAccount.
  • siblingHashes: The sibling hashes of the inclusion proof of the partnerchainAccount.
  • messageFeeTokenID: The Id of the token that will be used to pay cross-chain message fees in the direct channel.
  • minReturnFeePerByte: The minimum fee per byte to automatically send back a CCM from the partner chain in case of execution errors.
  • signature: The BLS signature of the sidechain validators attesting that they agree on the registration of the direct channel.
  • aggregationBits: The aggregation bits for the BLS signature of the sidechain validators.
Verification
def verify(trs: Transaction) -> None:
    trsParams = decode(directChannelRegistrationParams, trs.params)
    
    # Check that the own chain account exist, meaning that the mainchain has already been registered on the chain.
    if ownChainAccount does not exist:
        raise Exception("Mainchain has not been registered yet.")

    # Check that the partner chain has not been already registered.
    if chainAccount(trsParams.chainID) exists:
        raise Exception("Partner chain has already been registered.")

    # Check that the partner chain ID is not the sidechain ID.
    if trsParams.chainID == OWN_CHAIN_ID:
        raise Exception("Partner chain ID cannot be the same as the sidechain ID.")
    
    # Check that the partner chain ID is not the mainchain ID.
    if trsParams.chainID == getMainchainID():
        raise Exception("The mainchain cannot be registered as a direct sidechain channel.")

    partnerchainAccount = decode(chainDataSchema, trsParams.partnerchainAccount)
    validateObjectSchema(chainDataSchema, partnerchainAccount)

    # Check that the partner chain is active on mainchain.
    if partnerchainAccount.status != CHAIN_STATUS_ACTIVE:
        raise Exception("Partner chain is not active on mainchain.")

    # Check that the partner chain does not violate the liveness requirement on mainchain.
    if chainAccount(getMainchainID()).lastCertificate.timestamp - partnerchainAccount.lastCertificate.timestamp > LIVENESS_LIMIT:
        raise Exception("Partner chain violated the liveness requirement on mainchain.")

    # Check that message fee token is local to either sending or receiving chain.
    if Token.getChainID(trsParams.messageFeeTokenID) not in (OWN_CHAIN_ID, trsParams.chainID):
        raise Exception("The message fee token is not local to either sending or receiving chain.")

    # Check that message fee token is allowed on the chain.
    if not Token.isTokenSupported(trsParams.messageFeeTokenID):
        raise Exception("The message fee token is not supported.")

    # We can skip most checks on the partner chain validators since they are authenticated by the validatorsHash
    # which is certified by the mainchain.
    # We only check that the validators correspond to the validatorsHash in the chain account.
    # Notice that this means that the validators given in the params must match with the ones that were certified on the mainchain.
    if partnerchainAccount.lastCertificate.validatorsHash != computeValidatorsHash(trsParams.partnerchainValidators, trsParams.partnerchainCertificateThreshold):
        raise Exception("Validators do not match with the validators hash.")

    # Check the inclusion proof for the chain account
    queryKey = STORE_PREFIX_INTEROPERABILITY + SUBSTORE_PREFIX_CHAIN_DATA + sha256(trsParams.chainID)

    query = {
        "key": queryKey,
        "value": sha256(trsParams.partnerchainAccount),
        "bitmap": trsParams.bitmap
    }

    proofOfInclusion = { "siblingHashes": trsParams.siblingHashes, "queries" : [query] }

    if smtVerifyInclusionProof([queryKey], proofOfInclusion, chainAccount(getMainchainID()).lastCertificate.stateRoot) == False:
        raise Exception("Partner chain proof of inclusion is not valid.")

The function computeValidatorsHash is defined in LIP 0058 and the function smtVerifyInclusionProof is specified in LIP 0039.

Execution
def execute(trs: Transaction) -> None:
    trsParams = decode(directChannelRegistrationParams, trs.params)
    
    # Validate the signature (see https://github.com/LiskHQ/lips/blob/main/proposals/lip-0038.md).
    validators = sorted([(validator.blsKey, validator.bftWeight) for validator in Validators.getValidatorParams().validators], key = lambda v: v.blsKey)
    blsKeys = [params[0] for params in validators]
    bftWeights = [params[1] for params in validators]
    certificateThreshold = Validators.getValidatorParams().certificateThreshold

    registrationSignatureMessageSchema = {
        "type": "object",
        "required": [
            "chainID",
            "partnerchainAccount",
            "partnerchainValidators",
            "partnerchainCertificateThreshold",
            "messageFeeTokenID",
            "minReturnFeePerByte"
        ],
        "properties": {
            "chainID": {
                "dataType": "bytes",
                "length": CHAIN_ID_LENGTH,
                "fieldNumber": 1
            },
            "partnerchainAccount": {
                "dataType": "bytes",
                "fieldNumber": 2
            },
            "partnerchainValidators": {
                "type": "array",
                "fieldNumber": 3,
                "items": {
                    "type": "object",
                    "required": ["blsKey", "bftWeight"],
                    "properties": {
                        "blsKey": {
                            "dataType": "bytes",
                            "length": BLS_PUBLIC_KEY_LENGTH,
                            "fieldNumber": 1
                        },
                        "bftWeight": {
                            "dataType": "uint64",
                            "fieldNumber": 2
                        }
                    }
                }
            },
            "partnerchainCertificateThreshold": {
                "dataType": "uint64",
                "fieldNumber": 4
            },
            "messageFeeTokenID": {
                "dataType": "bytes",
                "length": TOKEN_ID_LENGTH,
                "fieldNumber": 5
            },
            "minReturnFeePerByte": {
                "dataType": "uint64",
                "fieldNumber": 6
            }
        }
    }

    message = encode(registrationSignatureMessageSchema, {
        "chainID": trsParams.chainID, 
        "partnerchainAccount": trsParams.partnerchainAccount, 
        "partnerchainValidators": trsParams.partnerchainValidators, 
        "partnerchainCertificateThreshold": trsParams.partnerchainCertificateThreshold,
        "messageFeeTokenID": trsParams.messageFeeTokenID, 
        "minReturnFeePerByte": trsParams.minReturnFeePerByte
    })

    # verifyWeightedAggSig is specified in LIP 0062.
    if verifyWeightedAggSig(
        blsKeys, 
        trsParams.aggregationBits, 
        trsParams.signature, 
        MESSAGE_TAG_CHAIN_REG, 
        OWN_CHAIN_ID, 
        bftWeights, 
        certificateThreshold, 
        message
        ) == False:
        emitPersistentEvent(
            module = MODULE_NAME_INTEROPERABILITY,
            name = EVENT_NAME_INVALID_REGISTRATION_SIGNATURE,
            data = {},
            topics = [trsParams.ownChainID]
        )
        raise Exception("Invalid signature property.")

    # Create chain account.
    partnerchainAccount = decode(chainDataSchema, trsParams.partnerchainAccount)
    partnerchainAccount.status = CHAIN_STATUS_REGISTERED
    create an entry in the chain data substore with
        storeKey = trsParams.chainID
        storeValue = encode(chainDataSchema, partnerchainAccount)

    # Create channel.
    partnerchainChannel = {
        "inbox": {
            "appendPath": [],
            "size": 0,
            "root": EMPTY_HASH
        },
        "outbox": {
            "appendPath": [],
            "size": 0,
            "root": EMPTY_HASH
        },
        "partnerchainOutboxRoot": EMPTY_HASH,
        "messageFeeTokenID": trsParams.messageFeeTokenID,
        "minReturnFeePerByte": trsParams.minReturnFeePerByte
    }
    create an entry in the channel data substore with
        storeKey = trsParams.chainID
        storeValue = encode(channelDataSchema, partnerchainChannel)

    # Create validators account.
    partnerchainValidators = {
        "activeValidators": trsParams.partnerchainValidators,
        "certificateThreshold": trsParams.partnerchainCertificateThreshold,
    }
    create an entry in the chain validators data substore with
        storeKey = trsParams.chainID
        storeValue = encode(validatorsSchema, partnerchainValidators)

    # Create outbox root entry.
    create an entry in the outbox root substore with
        storeKey = trsParams.chainID
        storeValue = encode(outboxRootSchema, {"root": partnerchainChannel.outbox.root})

    # Initialize escrow account for token used for message fees
    # if the token is local to the chain.
    if Token.isNativeToken(trsParams.messageFeeTokenID):
        Token.initializeEscrowAccount(trsParams.chainID, trsParams.messageFeeTokenID)

    # Emit chain account updated event.
    emitEvent(
        module = MODULE_NAME_INTEROPERABILITY,
        name = EVENT_NAME_CHAIN_ACCOUNT_UPDATED,
        data = decode(chainDataSchema, trsParams.partnerchainAccount),
        topics = [trsParams.chainID]
    )

    # Send registration CCM to the sidechain.
    # We do not call sendInternal because it would fail as
    # the receiving chain is not active yet.
    registrationCCMParams = {
        "name": partnerchainAccount.name,
        "chainID": trsParams.chainID,
        "messageFeeTokenID": partnerchainChannel.messageFeeTokenID,
        "minReturnFeePerByte": partnerchainChannel.minReturnFeePerByte,
    }

    ccm = {
        "nonce": ownChainAccount.nonce,
        "module": MODULE_NAME_INTEROPERABILITY,
        "crossChainCommand": CROSS_CHAIN_COMMAND_REGISTRATION,
        "sendingChainID": ownChainAccount.chainID,
        "receivingChainID": trsParams.chainID,
        "fee": 0,
        "status": CCM_STATUS_CODE_OK,
        "params": encode(registrationCCMParamsSchema, registrationCCMParams) # registrationCCMParamsSchema is defined in LIP 0049
    }

    addToOutbox(trsParams.chainID, ccm)
    ownChainAccount.nonce += 1

    # Emit CCM Sent Event.
    ccmID = sha256(encode(crossChainMessageSchema, ccm))
    emitEvent(
        module = MODULE_NAME_INTEROPERABILITY,
        name = EVENT_NAME_CCM_SENT_SUCCESS,
        data = {"ccm": ccm},
        topics = [ccm.sendingChainID, ccm.receivingChainID, ccmID]
    )

Auxiliary Functions

The following auxiliary functions are used internally by the Interoperability module to verify and execute cross-chain updates. The function verifyRoutingRules specified below supersedes the same function specified in LIP 0053.

verifyRoutingRules

def verifyRoutingRules(ccu: CCU, ccm: CCM) -> None:    
    # Sending and receiving chains must differ.
   if ccm.receivingChainID == ccm.sendingChainID:
       raise Exception("Sending and receiving chains must differ.")

   # Processing on the mainchain.
    if ownChainAccount.chainID == getMainchainID():
        # The CCM must come from the sending chain.
        if ccu.params.sendingChainID != ccm.sendingChainID:
            raise Exception("CCM is not from the sending chain.")
        if ccm.status == CCM_STATUS_CODE_CHANNEL_UNAVAILABLE:
            raise Exception("CCM status channel unavailable can only be set on the mainchain.")
    # Processing on a sidechain.
    else:
        # The CCM must be directed to the sidechain.
        if ownChainAccount.chainID != ccm.receivingChainID:
            raise Exception("CCM is not directed to the sidechain.")
        # The following checks are added for the case of a CCU coming from a direct channel.
        if ccu.params.sendingChainID != getMainchainID():
            # The CCM must come from the sending chain.
            if ccu.params.sendingChainID != ccm.sendingChainID:
                raise Exception("CCM is not from the sending chain.")
            if ccm.status == CCM_STATUS_CODE_CHANNEL_UNAVAILABLE:
                raise Exception("CCM status channel unavailable can only be set on the mainchain.")

Genesis State Initialization

During the genesis state initialization stage of a genesis block g, the following steps are executed. If any step fails, the block is discarded and has no further effect.

Let genesisBlockAssetBytes be the data bytes included in the genesis block assets for the Interoperability module and let interoperabilityAsset = decode(genesisInteroperabilityStoreSchema, genesisBlockAssetBytes). Let ownChainName = interoperabilityAsset.ownChainName, ownChainNonce = interoperabilityAsset.ownChainNonce, chainInfos = interoperabilityAsset.chainInfos, terminatedStateAccounts = interoperabilityAsset.terminatedStateAccounts, and terminatedOutboxAccounts = interoperabilityAsset.terminatedOutboxAccounts.

Genesis Asset Verification

The following verification checks override the rules given in LIP 0045. The rules for the mainchain are not changed, while the rules for a sidechain are modified to allow the initialization of a direct channel. In both cases, it is checked that validateObjectSchema(genesisInteroperabilityStoreSchema, genesisBlockAssetBytes) does not throw an error.

Sidechain

On a sidechain, the Interoperability state can contain the chain account for the mainchain and chain accounts for sidechains for which there is a direct channel. Both cases require that the mainchain registration was done. Hence, chainInfos is either empty or it contains one entry for the mainchain and zero or more entries for other sidechains.

If chainInfos is empty, then check that:

  • ownChainName is the empty string;
  • ownChainNonce == 0;
  • terminatedStateAccounts is empty;
  • terminatedOutboxAccounts is empty.

If chainInfos is not empty, then check that:

  • ownChainName has length between MIN_CHAIN_NAME_LENGTH and MAX_CHAIN_NAME_LENGTH, is from the character set a-z0-9!@$&_., and ownChainName != CHAIN_NAME_MAINCHAIN;
  • ownChainNonce > 0;
  • chainInfos contains one entry mainchainInfo = chainInfos[0] with:
    • mainchainInfo.chainID == getMainchainID();
    • mainchainInfo.chainData.name == CHAIN_NAME_MAINCHAIN and mainchainInfo.chainData.status is either equal to CHAIN_STATUS_REGISTERED or to CHAIN_STATUS_ACTIVE;
    • mainchainInfo.channelData.messageFeeTokenID == Token.getTokenIDLSK();
    • mainchainInfo.channelData.minReturnFeePerByte == MIN_RETURN_FEE_PER_BYTE_BEDDOWS.
  • Each entry chainInfo in chainInfos has a unique chainInfo.chainID and chainInfos is ordered lexicographically by chainInfo.chainID. Furthermore for each entry it holds:
    • chainInfo.chainId[0] == getMainchainID()[0].
  • For each entry chainInfo in chainInfos, let chainData = chainInfo.chainData. The entries chainData.name must be pairwise distinct. Furthermore for each entry it holds:
    • chainData.lastCertificate.timestamp < g.header.timestamp;
    • chainData.name only uses the character set a-z0-9!@$&_.;
    • property chainData.status is in set {CHAIN_STATUS_REGISTERED, CHAIN_STATUS_ACTIVE, CHAIN_STATUS_TERMINATED}.
  • For each entry chainInfo in chainInfos, let channelData = chainInfo.channelData, then check:
    • Token.getChainID(channelData.messageFeeTokenID) == OWN_CHAIN_ID or Token.getChainID(channelData.messageFeeTokenID) == chainInfo.chainID.
  • For each entry chainInfo in chainInfos, let activeValidators = chainInfo.chainValidators.activeValidators and let certificateThreshold = chainInfo.chainValidators.certificateThreshold, then check:
    • activeValidators must have at least 1 element and at most MAX_NUM_VALIDATORS elements;
    • activeValidators must be ordered lexicographically by blsKey property;
    • all blsKey properties must be pairwise distinct;
    • for each validator in activeValidators, validator.bftWeight > 0 must hold;
    • let totalWeight be the sum of the bftWeight property of every element in activeValidators. Then totalWeight has to be less than or equal to MAX_UINT64;
    • check that totalWeight//3 + 1 <= certificateThreshold <= totalWeight, where // indicates integer division;
    • check that the corresponding validatorsHash stored in chainInfo.chainData.lastCertificate.validatorsHash matches with the value computed from activeValidators and certificateThreshold.
  • Each entry stateAccount in terminatedStateAccounts has a unique stateAccount.chainID and terminatedStateAccounts is ordered lexicographically by stateAccount.chainID. Furthermore for each entry it holds stateAccount.chainID != getMainchainID(), stateAccount.chainID != OWN_CHAIN_ID and stateAccount.chainId[0] == getMainchainID()[0].
  • For each entry stateAccount in terminatedStateAccounts either:
    • stateAccount.terminatedStateAccount.stateRoot != EMPTY_HASH, stateAccount.terminatedStateAccount.mainchainStateRoot == EMPTY_HASH, and stateAccount.terminatedStateAccount.initialized == True;
    • or stateAccount.terminatedStateAccount.stateRoot == EMPTY_HASH, stateAccount.terminatedStateAccount.mainchainStateRoot != EMPTY_HASH, and stateAccount.terminatedStateAccount.initialized == False.
  • Each entry outboxAccount in terminatedOutboxAccounts has a unique outboxAccount.chainID and terminatedOutboxAccounts is ordered lexicographically by outboxAccount.chainID. Furthermore, an entry outboxAccount in terminatedOutboxAccounts must have a corresponding entry (i.e., with chainID == outboxAccount.chainID) in terminatedStateAccounts. Notice that the opposite is not necessarily true, so that there could be an entry in terminatedStateAccounts without a corresponding entry in terminatedOutboxAccounts.

Backwards Compatibility

This LIP results in a hard fork of the sidechain Interoperability module, as nodes following the previous protocol will reject blocks according to the proposed protocol. In particular, sidechains following this proposal can still communicate with sidechains using the standard Interoperability module; therefore, there is no need for other sidechains to perform this upgrade. There is no change in the mainchain Interoperability module.

Reference Implementation

TBD