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
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.
This LIP is licensed under the Creative Commons Zero 1.0 Universal.
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.
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 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.
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.
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).
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.
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. |
This command is used to create a direct channel.
Transactions executing this command have:
module = MODULE_NAME_INTEROPERABILITY
,command = COMMAND_REGISTER_DIRECT_CHANNEL
.
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 thepartnerchainValidators
to check thevalidatorsHash
property.bitmap
: The bitmap of the inclusion proof of thepartnerchainAccount
.siblingHashes
: The sibling hashes of the inclusion proof of thepartnerchainAccount
.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.
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.
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]
)
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.
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.")
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
.
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.
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 betweenMIN_CHAIN_NAME_LENGTH
andMAX_CHAIN_NAME_LENGTH
, is from the character seta-z0-9!@$&_.
, andownChainName != CHAIN_NAME_MAINCHAIN
;ownChainNonce > 0
;chainInfos
contains one entrymainchainInfo = chainInfos[0]
with:mainchainInfo.chainID == getMainchainID()
;mainchainInfo.chainData.name == CHAIN_NAME_MAINCHAIN
andmainchainInfo.chainData.status
is either equal toCHAIN_STATUS_REGISTERED
or toCHAIN_STATUS_ACTIVE
;mainchainInfo.channelData.messageFeeTokenID == Token.getTokenIDLSK()
;mainchainInfo.channelData.minReturnFeePerByte == MIN_RETURN_FEE_PER_BYTE_BEDDOWS
.
- Each entry
chainInfo
inchainInfos
has a uniquechainInfo.chainID
andchainInfos
is ordered lexicographically bychainInfo.chainID
. Furthermore for each entry it holds:chainInfo.chainId[0] == getMainchainID()[0]
.
- For each entry
chainInfo
inchainInfos
, letchainData = chainInfo.chainData
. The entrieschainData.name
must be pairwise distinct. Furthermore for each entry it holds:chainData.lastCertificate.timestamp < g.header.timestamp
;chainData.name
only uses the character seta-z0-9!@$&_.
;- property
chainData.status
is in set{CHAIN_STATUS_REGISTERED, CHAIN_STATUS_ACTIVE, CHAIN_STATUS_TERMINATED}
.
- For each entry
chainInfo
inchainInfos
, letchannelData = chainInfo.channelData
, then check:Token.getChainID(channelData.messageFeeTokenID) == OWN_CHAIN_ID
orToken.getChainID(channelData.messageFeeTokenID) == chainInfo.chainID
.
- For each entry
chainInfo
inchainInfos
, letactiveValidators = chainInfo.chainValidators.activeValidators
and letcertificateThreshold = chainInfo.chainValidators.certificateThreshold
, then check:activeValidators
must have at least 1 element and at mostMAX_NUM_VALIDATORS
elements;activeValidators
must be ordered lexicographically byblsKey
property;- all
blsKey
properties must be pairwise distinct; - for each
validator
inactiveValidators
,validator.bftWeight > 0
must hold; - let
totalWeight
be the sum of thebftWeight
property of every element inactiveValidators
. ThentotalWeight
has to be less than or equal toMAX_UINT64
; - check that
totalWeight//3 + 1 <= certificateThreshold <= totalWeight
, where//
indicates integer division; - check that the corresponding
validatorsHash
stored inchainInfo.chainData.lastCertificate.validatorsHash
matches with the value computed fromactiveValidators
andcertificateThreshold
.
- Each entry
stateAccount
interminatedStateAccounts
has a uniquestateAccount.chainID
andterminatedStateAccounts
is ordered lexicographically bystateAccount.chainID
. Furthermore for each entry it holdsstateAccount.chainID != getMainchainID()
,stateAccount.chainID != OWN_CHAIN_ID
andstateAccount.chainId[0] == getMainchainID()[0]
. - For each entry
stateAccount
interminatedStateAccounts
either:stateAccount.terminatedStateAccount.stateRoot != EMPTY_HASH
,stateAccount.terminatedStateAccount.mainchainStateRoot == EMPTY_HASH
, andstateAccount.terminatedStateAccount.initialized == True
;- or
stateAccount.terminatedStateAccount.stateRoot == EMPTY_HASH
,stateAccount.terminatedStateAccount.mainchainStateRoot != EMPTY_HASH
, andstateAccount.terminatedStateAccount.initialized == False
.
- Each entry
outboxAccount
interminatedOutboxAccounts
has a uniqueoutboxAccount.chainID
andterminatedOutboxAccounts
is ordered lexicographically byoutboxAccount.chainID
. Furthermore, an entryoutboxAccount
interminatedOutboxAccounts
must have a corresponding entry (i.e., withchainID == outboxAccount.chainID
) interminatedStateAccounts
. Notice that the opposite is not necessarily true, so that there could be an entry interminatedStateAccounts
without a corresponding entry interminatedOutboxAccounts
.
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.
TBD