diff --git a/proposals/lip-0045.md b/proposals/lip-0045.md index e9eadaa21..5d6349141 100644 --- a/proposals/lip-0045.md +++ b/proposals/lip-0045.md @@ -7,7 +7,7 @@ Discussions-To: https://research.lisk.com/t/introduce-interoperability-module/29 Status: Draft Type: Standards Track Created: 2021-05-21 -Updated: 2023-01-30 +Updated: 2023-02-17 Requires: 0043, 0049, 0053, 0054 ``` @@ -212,7 +212,7 @@ In this section, we specify the substores that are part of the Interoperability | `OWN_CHAIN_ID` | bytes | | The chain ID of the chain under consideration. | | `CHAIN_REGISTRATION_FEE` | uint64 | 1000000000 | Fee to pay for a sidechain registration command in Beddows. | | `EMPTY_HASH` | bytes | `sha256(b"")` | SHA-256 hash of empty bytes. | -| `EMPTY_CCM` | CCM | An object following the [`crossChainMessageSchema`][lip-0049#ccmschema] schema with all properties set to their default values (see exact definition [below](#empty-cross-chain-message)). | The empty ccm object. | +| `EMPTY_CCM` | CCM | A CCM object partially following the [`crossChainMessageSchema`][lip-0049#ccmschema] schema with all properties set to their default values (see exact definition [below](#empty-cross-chain-message)). | The empty ccm object. | | `LIVENESS_LIMIT` | uint32 | 30x24x3600 | The maximum time interval for the liveness condition. | | `MAX_NUM_VALIDATORS` | uint32 | 199 | The maximum number of validators that can be registered.| | `MAX_UINT64` | uint64 | 18446744073709551615 | The maximum value that can be encoded in a uint64. | @@ -236,7 +236,7 @@ In this section, we specify the substores that are part of the Interoperability | `MIN_CHAIN_NAME_LENGTH` | uint32 | 1 | The minimum length of a string specifying the name of a chain. | | `MAX_CHAIN_NAME_LENGTH` | uint32 | 32 | The maximum length of a string specifying the name of a chain. | -We further use the utility function `getMainchainID()` defined in [LIP 0037][lip-0037#getMainchainID] to obtain the chain ID of the mainchain. +We further use the utility function `getMainchainID` defined in [LIP 0037][lip-0037#getMainchainID] to obtain the chain ID of the mainchain. #### Empty Cross-chain Message @@ -330,6 +330,8 @@ chainDataSchema = { "properties": { "name": { "dataType": "string", + "minLength": MIN_CHAIN_NAME_LENGTH, + "maxLength": MAX_CHAIN_NAME_LENGTH, "fieldNumber": 1 }, "lastCertificate": { @@ -546,6 +548,8 @@ ownChainAccountSchema = { "properties": { "name": { "dataType": "string", + "minLength": MIN_CHAIN_NAME_LENGTH, + "maxLength": MAX_CHAIN_NAME_LENGTH, "fieldNumber": 1 }, "chainID": { @@ -1384,187 +1388,100 @@ def isChainIDAvailable(chainID: ChainID) -> bool: ### Genesis Block Processing -#### 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)`. Each top level entry in the schema corresponds to the substore of the Interoperability module store with the same name. - -* Within each substore, check that all entries have a different `storeKey`. -* For each entry in the chain data substore there must be a corresponding entry (with the same value of `storeKey`) in the outbox root, channel data, and chain validators substores and viceversa. However, if the chain account has status set to terminated, the entry in the outbox root must *not* be present. -* For each entry in the terminated outbox substore there must be a corresponding entry (with the same value of `storeKey`) in the terminated state substore. -* For each entry in the terminated state substore, if the property `initialized` is set to `False`, the corresponding entry in the terminated outbox substore must *not* be present. Furthermore, the `stateRoot` must be set to `EMPTY_HASH` and `mainchainStateRoot` must not be set to `EMPTY_HASH`. Conversely, if the property `initialized` is set to `True`, the `stateRoot` must not be set to `EMPTY_HASH` and `mainchainStateRoot` must be set to `EMPTY_HASH`. -* For each entry `chainValidators` in `interoperabilityAsset.chainValidatorsSubstore`, let `activeValidators = chainValidators.storeValue.activeValidators` and let `certificateThreshold = chainValidators.storeValue.certificateThreshold`: - * `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 have length `BLS_PUBLIC_KEY_LENGTH` and must be pairwise distinct. - * The `bftWeight` property of each element must be positive integer. - * 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 `chainAccount.lastCertificate.validatorsHash` matches with the value computed from `activeValidators` and `certificateThreshold`. -* On a sidechain, if a chain account for another sidechain is present (`storeKey != getMainchainID()`), then a chain account for the mainchain (`storeKey == getMainchainID()`) must be present, as well as the own chain account. -* For each entry `chainData` in `interoperabilityAsset.chainDataSubstore`: - * Check that `chainData.storeKey[0] == ownChainAccount.chainID[0]` and `chainData.storeKey != ownChainAccount.chainID`. - * Check that `chainData.lastCertificate.timestamp < g.header.timestamp. -* For each entry `channelData` in `interoperabilityAsset.channelDataSubstore`, let `channel = channelData.storeValue`: - * Either `channel.messageFeeTokenID==Token.getTokenIDLSK()` (corresponding to the LSK token), or `Token.getChainID(channel.messageFeeTokenID)` must be equal to `channelData.storeKey` or `ownChainAccount.chainID` (the message fee token must be a native token of either chains). -* On a sidechain, `interoperabilityAsset.registeredNamesSubstore` must be empty. -* For every substore, create all the corresponding entries in the Interoperability module state. +#### Genesis Assets Schema ```java genesisInteroperabilityStoreSchema = { "type": "object", "required": [ - "outboxRootSubstore", - "chainDataSubstore", - "channelDataSubstore", - "chainValidatorsSubstore", - "ownChainDataSubstore", - "terminatedStateSubstore", - "terminatedOutboxSubstore", - "registeredNamesSubstore" + "ownChainName", + "ownChainNonce", + "chainInfos", + "terminatedStateAccounts", + "terminatedOutboxAccounts" ], "properties": { - "outboxRootSubstore": { - "type": "array", - "fieldNumber": 1, - "items": { - "type": "object", - "required": ["storeKey", "storeValue"], - "properties": { - "storeKey": { - "dataType": "bytes", - "fieldNumber": 1 - }, - "storeValue": { - ...outboxRootSchema, - "fieldNumber": 2, - } - } - } + "ownChainName": { + "dataType": "string", + "maxLength": MAX_CHAIN_NAME_LENGTH, + "fieldNumber": 1 }, - "chainDataSubstore": { + "ownChainNonce": { + "dataType": "uint64", + "fieldNumber": 2 + } + "chainInfos": { "type": "array", - "fieldNumber": 2, + "fieldNumber": 3, "items": { "type": "object", - "required": ["storeKey", "storeValue"], + "required": [ + "chainID", + "chainData", + "channelData", + "chainValidators" + ], "properties": { - "storeKey": { + "chainID": { "dataType": "bytes", + "length": CHAIN_ID_LENGTH, "fieldNumber": 1 }, - "storeValue": { + "chainData": { ...chainDataSchema, - "fieldNumber": 2, - } - } - } - }, - "channelDataSubstore": { - "type": "array", - "fieldNumber": 3, - "items": { - "type": "object", - "required": ["storeKey", "storeValue"], - "properties": { - "storeKey": { - "dataType": "bytes", - "fieldNumber": 1 + "fieldNumber": 2 }, - "storeValue": { + "channelData": { ...channelDataSchema, - "fieldNumber": 2, - } - } - } - }, - "chainValidatorsSubstore": { - "type": "array", - "fieldNumber": 4, - "items": { - "type": "object", - "required": ["storeKey", "storeValue"], - "properties": { - "storeKey": { - "dataType": "bytes", - "fieldNumber": 1 + "fieldNumber": 3 }, - "storeValue": { + "chainValidators": { ...chainValidatorsSchema, - "fieldNumber": 2, + "fieldNumber": 4 } } } }, - "ownChainDataSubstore": { + "terminatedStateAccounts": { "type": "array", - "fieldNumber": 5, - "items": { - "type": "object", - "required": ["storeKey", "storeValue"], - "properties": { - "storeKey": { - "dataType": "bytes", - "fieldNumber": 1 - }, - "storeValue": { - ...ownChainAccountSchema, - "fieldNumber": 2, - } - } - } - }, - "terminatedStateSubstore": { - "type": "array", - "fieldNumber": 6, + "fieldNumber": 4, "items": { "type": "object", - "required": ["storeKey", "storeValue"], + "required": [ + "chainID", + "terminatedStateAccount" + ], "properties": { - "storeKey": { + "chainID": { "dataType": "bytes", + "length": CHAIN_ID_LENGTH, "fieldNumber": 1 }, - "storeValue": { + "terminatedStateAccount": { ...terminatedStateAccountSchema, - "fieldNumber": 2, + "fieldNumber": 2 } } } }, - "terminatedOutboxSubstore": { + "terminatedOutboxAccounts": { "type": "array", - "fieldNumber": 7, + "fieldNumber": 5, "items": { "type": "object", - "required": ["storeKey", "storeValue"], + "required": [ + "chainID", + "terminatedOutboxAccount" + ], "properties": { - "storeKey": { + "chainID": { "dataType": "bytes", + "length": CHAIN_ID_LENGTH, "fieldNumber": 1 }, - "storeValue": { + "terminatedOutboxAccount": { ...terminatedOutboxAccountSchema, - "fieldNumber": 2, - } - } - } - }, - "registeredNamesSubstore": { - "type": "array", - "fieldNumber": 8, - "items": { - "type": "object", - "required": ["storeKey", "storeValue"], - "properties": { - "storeKey": { - "dataType": "bytes", - "fieldNumber": 1 - }, - "storeValue": { - ...registeredNamesSchema, - "fieldNumber": 2, + "fieldNumber": 2 } } } @@ -1575,9 +1492,96 @@ genesisInteroperabilityStoreSchema = { Here, the `...` notation, borrowed from [JavaScript ES6 data destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Object_destructuring), indicates that the corresponding schema should be inserted in place, and it is just used for notational convenience. +#### 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 genesis asset verification follows different rules on the mainchain or on a sidechain. In both cases, it is checked that `validateObjectSchema(genesisInteroperabilityStoreSchema, genesisBlockAssetBytes)` does not throw an error. + +###### Mainchain + +On the mainchain, the following checks are performed: + +* `ownChainName == CHAIN_NAME_MAINCHAIN`. +* If `chainInfos` is non-empty, then `ownChainNonce > 0`. Conversely, if `chainInfos` is empty, then `ownChainNonce == 0`. +* 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 != getMainchainID()`; + * `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: + * `channelData.messageFeeTokenID == Token.getTokenIDLSK()`; + * `channelData.minReturnFeePerByte == MIN_RETURN_FEE_PER_BYTE_LSK`. +* 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`. +* For each entry `chainInfo` in `chainInfos`, `chainInfo.chainData.status == CHAIN_STATUS_TERMINATED` if and only if a corresponding entry (i.e., with `chainID == chainInfo.chainID`) exists in `terminatedStateAccounts`. +* Each entry `stateAccount` in `terminatedStateAccounts` has a unique `stateAccount.chainID` and `terminatedStateAccounts` is ordered lexicographically by `stateAccount.chainID`. +* For each entry `stateAccount` in `terminatedStateAccounts` holds `stateAccount.stateRoot == chainData.lastCertificate.stateRoot`, `stateAccount.mainchainStateRoot == EMPTY_HASH`, and `stateAccount.initialized == True`. Here `chainData` is the corresponding entry (i.e., with `chainID == stateAccount.chainID`) in `chainInfos`. +* 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`. + + +###### Sidechain + +On a sidechain, the Interoperability state can only contain the chain account for the mainchain (if the mainchain registration was done) or be empty. Hence, `chainInfos` is either empty or it contains exactly one entry for the mainchain. + +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` is from the character set `a-z0-9!@$&_.`, has length between `MIN_CHAIN_NAME_LENGTH` and `MAX_CHAIN_NAME_LENGTH`, and `ownChainName != CHAIN_NAME_MAINCHAIN`; +* `ownChainNonce > 0`; +* `chainInfos` contains exactly one entry `mainchainInfo` with: + * `mainchainInfo.chainID == getMainchainID()`; + * `mainchainInfo.chainData.name == CHAIN_NAME_MAINCHAIN`, `mainchainInfo.chainData.status` is either equal to `CHAIN_STATUS_REGISTERED` or to `CHAIN_STATUS_ACTIVE`, and `mainchainInfo.chainData.lastCertificate.timestamp < g.header.timestamp`; + * `mainchainInfo.channelData.messageFeeTokenID == Token.getTokenIDLSK()`; + * `mainchainInfo.channelData.minReturnFeePerByte == MIN_RETURN_FEE_PER_BYTE_LSK`. +* Let `activeValidators = mainchainInfo.chainValidators.activeValidators` and let `certificateThreshold = mainchainInfo.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 `mainchainInfo.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()` and `stateAccount.chainID != ownChainAccount.chainID`. +* For each entry `stateAccount` in `terminatedStateAccounts` either: + * `stateAccount.stateRoot != EMPTY_HASH`, `stateAccount.mainchainStateRoot == EMPTY_HASH`, and `stateAccount.initialized == True`; + * or `stateAccount.stateRoot == EMPTY_HASH`, `stateAccount.mainchainStateRoot != EMPTY_HASH`, and `stateAccount.initialized == False`. +* `terminatedOutboxAccounts` is empty; + +##### Genesis State Processing + +* If `ownChainName` is not the empty string and `ownChainNonce != 0`, add an entry to the own chain substore with key set to `EMPTY_BYTES` and value set to `{"name": ownChainName, "chainID": OWN_CHAIN_ID, "nonce": ownChainNonce}`. +* For each entry `chainInfo` in `chainInfos` add the following substore entries with key set to `chainInfo.chainID`: + * with the value `chainInfo.chainData` to the chain data substore; + * with the value `chainInfo.channelData` to the channel data substore; + * with the value `chainInfo.chainValidators` to the chain validators substore; + * with the value `chainInfo.channelData.outbox.root` to the outbox root substore, only if `chainInfo.chainData.status != CHAIN_STATUS_TERMINATED`. +* For each entry `stateAccount` in `terminatedStateAccounts` add an entry to the terminated state substore with key set to `stateAccount.chainID` and value set to `stateAccount.terminatedStateAccount`. +* For each entry `outboxAccount` in `terminatedOutboxAccounts` add an entry to the terminated outbox substore with key set to `outboxAccount.chainID` and value set to `outboxAccount.terminatedOutboxAccount`. +* On the mainchain, for each `chainInfo` in `chainInfos` add an entry to the registered names substore with key `chainInfo.chainData.name` and value `chainInfo.chainID`. Furthermore add an entry for the mainchain with key `CHAIN_NAME_MAINCHAIN` and value `getMainchainID()`. + #### Genesis State Finalization -The Interoperability module does not execute any logic during the genesis state finalization. +* For each entry `chainInfo` in `chainInfos`, check that `Token.escrowSubstoreExists(chainInfo.chainID, chainInfo.channelData.messageFeeTokenID) == True`. ## Backwards Compatibility