diff --git a/examples/pos-mainchain/config/default/genesis_assets.json b/examples/pos-mainchain/config/default/genesis_assets.json index 8553cfb15b6..d575139c811 100644 --- a/examples/pos-mainchain/config/default/genesis_assets.json +++ b/examples/pos-mainchain/config/default/genesis_assets.json @@ -3,91 +3,54 @@ { "module": "interoperability", "data": { - "outboxRootSubstore": [], - "chainDataSubstore": [], - "channelDataSubstore": [], - "chainValidatorsSubstore": [], - "ownChainDataSubstore": [ - { - "storeKey": "", - "storeValue": { - "name": "lisk_mainchain", - "chainID": "04000000", - "nonce": 0 - } - } - ], - "terminatedStateSubstore": [], - "terminatedOutboxSubstore": [], - "registeredNamesSubstore": [ - { - "storeKey": "lisk_mainchain", - "storeValue": { - "chainID": "04000000" - } - } - ] + "ownChainName": "lisk_mainchain", + "ownChainNonce": 0, + "chainInfos": [], + "terminatedStateAccounts": [], + "terminatedOutboxAccounts": [] }, "schema": { "$id": "/interoperability/module/genesis", "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": { - "$id": "/modules/interoperability/outbox", - "type": "object", - "required": ["root"], - "properties": { - "root": { - "dataType": "bytes", - "minLength": 32, - "maxLength": 32, - "fieldNumber": 1 - } - }, - "fieldNumber": 2 - } - } - } + "ownChainName": { + "dataType": "string", + "maxLength": 32, + "fieldNumber": 1 + }, + "ownChainNonce": { + "dataType": "uint64", + "fieldNumber": 2 }, - "chainDataSubstore": { + "chainInfos": { "type": "array", - "fieldNumber": 2, + "fieldNumber": 3, "items": { "type": "object", - "required": ["storeKey", "storeValue"], + "required": ["chainID", "chainData", "channelData", "chainValidators"], "properties": { - "storeKey": { + "chainID": { "dataType": "bytes", + "minLength": 4, + "maxLength": 4, "fieldNumber": 1 }, - "storeValue": { + "chainData": { "$id": "/modules/interoperability/chainData", "type": "object", "required": ["name", "lastCertificate", "status"], "properties": { "name": { "dataType": "string", + "minLength": 1, + "maxLength": 32, "fieldNumber": 1 }, "lastCertificate": { @@ -123,25 +86,17 @@ } }, "fieldNumber": 2 - } - } - } - }, - "channelDataSubstore": { - "type": "array", - "fieldNumber": 3, - "items": { - "type": "object", - "required": ["storeKey", "storeValue"], - "properties": { - "storeKey": { - "dataType": "bytes", - "fieldNumber": 1 }, - "storeValue": { + "channelData": { "$id": "/modules/interoperability/channel", "type": "object", - "required": ["inbox", "outbox", "partnerChainOutboxRoot", "messageFeeTokenID"], + "required": [ + "inbox", + "outbox", + "partnerChainOutboxRoot", + "messageFeeTokenID", + "minReturnFeePerByte" + ], "properties": { "inbox": { "type": "object", @@ -158,14 +113,14 @@ "fieldNumber": 1 }, "size": { - "dataType": "uint32", - "fieldNumber": 2 + "fieldNumber": 2, + "dataType": "uint32" }, "root": { + "fieldNumber": 3, "dataType": "bytes", "minLength": 32, - "maxLength": 32, - "fieldNumber": 3 + "maxLength": 32 } } }, @@ -184,14 +139,14 @@ "fieldNumber": 1 }, "size": { - "dataType": "uint32", - "fieldNumber": 2 + "fieldNumber": 2, + "dataType": "uint32" }, "root": { + "fieldNumber": 3, "dataType": "bytes", "minLength": 32, - "maxLength": 32, - "fieldNumber": 3 + "maxLength": 32 } } }, @@ -206,26 +161,15 @@ "minLength": 8, "maxLength": 8, "fieldNumber": 4 + }, + "minReturnFeePerByte": { + "dataType": "uint64", + "fieldNumber": 5 } }, - "fieldNumber": 2 - } - } - } - }, - "chainValidatorsSubstore": { - "type": "array", - "fieldNumber": 4, - "items": { - "type": "object", - "required": ["storeKey", "storeValue"], - "properties": { - "storeKey": { - "dataType": "bytes", - "fieldNumber": 1 + "fieldNumber": 3 }, - "storeValue": { - "fieldNumber": 2, + "chainValidators": { "$id": "/modules/interoperability/chainValidators", "type": "object", "required": ["activeValidators", "certificateThreshold"], @@ -256,59 +200,26 @@ "dataType": "uint64", "fieldNumber": 2 } - } - } - } - } - }, - "ownChainDataSubstore": { - "type": "array", - "fieldNumber": 5, - "items": { - "type": "object", - "required": ["storeKey", "storeValue"], - "properties": { - "storeKey": { - "dataType": "bytes", - "fieldNumber": 1 - }, - "storeValue": { - "$id": "/modules/interoperability/ownChainAccount", - "type": "object", - "required": ["name", "chainID", "nonce"], - "properties": { - "name": { - "dataType": "string", - "fieldNumber": 1 - }, - "chainID": { - "dataType": "bytes", - "minLength": 4, - "maxLength": 4, - "fieldNumber": 2 - }, - "nonce": { - "dataType": "uint64", - "fieldNumber": 3 - } }, - "fieldNumber": 2 + "fieldNumber": 4 } } } }, - "terminatedStateSubstore": { + "terminatedStateAccounts": { "type": "array", - "fieldNumber": 6, + "fieldNumber": 4, "items": { "type": "object", - "required": ["storeKey", "storeValue"], + "required": ["chainID", "terminatedStateAccount"], "properties": { - "storeKey": { + "chainID": { "dataType": "bytes", + "minLength": 4, + "maxLength": 4, "fieldNumber": 1 }, - "storeValue": { + "terminatedStateAccount": { "$id": "/modules/interoperability/terminatedState", "type": "object", "required": ["stateRoot", "mainchainStateRoot", "initialized"], @@ -335,18 +246,20 @@ } } }, - "terminatedOutboxSubstore": { + "terminatedOutboxAccounts": { "type": "array", - "fieldNumber": 7, + "fieldNumber": 5, "items": { "type": "object", - "required": ["storeKey", "storeValue"], + "required": ["chainID", "terminatedOutboxAccount"], "properties": { - "storeKey": { + "chainID": { "dataType": "bytes", + "minLength": 4, + "maxLength": 4, "fieldNumber": 1 }, - "storeValue": { + "terminatedOutboxAccount": { "$id": "/modules/interoperability/terminatedOutbox", "type": "object", "required": ["outboxRoot", "outboxSize", "partnerChainInboxSize"], @@ -370,34 +283,6 @@ } } } - }, - "registeredNamesSubstore": { - "type": "array", - "fieldNumber": 8, - "items": { - "type": "object", - "required": ["storeKey", "storeValue"], - "properties": { - "storeKey": { - "dataType": "bytes", - "fieldNumber": 1 - }, - "storeValue": { - "$id": "/modules/interoperability/chainId", - "type": "object", - "required": ["chainID"], - "properties": { - "chainID": { - "dataType": "bytes", - "minLength": 4, - "maxLength": 4, - "fieldNumber": 1 - } - }, - "fieldNumber": 2 - } - } - } } } } diff --git a/examples/pos-mainchain/config/default/genesis_block.blob b/examples/pos-mainchain/config/default/genesis_block.blob index 36ccaf1b64e..5164a62a9ab 100644 Binary files a/examples/pos-mainchain/config/default/genesis_block.blob and b/examples/pos-mainchain/config/default/genesis_block.blob differ diff --git a/framework/src/modules/interoperability/base_interoperability_module.ts b/framework/src/modules/interoperability/base_interoperability_module.ts index d217b78b01c..f110bf6aa3e 100644 --- a/framework/src/modules/interoperability/base_interoperability_module.ts +++ b/framework/src/modules/interoperability/base_interoperability_module.ts @@ -12,17 +12,18 @@ * Removal or modification of this copyright notice is prohibited. */ -import { codec } from '@liskhq/lisk-codec'; -import { dataStructures } from '@liskhq/lisk-utils'; -import { MAX_UINT64, validator } from '@liskhq/lisk-validator'; +import { bufferArrayUniqueItems } from '@liskhq/lisk-utils/dist-node/objects'; +import { MAX_UINT64 } from '@liskhq/lisk-validator'; import { GenesisBlockExecuteContext } from '../../state_machine'; -import { splitTokenID } from '../token/utils'; import { BaseCCCommand } from './base_cc_command'; import { BaseCCMethod } from './base_cc_method'; import { BaseInteroperableModule } from './base_interoperable_module'; -import { EMPTY_HASH, MODULE_NAME_INTEROPERABILITY } from './constants'; -import { genesisInteroperabilitySchema } from './schemas'; -import { ChainAccountStore, ChainStatus } from './stores/chain_account'; +import { + MODULE_NAME_INTEROPERABILITY, + MIN_RETURN_FEE_PER_BYTE_BEDDOWS, + MAX_NUM_VALIDATORS, +} from './constants'; +import { ChainAccountStore } from './stores/chain_account'; import { ChainValidatorsStore } from './stores/chain_validators'; import { ChannelDataStore } from './stores/channel_data'; import { OutboxRootStore } from './stores/outbox_root'; @@ -30,14 +31,14 @@ import { OwnChainAccountStore } from './stores/own_chain_account'; import { RegisteredNamesStore } from './stores/registered_names'; import { TerminatedOutboxStore } from './stores/terminated_outbox'; import { TerminatedStateStore } from './stores/terminated_state'; -import { GenesisInteroperability } from './types'; -import { getMainchainID, getMainchainTokenID } from './utils'; +import { ChainInfo, TerminatedStateAccountWithChainID } from './types'; +import { getMainchainTokenID, computeValidatorsHash } from './utils'; export abstract class BaseInteroperabilityModule extends BaseInteroperableModule { protected interoperableCCCommands = new Map(); protected interoperableCCMethods = new Map(); - public constructor() { + protected constructor() { super(); this.stores.register(OutboxRootStore, new OutboxRootStore(this.name, 0)); this.stores.register(ChainAccountStore, new ChainAccountStore(this.name, 1)); @@ -59,274 +60,101 @@ export abstract class BaseInteroperabilityModule extends BaseInteroperableModule this.interoperableCCCommands.set(module.name, module.crossChainCommand); } - public async initGenesisState(ctx: GenesisBlockExecuteContext): Promise { - const assetBytes = ctx.assets.getAsset(MODULE_NAME_INTEROPERABILITY); - if (!assetBytes) { - return; - } - const mainchainID = getMainchainID(ctx.chainID); - const mainchainTokenID = getMainchainTokenID(ctx.chainID); - - const genesisStore = codec.decode( - genesisInteroperabilitySchema, - assetBytes, - ); - validator.validate(genesisInteroperabilitySchema, genesisStore); - - const outboxRootStoreKeySet = new dataStructures.BufferSet(); - const outboxRootStore = this.stores.get(OutboxRootStore); - for (const outboxRootData of genesisStore.outboxRootSubstore) { - if (outboxRootStoreKeySet.has(outboxRootData.storeKey)) { - throw new Error( - `Outbox root store key ${outboxRootData.storeKey.toString('hex')} is duplicated.`, - ); - } - outboxRootStoreKeySet.add(outboxRootData.storeKey); - await outboxRootStore.set(ctx, outboxRootData.storeKey, outboxRootData.storeValue); - } + // @see https://github.com/LiskHQ/lips/blob/main/proposals/lip-0045.md#mainchain + // eslint-disable-next-line @typescript-eslint/require-await,@typescript-eslint/no-empty-function + public async initGenesisState(_ctx: GenesisBlockExecuteContext): Promise {} - const channelDataStoreKeySet = new dataStructures.BufferSet(); - const channelDataStore = this.stores.get(ChannelDataStore); - for (const channelData of genesisStore.channelDataSubstore) { - if (channelDataStoreKeySet.has(channelData.storeKey)) { - throw new Error( - `Channel data store key ${channelData.storeKey.toString('hex')} is duplicated.`, - ); - } - channelDataStoreKeySet.add(channelData.storeKey); - - const channel = channelData.storeValue; - const [chainID] = splitTokenID(channel.messageFeeTokenID); + protected _verifyChannelData(ctx: GenesisBlockExecuteContext, chainInfo: ChainInfo) { + const mainchainTokenID = getMainchainTokenID(ctx.chainID); - if ( - !channel.messageFeeTokenID.equals(mainchainTokenID) && // corresponding to the LSK token - !chainID.equals(channelData.storeKey) && // Token.getChainID(channel.messageFeeTokenID) must be equal to channelData.storeKey - !chainID.equals(ctx.chainID) // the message fee token must be a native token of either chains - ) { - throw new Error( - `messageFeeTokenID corresponding to the channel data store key ${channelData.storeKey.toString( - 'hex', - )} is not valid.`, - ); - } + const { channelData } = chainInfo; - await channelDataStore.set(ctx, channelData.storeKey, channelData.storeValue); + // channelData.messageFeeTokenID == Token.getTokenIDLSK(); + if (!channelData.messageFeeTokenID.equals(mainchainTokenID)) { + throw new Error('channelData.messageFeeTokenID is not equal to Token.getTokenIDLSK().'); } - const chainValidatorsStoreKeySet = new dataStructures.BufferSet(); - const chainValidatorsStore = this.stores.get(ChainValidatorsStore); - for (const chainValidators of genesisStore.chainValidatorsSubstore) { - if (chainValidatorsStoreKeySet.has(chainValidators.storeKey)) { - throw new Error( - `Chain validators store key ${chainValidators.storeKey.toString('hex')} is duplicated.`, - ); - } - chainValidatorsStoreKeySet.add(chainValidators.storeKey); - - const { activeValidators, certificateThreshold } = chainValidators.storeValue; - - let totalWeight = BigInt(0); - for (let j = 0; j < activeValidators.length; j += 1) { - const activeValidator = activeValidators[j]; - - const { blsKey } = activeValidator; - if ( - j < activeValidators.length - 1 && - blsKey.compare(activeValidators[j + 1].blsKey) >= 0 - ) { - throw new Error( - 'Active validators must be ordered lexicographically by blsKey property and pairwise distinct.', - ); - } - const { bftWeight } = activeValidator; - if (bftWeight <= BigInt(0)) { - throw new Error('BFTWeight must be a positive integer.'); - } - totalWeight += bftWeight; - } - - if (totalWeight > MAX_UINT64) { - throw new Error( - 'The total BFT weight of all active validators has to be less than or equal to MAX_UINT64.', - ); - } - - const checkBftWeightValue = totalWeight / BigInt(3) + BigInt(1); - if (certificateThreshold > totalWeight || checkBftWeightValue > certificateThreshold) { - throw new Error('The total BFT weight of all active validators is not valid.'); - } - - await chainValidatorsStore.set(ctx, chainValidators.storeKey, chainValidators.storeValue); + // channelData.minReturnFeePerByte == MIN_RETURN_FEE_PER_BYTE_LSK. + if (channelData.minReturnFeePerByte !== MIN_RETURN_FEE_PER_BYTE_BEDDOWS) { + throw new Error( + `channelData.minReturnFeePerByte is not equal to ${MIN_RETURN_FEE_PER_BYTE_BEDDOWS}.`, + ); } + } - const chainDataStoreKeySet = new dataStructures.BufferSet(); - const chainDataStore = this.stores.get(ChainAccountStore); - let hasSidechainAccount = false; - for (const chainData of genesisStore.chainDataSubstore) { - const chainDataStoreKey = chainData.storeKey; - if (chainDataStoreKeySet.has(chainDataStoreKey)) { - throw new Error(`Chain data store key ${chainDataStoreKey.toString('hex')} is duplicated.`); - } - chainDataStoreKeySet.add(chainDataStoreKey); - - const chainAccountStatus = chainData.storeValue.status; - if (chainAccountStatus === ChainStatus.TERMINATED) { - if (outboxRootStoreKeySet.has(chainDataStoreKey)) { - throw new Error('Outbox root store cannot have entry for a terminated chain account.'); - } - if ( - !channelDataStoreKeySet.has(chainDataStoreKey) || - !chainValidatorsStoreKeySet.has(chainDataStoreKey) - ) { - throw new Error( - `Chain data store key ${chainDataStoreKey.toString( - 'hex', - )} missing in some or all of channel data and chain validators stores.`, - ); - } - } else if ( - !outboxRootStoreKeySet.has(chainDataStoreKey) || - !channelDataStoreKeySet.has(chainDataStoreKey) || - !chainValidatorsStoreKeySet.has(chainDataStoreKey) - ) { - throw new Error( - `Chain data store key ${chainDataStoreKey.toString( - 'hex', - )} missing in some or all of outbox root, channel data and chain validators stores.`, - ); - } - - if (!(chainDataStoreKey.equals(ctx.chainID) || chainDataStoreKey.equals(mainchainID))) { - hasSidechainAccount = true; - } - - await chainDataStore.set(ctx, chainData.storeKey, chainData.storeValue); - } + protected _verifyChainValidators(chainInfo: ChainInfo) { + const { chainValidators, chainData } = chainInfo; + const { activeValidators, certificateThreshold } = chainValidators; - if ( - hasSidechainAccount && - !(chainDataStoreKeySet.has(mainchainID) && chainDataStoreKeySet.has(ctx.chainID)) - ) { + // activeValidators must have at least 1 element and at most MAX_NUM_VALIDATORS elements + if (activeValidators.length === 0 || activeValidators.length > MAX_NUM_VALIDATORS) { throw new Error( - 'If a chain account for another sidechain is present, then a chain account for the mainchain must be present, as well as the own chain account.', + `activeValidators must have at least 1 element and at most ${MAX_NUM_VALIDATORS} elements.`, ); } - for (const storeKey of outboxRootStoreKeySet) { - if (!chainDataStoreKeySet.has(storeKey)) { - throw new Error( - `Outbox root store key ${storeKey.toString('hex')} is missing in chain data store.`, - ); + // activeValidators must be ordered lexicographically by blsKey property + const sortedByBlsKeys = [...activeValidators].sort((a, b) => a.blsKey.compare(b.blsKey)); + for (let i = 0; i < activeValidators.length; i += 1) { + if (!activeValidators[i].blsKey.equals(sortedByBlsKeys[i].blsKey)) { + throw new Error('activeValidators must be ordered lexicographically by blsKey property.'); } } - for (const storeKey of channelDataStoreKeySet) { - if (!chainDataStoreKeySet.has(storeKey)) { - throw new Error( - `Channel data store key ${storeKey.toString('hex')} is missing in chain data store.`, - ); - } + // all blsKey properties must be pairwise distinct + const blsKeys = activeValidators.map(v => v.blsKey); + if (!bufferArrayUniqueItems(blsKeys)) { + throw new Error(`All blsKey properties must be pairwise distinct.`); } - for (const storeKey of chainValidatorsStoreKeySet) { - if (!chainDataStoreKeySet.has(storeKey)) { - throw new Error( - `Chain validators store key ${storeKey.toString('hex')} is missing in chain data store.`, - ); - } + // for each validator in activeValidators, validator.bftWeight > 0 must hold + if (activeValidators.filter(v => v.bftWeight <= 0).length > 0) { + throw new Error(`validator.bftWeight must be > 0.`); } - const ownChainAccountStore = this.stores.get(OwnChainAccountStore); - const ownChainDataStoreKeySet = new dataStructures.BufferSet(); - for (const ownChainData of genesisStore.ownChainDataSubstore) { - if (ownChainDataStoreKeySet.has(ownChainData.storeKey)) { - throw new Error( - `Own chain data store key ${ownChainData.storeKey.toString('hex')} is duplicated.`, - ); - } - ownChainDataStoreKeySet.add(ownChainData.storeKey); - - await ownChainAccountStore.set(ctx, ownChainData.storeKey, ownChainData.storeValue); + // 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 + const totalWeight = activeValidators.reduce( + (accumulator, v) => accumulator + v.bftWeight, + BigInt(0), + ); + if (totalWeight > MAX_UINT64) { + throw new Error(`totalWeight has to be less than or equal to MAX_UINT64.`); } - const terminatedOutboxStoreKeySet = new dataStructures.BufferSet(); - const terminatedOutboxStore = this.stores.get(TerminatedOutboxStore); - for (const terminatedOutbox of genesisStore.terminatedOutboxSubstore) { - if (terminatedOutboxStoreKeySet.has(terminatedOutbox.storeKey)) { - throw new Error( - `Terminated outbox store key ${terminatedOutbox.storeKey.toString('hex')} is duplicated.`, - ); - } - terminatedOutboxStoreKeySet.add(terminatedOutbox.storeKey); - - await terminatedOutboxStore.set(ctx, terminatedOutbox.storeKey, terminatedOutbox.storeValue); + // check that totalWeight//3 + 1 <= certificateThreshold <= totalWeight, where // indicates integer division + if ( + totalWeight / BigInt(3) + BigInt(1) > certificateThreshold || // e.g. (300/3) + 1 = 101 > 20 + certificateThreshold > totalWeight + ) { + throw new Error('Invalid certificateThreshold input.'); } - const terminatedStateStoreKeySet = new dataStructures.BufferSet(); - const terminatedStateStore = this.stores.get(TerminatedStateStore); - for (const terminatedState of genesisStore.terminatedStateSubstore) { - const terminatedStateStoreKey = terminatedState.storeKey; - if (terminatedStateStoreKeySet.has(terminatedStateStoreKey)) { - throw new Error( - `Terminated state store key ${terminatedStateStoreKey.toString('hex')} is duplicated.`, - ); - } - terminatedStateStoreKeySet.add(terminatedStateStoreKey); - - const terminatedStateStoreValue = terminatedState.storeValue; - if (!terminatedStateStoreValue.initialized) { - if (terminatedOutboxStoreKeySet.has(terminatedStateStoreKey)) { - throw new Error( - `Uninitialized account associated with terminated state store key ${terminatedStateStoreKey.toString( - 'hex', - )} cannot be present in terminated outbox store.`, - ); - } - if ( - !terminatedStateStoreValue.stateRoot.equals(EMPTY_HASH) || - terminatedStateStoreValue.mainchainStateRoot.equals(EMPTY_HASH) - ) { - throw new Error( - `For the uninitialized account associated with terminated state store key ${terminatedStateStoreKey.toString( - 'hex', - )} the stateRoot must be set to empty hash and mainchainStateRoot to a 32-bytes value.`, - ); - } - } else if ( - terminatedStateStoreValue.stateRoot.equals(EMPTY_HASH) || - !terminatedStateStoreValue.mainchainStateRoot.equals(EMPTY_HASH) - ) { - throw new Error( - `For the initialized account associated with terminated state store key ${terminatedStateStoreKey.toString( - 'hex', - )} the mainchainStateRoot must be set empty value and stateRoot to a 32-bytes value.`, - ); - } - - await terminatedStateStore.set(ctx, terminatedState.storeKey, terminatedState.storeValue); + // check that the corresponding validatorsHash stored in chainInfo.chainData.lastCertificate.validatorsHash + // matches with the value computed from activeValidators and certificateThreshold + const { validatorsHash } = chainData.lastCertificate; + if (!validatorsHash.equals(computeValidatorsHash(activeValidators, certificateThreshold))) { + throw new Error('Invalid validatorsHash from chainData.lastCertificate.'); } + } - for (const storeKey of terminatedOutboxStoreKeySet) { - if (!terminatedStateStoreKeySet.has(storeKey)) { - throw new Error( - `Terminated outbox store key ${storeKey.toString( - 'hex', - )} missing in terminated state store.`, - ); - } + protected _verifyTerminatedStateAccountsCommon( + terminatedStateAccounts: TerminatedStateAccountWithChainID[], + ) { + // Each entry stateAccount in terminatedStateAccounts has a unique stateAccount.chainID + const chainIDs = terminatedStateAccounts.map(a => a.chainID); + if (!bufferArrayUniqueItems(chainIDs)) { + throw new Error(`terminatedStateAccounts don't hold unique chainID.`); } - const registeredNamesStoreKeySet = new dataStructures.BufferSet(); - const registeredNamesStore = this.stores.get(RegisteredNamesStore); - for (const registeredNames of genesisStore.registeredNamesSubstore) { - if (registeredNamesStoreKeySet.has(registeredNames.storeKey)) { - throw new Error( - `Registered names store key ${registeredNames.storeKey.toString('hex')} is duplicated.`, - ); + // terminatedStateAccounts is ordered lexicographically by stateAccount.chainID + const sortedByChainID = [...terminatedStateAccounts].sort((a, b) => + a.chainID.compare(b.chainID), + ); + for (let i = 0; i < terminatedStateAccounts.length; i += 1) { + if (!terminatedStateAccounts[i].chainID.equals(sortedByChainID[i].chainID)) { + throw new Error('terminatedStateAccounts must be ordered lexicographically by chainID.'); } - registeredNamesStoreKeySet.add(registeredNames.storeKey); - - await registeredNamesStore.set(ctx, registeredNames.storeKey, registeredNames.storeValue); } } } diff --git a/framework/src/modules/interoperability/mainchain/module.ts b/framework/src/modules/interoperability/mainchain/module.ts index 035590f9d1f..3a1d504e6d7 100644 --- a/framework/src/modules/interoperability/mainchain/module.ts +++ b/framework/src/modules/interoperability/mainchain/module.ts @@ -12,6 +12,9 @@ * Removal or modification of this copyright notice is prohibited. */ +import { codec } from '@liskhq/lisk-codec'; +import { bufferArrayUniqueItems } from '@liskhq/lisk-utils/dist-node/objects'; +import { validator } from '@liskhq/lisk-validator'; import { ModuleMetadata } from '../../base_module'; import { BaseInteroperabilityModule } from '../base_interoperability_module'; import { MainchainInteroperabilityMethod } from './method'; @@ -32,7 +35,7 @@ import { isChainNameAvailableRequestSchema, isChainNameAvailableResponseSchema, } from '../schemas'; -import { chainDataSchema, allChainAccountsSchema } from '../stores/chain_account'; +import { chainDataSchema, allChainAccountsSchema, ChainStatus } from '../stores/chain_account'; import { channelSchema } from '../stores/channel_data'; import { ownChainAccountSchema } from '../stores/own_chain_account'; import { terminatedStateSchema } from '../stores/terminated_state'; @@ -51,11 +54,20 @@ import { TerminatedStateCreatedEvent } from '../events/terminated_state_created' import { TerminatedOutboxCreatedEvent } from '../events/terminated_outbox_created'; import { MainchainInteroperabilityInternalMethod } from './internal_method'; import { InitializeMessageRecoveryCommand } from './commands/initialize_message_recovery'; -import { FeeMethod } from '../types'; +import { + FeeMethod, + GenesisInteroperability, + ChainInfo, + TerminatedStateAccountWithChainID, + TerminatedOutboxAccountWithChainID, +} from '../types'; import { MainchainCCChannelTerminatedCommand, MainchainCCRegistrationCommand } from './cc_commands'; import { RecoverStateCommand } from './commands/recover_state'; import { CcmSentFailedEvent } from '../events/ccm_send_fail'; import { InvalidRegistrationSignatureEvent } from '../events/invalid_registration_signature'; +import { GenesisBlockExecuteContext } from '../../../state_machine'; +import { MODULE_NAME_INTEROPERABILITY, CHAIN_NAME_MAINCHAIN, EMPTY_HASH } from '../constants'; +import { getMainchainID, isValidName, validNameCharset } from '../utils'; export class MainchainInteroperabilityModule extends BaseInteroperabilityModule { public crossChainMethod = new MainchainCCMethod(this.stores, this.events); @@ -227,4 +239,238 @@ export class MainchainInteroperabilityModule extends BaseInteroperabilityModule ], }; } + + // @see https://github.com/LiskHQ/lips/blob/main/proposals/lip-0045.md#mainchain + // eslint-disable-next-line @typescript-eslint/require-await + public async initGenesisState(ctx: GenesisBlockExecuteContext): Promise { + const genesisBlockAssetBytes = ctx.assets.getAsset(MODULE_NAME_INTEROPERABILITY); + if (!genesisBlockAssetBytes) { + return; + } + + const genesisInteroperability = codec.decode( + genesisInteroperabilitySchema, + genesisBlockAssetBytes, + ); + + validator.validate( + genesisInteroperabilitySchema, + genesisInteroperability, + ); + + const { + ownChainName, + ownChainNonce, + chainInfos, + terminatedStateAccounts, + terminatedOutboxAccounts, + } = genesisInteroperability; + + // On the mainchain, the following checks are performed: + if (ctx.chainID.equals(getMainchainID(ctx.chainID))) { + // ownChainName == CHAIN_NAME_MAINCHAIN. + if (ownChainName !== CHAIN_NAME_MAINCHAIN) { + throw new Error(`ownChainName must be equal to ${CHAIN_NAME_MAINCHAIN}.`); + } + + // if chainInfos is empty, then ownChainNonce == 0 + if (chainInfos.length === 0) { + if (ownChainNonce !== BigInt(0)) { + throw new Error(`ownChainNonce must be 0 if chainInfos is empty.`); + } + } + + // If chainInfos is non-empty + // ownChainNonce > 0 + if (ownChainNonce <= 0) { + throw new Error(`ownChainNonce must be positive if chainInfos is not empty.`); + } + + // Each entry chainInfo in chainInfos has a unique chainInfo.chainID + const chainIDs = chainInfos.map(info => info.chainID); + if (!bufferArrayUniqueItems(chainIDs)) { + throw new Error(`chainInfos doesn't hold unique chainID.`); + } + + // chainInfos should be ordered lexicographically by chainInfo.chainID + const sortedByChainID = [...chainInfos].sort((a, b) => a.chainID.compare(b.chainID)); + for (let i = 0; i < chainInfos.length; i += 1) { + if (!chainInfos[i].chainID.equals(sortedByChainID[i].chainID)) { + throw new Error('chainInfos is not ordered lexicographically by chainID.'); + } + } + + // The entries chainData.name must be pairwise distinct + const chainDataNames = chainInfos.map(info => info.chainData.name); + if (new Set(chainDataNames).size !== chainDataNames.length) { + throw new Error(`chainData.name must be pairwise distinct.`); + } + + this._verifyChainInfos(ctx, chainInfos); + this._verifyTerminatedStateAccounts(chainInfos, terminatedStateAccounts); + this._verifyTerminatedOutboxAccounts( + chainInfos, + terminatedStateAccounts, + terminatedOutboxAccounts, + ); + } + } + + // https://github.com/LiskHQ/lips/blob/main/proposals/lip-0045.md#mainchain + private _verifyChainInfos(ctx: GenesisBlockExecuteContext, chainInfos: ChainInfo[]) { + const mainchainID = getMainchainID(ctx.chainID); + + // verify root level properties + for (const chainInfo of chainInfos) { + const { chainID } = chainInfo; + + // chainInfo.chainID != getMainchainID(); + if (chainID.equals(mainchainID)) { + throw new Error(`chainID must be not equal to ${mainchainID.toString('hex')}.`); + } + + // - chainInfo.chainId[0] == getMainchainID()[0]. + if (chainID[0] !== mainchainID[0]) { + throw new Error(`chainID[0] doesn't match ${mainchainID[0]}.`); + } + + this._verifyChainData(ctx, chainInfo); + this._verifyChannelData(ctx, chainInfo); + this._verifyChainValidators(chainInfo); + } + } + + private _verifyChainData(ctx: GenesisBlockExecuteContext, chainInfo: ChainInfo) { + const validStatuses = [ChainStatus.REGISTERED, ChainStatus.ACTIVE, ChainStatus.TERMINATED]; + + const { chainData } = chainInfo; + + // chainData.lastCertificate.timestamp < g.header.timestamp; + if (chainData.lastCertificate.timestamp >= ctx.header.timestamp) { + throw new Error(`chainData.lastCertificate.timestamp must be less than header.timestamp.`); + } + + // chainData.name only uses the character set a-z0-9!@$&_.; + if (!isValidName(chainData.name)) { + throw new Error(`chainData.name only uses the character set ${validNameCharset}.`); + } + + // chainData.status is in set {CHAIN_STATUS_REGISTERED, CHAIN_STATUS_ACTIVE, CHAIN_STATUS_TERMINATED}. + if (!validStatuses.includes(chainData.status)) { + throw new Error(`chainData.status must be one of ${validStatuses.join(', ')}`); + } + } + + // https://github.com/LiskHQ/lips/blob/main/proposals/lip-0045.md#mainchain + private _verifyTerminatedStateAccounts( + chainInfos: ChainInfo[], + terminatedStateAccounts: TerminatedStateAccountWithChainID[], + ) { + // Sanity check to fulfill if-and-only-if situation + for (const account of terminatedStateAccounts) { + const correspondingChainInfo = chainInfos.find(chainInfo => + chainInfo.chainID.equals(account.chainID), + ); + if ( + !correspondingChainInfo || + correspondingChainInfo.chainData.status !== ChainStatus.TERMINATED + ) { + throw new Error( + 'For each terminatedStateAccount there should be a corresponding chainInfo at TERMINATED state', + ); + } + } + + // Each entry stateAccount in terminatedStateAccounts has a unique stateAccount.chainID + const chainIDs = terminatedStateAccounts.map(a => a.chainID); + if (!bufferArrayUniqueItems(chainIDs)) { + throw new Error(`terminatedStateAccounts don't hold unique chainID.`); + } + + // terminatedStateAccounts is ordered lexicographically by stateAccount.chainID + const sortedByChainID = [...terminatedStateAccounts].sort((a, b) => + a.chainID.compare(b.chainID), + ); + for (let i = 0; i < terminatedStateAccounts.length; i += 1) { + if (!terminatedStateAccounts[i].chainID.equals(sortedByChainID[i].chainID)) { + throw new Error('terminatedStateAccounts must be ordered lexicographically by chainID.'); + } + } + + for (const chainInfo of chainInfos) { + // 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. + if (chainInfo.chainData.status === ChainStatus.TERMINATED) { + const terminatedAccount = terminatedStateAccounts.find(tAccount => + tAccount.chainID.equals(chainInfo.chainID), + ); + if (!terminatedAccount) { + throw new Error( + 'For each chainInfo with status terminated there should be a corresponding entry in terminatedStateAccounts', + ); + } + + // 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. + const stateAccount = terminatedAccount.terminatedStateAccount; + if (stateAccount) { + if (!stateAccount.stateRoot.equals(chainInfo.chainData.lastCertificate.stateRoot)) { + throw new Error( + "stateAccount.stateRoot doesn't match chainInfo.chainData.lastCertificate.stateRoot.", + ); + } + + if (!stateAccount.mainchainStateRoot.equals(EMPTY_HASH)) { + throw new Error( + `stateAccount.mainchainStateRoot is not equal to ${EMPTY_HASH.toString('hex')}.`, + ); + } + + if (!stateAccount.initialized) { + throw new Error('stateAccount is not initialized.'); + } + } + } + } + } + + // https://github.com/LiskHQ/lips/blob/main/proposals/lip-0045.md#mainchain + private _verifyTerminatedOutboxAccounts( + _chainInfos: ChainInfo[], + terminatedStateAccounts: TerminatedStateAccountWithChainID[], + terminatedOutboxAccounts: TerminatedOutboxAccountWithChainID[], + ) { + // Each entry outboxAccount in terminatedOutboxAccounts has a unique outboxAccount.chainID + const chainIDs = terminatedOutboxAccounts.map(a => a.chainID); + if (!bufferArrayUniqueItems(chainIDs)) { + throw new Error('terminatedOutboxAccounts do not hold unique chainID.'); + } + + // terminatedOutboxAccounts is ordered lexicographically by outboxAccount.chainID + const sortedByChainID = [...terminatedOutboxAccounts].sort((a, b) => + a.chainID.compare(b.chainID), + ); + for (let i = 0; i < terminatedOutboxAccounts.length; i += 1) { + if (!terminatedOutboxAccounts[i].chainID.equals(sortedByChainID[i].chainID)) { + throw new Error('terminatedOutboxAccounts must be ordered lexicographically by chainID.'); + } + } + + // Furthermore, an entry outboxAccount in terminatedOutboxAccounts must have a corresponding entry + // (i.e., with chainID == outboxAccount.chainID) in terminatedStateAccounts + for (const outboxAccount of terminatedOutboxAccounts) { + if ( + terminatedStateAccounts.find(a => a.chainID.equals(outboxAccount.chainID)) === undefined + ) { + throw new Error( + `Each entry outboxAccount in terminatedOutboxAccounts must have a corresponding entry in terminatedStateAccount. outboxAccount with chainID: ${outboxAccount.chainID.toString( + 'hex', + )} does not exist in terminatedStateAccounts`, + ); + } + } + } } diff --git a/framework/src/modules/interoperability/schemas.ts b/framework/src/modules/interoperability/schemas.ts index 4db1c019aab..e13aa43cf8e 100644 --- a/framework/src/modules/interoperability/schemas.ts +++ b/framework/src/modules/interoperability/schemas.ts @@ -29,11 +29,8 @@ import { import { chainDataSchema } from './stores/chain_account'; import { chainValidatorsSchema } from './stores/chain_validators'; import { channelSchema } from './stores/channel_data'; -import { outboxRootSchema } from './stores/outbox_root'; -import { ownChainAccountSchema } from './stores/own_chain_account'; -import { registeredNamesSchema } from './stores/registered_names'; -import { terminatedOutboxSchema } from './stores/terminated_outbox'; import { terminatedStateSchema } from './stores/terminated_state'; +import { terminatedOutboxSchema } from './stores/terminated_outbox'; // LIP: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0049.md#cross-chain-message-schema export const ccmSchema = { @@ -93,6 +90,28 @@ export const ccmSchema = { }, }; +const activeChainValidatorsSchema = { + type: 'array', + items: { + type: 'object', + required: ['blsKey', 'bftWeight'], + properties: { + blsKey: { + dataType: 'bytes', + fieldNumber: 1, + minLength: BLS_PUBLIC_KEY_LENGTH, + maxLength: BLS_PUBLIC_KEY_LENGTH, + }, + bftWeight: { + dataType: 'uint64', + fieldNumber: 2, + }, + }, + }, + minItems: 1, + // maxItems: MAX_NUM_VALIDATORS, +}; + export const sidechainRegParams = { $id: '/modules/interoperability/mainchain/sidechainRegistration', type: 'object', @@ -111,25 +130,8 @@ export const sidechainRegParams = { maxLength: MAX_CHAIN_NAME_LENGTH, }, sidechainValidators: { - type: 'array', + ...activeChainValidatorsSchema, fieldNumber: 3, - items: { - type: 'object', - required: ['blsKey', 'bftWeight'], - properties: { - blsKey: { - dataType: 'bytes', - fieldNumber: 1, - minLength: BLS_PUBLIC_KEY_LENGTH, - maxLength: BLS_PUBLIC_KEY_LENGTH, - }, - bftWeight: { - dataType: 'uint64', - fieldNumber: 2, - }, - }, - }, - minItems: 1, maxItems: MAX_NUM_VALIDATORS, }, sidechainCertificateThreshold: { @@ -164,25 +166,8 @@ export const mainchainRegParams = { maxLength: MAX_CHAIN_NAME_LENGTH, }, mainchainValidators: { - type: 'array', + ...activeChainValidatorsSchema, fieldNumber: 3, - items: { - type: 'object', - required: ['blsKey', 'bftWeight'], - properties: { - blsKey: { - dataType: 'bytes', - fieldNumber: 1, - minLength: BLS_PUBLIC_KEY_LENGTH, - maxLength: BLS_PUBLIC_KEY_LENGTH, - }, - bftWeight: { - dataType: 'uint64', - fieldNumber: 2, - }, - }, - }, - minItems: 1, maxItems: NUMBER_ACTIVE_VALIDATORS_MAINCHAIN, }, mainchainCertificateThreshold: { @@ -459,25 +444,8 @@ export const registrationSignatureMessageSchema = { maxLength: MAX_CHAIN_NAME_LENGTH, }, mainchainValidators: { - type: 'array', + ...activeChainValidatorsSchema, fieldNumber: 3, - items: { - type: 'object', - required: ['blsKey', 'bftWeight'], - properties: { - blsKey: { - dataType: 'bytes', - fieldNumber: 1, - minLength: BLS_PUBLIC_KEY_LENGTH, - maxLength: BLS_PUBLIC_KEY_LENGTH, - }, - bftWeight: { - dataType: 'uint64', - fieldNumber: 2, - }, - }, - }, - minItems: 1, maxItems: NUMBER_ACTIVE_VALIDATORS_MAINCHAIN, }, mainchainCertificateThreshold: { @@ -648,164 +616,95 @@ export const getTerminatedStateAccountRequestSchema = getChainAccountRequestSche export const getTerminatedOutboxAccountRequestSchema = getChainAccountRequestSchema; +// https://github.com/LiskHQ/lips/blob/main/proposals/lip-0045.md#genesis-assets-schema export const genesisInteroperabilitySchema = { $id: '/interoperability/module/genesis', type: 'object', required: [ - 'outboxRootSubstore', - 'chainDataSubstore', - 'channelDataSubstore', - 'chainValidatorsSubstore', - 'ownChainDataSubstore', - 'terminatedStateSubstore', - 'terminatedOutboxSubstore', - 'registeredNamesSubstore', + 'ownChainName', + 'ownChainNonce', + 'chainInfos', + 'terminatedStateAccounts', + 'terminatedOutboxAccounts', ], properties: { - outboxRootSubstore: { - type: 'array', + ownChainName: { + dataType: 'string', + maxLength: MAX_CHAIN_NAME_LENGTH, fieldNumber: 1, - items: { - type: 'object', - required: ['storeKey', 'storeValue'], - properties: { - storeKey: { - dataType: 'bytes', - fieldNumber: 1, - }, - storeValue: { - ...outboxRootSchema, - fieldNumber: 2, - }, - }, - }, }, - chainDataSubstore: { - type: 'array', + ownChainNonce: { + dataType: 'uint64', fieldNumber: 2, - items: { - type: 'object', - required: ['storeKey', 'storeValue'], - properties: { - storeKey: { - dataType: 'bytes', - fieldNumber: 1, - }, - storeValue: { - ...chainDataSchema, - fieldNumber: 2, - }, - }, - }, }, - channelDataSubstore: { + chainInfos: { type: 'array', fieldNumber: 3, items: { type: 'object', - required: ['storeKey', 'storeValue'], + required: ['chainID', 'chainData', 'channelData', 'chainValidators'], properties: { - storeKey: { + chainID: { dataType: 'bytes', + minLength: CHAIN_ID_LENGTH, + maxLength: CHAIN_ID_LENGTH, fieldNumber: 1, }, - storeValue: { - ...channelSchema, + chainData: { + ...chainDataSchema, fieldNumber: 2, }, - }, - }, - }, - chainValidatorsSubstore: { - type: 'array', - fieldNumber: 4, - items: { - type: 'object', - required: ['storeKey', 'storeValue'], - properties: { - storeKey: { - dataType: 'bytes', - fieldNumber: 1, + channelData: { + ...channelSchema, + fieldNumber: 3, }, - storeValue: { - fieldNumber: 2, + chainValidators: { ...chainValidatorsSchema, + fieldNumber: 4, }, }, }, }, - ownChainDataSubstore: { - type: 'array', - fieldNumber: 5, - items: { - type: 'object', - required: ['storeKey', 'storeValue'], - properties: { - storeKey: { - dataType: 'bytes', - fieldNumber: 1, - }, - storeValue: { - ...ownChainAccountSchema, - fieldNumber: 2, - }, - }, - }, - }, - terminatedStateSubstore: { + terminatedStateAccounts: { type: 'array', - fieldNumber: 6, + fieldNumber: 4, items: { type: 'object', - required: ['storeKey', 'storeValue'], + required: ['chainID', 'terminatedStateAccount'], properties: { - storeKey: { + chainID: { dataType: 'bytes', + minLength: CHAIN_ID_LENGTH, + maxLength: CHAIN_ID_LENGTH, fieldNumber: 1, }, - storeValue: { + terminatedStateAccount: { ...terminatedStateSchema, fieldNumber: 2, }, }, }, }, - terminatedOutboxSubstore: { + terminatedOutboxAccounts: { type: 'array', - fieldNumber: 7, + fieldNumber: 5, items: { type: 'object', - required: ['storeKey', 'storeValue'], + required: ['chainID', 'terminatedOutboxAccount'], properties: { - storeKey: { + chainID: { dataType: 'bytes', + minLength: CHAIN_ID_LENGTH, + maxLength: CHAIN_ID_LENGTH, fieldNumber: 1, }, - storeValue: { + terminatedOutboxAccount: { ...terminatedOutboxSchema, fieldNumber: 2, }, }, }, }, - registeredNamesSubstore: { - type: 'array', - fieldNumber: 8, - items: { - type: 'object', - required: ['storeKey', 'storeValue'], - properties: { - storeKey: { - dataType: 'bytes', - fieldNumber: 1, - }, - storeValue: { - ...registeredNamesSchema, - fieldNumber: 2, - }, - }, - }, - }, }, }; diff --git a/framework/src/modules/interoperability/sidechain/module.ts b/framework/src/modules/interoperability/sidechain/module.ts index 25fe8db9c47..ab4a6d57133 100644 --- a/framework/src/modules/interoperability/sidechain/module.ts +++ b/framework/src/modules/interoperability/sidechain/module.ts @@ -11,6 +11,8 @@ * * Removal or modification of this copyright notice is prohibited. */ +import { codec } from '@liskhq/lisk-codec'; +import { validator } from '@liskhq/lisk-validator'; import { ModuleInitArgs, ModuleMetadata } from '../../base_module'; import { BaseInteroperabilityModule } from '../base_interoperability_module'; import { SidechainInteroperabilityMethod } from './method'; @@ -28,9 +30,9 @@ import { getChainValidatorsResponseSchema, isChainIDAvailableRequestSchema, } from '../schemas'; -import { chainDataSchema, allChainAccountsSchema } from '../stores/chain_account'; +import { chainDataSchema, allChainAccountsSchema, ChainStatus } from '../stores/chain_account'; import { channelSchema } from '../stores/channel_data'; -import { ownChainAccountSchema } from '../stores/own_chain_account'; +import { ownChainAccountSchema, OwnChainAccountStore } from '../stores/own_chain_account'; import { terminatedStateSchema } from '../stores/terminated_state'; import { terminatedOutboxSchema } from '../stores/terminated_outbox'; import { ChainAccountUpdatedEvent } from '../events/chain_account_updated'; @@ -38,13 +40,28 @@ import { CcmProcessedEvent } from '../events/ccm_processed'; import { InvalidRegistrationSignatureEvent } from '../events/invalid_registration_signature'; import { CcmSendSuccessEvent } from '../events/ccm_send_success'; import { BaseCCMethod } from '../base_cc_method'; -import { TokenMethod, ValidatorsMethod } from '../types'; +import { + TokenMethod, + ValidatorsMethod, + GenesisInteroperability, + TerminatedStateAccountWithChainID, +} from '../types'; import { SidechainInteroperabilityInternalMethod } from './internal_method'; import { SubmitSidechainCrossChainUpdateCommand } from './commands'; import { InitializeStateRecoveryCommand } from './commands/initialize_state_recovery'; import { RecoverStateCommand } from './commands/recover_state'; import { SidechainCCChannelTerminatedCommand, SidechainCCRegistrationCommand } from './cc_commands'; import { CcmSentFailedEvent } from '../events/ccm_send_fail'; +import { GenesisBlockExecuteContext } from '../../../state_machine'; +import { + MODULE_NAME_INTEROPERABILITY, + MIN_CHAIN_NAME_LENGTH, + MAX_CHAIN_NAME_LENGTH, + CHAIN_NAME_MAINCHAIN, + EMPTY_BYTES, + EMPTY_HASH, +} from '../constants'; +import { isValidName, validNameCharset, getMainchainID } from '../utils'; export class SidechainInteroperabilityModule extends BaseInteroperabilityModule { public crossChainMethod: BaseCCMethod = new SidechainCCMethod(this.stores, this.events); @@ -195,4 +212,184 @@ export class SidechainInteroperabilityModule extends BaseInteroperabilityModule public async init(_args: ModuleInitArgs) { this._mainchainRegistrationCommand.addDependencies(this._validatorsMethod); } + + // @see https://github.com/LiskHQ/lips/blob/main/proposals/lip-0045.md#mainchain + // eslint-disable-next-line @typescript-eslint/require-await + public async initGenesisState(ctx: GenesisBlockExecuteContext): Promise { + const genesisBlockAssetBytes = ctx.assets.getAsset(MODULE_NAME_INTEROPERABILITY); + if (!genesisBlockAssetBytes) { + return; + } + + const genesisInteroperability = codec.decode( + genesisInteroperabilitySchema, + genesisBlockAssetBytes, + ); + + validator.validate( + genesisInteroperabilitySchema, + genesisInteroperability, + ); + + await this._verifyChainInfos(ctx, genesisInteroperability); + } + + private async _verifyChainInfos( + ctx: GenesisBlockExecuteContext, + genesisInteroperability: GenesisInteroperability, + ) { + const { + ownChainName, + ownChainNonce, + chainInfos, + terminatedStateAccounts, + terminatedOutboxAccounts, + } = genesisInteroperability; + + // If chainInfos is empty, then check that: + // + // ownChainName is the empty string; + // ownChainNonce == 0; + // terminatedStateAccounts is empty; + // terminatedOutboxAccounts is empty. + if (chainInfos.length === 0) { + const ifChainInfosIsEmpty = 'if chainInfos is empty.'; + if (ownChainName !== '') { + throw new Error(`ownChainName must be empty string, ${ifChainInfosIsEmpty}.`); + } + if (ownChainNonce !== BigInt(0)) { + throw new Error(`ownChainNonce must be 0, ${ifChainInfosIsEmpty}.`); + } + if (terminatedStateAccounts.length !== 0) { + throw new Error(`terminatedStateAccounts must be empty, ${ifChainInfosIsEmpty}.`); + } + if (terminatedOutboxAccounts.length !== 0) { + throw new Error(`terminatedOutboxAccounts must be empty, ${ifChainInfosIsEmpty}.`); + } + } else { + // 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; + if ( + ownChainName.length < MIN_CHAIN_NAME_LENGTH || + ownChainName.length > MAX_CHAIN_NAME_LENGTH // will only run if not already applied in schema + ) { + throw new Error( + `ownChainName.length must be between ${MIN_CHAIN_NAME_LENGTH} and ${MAX_CHAIN_NAME_LENGTH}`, + ); + } + // CAUTION! + // this check is intentionally applied after MIN_CHAIN_NAME_LENGTH, as it will fail for empty string + if (!isValidName(ownChainName)) { + throw new Error(`ownChainName must have only ${validNameCharset} character set.`); + } + if (ownChainName === CHAIN_NAME_MAINCHAIN) { + throw new Error(`ownChainName must be not equal to ${CHAIN_NAME_MAINCHAIN}.`); + } + + // ownChainNonce > 0 + if (ownChainNonce < 1) { + throw new Error('ownChainNonce must be > 0.'); + } + + // chainInfos contains exactly one entry mainchainInfo with: + if (chainInfos.length > 1) { + throw new Error('chainInfos must contain exactly one entry.'); + } + // mainchainInfo.chainID == getMainchainID(); + const mainchainInfo = chainInfos[0]; + const mainchainID = getMainchainID(mainchainInfo.chainID); + if (!mainchainInfo.chainID.equals(mainchainID)) { + throw new Error(`mainchainInfo.chainID must be equal to ${mainchainID.toString('hex')}.`); + } + // mainchainInfo.chainData.name == CHAIN_NAME_MAINCHAIN, + // mainchainInfo.chainData.status is either equal to CHAIN_STATUS_REGISTERED or to CHAIN_STATUS_ACTIVE, + // mainchainInfo.chainData.lastCertificate.timestamp < g.header.timestamp; + if (mainchainInfo.chainData.name !== CHAIN_NAME_MAINCHAIN) { + throw new Error(`chainData.name must be equal to ${CHAIN_NAME_MAINCHAIN}.`); + } + const validStatues = [ChainStatus.REGISTERED, ChainStatus.ACTIVE]; + if (!validStatues.includes(mainchainInfo.chainData.status)) { + throw new Error(`chainData.status must be one of ${validStatues.join(', ')}.`); + } + if (mainchainInfo.chainData.lastCertificate.timestamp > ctx.header.timestamp) { + throw new Error('chainData.lastCertificate.timestamp must be < header.timestamp.'); + } + // channelData + this._verifyChannelData(ctx, mainchainInfo); + + // activeValidators + this._verifyChainValidators(mainchainInfo); + } + + // terminatedStateAccounts + await this._verifyTerminatedStateAccounts(ctx, terminatedStateAccounts); + + // terminatedOutboxAccounts + if (terminatedOutboxAccounts.length !== 0) { + throw new Error('terminatedOutboxAccounts must be empty.'); + } + } + + private async _verifyTerminatedStateAccounts( + ctx: GenesisBlockExecuteContext, + terminatedStateAccounts: TerminatedStateAccountWithChainID[], + ) { + this._verifyTerminatedStateAccountsCommon(terminatedStateAccounts); + + const mainchainID = getMainchainID(ctx.chainID); + + for (const stateAccount of terminatedStateAccounts) { + // stateAccount.chainID != getMainchainID() + if (stateAccount.chainID.equals(mainchainID)) { + throw new Error( + `stateAccount.chainID most be not equal to ${mainchainID.toString('hex')}.`, + ); + } + + const ownChainAccount = await this.stores.get(OwnChainAccountStore).get(ctx, EMPTY_BYTES); + // and stateAccount.chainID != ownChainAccount.chainID. + if (stateAccount.chainID.equals(ownChainAccount.chainID)) { + throw new Error(`stateAccount.chainID must be not equal to 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. + const { terminatedStateAccount } = stateAccount; + if (terminatedStateAccount.initialized) { + if (terminatedStateAccount.stateRoot.equals(EMPTY_HASH)) { + throw new Error( + `stateAccount.stateRoot mst be not equal to "${EMPTY_HASH.toString( + 'hex', + )}", if initialized is true.`, + ); + } + if (!terminatedStateAccount.mainchainStateRoot.equals(EMPTY_HASH)) { + throw new Error( + `terminatedStateAccount.mainchainStateRoot must be equal to "${EMPTY_HASH.toString( + 'hex', + )}", if initialized is true`, + ); + } + } else { + // initialized is false + if (!terminatedStateAccount.stateRoot.equals(EMPTY_HASH)) { + throw new Error( + `stateAccount.stateRoot mst be equal to "${EMPTY_HASH.toString( + 'hex', + )}", if initialized is false.`, + ); + } + if (terminatedStateAccount.mainchainStateRoot.equals(EMPTY_HASH)) { + throw new Error( + `terminatedStateAccount.mainchainStateRoot must be not equal to "${EMPTY_HASH.toString( + 'hex', + )}", if initialized is false.`, + ); + } + } + } + } } diff --git a/framework/src/modules/interoperability/stores/chain_account.ts b/framework/src/modules/interoperability/stores/chain_account.ts index e7c6e1cfd19..b149f5f6d46 100644 --- a/framework/src/modules/interoperability/stores/chain_account.ts +++ b/framework/src/modules/interoperability/stores/chain_account.ts @@ -13,7 +13,13 @@ */ import { utils } from '@liskhq/lisk-cryptography'; import { BaseStore, ImmutableStoreGetter } from '../../base_store'; -import { HASH_LENGTH, MAX_UINT32, STORE_PREFIX } from '../constants'; +import { + HASH_LENGTH, + MAX_CHAIN_NAME_LENGTH, + MAX_UINT32, + MIN_CHAIN_NAME_LENGTH, + STORE_PREFIX, +} from '../constants'; // Chain status export const enum ChainStatus { @@ -48,6 +54,8 @@ const chainDataJSONSchema = { properties: { name: { dataType: 'string', + minLength: MIN_CHAIN_NAME_LENGTH, + maxLength: MAX_CHAIN_NAME_LENGTH, fieldNumber: 1, }, lastCertificate: { diff --git a/framework/src/modules/interoperability/types.ts b/framework/src/modules/interoperability/types.ts index 50211f1fcd2..6c707223f94 100644 --- a/framework/src/modules/interoperability/types.ts +++ b/framework/src/modules/interoperability/types.ts @@ -24,8 +24,6 @@ import { SubStore, Validator, } from '../../state_machine/types'; -import { OutboxRoot } from './stores/outbox_root'; -import { ChainID } from './stores/registered_names'; import { TerminatedOutboxAccount } from './stores/terminated_outbox'; import { TerminatedStateAccount } from './stores/terminated_state'; @@ -361,39 +359,29 @@ export interface ChainValidatorsJSON { certificateThreshold: string; } +export interface ChainInfo { + chainID: Buffer; + chainData: ChainAccount; + channelData: ChannelData; + chainValidators: ChainValidators; +} + +export interface TerminatedStateAccountWithChainID { + chainID: Buffer; + terminatedStateAccount: TerminatedStateAccount; +} + +export interface TerminatedOutboxAccountWithChainID { + chainID: Buffer; + terminatedOutboxAccount: TerminatedOutboxAccount; +} + export interface GenesisInteroperability { - outboxRootSubstore: { - storeKey: Buffer; - storeValue: OutboxRoot; - }[]; - chainDataSubstore: { - storeKey: Buffer; - storeValue: ChainAccount; - }[]; - channelDataSubstore: { - storeKey: Buffer; - storeValue: ChannelData; - }[]; - chainValidatorsSubstore: { - storeKey: Buffer; - storeValue: ValidatorsHashInput; - }[]; - ownChainDataSubstore: { - storeKey: Buffer; - storeValue: OwnChainAccount; - }[]; - terminatedStateSubstore: { - storeKey: Buffer; - storeValue: TerminatedStateAccount; - }[]; - terminatedOutboxSubstore: { - storeKey: Buffer; - storeValue: TerminatedOutboxAccount; - }[]; - registeredNamesSubstore: { - storeKey: Buffer; - storeValue: ChainID; - }[]; + ownChainName: string; + ownChainNonce: bigint; + chainInfos: ChainInfo[]; + terminatedStateAccounts: TerminatedStateAccountWithChainID[]; + terminatedOutboxAccounts: TerminatedOutboxAccountWithChainID[]; } export interface CCMRegistrationParams { diff --git a/framework/src/modules/interoperability/utils.ts b/framework/src/modules/interoperability/utils.ts index 2cf91bafcbc..f1678f15cf8 100644 --- a/framework/src/modules/interoperability/utils.ts +++ b/framework/src/modules/interoperability/utils.ts @@ -90,6 +90,9 @@ export const handlePromiseErrorWithNull = async (promise: Promise) => { }; export const isValidName = (username: string): boolean => /^[a-z0-9!@$&_.]+$/g.test(username); +// CAUTION! +// It must hold range from above `isValidName` function (as it's used in relevant error messages) +export const validNameCharset = 'a-z0-9!@$&_.'; export const computeValidatorsHash = ( initValidators: ActiveValidators[], diff --git a/framework/src/testing/create_contexts.ts b/framework/src/testing/create_contexts.ts index f57533be581..e1a04829a6e 100644 --- a/framework/src/testing/create_contexts.ts +++ b/framework/src/testing/create_contexts.ts @@ -63,20 +63,24 @@ const createTestHeader = () => validatorsHash: utils.hash(Buffer.alloc(0)), }); -export const createGenesisBlockContext = (params: { +export type CreateGenesisBlockContextParams = { header?: BlockHeader; stateStore?: PrefixedStateReadWriter; eventQueue?: EventQueue; assets?: BlockAssets; logger?: Logger; chainID?: Buffer; -}): GenesisBlockContext => { +}; + +export const createGenesisBlockContext = ( + params: CreateGenesisBlockContextParams, +): GenesisBlockContext => { const logger = params.logger ?? loggerMock; const stateStore = params.stateStore ?? new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); const eventQueue = params.eventQueue ?? new EventQueue(params.header ? params.header.height : 0); const header = params.header ?? createTestHeader(); - const ctx = new GenesisBlockContext({ + return new GenesisBlockContext({ eventQueue, stateStore, header, @@ -84,7 +88,6 @@ export const createGenesisBlockContext = (params: { logger, chainID: params.chainID ?? Buffer.from('10000000', 'hex'), }); - return ctx; }; export const createBlockContext = (params: { @@ -103,7 +106,7 @@ export const createBlockContext = (params: { const contextStore = params.contextStore ?? new Map(); const eventQueue = params.eventQueue ?? new EventQueue(params.header ? params.header.height : 0); const header = params.header ?? createTestHeader(); - const ctx = new BlockContext({ + return new BlockContext({ stateStore, contextStore, logger, @@ -113,7 +116,6 @@ export const createBlockContext = (params: { assets: params.assets ?? new BlockAssets(), chainID: params.chainID ?? utils.getRandomBytes(4), }); - return ctx; }; export const createBlockGenerateContext = (params: { @@ -136,7 +138,7 @@ export const createBlockGenerateContext = (params: { const getStore = (moduleID: Buffer, storePrefix: Buffer) => stateStore.getStore(moduleID, storePrefix); - const ctx: InsertAssetContext = { + return { stateStore, contextStore, assets: params.assets ?? new BlockAssets([]), @@ -154,8 +156,6 @@ export const createBlockGenerateContext = (params: { getFinalizedHeight: () => params.finalizedHeight ?? 0, header, }; - - return ctx; }; export const createTransactionContext = (params: { @@ -174,7 +174,7 @@ export const createTransactionContext = (params: { const contextStore = params.contextStore ?? new Map(); const eventQueue = params.eventQueue ?? new EventQueue(params.header ? params.header.height : 0); const header = params.header ?? createTestHeader(); - const ctx = new TransactionContext({ + return new TransactionContext({ stateStore, contextStore, logger, @@ -184,7 +184,6 @@ export const createTransactionContext = (params: { chainID: params.chainID ?? utils.getRandomBytes(32), transaction: params.transaction, }); - return ctx; }; export const createTransientMethodContext = (params: { @@ -196,8 +195,7 @@ export const createTransientMethodContext = (params: { params.stateStore ?? new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); const contextStore = params.contextStore ?? new Map(); const eventQueue = params.eventQueue ?? new EventQueue(0); - const ctx = createMethodContext({ stateStore, eventQueue, contextStore }); - return ctx; + return createMethodContext({ stateStore, eventQueue, contextStore }); }; export const createTransientModuleEndpointContext = (params: { @@ -214,7 +212,7 @@ export const createTransientModuleEndpointContext = (params: { const parameters = params.params ?? {}; const logger = params.logger ?? loggerMock; const chainID = params.chainID ?? Buffer.alloc(0); - const ctx = { + return { getStore: (moduleID: Buffer, storePrefix: Buffer) => stateStore.getStore(moduleID, storePrefix), getOffchainStore: (moduleID: Buffer, storePrefix: Buffer) => moduleStore.getStore(moduleID, storePrefix), @@ -224,7 +222,6 @@ export const createTransientModuleEndpointContext = (params: { logger, chainID, }; - return ctx; }; export const createCrossChainMessageContext = (params: { diff --git a/framework/test/unit/modules/interoperability/base_interoperability_module.spec.ts b/framework/test/unit/modules/interoperability/base_interoperability_module.spec.ts index 4c1e9dd40f3..be895ff27c6 100644 --- a/framework/test/unit/modules/interoperability/base_interoperability_module.spec.ts +++ b/framework/test/unit/modules/interoperability/base_interoperability_module.spec.ts @@ -1,1013 +1,524 @@ -/* - * Copyright © 2022 Lisk Foundation - * - * See the LICENSE file at the top-level directory of this distribution - * for licensing information. - * - * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, - * no part of this software, including this file, may be copied, modified, - * propagated, or distributed except according to the terms contained in the - * LICENSE file. - * - * Removal or modification of this copyright notice is prohibited. - */ - -import { BlockAssets } from '@liskhq/lisk-chain'; +import { MAX_UINT64 } from '@liskhq/lisk-validator'; import { codec } from '@liskhq/lisk-codec'; -import { utils } from '@liskhq/lisk-cryptography'; -import { MainchainInteroperabilityModule } from '../../../../src'; -import { BaseInteroperabilityModule } from '../../../../src/modules/interoperability/base_interoperability_module'; +import { BlockAssets } from '@liskhq/lisk-chain'; +import { + genesisInteroperability, + chainInfo, + chainValidators, + activeValidator, + activeValidators, + chainData, + lastCertificate, + terminatedStateAccount, + channelData, +} from './interopFixtures'; +import { + ActiveValidator, + GenesisBlockExecuteContext, + MODULE_NAME_INTEROPERABILITY, + MainchainInteroperabilityModule, + ChainStatus, +} from '../../../../src'; import { - BLS_PUBLIC_KEY_LENGTH, - CHAIN_ID_LENGTH, - EMPTY_HASH, - HASH_LENGTH, MAX_NUM_VALIDATORS, - MAX_UINT64, MIN_RETURN_FEE_PER_BYTE_BEDDOWS, - MODULE_NAME_INTEROPERABILITY, } from '../../../../src/modules/interoperability/constants'; -import { genesisInteroperabilitySchema } from '../../../../src/modules/interoperability/schemas'; +import { GenesisInteroperability } from '../../../../src/modules/interoperability/types'; import { - ChainAccountStore, - ChainStatus, -} from '../../../../src/modules/interoperability/stores/chain_account'; -import { ChainValidatorsStore } from '../../../../src/modules/interoperability/stores/chain_validators'; -import { ChannelDataStore } from '../../../../src/modules/interoperability/stores/channel_data'; -import { OutboxRootStore } from '../../../../src/modules/interoperability/stores/outbox_root'; -import { OwnChainAccountStore } from '../../../../src/modules/interoperability/stores/own_chain_account'; -import { RegisteredNamesStore } from '../../../../src/modules/interoperability/stores/registered_names'; -import { TerminatedOutboxStore } from '../../../../src/modules/interoperability/stores/terminated_outbox'; -import { TerminatedStateStore } from '../../../../src/modules/interoperability/stores/terminated_state'; + CreateGenesisBlockContextParams, + createGenesisBlockContext, + InMemoryPrefixedStateDB, +} from '../../../../src/testing'; +import { genesisInteroperabilitySchema } from '../../../../src/modules/interoperability/schemas'; import { PrefixedStateReadWriter } from '../../../../src/state_machine/prefixed_state_read_writer'; -import { createGenesisBlockContext } from '../../../../src/testing'; -import { InMemoryPrefixedStateDB } from '../../../../src/testing/in_memory_prefixed_state'; +import { computeValidatorsHash } from '../../../../src/modules/interoperability/utils'; + +const createInitGenesisStateContext = ( + genesisInterop: GenesisInteroperability, + params: CreateGenesisBlockContextParams, +): GenesisBlockExecuteContext => { + const encodedAsset = codec.encode(genesisInteroperabilitySchema, genesisInterop); -describe('BaseInteroperabilityModule', () => { - let interopMod: BaseInteroperabilityModule; + return createGenesisBlockContext({ + ...params, + assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), + }).createInitGenesisStateContext(); +}; - const mainchainID = Buffer.from([0, 0, 0, 0]); - const ownChainID = Buffer.from([0, 0, 1, 0]); - const mainchainTokenID = Buffer.concat([mainchainID, Buffer.alloc(4)]); +describe('initGenesisState Common Tests', () => { + const chainID = Buffer.from([0, 0, 0, 0]); + + let stateStore: PrefixedStateReadWriter; + let interopMod: MainchainInteroperabilityModule; + let certificateThreshold = BigInt(0); + let params: CreateGenesisBlockContextParams; beforeEach(() => { + stateStore = new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); interopMod = new MainchainInteroperabilityModule(); + params = { + stateStore, + chainID, + }; }); - describe('initGenesisState', () => { - const timestamp = 2592000 * 100; - const chainAccount = { - name: 'account1', - chainID: Buffer.alloc(CHAIN_ID_LENGTH), - lastCertificate: { - height: 567467, - timestamp: timestamp - 500000, - stateRoot: Buffer.alloc(HASH_LENGTH), - validatorsHash: Buffer.alloc(HASH_LENGTH), - }, - status: 2739, - }; - const sidechainChainAccount = { - name: 'sidechain1', - chainID: Buffer.alloc(CHAIN_ID_LENGTH), - lastCertificate: { - height: 10, - stateRoot: utils.getRandomBytes(32), - timestamp: 100, - validatorsHash: utils.getRandomBytes(32), - }, - status: ChainStatus.TERMINATED, - }; - const ownChainAccount = { - name: 'mainchain', - chainID: mainchainID, - nonce: BigInt('0'), - }; - const channelData = { - inbox: { - appendPath: [Buffer.alloc(HASH_LENGTH), Buffer.alloc(HASH_LENGTH)], - root: utils.getRandomBytes(HASH_LENGTH), - size: 18, - }, - messageFeeTokenID: mainchainTokenID, - outbox: { - appendPath: [Buffer.alloc(HASH_LENGTH), Buffer.alloc(HASH_LENGTH)], - root: utils.getRandomBytes(HASH_LENGTH), - size: 18, - }, - partnerChainOutboxRoot: utils.getRandomBytes(HASH_LENGTH), - minReturnFeePerByte: MIN_RETURN_FEE_PER_BYTE_BEDDOWS, - }; - const outboxRoot = { root: utils.getRandomBytes(HASH_LENGTH) }; - const validatorsHashInput = { - activeValidators: [ + describe('channelData', () => { + it(`should throw error if channelData.messageFeeTokenID is not equal to Token.getTokenIDLSK()`, async () => { + const context = createInitGenesisStateContext( { - blsKey: Buffer.from( - '3c1e6f29e3434f816cd6697e56cc54bc8d80927bf65a1361b383aa338cd3f63cbf82ce801b752cb32f8ecb3f8cc16835', - 'hex', - ), - bftWeight: BigInt(10), + ...genesisInteroperability, + chainInfos: [ + { + ...chainInfo, + channelData: { + ...genesisInteroperability.chainInfos[0].channelData, + messageFeeTokenID: Buffer.from('12345678'), + }, + }, + ], }, { - blsKey: Buffer.from( - '4c1e6f29e3434f816cd6697e56cc54bc8d80927bf65a1361b383aa338cd3f63cbf82ce801b752cb32f8ecb3f8cc16835', - 'hex', - ), - bftWeight: BigInt(10), + ...params, + header: { + timestamp: Date.now(), + } as any, }, - ], - certificateThreshold: BigInt(10), - }; - const terminatedStateAccount = { - stateRoot: sidechainChainAccount.lastCertificate.stateRoot, - mainchainStateRoot: EMPTY_HASH, - initialized: true, - }; - const terminatedOutboxAccount = { - outboxRoot: utils.getRandomBytes(HASH_LENGTH), - outboxSize: 1, - partnerChainInboxSize: 1, - }; - const registeredNameId = { chainID: Buffer.alloc(CHAIN_ID_LENGTH) }; - const registeredChainId = { chainID: Buffer.alloc(CHAIN_ID_LENGTH) }; - const validData = { - outboxRootSubstore: [ - { storeKey: mainchainID, storeValue: outboxRoot }, - { storeKey: ownChainID, storeValue: outboxRoot }, - ], - chainDataSubstore: [ - { storeKey: mainchainID, storeValue: chainAccount }, - { storeKey: ownChainID, storeValue: chainAccount }, - ], - channelDataSubstore: [ - { storeKey: mainchainID, storeValue: channelData }, - { storeKey: ownChainID, storeValue: channelData }, - ], - chainValidatorsSubstore: [ - { storeKey: mainchainID, storeValue: validatorsHashInput }, - { storeKey: ownChainID, storeValue: validatorsHashInput }, - ], - ownChainDataSubstore: [ - { storeKey: mainchainID, storeValue: ownChainAccount }, - { storeKey: ownChainID, storeValue: ownChainAccount }, - ], - terminatedStateSubstore: [ - { storeKey: mainchainID, storeValue: terminatedStateAccount }, - { storeKey: ownChainID, storeValue: terminatedStateAccount }, - ], - terminatedOutboxSubstore: [ - { storeKey: mainchainID, storeValue: terminatedOutboxAccount }, - { storeKey: ownChainID, storeValue: terminatedOutboxAccount }, - ], - registeredNamesSubstore: [ - { storeKey: mainchainID, storeValue: registeredNameId }, - { storeKey: ownChainID, storeValue: registeredNameId }, - ], - registeredChainIDsSubstore: [ - { storeKey: mainchainID, storeValue: registeredChainId }, - { storeKey: ownChainID, storeValue: registeredChainId }, - ], - }; - - const invalidData = { - ...validData, - outboxRootSubstore: [ - { storeKey: mainchainID, storeValue: { root: utils.getRandomBytes(37) } }, - { storeKey: ownChainID, storeValue: { root: utils.getRandomBytes(5) } }, - ], - }; - - let stateStore: PrefixedStateReadWriter; - let channelDataSubstore: ChannelDataStore; - let outboxRootSubstore: OutboxRootStore; - let terminatedOutboxSubstore: TerminatedOutboxStore; - let chainDataSubstore: ChainAccountStore; - let terminatedStateSubstore: TerminatedStateStore; - let chainValidatorsSubstore: ChainValidatorsStore; - let ownChainDataSubstore: OwnChainAccountStore; - let registeredNamesSubstore: RegisteredNamesStore; - - beforeEach(() => { - stateStore = new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); - ownChainDataSubstore = interopMod.stores.get(OwnChainAccountStore); - channelDataSubstore = interopMod.stores.get(ChannelDataStore); - chainValidatorsSubstore = interopMod.stores.get(ChainValidatorsStore); - outboxRootSubstore = interopMod.stores.get(OutboxRootStore); - terminatedOutboxSubstore = interopMod.stores.get(TerminatedOutboxStore); - chainDataSubstore = interopMod.stores.get(ChainAccountStore); - terminatedStateSubstore = interopMod.stores.get(TerminatedStateStore); - registeredNamesSubstore = interopMod.stores.get(RegisteredNamesStore); - }); - - it('should not throw error if asset does not exist', async () => { - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - }).createInitGenesisStateContext(); - jest.spyOn(context, 'getStore'); - - await expect(interopMod.initGenesisState(context)).toResolve(); - expect(context.getStore).not.toHaveBeenCalled(); - }); - - it('should throw if the asset object is invalid', async () => { - const encodedAsset = codec.encode(genesisInteroperabilitySchema, invalidData); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); - - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'Lisk validator found 2 error', - ); - }); - - it('should throw if outbox root store key is duplicated', async () => { - const validData1 = { - ...validData, - outboxRootSubstore: [ - { storeKey: ownChainID, storeValue: outboxRoot }, - { storeKey: ownChainID, storeValue: outboxRoot }, - ], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); - await expect(interopMod.initGenesisState(context)).rejects.toThrow('Outbox root store key'); - }); - - it('should throw if chain data store key is duplicated', async () => { - const validData1 = { - ...validData, - chainDataSubstore: [ - { storeKey: ownChainID, storeValue: chainAccount }, - { storeKey: ownChainID, storeValue: chainAccount }, - ], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); - await expect(interopMod.initGenesisState(context)).rejects.toThrow('Chain data store key'); - }); - - it('should throw if channel data store key is duplicated', async () => { - const validData1 = { - ...validData, - channelDataSubstore: [ - { storeKey: ownChainID, storeValue: channelData }, - { storeKey: ownChainID, storeValue: channelData }, - ], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); - await expect(interopMod.initGenesisState(context)).rejects.toThrow('Channel data store key'); - }); - - it('should throw if chain validators store key is duplicated', async () => { - const validData1 = { - ...validData, - chainValidatorsSubstore: [ - { storeKey: ownChainID, storeValue: validatorsHashInput }, - { storeKey: ownChainID, storeValue: validatorsHashInput }, - ], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'Chain validators store key', - ); - }); - - it('should throw if own chain store key is duplicated', async () => { - const validData1 = { - ...validData, - ownChainDataSubstore: [ - { storeKey: ownChainID, storeValue: ownChainAccount }, - { storeKey: ownChainID, storeValue: ownChainAccount }, - ], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'Own chain data store key', ); - }); - - it('should throw if terminated state store key is duplicated', async () => { - const validData1 = { - ...validData, - terminatedStateSubstore: [ - { storeKey: ownChainID, storeValue: terminatedStateAccount }, - { storeKey: ownChainID, storeValue: terminatedStateAccount }, - ], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'Terminated state store key', + `channelData.messageFeeTokenID is not equal to Token.getTokenIDLSK().`, ); }); - it('should throw if terminated outbox store key is duplicated', async () => { - const validData1 = { - ...validData, - terminatedOutboxSubstore: [ - { storeKey: ownChainID, storeValue: terminatedOutboxAccount }, - { storeKey: ownChainID, storeValue: terminatedOutboxAccount }, - ], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'Terminated outbox store key', - ); - }); - - it('should throw if registered names store key is duplicated', async () => { - const validData1 = { - ...validData, - registeredNamesSubstore: [ - { storeKey: ownChainID, storeValue: registeredNameId }, - { storeKey: ownChainID, storeValue: registeredNameId }, - ], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'Registered names store ke', + it(`should throw error if channelData.minReturnFeePerByte is not equal to MIN_RETURN_FEE_PER_BYTE_BEDDOWS`, async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + chainInfos: [ + { + ...chainInfo, + channelData: { + ...channelData, + minReturnFeePerByte: MIN_RETURN_FEE_PER_BYTE_BEDDOWS + BigInt(1), + }, + }, + ], + }, + params, ); - }); - - it('should throw if some store key in chain data substore is missing in outbox root substore and the corresponding chain account is not inactive', async () => { - const validData1 = { - ...validData, - outboxRootSubstore: [{ storeKey: mainchainID, storeValue: outboxRoot }], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'missing in some or all of outbox root, channel data and chain validators stores', + `channelData.minReturnFeePerByte is not equal to ${MIN_RETURN_FEE_PER_BYTE_BEDDOWS}.`, ); }); + }); - it('should throw if some store key in chain data substore is present in outbox root substore but the corresponding chain account is inactive', async () => { - const validData1 = { - ...validData, - chainDataSubstore: [ - { storeKey: mainchainID, storeValue: { ...chainAccount, status: 2 } }, - { storeKey: ownChainID, storeValue: chainAccount }, - ], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'Outbox root store cannot have entry for a terminated chain acco', + describe('chainValidators.activeValidators', () => { + it(`should throw error if activeValidators have 0 elements`, async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + chainInfos: [ + { + ...chainInfo, + chainValidators: { + ...chainValidators, + activeValidators: [], + }, + }, + ], + }, + params, ); - }); - - it('should throw if some store key in chain data substore is missing in channel data substore', async () => { - const validData1 = { - ...validData, - channelDataSubstore: [{ storeKey: mainchainID, storeValue: channelData }], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'missing in some or all of outbox root, channel data and chain validators stores', + 'Lisk validator found 1 error[s]:\nmust NOT have fewer than 1 items', ); }); - it('should throw if some store key in chain data substore is missing in chain validators substore', async () => { - const validData1 = { - ...validData, - chainValidatorsSubstore: [{ storeKey: mainchainID, storeValue: validatorsHashInput }], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'missing in some or all of outbox root, channel data and chain validators stores', - ); - }); + it(`should throw error if activeValidators have more than MAX_NUM_VALIDATORS elements`, async () => { + const activeValidatorsTemp: ActiveValidator[] = []; + const max = MAX_NUM_VALIDATORS + 10; + for (let i = 1; i < max; i += 1) { + activeValidatorsTemp.push({ + blsKey: Buffer.from(i.toString(), 'hex'), + bftWeight: BigInt(i + 10), + }); + } - it('should throw if some store key in outbox data substore is missing in chain data substore', async () => { - const validData1 = { - ...validData, - chainDataSubstore: [ - { storeKey: mainchainID, storeValue: chainAccount }, - { storeKey: Buffer.from([0, 0, 1, 1]), storeValue: chainAccount }, - ], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'missing in some or all of outbox root, channel data and chain validators stores', + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + chainInfos: [ + { + ...chainInfo, + chainValidators: { + ...chainValidators, + activeValidators: activeValidatorsTemp, + }, + }, + ], + }, + params, ); - }); - it('should throw if some store key in channel data substore is missing in chain data substore', async () => { - const validData1 = { - ...validData, - chainDataSubstore: [ - { storeKey: mainchainID, storeValue: chainAccount }, - { storeKey: Buffer.from([0, 0, 1, 1]), storeValue: chainAccount }, - ], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'missing in some or all of outbox root, channel data and chain validators stores', + `Lisk validator found ${max} error[s]: +must NOT have more than ${MAX_NUM_VALIDATORS} items`, ); }); - it('should throw if some store key in chain validators substore is missing in chain data substore', async () => { - const validData1 = { - ...validData, - chainDataSubstore: [ - { storeKey: mainchainID, storeValue: chainAccount }, - { storeKey: Buffer.from([0, 0, 1, 1]), storeValue: chainAccount }, - ], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'missing in some or all of outbox root, channel data and chain validators stores', + it(`should throw error if activeValidators are not ordered lexicographically by blsKey property`, async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + chainInfos: [ + { + ...chainInfo, + chainValidators: { + ...chainValidators, + activeValidators: [ + { + // utils.getRandomBytes(BLS_PUBLIC_KEY_LENGTH).toString('hex') + blsKey: Buffer.from( + 'c1d3c7919a4ea7e3b5d5b0068513c2cd7fe047a632e13d9238a51fcd6a4afd7ee16906978992a702bccf1f0149fa5d39', + 'hex', + ), + bftWeight: BigInt(10), + }, + { + blsKey: Buffer.from( + '901550cf1fde7dde29218ee82c5196754efea99813af079bb2809a7fad8a053f93726d1e61ccf427118dcc27b0c07d9a', + 'hex', + ), + bftWeight: BigInt(10), + }, + ], + }, + }, + ], + }, + params, ); - }); - it('should throw if some store key in terminated outbox substore is missing in the terminated state substore', async () => { - const validData1 = { - ...validData, - terminatedStateSubstore: [{ storeKey: mainchainID, storeValue: terminatedStateAccount }], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'missing in terminated state store', + `activeValidators must be ordered lexicographically by blsKey property.`, ); }); - it('should throw if some store key in terminated state substore is present in the terminated outbox substore but the property initialized is set to false', async () => { - const validData1 = { - ...validData, - terminatedStateSubstore: [ - { - storeKey: mainchainID, - storeValue: { ...terminatedStateAccount, initialized: false }, - }, - { storeKey: ownChainID, storeValue: terminatedStateAccount }, - ], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'Uninitialized account associated with terminated state store key ', + it(`should throw error if not all blsKey are pairwise distinct`, async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + chainInfos: [ + { + ...chainInfo, + chainValidators: { + ...chainValidators, + activeValidators: [ + { + // utils.getRandomBytes(BLS_PUBLIC_KEY_LENGTH).toString('hex') + blsKey: Buffer.from( + 'c1d3c7919a4ea7e3b5d5b0068513c2cd7fe047a632e13d9238a51fcd6a4afd7ee16906978992a702bccf1f0149fa5d39', + 'hex', + ), + bftWeight: BigInt(10), + }, + { + blsKey: Buffer.from( + 'c1d3c7919a4ea7e3b5d5b0068513c2cd7fe047a632e13d9238a51fcd6a4afd7ee16906978992a702bccf1f0149fa5d39', + 'hex', + ), + bftWeight: BigInt(10), + }, + ], + }, + }, + ], + }, + params, ); - }); - it('should throw if some store key in terminated state substore has the property initialized set to false but stateRoot is not set to empty bytes and mainchainStateRoot not set to a 32-bytes value', async () => { - const validData1 = { - ...validData, - terminatedStateSubstore: [ - { - storeKey: mainchainID, - storeValue: { ...terminatedStateAccount, initialized: false }, - }, - { storeKey: ownChainID, storeValue: terminatedStateAccount }, - ], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'Uninitialized account associated with terminated state store key ', + `All blsKey properties must be pairwise distinct.`, ); }); - it('should throw if some store key in terminated state substore has the property initialized set to true but mainchainStateRoot is not set to empty hash and stateRoot not set to a 32-bytes value', async () => { - const validData1 = { - ...validData, - terminatedStateSubstore: [ - { - storeKey: mainchainID, - storeValue: { - ...terminatedStateAccount, - initialized: true, - mainchainStateRoot: utils.getRandomBytes(32), - stateRoot: utils.getRandomBytes(32), + it(`should throw error if each validator in activeValidators have bftWeight <=0`, async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + chainInfos: [ + { + ...chainInfo, + chainValidators: { + ...chainValidators, + activeValidators: [ + { + ...activeValidator, + bftWeight: BigInt(0), + }, + ], + }, }, - }, - { storeKey: ownChainID, storeValue: terminatedStateAccount }, - ], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'For the initialized account associated with terminated state store', + ], + }, + params, ); - }); - it('should throw if active validators have less than 1 element', async () => { - const validData1 = { - ...validData, - chainValidatorsSubstore: [ - { - storeKey: mainchainID, - storeValue: { ...validatorsHashInput, activeValidators: [] }, - }, - { storeKey: ownChainID, storeValue: validatorsHashInput }, - ], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'must NOT have fewer than 1 items', + `validator.bftWeight must be > 0.`, ); }); - it('should throw if active validators have more than MAX_NUM_VALIDATORS elements', async () => { - const validData1 = { - ...validData, - chainValidatorsSubstore: [ - { - storeKey: mainchainID, - storeValue: { - ...validatorsHashInput, - activeValidators: new Array(MAX_NUM_VALIDATORS + 1).fill(0).map((_, i) => ({ - blsKey: Buffer.alloc(BLS_PUBLIC_KEY_LENGTH, i), - bftWeight: BigInt(1), - })), + it(`should throw error if activeValidators total bftWeight > MAX_UINT64`, async () => { + const bftWeight = MAX_UINT64 - BigInt(100); + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + chainInfos: [ + { + ...chainInfo, + chainValidators: { + ...chainValidators, + activeValidators: [ + { + blsKey: Buffer.from( + '901550cf1fde7dde29218ee82c5196754efea99813af079bb2809a7fad8a053f93726d1e61ccf427118dcc27b0c07d9a', + 'hex', + ), + bftWeight, + }, + { + // utils.getRandomBytes(BLS_PUBLIC_KEY_LENGTH).toString('hex') + blsKey: Buffer.from( + 'c1d3c7919a4ea7e3b5d5b0068513c2cd7fe047a632e13d9238a51fcd6a4afd7ee16906978992a702bccf1f0149fa5d39', + 'hex', + ), + bftWeight, + }, + ], + }, }, - }, - { storeKey: ownChainID, storeValue: validatorsHashInput }, - ], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'must NOT have more than 199 items', + ], + }, + params, ); - }); - it('should throw if active validators are not ordered lexicographically by blsKey', async () => { - const validData1 = { - ...validData, - chainValidatorsSubstore: [ - { - storeKey: mainchainID, - storeValue: { - ...validatorsHashInput, - activeValidators: [ - { - blsKey: Buffer.from( - '4c1e6f29e3434f816cd6697e56cc54bc8d80927bf65a1361b383aa338cd3f63cbf82ce801b752cb32f8ecb3f8cc16835', - 'hex', - ), - bftWeight: BigInt(10), - }, - { - blsKey: Buffer.from( - '3c1e6f29e3434f816cd6697e56cc54bc8d80927bf65a1361b383aa338cd3f63cbf82ce801b752cb32f8ecb3f8cc16835', - 'hex', - ), - bftWeight: BigInt(10), - }, - ], - }, - }, - { storeKey: ownChainID, storeValue: validatorsHashInput }, - ], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'Active validators must be ordered lexicographically by blsKey property and pairwise distinct', + `totalWeight has to be less than or equal to MAX_UINT64.`, ); }); - it('should throw if some active validators have blsKey which is not 48 bytes', async () => { - const validData1 = { - ...validData, - chainValidatorsSubstore: [ + describe('activeValidators.certificateThreshold', () => { + it(`should throw error if 'totalWeight / BigInt(3) + BigInt(1) > certificateThreshold'`, async () => { + const context = createInitGenesisStateContext( { - storeKey: mainchainID, - storeValue: { - ...validatorsHashInput, - activeValidators: [ - { - blsKey: utils.getRandomBytes(21), - bftWeight: BigInt(10), + ...genesisInteroperability, + chainInfos: [ + { + ...chainInfo, + chainValidators: { + activeValidators: [ + { + blsKey: Buffer.from( + '901550cf1fde7dde29218ee82c5196754efea99813af079bb2809a7fad8a053f93726d1e61ccf427118dcc27b0c07d9a', + 'hex', + ), + bftWeight: BigInt(100), + }, + { + // utils.getRandomBytes(BLS_PUBLIC_KEY_LENGTH).toString('hex') + blsKey: Buffer.from( + 'c1d3c7919a4ea7e3b5d5b0068513c2cd7fe047a632e13d9238a51fcd6a4afd7ee16906978992a702bccf1f0149fa5d39', + 'hex', + ), + bftWeight: BigInt(200), + }, + ], + // totalWeight / BigInt(3) + BigInt(1) = (100 + 200)/3 + 1 = 101 + // totalWeight / BigInt(3) + BigInt(1) > certificateThreshold + certificateThreshold: BigInt(10), // 101 > 10 }, - { - blsKey: Buffer.from( - '3c1e6f29e3434f816cd6697e56cc54bc8d80927bf65a1361b383aa338cd3f63cbf82ce801b752cb32f8ecb3f8cc16835', - 'hex', - ), - bftWeight: BigInt(10), - }, - ], - }, + }, + ], }, - { storeKey: ownChainID, storeValue: validatorsHashInput }, - ], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); - await expect(interopMod.initGenesisState(context)).rejects.toThrow('minLength not satisfied'); - }); + params, + ); - it('should throw if some active validators have blsKey which is not pairwise distinct', async () => { - const validData1 = { - ...validData, - chainValidatorsSubstore: [ + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `Invalid certificateThreshold input.`, + ); + }); + + it(`should throw error if certificateThreshold > totalWeight`, async () => { + const context = createInitGenesisStateContext( { - storeKey: mainchainID, - storeValue: { - ...validatorsHashInput, - activeValidators: [ - { - blsKey: Buffer.from( - '3c1e6f29e3434f816cd6697e56cc54bc8d80927bf65a1361b383aa338cd3f63cbf82ce801b752cb32f8ecb3f8cc16835', - 'hex', - ), - bftWeight: BigInt(10), - }, - { - blsKey: Buffer.from( - '3c1e6f29e3434f816cd6697e56cc54bc8d80927bf65a1361b383aa338cd3f63cbf82ce801b752cb32f8ecb3f8cc16835', - 'hex', - ), - bftWeight: BigInt(10), + ...genesisInteroperability, + chainInfos: [ + { + ...chainInfo, + chainValidators: { + activeValidators: [ + { + blsKey: Buffer.from( + '901550cf1fde7dde29218ee82c5196754efea99813af079bb2809a7fad8a053f93726d1e61ccf427118dcc27b0c07d9a', + 'hex', + ), + bftWeight: BigInt(10), + }, + ], + certificateThreshold: BigInt(20), }, - ], - }, + }, + ], }, - { storeKey: ownChainID, storeValue: validatorsHashInput }, - ], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'Active validators must be ordered lexicographically by blsKey property and pairwise distinct', - ); + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `Invalid certificateThreshold input.`, + ); + }); }); + }); - it('should throw if some active validators have bftWeight which is not a positive integer', async () => { - const validData1 = { - ...validData, - chainValidatorsSubstore: [ - { - storeKey: mainchainID, - storeValue: { - ...validatorsHashInput, - activeValidators: [ - { - blsKey: Buffer.from( - '3c1e6f29e3434f816cd6697e56cc54bc8d80927bf65a1361b383aa338cd3f63cbf82ce801b752cb32f8ecb3f8cc16835', - 'hex', - ), - bftWeight: BigInt(0), - }, - { - blsKey: Buffer.from( - '4c1e6f29e3434f816cd6697e56cc54bc8d80927bf65a1361b383aa338cd3f63cbf82ce801b752cb32f8ecb3f8cc16835', - 'hex', - ), - bftWeight: BigInt(10), - }, - ], + // it is defined here, since it applies to both chainData & chainValidators + describe('validatorsHash', () => { + it(`should throw error if invalid validatorsHash provided`, async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + chainInfos: [ + { + ...chainInfo, + chainValidators: { + activeValidators, + certificateThreshold: BigInt(10), + }, }, - }, - { storeKey: ownChainID, storeValue: validatorsHashInput }, - ], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); + ], + }, + params, + ); await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'BFTWeight must be a positive integer', + 'Invalid validatorsHash from chainData.lastCertificate.', ); }); - it('should throw if total bft weight of active validators is greater than MAX_UINT64', async () => { - const validData1 = { - ...validData, - chainValidatorsSubstore: [ - { - storeKey: mainchainID, - storeValue: { - ...validatorsHashInput, - activeValidators: [ - { - blsKey: Buffer.from( - '3c1e6f29e3434f816cd6697e56cc54bc8d80927bf65a1361b383aa338cd3f63cbf82ce801b752cb32f8ecb3f8cc16835', - 'hex', - ), - bftWeight: BigInt(MAX_UINT64), - }, - { - blsKey: Buffer.from( - '4c1e6f29e3434f816cd6697e56cc54bc8d80927bf65a1361b383aa338cd3f63cbf82ce801b752cb32f8ecb3f8cc16835', - 'hex', - ), - bftWeight: BigInt(10), + it(`should not throw error if valid validatorsHash provided`, async () => { + certificateThreshold = BigInt(10); + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + chainInfos: [ + { + ...chainInfo, + chainData: { + ...chainData, + lastCertificate: { + ...lastCertificate, + validatorsHash: computeValidatorsHash(activeValidators, certificateThreshold), }, - ], + }, + chainValidators: { + activeValidators, + certificateThreshold, + }, }, - }, - { storeKey: ownChainID, storeValue: validatorsHashInput }, - ], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'The total BFT weight of all active validators has to be less than or equal to MAX_UINT64', + ], + }, + params, ); - }); - it('should throw if total bft weight of active validators is less than the value check', async () => { - const validatorsHashInput1 = { - ...validatorsHashInput, - activeValidators: [ - { - blsKey: Buffer.from( - '3c1e6f29e3434f816cd6697e56cc54bc8d80927bf65a1361b383aa338cd3f63cbf82ce801b752cb32f8ecb3f8cc16835', - 'hex', - ), - bftWeight: BigInt(1), - }, - ], - }; - const validData1 = { - ...validData, - chainValidatorsSubstore: [ - { storeKey: mainchainID, storeValue: validatorsHashInput1 }, - { storeKey: ownChainID, storeValue: validatorsHashInput1 }, - ], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'The total BFT weight of all active validators is not valid', - ); + await expect(interopMod.initGenesisState(context)).resolves.toBeUndefined(); }); + }); - it('should throw if certificateThreshold is less than the value check', async () => { - const validData1 = { - ...validData, - chainValidatorsSubstore: [ - { - storeKey: mainchainID, - storeValue: { ...validatorsHashInput, certificateThreshold: BigInt(1) }, + describe('terminatedStateAccounts', () => { + certificateThreshold = BigInt(10); + const validChainInfos = [ + { + ...chainInfo, + chainData: { + ...chainData, + status: ChainStatus.TERMINATED, + lastCertificate: { + ...lastCertificate, + validatorsHash: computeValidatorsHash(activeValidators, certificateThreshold), }, - { storeKey: ownChainID, storeValue: validatorsHashInput }, - ], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'The total BFT weight of all active validators is not valid', - ); - }); + }, + chainValidators: { + activeValidators, + certificateThreshold, + }, + }, + ]; - it('should throw if a chain account for another sidechain is present but chain account for mainchain is not present', async () => { - const validData1 = { - ...validData, - chainDataSubstore: [ - { storeKey: mainchainID, storeValue: chainAccount }, - { storeKey: ownChainID, storeValue: chainAccount }, - { storeKey: Buffer.from([0, 7, 7, 7]), storeValue: chainAccount }, - ], - outboxRootSubstore: [ - { storeKey: mainchainID, storeValue: outboxRoot }, - { storeKey: ownChainID, storeValue: outboxRoot }, - ], - channelDataSubstore: [ - { storeKey: mainchainID, storeValue: channelData }, - { storeKey: ownChainID, storeValue: channelData }, - ], - chainValidatorsSubstore: [ - { storeKey: mainchainID, storeValue: validatorsHashInput }, - { storeKey: ownChainID, storeValue: validatorsHashInput }, - ], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'missing in some or all of outbox root, channel data and chain validators stor', + it("should throw error if terminatedStateAccounts don't hold unique chainID", async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + // this is needed to verify `validatorsHash` related tests (above) + chainInfos: validChainInfos, + terminatedStateAccounts: [ + { + chainID: chainInfo.chainID, + terminatedStateAccount, + }, + { + chainID: chainInfo.chainID, + terminatedStateAccount, + }, + ], + }, + params, ); - }); - it('should throw if a chain account for another sidechain is present but chain account for ownchain is not present', async () => { - const otherChainID = Buffer.from([0, 2, 2, 2]); - const validData1 = { - ...validData, - chainDataSubstore: [ - { storeKey: mainchainID, storeValue: chainAccount }, - { storeKey: otherChainID, storeValue: chainAccount }, - ], - outboxRootSubstore: [ - { storeKey: mainchainID, storeValue: outboxRoot }, - { storeKey: otherChainID, storeValue: outboxRoot }, - ], - channelDataSubstore: [ - { storeKey: mainchainID, storeValue: channelData }, - { storeKey: otherChainID, storeValue: channelData }, - ], - chainValidatorsSubstore: [ - { storeKey: mainchainID, storeValue: validatorsHashInput }, - { storeKey: otherChainID, storeValue: validatorsHashInput }, - ], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'If a chain account for another sidechain is present, then a chain account for the mainchain must be present', + "terminatedStateAccounts don't hold unique chainID", ); }); - it('should not throw if some chain id corresponding to message fee token id of a channel is not 1 but is corresponding native token id of either chains', async () => { - const validData1 = { - ...validData, - channelDataSubstore: [ - { storeKey: mainchainID, storeValue: channelData }, - { - storeKey: ownChainID, - storeValue: { - ...channelData, - messageFeeTokenID: Buffer.from([0, 0, 1, 0, 0, 0, 1, 0]), + it('should throw error if terminatedStateAccounts is not ordered lexicographically by chainID', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + // this is needed to verify `validatorsHash` related tests (above) + chainInfos: [ + ...validChainInfos, + { + ...chainInfo, + chainID: Buffer.from([0, 0, 0, 2]), + chainData: { + ...chainData, + status: ChainStatus.TERMINATED, + lastCertificate: { + ...lastCertificate, + validatorsHash: computeValidatorsHash(activeValidators, certificateThreshold), + }, + name: 'sidechain2', + }, + chainValidators: { + activeValidators, + certificateThreshold, + }, }, - }, - ], - }; - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData1); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); - await expect(interopMod.initGenesisState(context)).resolves.toBeUndefined(); - }); - - it('should create all the corresponding entries in the interoperability module state for every substore for valid input', async () => { - const encodedAsset = codec.encode(genesisInteroperabilitySchema, validData); - const context = createGenesisBlockContext({ - stateStore, - chainID: ownChainID, - assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), - }).createInitGenesisStateContext(); - - await expect(interopMod.initGenesisState(context)).toResolve(); - - channelDataSubstore = interopMod.stores.get(ChannelDataStore); - chainValidatorsSubstore = interopMod.stores.get(ChainValidatorsStore); - outboxRootSubstore = interopMod.stores.get(OutboxRootStore); - terminatedOutboxSubstore = interopMod.stores.get(TerminatedOutboxStore); - chainDataSubstore = interopMod.stores.get(ChainAccountStore); - terminatedStateSubstore = interopMod.stores.get(TerminatedStateStore); - registeredNamesSubstore = interopMod.stores.get(RegisteredNamesStore); - ownChainDataSubstore = interopMod.stores.get(OwnChainAccountStore); + ], + terminatedStateAccounts: [ + { + chainID: Buffer.from([0, 0, 0, 2]), + terminatedStateAccount, + }, + { + chainID: Buffer.from([0, 0, 0, 1]), + terminatedStateAccount, + }, + ], + }, + params, + ); - for (const data of validData.chainDataSubstore) { - await expect(chainDataSubstore.has(stateStore, data.storeKey)).resolves.toBeTrue(); - } - for (const data of validData.chainValidatorsSubstore) { - await expect(chainValidatorsSubstore.has(stateStore, data.storeKey)).resolves.toBeTrue(); - } - for (const data of validData.outboxRootSubstore) { - await expect(outboxRootSubstore.has(stateStore, data.storeKey)).resolves.toBeTrue(); - } - for (const data of validData.terminatedOutboxSubstore) { - await expect(terminatedOutboxSubstore.has(stateStore, data.storeKey)).resolves.toBeTrue(); - } - for (const data of validData.channelDataSubstore) { - await expect(channelDataSubstore.has(stateStore, data.storeKey)).resolves.toBeTrue(); - } - for (const data of validData.terminatedStateSubstore) { - await expect(terminatedStateSubstore.has(stateStore, data.storeKey)).resolves.toBeTrue(); - } - for (const data of validData.registeredNamesSubstore) { - await expect(registeredNamesSubstore.has(stateStore, data.storeKey)).resolves.toBeTrue(); - } - for (const data of validData.ownChainDataSubstore) { - await expect(ownChainDataSubstore.has(stateStore, data.storeKey)).resolves.toBeTrue(); - } + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + 'terminatedStateAccounts must be ordered lexicographically by chainID.', + ); }); }); }); diff --git a/framework/test/unit/modules/interoperability/interopFixtures.ts b/framework/test/unit/modules/interoperability/interopFixtures.ts new file mode 100644 index 00000000000..b1d621c1648 --- /dev/null +++ b/framework/test/unit/modules/interoperability/interopFixtures.ts @@ -0,0 +1,85 @@ +import { utils } from '@liskhq/lisk-cryptography'; +import { ChainStatus } from '../../../../src'; +import { + HASH_LENGTH, + MIN_RETURN_FEE_PER_BYTE_BEDDOWS, + EMPTY_HASH, + CHAIN_NAME_MAINCHAIN, +} from '../../../../src/modules/interoperability/constants'; +import { TerminatedStateAccount } from '../../../../src/modules/interoperability/stores/terminated_state'; +import { TerminatedOutboxAccount } from '../../../../src/modules/interoperability/stores/terminated_outbox'; +import { GenesisInteroperability } from '../../../../src/modules/interoperability/types'; + +const mainchainID = Buffer.from([0, 0, 0, 0]); +const mainchainTokenID = Buffer.concat([mainchainID, Buffer.alloc(4)]); + +export const channelData = { + inbox: { + appendPath: [Buffer.alloc(HASH_LENGTH), Buffer.alloc(HASH_LENGTH)], + root: utils.getRandomBytes(HASH_LENGTH), + size: 18, + }, + outbox: { + appendPath: [Buffer.alloc(HASH_LENGTH), Buffer.alloc(HASH_LENGTH)], + root: utils.getRandomBytes(HASH_LENGTH), + size: 18, + }, + partnerChainOutboxRoot: utils.getRandomBytes(HASH_LENGTH), + messageFeeTokenID: mainchainTokenID, + minReturnFeePerByte: MIN_RETURN_FEE_PER_BYTE_BEDDOWS, +}; + +export const activeValidator = { + // utils.getRandomBytes(BLS_PUBLIC_KEY_LENGTH).toString('hex') + blsKey: Buffer.from( + '3c1e6f29e3434f816cd6697e56cc54bc8d80927bf65a1361b383aa338cd3f63cbf82ce801b752cb32f8ecb3f8cc16835', + 'hex', + ), + bftWeight: BigInt(10), +}; + +export const activeValidators = [activeValidator]; +export const chainValidators = { + activeValidators, + certificateThreshold: BigInt(20), +}; + +export const lastCertificate = { + height: 567467, + timestamp: Date.now() / 10000000000, + stateRoot: Buffer.alloc(HASH_LENGTH), + validatorsHash: Buffer.alloc(HASH_LENGTH), +}; + +export const chainData = { + name: 'dummy', + lastCertificate, + status: ChainStatus.REGISTERED, +}; + +export const chainInfo = { + chainID: Buffer.from([0, 0, 0, 1]), + chainData, + channelData, + chainValidators, +}; + +export const terminatedStateAccount: TerminatedStateAccount = { + stateRoot: lastCertificate.stateRoot, + mainchainStateRoot: EMPTY_HASH, + initialized: true, +}; + +export const terminatedOutboxAccount: TerminatedOutboxAccount = { + outboxRoot: utils.getRandomBytes(HASH_LENGTH), + outboxSize: 1, + partnerChainInboxSize: 1, +}; + +export const genesisInteroperability: GenesisInteroperability = { + ownChainName: CHAIN_NAME_MAINCHAIN, + ownChainNonce: BigInt(123), + chainInfos: [chainInfo], + terminatedStateAccounts: [], // handle it in `describe('terminatedStateAccounts'` + terminatedOutboxAccounts: [], +}; diff --git a/framework/test/unit/modules/interoperability/mainchain/module.spec.ts b/framework/test/unit/modules/interoperability/mainchain/module.spec.ts new file mode 100644 index 00000000000..8907ad95616 --- /dev/null +++ b/framework/test/unit/modules/interoperability/mainchain/module.spec.ts @@ -0,0 +1,738 @@ +import { utils } from '@liskhq/lisk-cryptography'; +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { codec } from '@liskhq/lisk-codec'; +import { BlockAssets } from '@liskhq/lisk-chain'; +import { PrefixedStateReadWriter } from '../../../../../src/state_machine/prefixed_state_read_writer'; +import { + HASH_LENGTH, + MODULE_NAME_INTEROPERABILITY, + CHAIN_NAME_MAINCHAIN, + EMPTY_HASH, +} from '../../../../../src/modules/interoperability/constants'; +import { + ChainStatus, + MainchainInteroperabilityModule, + GenesisBlockExecuteContext, +} from '../../../../../src'; +import { + ChainInfo, + GenesisInteroperability, +} from '../../../../../src/modules/interoperability/types'; +import { + InMemoryPrefixedStateDB, + createGenesisBlockContext, + CreateGenesisBlockContextParams, +} from '../../../../../src/testing'; +import { + computeValidatorsHash, + validNameCharset, + getMainchainID, +} from '../../../../../src/modules/interoperability/utils'; +import { genesisInteroperabilitySchema } from '../../../../../src/modules/interoperability/schemas'; +import { + genesisInteroperability, + activeValidators, + chainInfo, + chainData, + lastCertificate, + terminatedStateAccount, + terminatedOutboxAccount, +} from '../interopFixtures'; + +const createInitGenesisStateContext = ( + genesisInterop: GenesisInteroperability, + params: CreateGenesisBlockContextParams, +): GenesisBlockExecuteContext => { + const encodedAsset = codec.encode(genesisInteroperabilitySchema, genesisInterop); + + return createGenesisBlockContext({ + ...params, + assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), + }).createInitGenesisStateContext(); +}; + +describe('initGenesisState', () => { + const chainID = Buffer.from([0, 0, 0, 0]); + const mainchainID = Buffer.from([0, 0, 0, 0]); + let stateStore: PrefixedStateReadWriter; + let interopMod: MainchainInteroperabilityModule; + let certificateThreshold = BigInt(0); + + let params: CreateGenesisBlockContextParams; + beforeEach(() => { + stateStore = new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); + interopMod = new MainchainInteroperabilityModule(); + params = { + stateStore, + chainID, + }; + }); + + it('should not throw error if asset does not exist', async () => { + const context = createGenesisBlockContext({ + stateStore, + chainID, + }).createInitGenesisStateContext(); + jest.spyOn(context, 'getStore'); + + await expect(interopMod.initGenesisState(context)).toResolve(); + expect(context.getStore).not.toHaveBeenCalled(); + }); + + it(`should throw error if ownChainName !== CHAIN_NAME_MAINCHAIN`, async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + ownChainName: 'dummy', + }, + params, + ); + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `ownChainName must be equal to ${CHAIN_NAME_MAINCHAIN}.`, + ); + }); + + describe('if chainInfos is empty', () => { + it('should throw error if ownChainNonce !== 0', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + chainInfos: [], + ownChainNonce: BigInt(123), + }, + params, + ); + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + 'ownChainNonce must be 0 if chainInfos is empty.', + ); + }); + }); + + describe('when chainInfos is not empty', () => { + let validChainInfos: ChainInfo[]; + beforeEach(() => { + certificateThreshold = BigInt(10); + validChainInfos = [ + { + ...chainInfo, + chainData: { + ...chainData, + status: ChainStatus.TERMINATED, + lastCertificate: { + ...lastCertificate, + validatorsHash: computeValidatorsHash(activeValidators, certificateThreshold), + }, + }, + chainValidators: { + activeValidators, + certificateThreshold, + }, + }, + ]; + }); + + it('should throw error if ownChainNonce <= 0', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + ownChainNonce: BigInt(0), + }, + params, + ); + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + 'ownChainNonce must be positive if chainInfos is not empty.', + ); + }); + + it('should throw error if chainInfos does not hold unique chainID', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + chainInfos: [{ ...chainInfo }, { ...chainInfo }], + }, + params, + ); + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + "chainInfos doesn't hold unique chainID.", + ); + }); + + it('should throw error if chainInfos is not ordered lexicographically by chainID.', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + chainInfos: [ + { + ...chainInfo, + chainID: Buffer.from([2, 0, 0, 0]), + }, + { + ...chainInfo, + chainID: Buffer.from([1, 0, 0, 0]), + }, + ], + }, + params, + ); + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + 'chainInfos is not ordered lexicographically by chainID.', + ); + }); + + it('should throw error if chainInfo.chainID equals getMainchainID()', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + chainInfos: [ + { + ...chainInfo, + chainID: mainchainID, + }, + ], + }, + params, + ); + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `chainID must be not equal to ${getMainchainID(chainID).toString('hex')}.`, + ); + }); + + it('should throw error if chainInfo.chainID[0] !== getMainchainID()[0]', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + chainInfos: [ + { + ...chainInfo, + chainID: Buffer.from([1, 0, 0, 0]), + }, + ], + }, + params, + ); + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `chainID[0] doesn't match ${getMainchainID(chainID)[0]}.`, + ); + }); + + describe('chainInfo.chainData', () => { + it('should throw error if chainData.lastCertificate.timestamp >= g.header.timestamp', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + chainInfos: [ + { + ...chainInfo, + chainData: { + ...genesisInteroperability.chainInfos[0].chainData, + lastCertificate: { + ...genesisInteroperability.chainInfos[0].chainData.lastCertificate, + timestamp: Date.now() * 10, + }, + }, + }, + ], + }, + params, + ); + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + 'chainData.lastCertificate.timestamp must be less than header.timestamp.', + ); + }); + + it(`should throw error if chainData.name has chars outside [${validNameCharset}] range`, async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + chainInfos: [ + { + ...chainInfo, + chainData: { + ...genesisInteroperability.chainInfos[0].chainData, + name: '>(bogus_name)<', + }, + }, + ], + }, + { + ...params, + header: { + timestamp: Date.now(), + } as any, + }, + ); + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `chainData.name only uses the character set ${validNameCharset}.`, + ); + }); + + it(`should throw error if not 'chainData.status is in set {CHAIN_STATUS_REGISTERED, CHAIN_STATUS_ACTIVE, CHAIN_STATUS_TERMINATED}'`, async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + chainInfos: [ + { + ...chainInfo, + chainData: { + ...genesisInteroperability.chainInfos[0].chainData, + status: 123, + }, + }, + ], + }, + { + ...params, + header: { + timestamp: Date.now(), + } as any, + }, + ); + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `chainData.status must be one of ${[ + ChainStatus.REGISTERED, + ChainStatus.ACTIVE, + ChainStatus.TERMINATED, + ].join(', ')}`, + ); + }); + }); + + describe('terminatedStateAccounts', () => { + it('should not throw error if length of terminatedStateAccounts is zero', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + // this is needed to verify `validatorsHash` related tests (above) + chainInfos: [ + { + ...chainInfo, + chainData: { + ...chainData, + lastCertificate: { + ...lastCertificate, + validatorsHash: computeValidatorsHash(activeValidators, certificateThreshold), + }, + }, + chainValidators: { + activeValidators, + certificateThreshold, + }, + }, + ], + terminatedStateAccounts: [], + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).resolves.not.toThrow(); + }); + + it('should throw if chainInfo.chainData.status===TERMINATED exists but no terminateStateAccount', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + chainInfos: [ + { + ...chainInfo, + chainData: { + ...chainData, + status: ChainStatus.TERMINATED, + lastCertificate: { + ...lastCertificate, + validatorsHash: computeValidatorsHash(activeValidators, certificateThreshold), + }, + }, + chainValidators: { + activeValidators, + certificateThreshold, + }, + }, + ], + // No terminatedStateAccount + terminatedStateAccounts: [], + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `For each chainInfo with status terminated there should be a corresponding entry in terminatedStateAccounts`, + ); + }); + + it('should throw if there is an entry in terminateStateAccounts for a chainID that is ACTIVE in chainInfos', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + chainInfos: [ + { + ...chainInfo, + chainData: { + ...chainData, + status: ChainStatus.ACTIVE, + lastCertificate: { + ...lastCertificate, + validatorsHash: computeValidatorsHash(activeValidators, certificateThreshold), + }, + }, + chainValidators: { + activeValidators, + certificateThreshold, + }, + }, + ], + terminatedStateAccounts: [ + { + chainID: chainInfo.chainID, + terminatedStateAccount, + }, + ], + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `For each terminatedStateAccount there should be a corresponding chainInfo at TERMINATED state`, + ); + }); + + it('should throw error if chainInfo.chainID exists in terminatedStateAccounts & chainInfo.chainData.status !== CHAIN_STATUS_TERMINATED', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + // this is needed to verify `validatorsHash` related tests (above) + chainInfos: [ + { + ...chainInfo, + chainData: { + ...chainData, + lastCertificate: { + ...lastCertificate, + validatorsHash: computeValidatorsHash(activeValidators, certificateThreshold), + }, + }, + chainValidators: { + activeValidators, + certificateThreshold, + }, + }, + ], + terminatedStateAccounts: [ + { + chainID: chainInfo.chainID, + terminatedStateAccount, + }, + ], + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `For each terminatedStateAccount there should be a corresponding chainInfo at TERMINATED state`, + ); + }); + + it('should throw error if chainID in terminatedStateAccounts does not exist in chainInfo', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + // this is needed to verify `validatorsHash` related tests (above) + chainInfos: [ + { + ...chainInfo, + chainData: { + ...chainData, + lastCertificate: { + ...lastCertificate, + validatorsHash: computeValidatorsHash(activeValidators, certificateThreshold), + }, + status: ChainStatus.TERMINATED, + }, + chainValidators: { + activeValidators, + certificateThreshold, + }, + }, + ], + terminatedStateAccounts: [ + { + chainID: Buffer.from([0, 0, 0, 2]), + terminatedStateAccount, + }, + ], + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + 'For each terminatedStateAccount there should be a corresponding chainInfo at TERMINATED state', + ); + }); + + it("should throw error if terminatedStateAccounts don't hold unique chainID", async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + // this is needed to verify `validatorsHash` related tests (above) + chainInfos: validChainInfos, + terminatedStateAccounts: [ + { + chainID: chainInfo.chainID, + terminatedStateAccount, + }, + { + chainID: chainInfo.chainID, + terminatedStateAccount, + }, + ], + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + "terminatedStateAccounts don't hold unique chainID", + ); + }); + + it('should throw error if terminatedStateAccounts is not ordered lexicographically by chainID', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + // this is needed to verify `validatorsHash` related tests (above) + chainInfos: [ + { + ...validChainInfos[0], + chainData: { + ...validChainInfos[0].chainData, + name: 'dummy1', + }, + chainID: Buffer.from([0, 0, 0, 1]), + }, + { + ...validChainInfos[0], + chainData: { + ...validChainInfos[0].chainData, + name: 'dummy2', + }, + chainID: Buffer.from([0, 0, 0, 2]), + }, + ], + terminatedStateAccounts: [ + { + chainID: Buffer.from([0, 0, 0, 2]), + terminatedStateAccount, + }, + { + chainID: Buffer.from([0, 0, 0, 1]), + terminatedStateAccount, + }, + ], + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + 'terminatedStateAccounts must be ordered lexicographically by chainID.', + ); + }); + + it('should throw error if some stateAccount in terminatedStateAccounts have stateRoot not equal to chainData.lastCertificate.stateRoot', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + // this is needed to verify `validatorsHash` related tests (above) + chainInfos: validChainInfos, + terminatedStateAccounts: [ + { + chainID: Buffer.from([0, 0, 0, 1]), + terminatedStateAccount: { + ...terminatedStateAccount, + stateRoot: Buffer.from(utils.getRandomBytes(HASH_LENGTH)), + }, + }, + ], + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + "stateAccount.stateRoot doesn't match chainInfo.chainData.lastCertificate.stateRoot.", + ); + }); + + it('should throw error if some stateAccount in terminatedStateAccounts have mainchainStateRoot not equal to EMPTY_HASH', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + // this is needed to verify `validatorsHash` related tests (above) + chainInfos: validChainInfos, + terminatedStateAccounts: [ + { + chainID: Buffer.from([0, 0, 0, 1]), + terminatedStateAccount: { + ...terminatedStateAccount, + mainchainStateRoot: Buffer.from(utils.getRandomBytes(HASH_LENGTH)), + }, + }, + ], + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `stateAccount.mainchainStateRoot is not equal to ${EMPTY_HASH.toString('hex')}.`, + ); + }); + + it('should throw error if some stateAccount in terminatedStateAccounts is not initialized', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + // this is needed to verify `validatorsHash` related tests (above) + chainInfos: validChainInfos, + terminatedStateAccounts: [ + { + chainID: Buffer.from([0, 0, 0, 1]), + terminatedStateAccount: { + ...terminatedStateAccount, + initialized: false, + }, + }, + ], + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + 'stateAccount is not initialized.', + ); + }); + }); + + describe('terminatedOutboxAccounts', () => { + it('should throw error if terminatedOutboxAccounts do not hold unique chainID', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + // this is needed to verify `validatorsHash` related tests (above) + chainInfos: validChainInfos, + terminatedOutboxAccounts: [ + { + chainID: chainInfo.chainID, + terminatedOutboxAccount, + }, + { + chainID: chainInfo.chainID, + terminatedOutboxAccount, + }, + ], + terminatedStateAccounts: [ + { + chainID: chainInfo.chainID, + terminatedStateAccount, + }, + ], + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + 'terminatedOutboxAccounts do not hold unique chainID', + ); + }); + + it('should throw error if terminatedOutboxAccounts is not ordered lexicographically by chainID', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + // this is needed to verify `validatorsHash` related tests (above) + chainInfos: [ + { + ...validChainInfos[0], + chainData: { + ...validChainInfos[0].chainData, + name: 'dummy1', + }, + chainID: Buffer.from([0, 0, 0, 1]), + }, + { + ...validChainInfos[0], + chainData: { + ...validChainInfos[0].chainData, + name: 'dummy2', + }, + chainID: Buffer.from([0, 0, 0, 2]), + }, + ], + terminatedOutboxAccounts: [ + { + chainID: Buffer.from([0, 0, 0, 2]), + terminatedOutboxAccount, + }, + { + chainID: Buffer.from([0, 0, 0, 1]), + terminatedOutboxAccount, + }, + ], + terminatedStateAccounts: [ + { + chainID: Buffer.from([0, 0, 0, 1]), + terminatedStateAccount, + }, + { + chainID: Buffer.from([0, 0, 0, 2]), + terminatedStateAccount, + }, + ], + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + 'terminatedOutboxAccounts must be ordered lexicographically by chainID.', + ); + }); + + it("should throw error if terminatedOutboxAccounts don't have a corresponding entry (with chainID == outboxAccount.chainID) in terminatedStateAccounts", async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + // this is needed to verify `validatorsHash` related tests (above) + chainInfos: validChainInfos, + terminatedStateAccounts: [ + { + chainID: Buffer.from([0, 0, 0, 1]), + terminatedStateAccount, + }, + ], + terminatedOutboxAccounts: [ + { + chainID: Buffer.from([0, 0, 0, 2]), + terminatedOutboxAccount, + }, + ], + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `Each entry outboxAccount in terminatedOutboxAccounts must have a corresponding entry in terminatedStateAccount. outboxAccount with chainID: ${Buffer.from( + [0, 0, 0, 2], + ).toString('hex')} does not exist in terminatedStateAccounts`, + ); + }); + }); + }); +}); diff --git a/framework/test/unit/modules/interoperability/sidechain/modue.spec.ts b/framework/test/unit/modules/interoperability/sidechain/modue.spec.ts new file mode 100644 index 00000000000..483e86986e3 --- /dev/null +++ b/framework/test/unit/modules/interoperability/sidechain/modue.spec.ts @@ -0,0 +1,634 @@ +import { codec } from '@liskhq/lisk-codec'; +import { BlockAssets } from '@liskhq/lisk-chain'; +import { utils } from '@liskhq/lisk-cryptography'; +import { GenesisInteroperability } from '../../../../../src/modules/interoperability/types'; +import { + CreateGenesisBlockContextParams, + createGenesisBlockContext, + InMemoryPrefixedStateDB, +} from '../../../../../src/testing'; +import { + GenesisBlockExecuteContext, + MODULE_NAME_INTEROPERABILITY, + SidechainInteroperabilityModule, + ChainStatus, +} from '../../../../../src'; +import { genesisInteroperabilitySchema } from '../../../../../src/modules/interoperability/schemas'; +import { + genesisInteroperability, + terminatedStateAccount, + terminatedOutboxAccount, + chainInfo, + chainData, + lastCertificate, + chainValidators, + activeValidator, +} from '../interopFixtures'; +import { PrefixedStateReadWriter } from '../../../../../src/state_machine/prefixed_state_read_writer'; +import { + getMainchainID, + validNameCharset, + getMainchainTokenID, + computeValidatorsHash, +} from '../../../../../src/modules/interoperability/utils'; +import { + MIN_CHAIN_NAME_LENGTH, + MAX_CHAIN_NAME_LENGTH, + CHAIN_NAME_MAINCHAIN, + MIN_RETURN_FEE_PER_BYTE_BEDDOWS, + EMPTY_HASH, + HASH_LENGTH, + CHAIN_ID_LENGTH, +} from '../../../../../src/modules/interoperability/constants'; +import { OwnChainAccountStore } from '../../../../../src/modules/interoperability/stores/own_chain_account'; + +const createInitGenesisStateContext = ( + genesisInterop: GenesisInteroperability, + params: CreateGenesisBlockContextParams, +): GenesisBlockExecuteContext => { + const encodedAsset = codec.encode(genesisInteroperabilitySchema, genesisInterop); + + return createGenesisBlockContext({ + ...params, + assets: new BlockAssets([{ module: MODULE_NAME_INTEROPERABILITY, data: encodedAsset }]), + }).createInitGenesisStateContext(); +}; + +describe('initGenesisState', () => { + const chainID = Buffer.from([1, 2, 3, 4]); + + let params: CreateGenesisBlockContextParams; + let stateStore: PrefixedStateReadWriter; + let interopMod: SidechainInteroperabilityModule; + + const ownChainAccountStoreMock = { + get: jest.fn(), + set: jest.fn(), + has: jest.fn(), + }; + + beforeEach(() => { + stateStore = new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); + interopMod = new SidechainInteroperabilityModule(); + params = { + stateStore, + chainID, + }; + + interopMod.stores.register(OwnChainAccountStore, ownChainAccountStoreMock as never); + }); + + describe('If chainInfos is empty', () => { + const genesisInteropWithEmptyChainInfos = { + ...genesisInteroperability, + chainInfos: [], + }; + const ifChainInfosIsEmpty = 'if chainInfos is empty.'; + + it('should throw error if ownChainName is the not empty string', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteropWithEmptyChainInfos, + ownChainName: 'xyz', + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `ownChainName must be empty string, ${ifChainInfosIsEmpty}`, + ); + }); + + it('should throw error if ownChainNonce !== 0', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteropWithEmptyChainInfos, + ownChainName: '', + ownChainNonce: BigInt(1), + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `ownChainNonce must be 0, ${ifChainInfosIsEmpty}.`, + ); + }); + + it('should throw error terminatedStateAccounts is not empty', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteropWithEmptyChainInfos, + ownChainName: '', + ownChainNonce: BigInt(0), + terminatedStateAccounts: [ + { + chainID, + terminatedStateAccount, + }, + ], + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `terminatedStateAccounts must be empty, ${ifChainInfosIsEmpty}.`, + ); + }); + + it('should throw error terminatedOutboxAccounts is not empty', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteropWithEmptyChainInfos, + ownChainName: '', + ownChainNonce: BigInt(0), + terminatedOutboxAccounts: [ + { + chainID, + terminatedOutboxAccount, + }, + ], + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `terminatedOutboxAccounts must be empty, ${ifChainInfosIsEmpty}.`, + ); + }); + }); + + describe('If chainInfos is not empty', () => { + const defaultData = { + ...genesisInteroperability, + ownChainName: 'dummy', + chainInfos: [ + { + ...chainInfo, + chainID: getMainchainID(chainID), + chainData: { + ...chainData, + name: CHAIN_NAME_MAINCHAIN, + }, + }, + ], + }; + + describe('ownChainName', () => { + it(`should throw error if doesn't have length between ${MIN_CHAIN_NAME_LENGTH} and ${MAX_CHAIN_NAME_LENGTH}`, async () => { + const context1 = createInitGenesisStateContext( + { + ...defaultData, + ownChainName: '', + }, + params, + ); + await expect(interopMod.initGenesisState(context1)).rejects.toThrow( + `ownChainName.length must be between ${MIN_CHAIN_NAME_LENGTH} and ${MAX_CHAIN_NAME_LENGTH}`, + ); + + const context2 = createInitGenesisStateContext( + { + ...defaultData, + ownChainName: + 'some very very very very very very very very long very very long chain name', + }, + params, + ); + // MAX_CHAIN_NAME_LENGTH check already applied in schema + await expect(interopMod.initGenesisState(context2)).rejects.toThrow( + `.ownChainName' must NOT have more than ${MAX_CHAIN_NAME_LENGTH} characters`, + ); + }); + + it(`should throw error if doesn't contain character set from ${validNameCharset}`, async () => { + const context = createInitGenesisStateContext( + { + ...defaultData, + ownChainName: 'a%b', + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `ownChainName must have only ${validNameCharset} character set.`, + ); + }); + + it(`should throw error if === ${CHAIN_NAME_MAINCHAIN}`, async () => { + const context = createInitGenesisStateContext( + { + ...defaultData, + ownChainName: CHAIN_NAME_MAINCHAIN, + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `ownChainName must be not equal to ${CHAIN_NAME_MAINCHAIN}.`, + ); + }); + }); + + it('should throw error if not ownChainNonce > 0', async () => { + const context = createInitGenesisStateContext( + { + ...defaultData, + ownChainNonce: BigInt(0), + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + 'ownChainNonce must be > 0.', + ); + }); + + it('should throw error if chainInfos.length > 1', async () => { + const context = createInitGenesisStateContext( + { + ...defaultData, + chainInfos: [chainInfo, chainInfo], + }, + params, + ); + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + 'chainInfos must contain exactly one entry.', + ); + }); + + it('should throw error if mainchainInfo.chainID is not equal to getMainchainID()', async () => { + const context = createInitGenesisStateContext( + { + ...defaultData, + chainInfos: [ + { + ...chainInfo, + chainID, + }, + ], + }, + params, + ); + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `mainchainInfo.chainID must be equal to ${getMainchainID(chainID).toString('hex')}.`, + ); + }); + + describe('chainInfo.chainData', () => { + it(`should throw error if chainData.name !== ${CHAIN_NAME_MAINCHAIN}`, async () => { + const context = createInitGenesisStateContext( + { + ...defaultData, + chainInfos: [ + { + ...defaultData.chainInfos[0], + chainData: { + ...chainData, + }, + }, + ], + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `chainData.name must be equal to ${CHAIN_NAME_MAINCHAIN}.`, + ); + }); + + it('should throw error if chainData.status is not CHAIN_STATUS_REGISTERED or CHAIN_STATUS_ACTIVE', async () => { + const context = createInitGenesisStateContext( + { + ...defaultData, + chainInfos: [ + { + ...defaultData.chainInfos[0], + chainData: { + ...defaultData.chainInfos[0].chainData, + status: ChainStatus.TERMINATED, + }, + }, + ], + }, + params, + ); + + const validStatues = [ChainStatus.REGISTERED, ChainStatus.ACTIVE]; + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `chainData.status must be one of ${validStatues.join(', ')}.`, + ); + }); + + it('should throw error if chainData.lastCertificate.timestamp > g.header.timestamp', async () => { + const context = createInitGenesisStateContext( + { + ...defaultData, + chainInfos: [ + { + ...defaultData.chainInfos[0], + chainData: { + ...defaultData.chainInfos[0].chainData, + lastCertificate: { + ...lastCertificate, + timestamp: Date.now(), + }, + }, + }, + ], + }, + { + ...params, + header: { + timestamp: Date.now() / 10000, + } as any, + }, + ); + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + 'chainData.lastCertificate.timestamp must be < header.timestamp.', + ); + }); + }); + + describe('chainInfo.channelData', () => { + it('should throw error if channelData.messageFeeTokenID is not equal to Token.getTokenIDLSK()', async () => { + const context = createInitGenesisStateContext( + { + ...defaultData, + chainInfos: [ + { + ...defaultData.chainInfos[0], + chainData: { + ...defaultData.chainInfos[0].chainData, + lastCertificate: { + ...lastCertificate, + timestamp: Date.now() / 10000, + }, + }, + }, + ], + }, + { + ...params, + header: { + timestamp: Date.now(), + } as any, + }, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + 'channelData.messageFeeTokenID is not equal to Token.getTokenIDLSK().', + ); + }); + + it('should throw error if channelData.minReturnFeePerByte is not equal to MIN_RETURN_FEE_PER_BYTE_LSK', async () => { + const context = createInitGenesisStateContext( + { + ...defaultData, + chainInfos: [ + { + ...defaultData.chainInfos[0], + chainData: { + ...defaultData.chainInfos[0].chainData, + lastCertificate: { + ...lastCertificate, + timestamp: Date.now() / 10000, + }, + }, + channelData: { + ...defaultData.chainInfos[0].channelData, + messageFeeTokenID: getMainchainTokenID(chainID), + minReturnFeePerByte: BigInt(1), + }, + }, + ], + }, + { + ...params, + header: { + timestamp: Date.now(), + } as any, + }, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `channelData.minReturnFeePerByte is not equal to ${MIN_RETURN_FEE_PER_BYTE_BEDDOWS}.`, + ); + }); + }); + + describe('chainInfo.terminatedStateAccounts', () => { + const activeValidators = [ + { + ...activeValidator, + bftWeight: BigInt(300), + }, + ]; + const certificateThreshold = BigInt(150); + const chainInfosDefault = [ + { + ...defaultData.chainInfos[0], + chainData: { + ...defaultData.chainInfos[0].chainData, + lastCertificate: { + ...lastCertificate, + timestamp: Date.now() / 10000, + validatorsHash: computeValidatorsHash(activeValidators, certificateThreshold), + }, + }, + channelData: { + ...defaultData.chainInfos[0].channelData, + messageFeeTokenID: getMainchainTokenID(chainID), + }, + chainValidators: { + ...chainValidators, + activeValidators, + certificateThreshold, + }, + }, + ]; + + beforeEach(() => { + ownChainAccountStoreMock.get.mockResolvedValue({ + chainID: utils.getRandomBytes(CHAIN_ID_LENGTH), + } as any); + }); + + it(`should throw error if stateAccount.chainID is equal to getMainchainID()`, async () => { + const chainIDDefault = getMainchainID(chainID); + const context = createInitGenesisStateContext( + { + ...defaultData, + chainInfos: [ + { + ...defaultData.chainInfos[0], + chainData: { + ...defaultData.chainInfos[0].chainData, + lastCertificate: { + ...lastCertificate, + timestamp: Date.now() / 10000, + validatorsHash: computeValidatorsHash(activeValidators, certificateThreshold), + }, + }, + channelData: { + ...defaultData.chainInfos[0].channelData, + messageFeeTokenID: getMainchainTokenID(chainID), + }, + chainValidators: { + ...chainValidators, + activeValidators, + certificateThreshold, + }, + }, + ], + terminatedStateAccounts: [ + { + chainID: chainIDDefault, + terminatedStateAccount, + }, + ], + }, + { + ...params, + header: { + timestamp: Date.now(), + } as any, + }, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `stateAccount.chainID most be not equal to ${chainIDDefault.toString('hex')}.`, + ); + }); + + it(`should throw error if stateAccount.chainID is equal to ownChainAccount.chainID`, async () => { + ownChainAccountStoreMock.get.mockResolvedValue({ + chainID, + } as any); + + const context = createInitGenesisStateContext( + { + ...defaultData, + chainInfos: chainInfosDefault, + terminatedStateAccounts: [ + { + chainID, + terminatedStateAccount, + }, + ], + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `stateAccount.chainID must be not equal to ownChainAccount.chainID.`, + ); + }); + + it(`should throw error if stateAccount.stateRoot equals EMPTY_HASH, if initialised is true`, async () => { + const context = createInitGenesisStateContext( + { + ...defaultData, + chainInfos: chainInfosDefault, + terminatedStateAccounts: [ + { + chainID, + terminatedStateAccount: { + ...terminatedStateAccount, + stateRoot: EMPTY_HASH, + initialized: true, + }, + }, + ], + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `stateAccount.stateRoot mst be not equal to "${EMPTY_HASH.toString( + 'hex', + )}", if initialized is true.`, + ); + }); + + it(`should throw error if stateAccount.mainchainStateRoot is not equal to EMPTY_HASH, if initialised is true`, async () => { + const context = createInitGenesisStateContext( + { + ...defaultData, + chainInfos: chainInfosDefault, + terminatedStateAccounts: [ + { + chainID, + terminatedStateAccount: { + ...terminatedStateAccount, + stateRoot: utils.getRandomBytes(HASH_LENGTH), + mainchainStateRoot: utils.getRandomBytes(HASH_LENGTH), + initialized: true, + }, + }, + ], + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `terminatedStateAccount.mainchainStateRoot must be equal to "${EMPTY_HASH.toString( + 'hex', + )}", if initialized is true`, + ); + }); + + it(`should throw error if stateAccount.stateRoot is not equal to EMPTY_HASH, if initialised is false`, async () => { + const context = createInitGenesisStateContext( + { + ...defaultData, + chainInfos: chainInfosDefault, + terminatedStateAccounts: [ + { + chainID, + terminatedStateAccount: { + ...terminatedStateAccount, + stateRoot: utils.getRandomBytes(HASH_LENGTH), + initialized: false, + }, + }, + ], + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `stateAccount.stateRoot mst be equal to "${EMPTY_HASH.toString( + 'hex', + )}", if initialized is false.`, + ); + }); + + it(`should throw error if stateAccount.mainchainStateRoot is equal to EMPTY_HASH, if initialised is false`, async () => { + const context = createInitGenesisStateContext( + { + ...defaultData, + chainInfos: chainInfosDefault, + terminatedStateAccounts: [ + { + chainID, + terminatedStateAccount: { + ...terminatedStateAccount, + stateRoot: EMPTY_HASH, + mainchainStateRoot: EMPTY_HASH, + initialized: false, + }, + }, + ], + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `terminatedStateAccount.mainchainStateRoot must be not equal to "${EMPTY_HASH.toString( + 'hex', + )}", if initialized is false.`, + ); + }); + }); + }); +});