From 291f40b3d79f7689dd940da8ae65b8928b7023a1 Mon Sep 17 00:00:00 2001 From: shuse2 Date: Wed, 16 Nov 2022 18:57:34 +0100 Subject: [PATCH] :seedling: Update sidechain ccu command --- .../base_cross_chain_update_command.ts | 117 ++- .../mainchain/commands/cc_update.ts | 104 +-- .../sidechain/commands/cc_update.ts | 222 +----- .../base_cross_chain_update_command.spec.ts | 671 +++++++++++++++++- .../mainchain/commands/cc_update.spec.ts | 468 +----------- .../sidechain/commands/cc_update.spec.ts | 477 ++++--------- 6 files changed, 966 insertions(+), 1093 deletions(-) diff --git a/framework/src/modules/interoperability/base_cross_chain_update_command.ts b/framework/src/modules/interoperability/base_cross_chain_update_command.ts index df146ee700a..d843fddb47f 100644 --- a/framework/src/modules/interoperability/base_cross_chain_update_command.ts +++ b/framework/src/modules/interoperability/base_cross_chain_update_command.ts @@ -14,6 +14,7 @@ import { codec } from '@liskhq/lisk-codec'; import { utils } from '@liskhq/lisk-cryptography'; +import { CommandExecuteContext, CommandVerifyContext } from '../../state_machine'; import { ImmutableStoreGetter, StoreGetter } from '../base_store'; import { BaseInteroperabilityCommand } from './base_interoperability_command'; import { BaseInteroperabilityInternalMethod } from './base_interoperability_internal_methods'; @@ -22,7 +23,16 @@ import { CCMStatusCode, MIN_RETURN_FEE } from './constants'; import { CCMProcessedCode, CcmProcessedEvent, CCMProcessedResult } from './events/ccm_processed'; import { CcmSendSuccessEvent } from './events/ccm_send_success'; import { ccmSchema, crossChainUpdateTransactionParams } from './schemas'; -import { CrossChainMessageContext, TokenMethod } from './types'; +import { ChainAccountStore, ChainStatus } from './stores/chain_account'; +import { ChainValidatorsStore } from './stores/chain_validators'; +import { ChannelDataStore } from './stores/channel_data'; +import { + CCMsg, + CrossChainMessageContext, + CrossChainUpdateTransactionParams, + TokenMethod, +} from './types'; +import { isInboxUpdateEmpty, validateFormat } from './utils'; export abstract class BaseCrossChainUpdateCommand extends BaseInteroperabilityCommand { public schema = crossChainUpdateTransactionParams; @@ -35,6 +45,111 @@ export abstract class BaseCrossChainUpdateCommand extends BaseInteroperabilityCo this._interopsMethod = interopsMethod; } + protected async verifyCommon(context: CommandVerifyContext) { + const { params } = context; + const internalMethod = this.getInteroperabilityInternalMethod(context); + const sendingChainAccount = await this.stores + .get(ChainAccountStore) + .get(context, params.sendingChainID); + if (sendingChainAccount.status === ChainStatus.REGISTERED && params.certificate.length === 0) { + throw new Error('The first CCU must contain a non-empty certificate.'); + } + if (params.certificate.length > 0) { + await internalMethod.verifyCertificate(context, params, context.header.timestamp); + } + const sendingChainValidators = await this.stores + .get(ChainValidatorsStore) + .get(context, params.sendingChainID); + if ( + params.activeValidatorsUpdate.length > 0 || + params.certificateThreshold !== sendingChainValidators.certificateThreshold + ) { + await internalMethod.verifyValidatorsUpdate(context, params); + } + + if (!isInboxUpdateEmpty(params.inboxUpdate)) { + await internalMethod.verifyPartnerChainOutboxRoot(context, params); + } + } + + protected async executeCommon( + context: CommandExecuteContext, + isMainchain: boolean, + ): Promise<[CCMsg[], boolean]> { + const { params, transaction } = context; + const internalMethod = this.getInteroperabilityInternalMethod(context); + + await internalMethod.verifyCertificateSignature(context, params); + + if (!isInboxUpdateEmpty(params.inboxUpdate)) { + // Initialize the relayer account for the message fee token. + // This is necessary to ensure that the relayer can receive the CCM fees + // If the account already exists, nothing is done. + const messageFeeTokenID = await this._interopsMethod.getMessageFeeTokenID( + context, + params.sendingChainID, + ); + // FIXME: When updating fee logic, the fix value should be removed. + await this._tokenMethod.initializeUserAccount( + context, + transaction.senderAddress, + messageFeeTokenID, + transaction.senderAddress, + BigInt(500000000), + ); + } + + const decodedCCMs = []; + for (const ccmBytes of params.inboxUpdate.crossChainMessages) { + try { + const ccm = codec.decode(ccmSchema, ccmBytes); + validateFormat(ccm); + decodedCCMs.push(ccm); + if (!ccm.sendingChainID.equals(params.sendingChainID)) { + throw new Error('CCM is not from the sending chain.'); + } + if (ccm.sendingChainID.equals(ccm.receivingChainID)) { + throw new Error('Sending and receiving chains must differ.'); + } + if (isMainchain && ccm.status === CCMStatusCode.CHANNEL_UNAVAILABLE) { + throw new Error('CCM status channel unavailable can only be set on the mainchain.'); + } + } catch (error) { + await internalMethod.terminateChainInternal(params.sendingChainID, context); + const ccmID = utils.hash(ccmBytes); + this.events.get(CcmProcessedEvent).log(context, params.sendingChainID, context.chainID, { + ccmID, + code: CCMProcessedCode.INVALID_CCM_VALIDATION_EXCEPTION, + result: CCMProcessedResult.DISCARDED, + }); + return [[], false]; + } + } + + const sendingChainValidators = await this.stores + .get(ChainValidatorsStore) + .get(context, params.sendingChainID); + if ( + params.activeValidatorsUpdate.length > 0 || + params.certificateThreshold !== sendingChainValidators.certificateThreshold + ) { + await internalMethod.updateValidators(context, params); + } + if (params.certificate.length > 0) { + await internalMethod.updateCertificate(context, params); + } + if (!isInboxUpdateEmpty(params.inboxUpdate)) { + await this.stores + .get(ChannelDataStore) + .updatePartnerChainOutboxRoot( + context, + params.sendingChainID, + params.inboxUpdate.messageWitnessHashes, + ); + } + return [decodedCCMs, true]; + } + protected async apply(context: CrossChainMessageContext): Promise { const { ccm, logger } = context; const encodedCCM = codec.encode(ccmSchema, ccm); diff --git a/framework/src/modules/interoperability/mainchain/commands/cc_update.ts b/framework/src/modules/interoperability/mainchain/commands/cc_update.ts index b0732a1265f..6e0473b3fd3 100644 --- a/framework/src/modules/interoperability/mainchain/commands/cc_update.ts +++ b/framework/src/modules/interoperability/mainchain/commands/cc_update.ts @@ -41,10 +41,8 @@ import { sidechainTerminatedCCMParamsSchema, } from '../../schemas'; import { ChainAccount, ChainAccountStore, ChainStatus } from '../../stores/chain_account'; -import { ChainValidatorsStore } from '../../stores/chain_validators'; -import { ChannelDataStore } from '../../stores/channel_data'; -import { CCMsg, CrossChainMessageContext, CrossChainUpdateTransactionParams } from '../../types'; -import { getMainchainID, isInboxUpdateEmpty, validateFormat } from '../../utils'; +import { CrossChainMessageContext, CrossChainUpdateTransactionParams } from '../../types'; +import { getMainchainID } from '../../utils'; import { MainchainInteroperabilityInternalMethod } from '../store'; export class MainchainCCUpdateCommand extends BaseCrossChainUpdateCommand { @@ -64,28 +62,7 @@ export class MainchainCCUpdateCommand extends BaseCrossChainUpdateCommand { throw new Error('The sending chain is not live.'); } - const sendingChainAccount = await this.stores - .get(ChainAccountStore) - .get(context, params.sendingChainID); - if (sendingChainAccount.status === ChainStatus.REGISTERED && params.certificate.length === 0) { - throw new Error('The first CCU must contain a non-empty certificate.'); - } - if (params.certificate.length > 0) { - await internalMethod.verifyCertificate(context, params, context.header.timestamp); - } - const sendingChainValidators = await this.stores - .get(ChainValidatorsStore) - .get(context, params.sendingChainID); - if ( - params.activeValidatorsUpdate.length > 0 || - params.certificateThreshold !== sendingChainValidators.certificateThreshold - ) { - await internalMethod.verifyValidatorsUpdate(context, params); - } - - if (!isInboxUpdateEmpty(params.inboxUpdate)) { - await internalMethod.verifyPartnerChainOutboxRoot(context, params); - } + await this.verifyCommon(context); return { status: VerifyStatus.OK, @@ -95,77 +72,12 @@ export class MainchainCCUpdateCommand extends BaseCrossChainUpdateCommand { public async execute( context: CommandExecuteContext, ): Promise { - const { params, transaction } = context; - const internalMethod = this.getInteroperabilityInternalMethod(context); - - await internalMethod.verifyCertificateSignature(context, params); - - if (!isInboxUpdateEmpty(params.inboxUpdate)) { - // Initialize the relayer account for the message fee token. - // This is necessary to ensure that the relayer can receive the CCM fees - // If the account already exists, nothing is done. - const messageFeeTokenID = await this._interopsMethod.getMessageFeeTokenID( - context, - params.sendingChainID, - ); - // FIXME: When updating fee logic, the fix value should be removed. - await this._tokenMethod.initializeUserAccount( - context, - transaction.senderAddress, - messageFeeTokenID, - transaction.senderAddress, - BigInt(500000000), - ); - } - - const decodedCCMs = []; - for (const ccmBytes of params.inboxUpdate.crossChainMessages) { - try { - const ccm = codec.decode(ccmSchema, ccmBytes); - validateFormat(ccm); - decodedCCMs.push(ccm); - if (!ccm.sendingChainID.equals(params.sendingChainID)) { - throw new Error('CCM is not from the sending chain.'); - } - if (ccm.sendingChainID.equals(ccm.receivingChainID)) { - throw new Error('Sending and receiving chains must differ.'); - } - if (ccm.status === CCMStatusCode.CHANNEL_UNAVAILABLE) { - throw new Error('CCM status channel unavailable can only be set on the mainchain.'); - } - } catch (error) { - await internalMethod.terminateChainInternal(params.sendingChainID, context); - const ccmID = utils.hash(ccmBytes); - this.events.get(CcmProcessedEvent).log(context, params.sendingChainID, context.chainID, { - ccmID, - code: CCMProcessedCode.INVALID_CCM_VALIDATION_EXCEPTION, - result: CCMProcessedResult.DISCARDED, - }); - return; - } - } - - const sendingChainValidators = await this.stores - .get(ChainValidatorsStore) - .get(context, params.sendingChainID); - if ( - params.activeValidatorsUpdate.length > 0 || - params.certificateThreshold !== sendingChainValidators.certificateThreshold - ) { - await internalMethod.updateValidators(context, params); - } - if (params.certificate.length > 0) { - await internalMethod.updateCertificate(context, params); - } - if (!isInboxUpdateEmpty(params.inboxUpdate)) { - await this.stores - .get(ChannelDataStore) - .updatePartnerChainOutboxRoot( - context, - params.sendingChainID, - params.inboxUpdate.messageWitnessHashes, - ); + const [decodedCCMs, ok] = await this.executeCommon(context, true); + if (!ok) { + return; } + const { params } = context; + const internalMethod = this.getInteroperabilityInternalMethod(context); for (let i = 0; i < decodedCCMs.length; i += 1) { const ccm = decodedCCMs[i]; diff --git a/framework/src/modules/interoperability/sidechain/commands/cc_update.ts b/framework/src/modules/interoperability/sidechain/commands/cc_update.ts index d7b42b92b0e..f476503ae24 100644 --- a/framework/src/modules/interoperability/sidechain/commands/cc_update.ts +++ b/framework/src/modules/interoperability/sidechain/commands/cc_update.ts @@ -12,10 +12,7 @@ * Removal or modification of this copyright notice is prohibited. */ -import { codec } from '@liskhq/lisk-codec'; import { validator } from '@liskhq/lisk-validator'; -import { certificateSchema } from '../../../../engine/consensus/certificate_generation/schema'; -import { Certificate } from '../../../../engine/consensus/certificate_generation/types'; import { CommandExecuteContext, CommandVerifyContext, @@ -24,112 +21,31 @@ import { } from '../../../../state_machine'; import { ImmutableStoreGetter, StoreGetter } from '../../../base_store'; import { BaseCrossChainUpdateCommand } from '../../base_cross_chain_update_command'; -import { CROSS_CHAIN_COMMAND_NAME_REGISTRATION } from '../../constants'; -import { ccmSchema, crossChainUpdateTransactionParams } from '../../schemas'; -import { ChainAccountStore, ChainStatus } from '../../stores/chain_account'; -import { ChainValidatorsStore } from '../../stores/chain_validators'; -import { ChannelDataStore } from '../../stores/channel_data'; -import { CCMsg, CrossChainUpdateTransactionParams } from '../../types'; -import { - checkCertificateTimestamp, - checkCertificateValidity, - checkInboxUpdateValidity, - checkLivenessRequirementFirstCCU, - checkValidatorsHashWithCertificate, - checkValidCertificateLiveness, - commonCCUExecutelogic, - isInboxUpdateEmpty, - validateFormat, -} from '../../utils'; +import { crossChainUpdateTransactionParams } from '../../schemas'; +import { CrossChainUpdateTransactionParams } from '../../types'; +import { getMainchainID } from '../../utils'; import { SidechainInteroperabilityInternalMethod } from '../store'; export class SidechainCCUpdateCommand extends BaseCrossChainUpdateCommand { public async verify( context: CommandVerifyContext, ): Promise { - const { params: txParams } = context; - - try { - validator.validate(crossChainUpdateTransactionParams, context.params); - } catch (err) { - return { - status: VerifyStatus.FAIL, - error: err as Error, - }; - } - - const partnerChainIDBuffer = txParams.sendingChainID; - const partnerChainStore = this.stores.get(ChainAccountStore); - const partnerChainAccount = await partnerChainStore.get(context, partnerChainIDBuffer); - // Section: Liveness of Partner Chain - if (partnerChainAccount.status === ChainStatus.TERMINATED) { - return { - status: VerifyStatus.FAIL, - error: new Error( - `Sending partner chain ${txParams.sendingChainID.readInt32BE(0)} is terminated.`, - ), - }; - } - const interoperabilityInternalMethod = this.getInteroperabilityInternalMethod(context); - if (partnerChainAccount.status === ChainStatus.ACTIVE) { - const isLive = await interoperabilityInternalMethod.isLive(partnerChainIDBuffer); - if (!isLive) { - return { - status: VerifyStatus.FAIL, - error: new Error( - `Sending partner chain ${txParams.sendingChainID.readInt32BE(0)} is not live.`, - ), - }; - } - } - - // Section: Liveness Requirement for the First CCU - const livenessRequirementFirstCCU = checkLivenessRequirementFirstCCU( - partnerChainAccount, - txParams, + const { params } = context; + validator.validate( + crossChainUpdateTransactionParams, + context.params, ); - if (livenessRequirementFirstCCU.error) { - return livenessRequirementFirstCCU; - } - // Section: Certificate and Validators Update Validity - const certificateValidity = checkCertificateValidity(partnerChainAccount, txParams.certificate); - if (certificateValidity.error) { - return certificateValidity; + if (!params.sendingChainID.equals(getMainchainID(context.chainID))) { + throw new Error('Only the mainchain can send a sidechain cross-chain update.'); } - - const partnerValidatorStore = this.stores.get(ChainValidatorsStore); - const partnerValidators = await partnerValidatorStore.get(context, partnerChainIDBuffer); - // If params contains a non-empty activeValidatorsUpdate and non-empty certificate - const validatorsHashValidity = checkValidatorsHashWithCertificate(txParams, partnerValidators); - if (validatorsHashValidity.error) { - return validatorsHashValidity; + const internalMethod = this.getInteroperabilityInternalMethod(context); + const isLive = await internalMethod.isLive(params.sendingChainID); + if (!isLive) { + throw new Error('The sending chain is not live.'); } - // If params contains a non-empty activeValidatorsUpdate - if ( - txParams.activeValidatorsUpdate.length > 0 || - partnerValidators.certificateThreshold !== txParams.certificateThreshold - ) { - await this.getInteroperabilityInternalMethod(context).verifyValidatorsUpdate( - context.getMethodContext(), - txParams, - ); - } - - // When certificate is non-empty - await interoperabilityInternalMethod.verifyCertificateSignature( - context.getMethodContext(), - txParams, - ); - - const partnerChannelStore = this.stores.get(ChannelDataStore); - const partnerChannelData = await partnerChannelStore.get(context, partnerChainIDBuffer); - // Section: InboxUpdate Validity - const inboxUpdateValidity = checkInboxUpdateValidity(this.stores, txParams, partnerChannelData); - if (inboxUpdateValidity.error) { - return inboxUpdateValidity; - } + await this.verifyCommon(context); return { status: VerifyStatus.OK, @@ -139,104 +55,24 @@ export class SidechainCCUpdateCommand extends BaseCrossChainUpdateCommand { public async execute( context: CommandExecuteContext, ): Promise { - const { header, params: txParams, transaction } = context; - const chainIDBuffer = txParams.sendingChainID; - const partnerChainStore = this.stores.get(ChainAccountStore); - const partnerChainAccount = await partnerChainStore.get(context, chainIDBuffer); - - const decodedCertificate = codec.decode(certificateSchema, txParams.certificate); - - // if the CCU also contains a non-empty inboxUpdate, check the validity of certificate with liveness check - checkValidCertificateLiveness(txParams, header, decodedCertificate); - - const partnerValidatorStore = this.stores.get(ChainValidatorsStore); - const partnerValidators = await partnerValidatorStore.get(context, chainIDBuffer); - - // Certificate timestamp Validity - checkCertificateTimestamp(txParams, decodedCertificate, header); - - // CCM execution - const interoperabilityInternalMethod = this.getInteroperabilityInternalMethod(context); - const terminateChainInternal = async () => - interoperabilityInternalMethod.terminateChainInternal(txParams.sendingChainID, { - eventQueue: context.eventQueue, - getMethodContext: context.getMethodContext, - getStore: context.getStore, - logger: context.logger, - chainID: context.chainID, - transaction, - header, - stateStore: context.stateStore, - }); - let decodedCCMs; - try { - decodedCCMs = txParams.inboxUpdate.crossChainMessages.map(ccm => ({ - serialized: ccm, - deserialized: codec.decode(ccmSchema, ccm), - })); - } catch (err) { - await terminateChainInternal(); - - throw err; + const [decodedCCMs, ok] = await this.executeCommon(context, false); + if (!ok) { + return; } - if ( - partnerChainAccount.status === ChainStatus.REGISTERED && - !isInboxUpdateEmpty(txParams.inboxUpdate) - ) { - // If the first CCM in inboxUpdate is a registration CCM - if ( - decodedCCMs[0].deserialized.crossChainCommand === CROSS_CHAIN_COMMAND_NAME_REGISTRATION && - decodedCCMs[0].deserialized.sendingChainID.equals(txParams.sendingChainID) - ) { - partnerChainAccount.status = ChainStatus.ACTIVE; - } else { - await terminateChainInternal(); - - return; // Exit CCU processing - } - } - - for (const ccm of decodedCCMs) { - if (!txParams.sendingChainID.equals(ccm.deserialized.sendingChainID)) { - await terminateChainInternal(); - - continue; - } - try { - validateFormat(ccm.deserialized); - } catch (error) { - await terminateChainInternal(); - - continue; - } - await interoperabilityInternalMethod.appendToInboxTree( - txParams.sendingChainID, - ccm.serialized, - ); + const { params } = context; + const internalMethod = this.getInteroperabilityInternalMethod(context); + + for (let i = 0; i < decodedCCMs.length; i += 1) { + const ccm = decodedCCMs[i]; + const ccmBytes = params.inboxUpdate.crossChainMessages[i]; + const ccmContext = { + ...context, + ccm, + }; - await this.apply({ - ccm: ccm.deserialized, - transaction: context.transaction, - eventQueue: context.eventQueue, - getMethodContext: context.getMethodContext, - getStore: context.getStore, - logger: context.logger, - chainID: context.chainID, - header: context.header, - stateStore: context.stateStore, - }); + await this.apply(ccmContext); + await internalMethod.appendToInboxTree(params.sendingChainID, ccmBytes); } - // Common ccm execution logic - await commonCCUExecutelogic({ - stores: this.stores, - certificate: decodedCertificate, - chainIDBuffer, - context, - partnerChainAccount, - partnerChainStore, - partnerValidatorStore, - partnerValidators, - }); } protected getInteroperabilityInternalMethod( diff --git a/framework/test/unit/modules/interoperability/base_cross_chain_update_command.spec.ts b/framework/test/unit/modules/interoperability/base_cross_chain_update_command.spec.ts index b44687c0251..7dc6ecd7184 100644 --- a/framework/test/unit/modules/interoperability/base_cross_chain_update_command.spec.ts +++ b/framework/test/unit/modules/interoperability/base_cross_chain_update_command.spec.ts @@ -13,13 +13,27 @@ */ /* eslint-disable max-classes-per-file */ import { utils } from '@liskhq/lisk-cryptography'; -import { CommandExecuteContext, MainchainInteroperabilityModule } from '../../../../src'; +import { codec } from '@liskhq/lisk-codec'; +import { + CommandExecuteContext, + CommandVerifyContext, + MainchainInteroperabilityModule, + Transaction, +} from '../../../../src'; import { ImmutableStoreGetter, StoreGetter } from '../../../../src/modules/base_store'; import { BaseCCCommand } from '../../../../src/modules/interoperability/base_cc_command'; import { BaseCrossChainUpdateCommand } from '../../../../src/modules/interoperability/base_cross_chain_update_command'; import { BaseInteroperabilityInternalMethod } from '../../../../src/modules/interoperability/base_interoperability_internal_methods'; import { BaseCCMethod } from '../../../../src/modules/interoperability/base_cc_method'; -import { CCMStatusCode, MIN_RETURN_FEE } from '../../../../src/modules/interoperability/constants'; +import { + BLS_PUBLIC_KEY_LENGTH, + CCMStatusCode, + CROSS_CHAIN_COMMAND_NAME_REGISTRATION, + CROSS_CHAIN_COMMAND_NAME_SIDECHAIN_TERMINATED, + HASH_LENGTH, + MIN_RETURN_FEE, + MODULE_NAME_INTEROPERABILITY, +} from '../../../../src/modules/interoperability/constants'; import { CCMProcessedCode, CcmProcessedEvent, @@ -27,8 +41,28 @@ import { } from '../../../../src/modules/interoperability/events/ccm_processed'; import { CcmSendSuccessEvent } from '../../../../src/modules/interoperability/events/ccm_send_success'; import { MainchainInteroperabilityInternalMethod } from '../../../../src/modules/interoperability/mainchain/store'; -import { CrossChainMessageContext } from '../../../../src/modules/interoperability/types'; -import { createCrossChainMessageContext } from '../../../../src/testing'; +import { + CrossChainMessageContext, + CrossChainUpdateTransactionParams, +} from '../../../../src/modules/interoperability/types'; +import { + createCrossChainMessageContext, + createTransactionContext, + InMemoryPrefixedStateDB, +} from '../../../../src/testing'; +import { + ccmSchema, + crossChainUpdateTransactionParams, +} from '../../../../src/modules/interoperability/schemas'; +import { certificateSchema } from '../../../../src/engine/consensus/certificate_generation/schema'; +import { CROSS_CHAIN_COMMAND_NAME_FORWARD } from '../../../../src/modules/token/constants'; +import { + ChainAccountStore, + ChainStatus, +} from '../../../../src/modules/interoperability/stores/chain_account'; +import { PrefixedStateReadWriter } from '../../../../src/state_machine/prefixed_state_read_writer'; +import { ChainValidatorsStore } from '../../../../src/modules/interoperability/stores/chain_validators'; +import { ChannelDataStore } from '../../../../src/modules/interoperability/stores/channel_data'; class CrossChainUpdateCommand extends BaseCrossChainUpdateCommand { // eslint-disable-next-line @typescript-eslint/require-await @@ -48,6 +82,100 @@ class CrossChainUpdateCommand extends BaseCrossChainUpdateCommand { } describe('BaseCrossChainUpdateCommand', () => { + const interopsModule = new MainchainInteroperabilityModule(); + const senderPublicKey = utils.getRandomBytes(32); + const messageFeeTokenID = Buffer.alloc(8, 0); + const chainID = Buffer.alloc(4, 0); + const defaultTransaction = { + fee: BigInt(0), + module: interopsModule.name, + nonce: BigInt(1), + senderPublicKey, + signatures: [], + }; + const defaultSendingChainID = Buffer.from([0, 0, 2, 0]); + const params = { + activeValidatorsUpdate: [ + { blsKey: utils.getRandomBytes(48), bftWeight: BigInt(1) }, + { blsKey: utils.getRandomBytes(48), bftWeight: BigInt(3) }, + { blsKey: utils.getRandomBytes(48), bftWeight: BigInt(4) }, + { blsKey: utils.getRandomBytes(48), bftWeight: BigInt(3) }, + ].sort((v1, v2) => v1.blsKey.compare(v2.blsKey)), + certificate: codec.encode(certificateSchema, { + blockID: utils.getRandomBytes(32), + height: 21, + timestamp: Math.floor(Date.now() / 1000), + stateRoot: utils.getRandomBytes(HASH_LENGTH), + validatorsHash: utils.getRandomBytes(48), + aggregationBits: utils.getRandomBytes(38), + signature: utils.getRandomBytes(32), + }), + inboxUpdate: { + crossChainMessages: [ + { + crossChainCommand: CROSS_CHAIN_COMMAND_NAME_REGISTRATION, + fee: BigInt(0), + module: MODULE_NAME_INTEROPERABILITY, + nonce: BigInt(1), + params: Buffer.alloc(2), + receivingChainID: Buffer.from([0, 0, 0, 2]), + sendingChainID: defaultSendingChainID, + status: CCMStatusCode.OK, + }, + { + crossChainCommand: CROSS_CHAIN_COMMAND_NAME_SIDECHAIN_TERMINATED, + fee: BigInt(0), + module: MODULE_NAME_INTEROPERABILITY, + nonce: BigInt(1), + params: Buffer.alloc(2), + receivingChainID: chainID, + sendingChainID: defaultSendingChainID, + status: CCMStatusCode.OK, + }, + { + crossChainCommand: CROSS_CHAIN_COMMAND_NAME_FORWARD, + fee: BigInt(0), + module: MODULE_NAME_INTEROPERABILITY, + nonce: BigInt(1), + params: Buffer.alloc(2), + receivingChainID: Buffer.from([0, 0, 0, 4]), + sendingChainID: defaultSendingChainID, + status: CCMStatusCode.OK, + }, + ].map(ccm => codec.encode(ccmSchema, ccm)), + messageWitnessHashes: [Buffer.alloc(32)], + outboxRootWitness: { + bitmap: Buffer.alloc(1), + siblingHashes: [Buffer.alloc(32)], + }, + }, + certificateThreshold: BigInt(20), + sendingChainID: defaultSendingChainID, + }; + const partnerChainAccount = { + lastCertificate: { + height: 10, + stateRoot: utils.getRandomBytes(38), + timestamp: Math.floor(Date.now() / 1000), + validatorsHash: utils.getRandomBytes(48), + }, + name: 'sidechain1', + status: ChainStatus.ACTIVE, + }; + const partnerChannel = { + inbox: { + appendPath: [Buffer.alloc(32), Buffer.alloc(32)], + root: utils.getRandomBytes(32), + size: 18, + }, + messageFeeTokenID: Buffer.from('0000000000000011', 'hex'), + outbox: { + appendPath: [Buffer.alloc(32), Buffer.alloc(32)], + root: utils.getRandomBytes(32), + size: 18, + }, + partnerChainOutboxRoot: utils.getRandomBytes(32), + }; const defaultCCM = { nonce: BigInt(0), module: 'token', @@ -65,7 +193,7 @@ describe('BaseCrossChainUpdateCommand', () => { let internalMethod: BaseInteroperabilityInternalMethod; beforeEach(() => { - const interopModule = new MainchainInteroperabilityModule(); + const interopsModuleule = new MainchainInteroperabilityModule(); ccMethods = new Map(); ccMethods.set( 'token', @@ -73,7 +201,7 @@ describe('BaseCrossChainUpdateCommand', () => { public verifyCrossChainMessage = jest.fn(); public beforeCrossChainCommandExecute = jest.fn(); public afterCrossChainCommandExecute = jest.fn(); - })(interopModule.stores, interopModule.events), + })(interopsModuleule.stores, interopsModuleule.events), ); ccCommands = new Map(); ccCommands.set('token', [ @@ -81,11 +209,11 @@ describe('BaseCrossChainUpdateCommand', () => { public schema = { $id: 'test/ccu', properties: {}, type: 'object' }; public verify = jest.fn(); public execute = jest.fn(); - })(interopModule.stores, interopModule.events), + })(interopsModuleule.stores, interopsModuleule.events), ]); command = new CrossChainUpdateCommand( - interopModule.stores, - interopModule.events, + interopsModuleule.stores, + interopsModuleule.events, ccMethods, ccCommands, ); @@ -93,17 +221,542 @@ describe('BaseCrossChainUpdateCommand', () => { isLive: jest.fn().mockResolvedValue(true), addToOutbox: jest.fn(), terminateChainInternal: jest.fn(), + verifyCertificate: jest.fn(), + verifyCertificateSignature: jest.fn(), + verifyValidatorsUpdate: jest.fn(), + verifyPartnerChainOutboxRoot: jest.fn(), + updateValidators: jest.fn(), + updateCertificate: jest.fn(), } as unknown) as BaseInteroperabilityInternalMethod; jest .spyOn(command, 'getInteroperabilityInternalMethod' as never) .mockReturnValue(internalMethod as never); jest.spyOn(command['events'].get(CcmProcessedEvent), 'log'); jest.spyOn(command['events'].get(CcmSendSuccessEvent), 'log'); + + command.init( + { + getMessageFeeTokenID: jest.fn().mockResolvedValue(messageFeeTokenID), + } as any, + { + initializeUserAccount: jest.fn(), + }, + ); context = createCrossChainMessageContext({ ccm: defaultCCM, }); }); + describe('verifyCommon', () => { + let verifyContext: CommandVerifyContext; + let stateStore: PrefixedStateReadWriter; + + beforeEach(async () => { + stateStore = new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); + verifyContext = createTransactionContext({ + chainID, + stateStore, + transaction: new Transaction({ + ...defaultTransaction, + command: command.name, + params: codec.encode(crossChainUpdateTransactionParams, params), + }), + }).createCommandVerifyContext(command.schema); + await interopsModule.stores + .get(ChainAccountStore) + .set(stateStore, defaultSendingChainID, partnerChainAccount); + await interopsModule.stores.get(ChainValidatorsStore).set(stateStore, defaultSendingChainID, { + activeValidators: params.activeValidatorsUpdate, + certificateThreshold: params.certificateThreshold, + }); + }); + + it('should reject when sending chain status is registered but certificate is empty', async () => { + await interopsModule.stores.get(ChainAccountStore).set(stateStore, params.sendingChainID, { + ...partnerChainAccount, + status: ChainStatus.REGISTERED, + }); + + await expect( + command['verifyCommon']({ + ...verifyContext, + params: { + ...params, + certificate: Buffer.alloc(0), + }, + }), + ).rejects.toThrow('The first CCU must contain a non-empty certificate'); + }); + + it('should verify validators update when active validator update exist', async () => { + await expect( + command['verifyCommon']({ + ...verifyContext, + params: { + ...params, + activeValidatorsUpdate: [ + { bftWeight: BigInt(0), blsKey: utils.getRandomBytes(BLS_PUBLIC_KEY_LENGTH) }, + ], + }, + }), + ).resolves.toBeUndefined(); + + expect(internalMethod.verifyValidatorsUpdate).toHaveBeenCalledTimes(1); + }); + + it('should verify validators update when certificate threshold changes', async () => { + await expect( + command['verifyCommon']({ + ...verifyContext, + params: { + ...params, + certificateThreshold: BigInt(1), + }, + }), + ).resolves.toBeUndefined(); + + expect(internalMethod.verifyValidatorsUpdate).toHaveBeenCalledTimes(1); + }); + + it('should verify partnerchain outbox root when inbox is not empty', async () => { + await expect( + command['verifyCommon']({ + ...verifyContext, + params: { + ...params, + inboxUpdate: { + crossChainMessages: [utils.getRandomBytes(100)], + messageWitnessHashes: [utils.getRandomBytes(32)], + outboxRootWitness: { + bitmap: utils.getRandomBytes(2), + siblingHashes: [utils.getRandomBytes(32)], + }, + }, + }, + }), + ).resolves.toBeUndefined(); + + expect(internalMethod.verifyPartnerChainOutboxRoot).toHaveBeenCalledTimes(1); + }); + }); + + describe('executeCommon', () => { + let executeContext: CommandExecuteContext; + let stateStore: PrefixedStateReadWriter; + + beforeEach(async () => { + stateStore = new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); + executeContext = createTransactionContext({ + chainID, + stateStore, + transaction: new Transaction({ + ...defaultTransaction, + command: command.name, + params: codec.encode(crossChainUpdateTransactionParams, params), + }), + }).createCommandExecuteContext(command.schema); + jest.spyOn(interopsModule.events.get(CcmProcessedEvent), 'log'); + await interopsModule.stores + .get(ChainAccountStore) + .set(stateStore, defaultSendingChainID, partnerChainAccount); + await interopsModule.stores.get(ChainValidatorsStore).set(stateStore, defaultSendingChainID, { + activeValidators: params.activeValidatorsUpdate, + certificateThreshold: params.certificateThreshold, + }); + await interopsModule.stores + .get(ChannelDataStore) + .set(stateStore, defaultSendingChainID, partnerChannel); + }); + + it('should verify certificate signature', async () => { + executeContext = createTransactionContext({ + chainID, + stateStore, + transaction: new Transaction({ + ...defaultTransaction, + command: command.name, + params: codec.encode(crossChainUpdateTransactionParams, { + ...params, + }), + }), + }).createCommandExecuteContext(command.schema); + + await expect(command['executeCommon'](executeContext, true)).resolves.toEqual([ + expect.toBeArrayOfSize(params.inboxUpdate.crossChainMessages.length), + true, + ]); + expect(internalMethod.verifyCertificateSignature).toHaveBeenCalledTimes(1); + }); + + it('should initialize user account for message fee token ID when inboxUpdate is not empty', async () => { + executeContext = createTransactionContext({ + chainID, + stateStore, + transaction: new Transaction({ + ...defaultTransaction, + command: command.name, + params: codec.encode(crossChainUpdateTransactionParams, { + ...params, + }), + }), + }).createCommandExecuteContext(command.schema); + + await expect(command['executeCommon'](executeContext, true)).resolves.toEqual([ + expect.toBeArrayOfSize(params.inboxUpdate.crossChainMessages.length), + true, + ]); + expect(command['_interopsMethod'].getMessageFeeTokenID).toHaveBeenCalledWith( + expect.anything(), + params.sendingChainID, + ); + expect(command['_tokenMethod'].initializeUserAccount).toHaveBeenCalledWith( + expect.anything(), + executeContext.transaction.senderAddress, + messageFeeTokenID, + expect.anything(), + expect.anything(), + ); + }); + + it('should not initialize user account for message fee token ID when inboxUpdate is empty', async () => { + executeContext = createTransactionContext({ + chainID, + stateStore, + transaction: new Transaction({ + ...defaultTransaction, + command: command.name, + params: codec.encode(crossChainUpdateTransactionParams, { + ...params, + inboxUpdate: { + crossChainMessages: [], + messageWitnessHashes: [], + outboxRootWitness: { + bitmap: Buffer.alloc(0), + siblingHashes: [], + }, + }, + }), + }), + }).createCommandExecuteContext(command.schema); + + await expect(command['executeCommon'](executeContext, true)).resolves.toEqual([[], true]); + expect(command['_interopsMethod'].getMessageFeeTokenID).not.toHaveBeenCalled(); + expect(command['_tokenMethod'].initializeUserAccount).not.toHaveBeenCalled(); + }); + + it('should reject terminate the chain and add an event when ccm format is invalid', async () => { + executeContext = createTransactionContext({ + chainID, + stateStore, + transaction: new Transaction({ + ...defaultTransaction, + command: command.name, + params: codec.encode(crossChainUpdateTransactionParams, { + ...params, + inboxUpdate: { + ...params.inboxUpdate, + crossChainMessages: [ + ...params.inboxUpdate.crossChainMessages, + codec.encode(ccmSchema, { + crossChainCommand: CROSS_CHAIN_COMMAND_NAME_REGISTRATION, + fee: BigInt(0), + module: '___INVALID___NAME___', + nonce: BigInt(1), + params: utils.getRandomBytes(10), + receivingChainID: utils.intToBuffer(2, 4), + sendingChainID: defaultSendingChainID, + status: CCMStatusCode.OK, + }), + ], + }, + }), + }), + }).createCommandExecuteContext(command.schema); + + await expect(command['executeCommon'](executeContext, true)).resolves.toEqual([[], false]); + expect(internalMethod.terminateChainInternal).toHaveBeenCalledWith( + params.sendingChainID, + expect.anything(), + ); + expect(command['events'].get(CcmProcessedEvent).log).toHaveBeenCalledWith( + expect.anything(), + params.sendingChainID, + chainID, + { + ccmID: expect.any(Buffer), + code: CCMProcessedCode.INVALID_CCM_VALIDATION_EXCEPTION, + result: CCMProcessedResult.DISCARDED, + }, + ); + }); + + it('should reject terminate the chain and add an event when CCM sending chain and ccu sending chain is not the same', async () => { + executeContext = createTransactionContext({ + chainID, + stateStore, + transaction: new Transaction({ + ...defaultTransaction, + command: command.name, + params: codec.encode(crossChainUpdateTransactionParams, { + ...params, + inboxUpdate: { + ...params.inboxUpdate, + crossChainMessages: [ + ...params.inboxUpdate.crossChainMessages, + codec.encode(ccmSchema, { + crossChainCommand: CROSS_CHAIN_COMMAND_NAME_REGISTRATION, + fee: BigInt(0), + module: MODULE_NAME_INTEROPERABILITY, + nonce: BigInt(1), + params: utils.getRandomBytes(10), + receivingChainID: utils.intToBuffer(2, 4), + sendingChainID: Buffer.from([1, 2, 3, 4]), + status: CCMStatusCode.OK, + }), + ], + }, + }), + }), + }).createCommandExecuteContext(command.schema); + + await expect(command['executeCommon'](executeContext, true)).resolves.toEqual([[], false]); + expect(internalMethod.terminateChainInternal).toHaveBeenCalledWith( + params.sendingChainID, + expect.anything(), + ); + expect(command['events'].get(CcmProcessedEvent).log).toHaveBeenCalledWith( + expect.anything(), + params.sendingChainID, + chainID, + { + ccmID: expect.any(Buffer), + code: CCMProcessedCode.INVALID_CCM_VALIDATION_EXCEPTION, + result: CCMProcessedResult.DISCARDED, + }, + ); + }); + + it('should reject terminate the chain and add an event when receiving chain is the same as sending chain', async () => { + executeContext = createTransactionContext({ + chainID, + stateStore, + transaction: new Transaction({ + ...defaultTransaction, + command: command.name, + params: codec.encode(crossChainUpdateTransactionParams, { + ...params, + inboxUpdate: { + ...params.inboxUpdate, + crossChainMessages: [ + ...params.inboxUpdate.crossChainMessages, + codec.encode(ccmSchema, { + crossChainCommand: CROSS_CHAIN_COMMAND_NAME_REGISTRATION, + fee: BigInt(0), + module: MODULE_NAME_INTEROPERABILITY, + nonce: BigInt(1), + params: utils.getRandomBytes(10), + receivingChainID: defaultSendingChainID, + sendingChainID: defaultSendingChainID, + status: CCMStatusCode.OK, + }), + ], + }, + }), + }), + }).createCommandExecuteContext(command.schema); + + await expect(command['executeCommon'](executeContext, true)).resolves.toEqual([[], false]); + expect(internalMethod.terminateChainInternal).toHaveBeenCalledWith( + params.sendingChainID, + expect.anything(), + ); + expect(command['events'].get(CcmProcessedEvent).log).toHaveBeenCalledWith( + expect.anything(), + params.sendingChainID, + chainID, + { + ccmID: expect.any(Buffer), + code: CCMProcessedCode.INVALID_CCM_VALIDATION_EXCEPTION, + result: CCMProcessedResult.DISCARDED, + }, + ); + }); + + it('should reject with terminate the chain and add an event when ccm status is CCMStatusCode.CHANNEL_UNAVAILABLE and mainchain is true', async () => { + executeContext = createTransactionContext({ + chainID, + stateStore, + transaction: new Transaction({ + ...defaultTransaction, + command: command.name, + params: codec.encode(crossChainUpdateTransactionParams, { + ...params, + inboxUpdate: { + ...params.inboxUpdate, + crossChainMessages: [ + ...params.inboxUpdate.crossChainMessages, + codec.encode(ccmSchema, { + crossChainCommand: CROSS_CHAIN_COMMAND_NAME_REGISTRATION, + fee: BigInt(0), + module: MODULE_NAME_INTEROPERABILITY, + nonce: BigInt(1), + params: utils.getRandomBytes(10), + receivingChainID: utils.intToBuffer(2, 4), + sendingChainID: defaultSendingChainID, + status: CCMStatusCode.CHANNEL_UNAVAILABLE, + }), + ], + }, + }), + }), + }).createCommandExecuteContext(command.schema); + + await expect(command['executeCommon'](executeContext, true)).resolves.toEqual([[], false]); + expect(internalMethod.terminateChainInternal).toHaveBeenCalledWith( + params.sendingChainID, + expect.anything(), + ); + expect(command['events'].get(CcmProcessedEvent).log).toHaveBeenCalledWith( + expect.anything(), + params.sendingChainID, + chainID, + { + ccmID: expect.any(Buffer), + code: CCMProcessedCode.INVALID_CCM_VALIDATION_EXCEPTION, + result: CCMProcessedResult.DISCARDED, + }, + ); + }); + + it('should resolve when ccm status is CCMStatusCode.CHANNEL_UNAVAILABLE and mainchain is false', async () => { + executeContext = createTransactionContext({ + chainID, + stateStore, + transaction: new Transaction({ + ...defaultTransaction, + command: command.name, + params: codec.encode(crossChainUpdateTransactionParams, { + ...params, + inboxUpdate: { + ...params.inboxUpdate, + crossChainMessages: [ + ...params.inboxUpdate.crossChainMessages, + codec.encode(ccmSchema, { + crossChainCommand: CROSS_CHAIN_COMMAND_NAME_REGISTRATION, + fee: BigInt(0), + module: MODULE_NAME_INTEROPERABILITY, + nonce: BigInt(1), + params: utils.getRandomBytes(10), + receivingChainID: utils.intToBuffer(2, 4), + sendingChainID: defaultSendingChainID, + status: CCMStatusCode.CHANNEL_UNAVAILABLE, + }), + ], + }, + }), + }), + }).createCommandExecuteContext(command.schema); + + await expect(command['executeCommon'](executeContext, false)).resolves.toEqual([ + expect.toBeArrayOfSize(params.inboxUpdate.crossChainMessages.length + 1), + true, + ]); + expect(internalMethod.terminateChainInternal).not.toHaveBeenCalled(); + expect(command['events'].get(CcmProcessedEvent).log).not.toHaveBeenCalled(); + }); + + it('should update validators when activeValidatorsUpdate is not empty', async () => { + executeContext = createTransactionContext({ + chainID, + stateStore, + transaction: new Transaction({ + ...defaultTransaction, + command: command.name, + params: codec.encode(crossChainUpdateTransactionParams, { + ...params, + inboxUpdate: { + ...params.inboxUpdate, + activeValidatorsUpdate: [ + { bftWeight: BigInt(1), blsKey: utils.getRandomBytes(BLS_PUBLIC_KEY_LENGTH) }, + ], + }, + }), + }), + }).createCommandExecuteContext(command.schema); + + await expect(command['executeCommon'](executeContext, true)).resolves.toEqual([ + expect.toBeArrayOfSize(params.inboxUpdate.crossChainMessages.length), + true, + ]); + expect(internalMethod.updateValidators).toHaveBeenCalledTimes(1); + }); + + it('should update validators when certificate threshold is different', async () => { + executeContext = createTransactionContext({ + chainID, + stateStore, + transaction: new Transaction({ + ...defaultTransaction, + command: command.name, + params: codec.encode(crossChainUpdateTransactionParams, { + ...params, + inboxUpdate: { + ...params.inboxUpdate, + certificateThreshold: BigInt(1), + }, + }), + }), + }).createCommandExecuteContext(command.schema); + + await expect(command['executeCommon'](executeContext, true)).resolves.toEqual([ + expect.toBeArrayOfSize(params.inboxUpdate.crossChainMessages.length), + true, + ]); + expect(internalMethod.updateValidators).toHaveBeenCalledTimes(1); + }); + + it('should update certificate when certificate is not empty', async () => { + executeContext = createTransactionContext({ + chainID, + stateStore, + transaction: new Transaction({ + ...defaultTransaction, + command: command.name, + params: codec.encode(crossChainUpdateTransactionParams, { + ...params, + }), + }), + }).createCommandExecuteContext(command.schema); + + await expect(command['executeCommon'](executeContext, true)).resolves.toEqual([ + expect.toBeArrayOfSize(params.inboxUpdate.crossChainMessages.length), + true, + ]); + expect(internalMethod.updateCertificate).toHaveBeenCalledTimes(1); + }); + + it('should not update certificate when certificate is empty', async () => { + executeContext = createTransactionContext({ + chainID, + stateStore, + transaction: new Transaction({ + ...defaultTransaction, + command: command.name, + params: codec.encode(crossChainUpdateTransactionParams, { + ...params, + certificate: Buffer.alloc(0), + }), + }), + }).createCommandExecuteContext(command.schema); + + await expect(command['executeCommon'](executeContext, true)).resolves.toEqual([ + expect.toBeArrayOfSize(params.inboxUpdate.crossChainMessages.length), + true, + ]); + expect(internalMethod.updateCertificate).not.toHaveBeenCalled(); + }); + }); + describe('apply', () => { it('should terminate the chain and log event when sending chain is not live', async () => { (internalMethod.isLive as jest.Mock).mockResolvedValue(false); diff --git a/framework/test/unit/modules/interoperability/mainchain/commands/cc_update.spec.ts b/framework/test/unit/modules/interoperability/mainchain/commands/cc_update.spec.ts index 074b2b23fdc..aaa6e6a8596 100644 --- a/framework/test/unit/modules/interoperability/mainchain/commands/cc_update.spec.ts +++ b/framework/test/unit/modules/interoperability/mainchain/commands/cc_update.spec.ts @@ -13,8 +13,7 @@ */ /* eslint-disable max-classes-per-file */ -import { utils } from '@liskhq/lisk-cryptography'; -import * as cryptography from '@liskhq/lisk-cryptography'; +import { bls, utils } from '@liskhq/lisk-cryptography'; import { codec } from '@liskhq/lisk-codec'; import { CommandExecuteContext, @@ -42,7 +41,6 @@ import { sidechainTerminatedCCMParamsSchema, } from '../../../../../../src/modules/interoperability/schemas'; import { - BLS_PUBLIC_KEY_LENGTH, CCMStatusCode, CROSS_CHAIN_COMMAND_NAME_REGISTRATION, CROSS_CHAIN_COMMAND_NAME_SIDECHAIN_TERMINATED, @@ -75,29 +73,25 @@ import { CCMProcessedResult, } from '../../../../../../src/modules/interoperability/events/ccm_processed'; -jest.mock('@liskhq/lisk-cryptography', () => ({ - ...jest.requireActual('@liskhq/lisk-cryptography'), -})); - describe('CrossChainUpdateCommand', () => { const interopMod = new MainchainInteroperabilityModule(); const chainID = Buffer.alloc(4, 0); - const senderPublicKey = cryptography.utils.getRandomBytes(32); + const senderPublicKey = utils.getRandomBytes(32); const messageFeeTokenID = Buffer.alloc(8, 0); const defaultCertificateValues: Certificate = { - blockID: cryptography.utils.getRandomBytes(20), + blockID: utils.getRandomBytes(20), height: 21, timestamp: Math.floor(Date.now() / 1000), - stateRoot: cryptography.utils.getRandomBytes(HASH_LENGTH), - validatorsHash: cryptography.utils.getRandomBytes(48), - aggregationBits: cryptography.utils.getRandomBytes(38), - signature: cryptography.utils.getRandomBytes(32), + stateRoot: utils.getRandomBytes(HASH_LENGTH), + validatorsHash: utils.getRandomBytes(48), + aggregationBits: utils.getRandomBytes(38), + signature: utils.getRandomBytes(32), }; const defaultNewCertificateThreshold = BigInt(20); const defaultSendingChainID = 20; - const defaultSendingChainIDBuffer = cryptography.utils.intToBuffer(defaultSendingChainID, 4); + const defaultSendingChainIDBuffer = utils.intToBuffer(defaultSendingChainID, 4); const defaultCCMs: CCMsg[] = [ { crossChainCommand: CROSS_CHAIN_COMMAND_NAME_REGISTRATION, @@ -178,10 +172,10 @@ describe('CrossChainUpdateCommand', () => { ); stateStore = new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); activeValidatorsUpdate = [ - { blsKey: cryptography.utils.getRandomBytes(48), bftWeight: BigInt(1) }, - { blsKey: cryptography.utils.getRandomBytes(48), bftWeight: BigInt(3) }, - { blsKey: cryptography.utils.getRandomBytes(48), bftWeight: BigInt(4) }, - { blsKey: cryptography.utils.getRandomBytes(48), bftWeight: BigInt(3) }, + { blsKey: utils.getRandomBytes(48), bftWeight: BigInt(1) }, + { blsKey: utils.getRandomBytes(48), bftWeight: BigInt(3) }, + { blsKey: utils.getRandomBytes(48), bftWeight: BigInt(4) }, + { blsKey: utils.getRandomBytes(48), bftWeight: BigInt(3) }, ].sort((v1, v2) => v2.blsKey.compare(v1.blsKey)); // unsorted list const partnerValidators: any = { @@ -210,9 +204,9 @@ describe('CrossChainUpdateCommand', () => { partnerChainAccount = { lastCertificate: { height: 10, - stateRoot: cryptography.utils.getRandomBytes(38), + stateRoot: utils.getRandomBytes(38), timestamp: Math.floor(Date.now() / 1000), - validatorsHash: cryptography.utils.getRandomBytes(48), + validatorsHash: utils.getRandomBytes(48), }, name: 'sidechain1', status: ChainStatus.ACTIVE, @@ -220,16 +214,16 @@ describe('CrossChainUpdateCommand', () => { partnerChannelAccount = { inbox: { appendPath: [Buffer.alloc(1), Buffer.alloc(1)], - root: cryptography.utils.getRandomBytes(38), + root: utils.getRandomBytes(38), size: 18, }, messageFeeTokenID: Buffer.from('0000000000000011', 'hex'), outbox: { appendPath: [Buffer.alloc(1), Buffer.alloc(1)], - root: cryptography.utils.getRandomBytes(38), + root: utils.getRandomBytes(38), size: 18, }, - partnerChainOutboxRoot: cryptography.utils.getRandomBytes(38), + partnerChainOutboxRoot: utils.getRandomBytes(38), }; params = { @@ -267,7 +261,7 @@ describe('CrossChainUpdateCommand', () => { jest.spyOn(MainchainInteroperabilityInternalMethod.prototype, 'isLive').mockResolvedValue(true); jest.spyOn(interopUtils, 'computeValidatorsHash').mockReturnValue(validatorsHash); - jest.spyOn(cryptography.bls, 'verifyWeightedAggSig').mockReturnValue(true); + jest.spyOn(bls, 'verifyWeightedAggSig').mockReturnValue(true); }); describe('verify', () => { @@ -284,18 +278,6 @@ describe('CrossChainUpdateCommand', () => { jest .spyOn(MainchainInteroperabilityInternalMethod.prototype, 'isLive') .mockResolvedValue(true); - jest - .spyOn(MainchainInteroperabilityInternalMethod.prototype, 'isLive') - .mockResolvedValue(true); - jest - .spyOn(MainchainInteroperabilityInternalMethod.prototype, 'verifyCertificate') - .mockResolvedValue(); - jest - .spyOn(MainchainInteroperabilityInternalMethod.prototype, 'verifyValidatorsUpdate') - .mockResolvedValue(); - jest - .spyOn(MainchainInteroperabilityInternalMethod.prototype, 'verifyPartnerChainOutboxRoot') - .mockResolvedValue(); }); it('should reject when ccu params validation fails', async () => { @@ -316,78 +298,20 @@ describe('CrossChainUpdateCommand', () => { ); }); - it('should reject when sending chain status is registered but certificate is empty', async () => { - await interopMod.stores.get(ChainAccountStore).set(stateStore, params.sendingChainID, { - ...partnerChainAccount, - status: ChainStatus.REGISTERED, - }); - - await expect( - mainchainCCUUpdateCommand.verify({ - ...verifyContext, - params: { - ...params, - certificate: Buffer.alloc(0), - }, - }), - ).rejects.toThrow('The first CCU must contain a non-empty certificate'); - }); - - it('should verify validators update when active validator update exist', async () => { - await expect( - mainchainCCUUpdateCommand.verify({ - ...verifyContext, - params: { - ...params, - activeValidatorsUpdate: [ - { bftWeight: BigInt(0), blsKey: utils.getRandomBytes(BLS_PUBLIC_KEY_LENGTH) }, - ], - }, - }), - ).resolves.toEqual({ status: VerifyStatus.OK }); - - expect( - MainchainInteroperabilityInternalMethod.prototype.verifyValidatorsUpdate, - ).toHaveBeenCalledTimes(1); - }); - - it('should verify validators update when certificate threshold changes', async () => { + it('should call verifyCommon', async () => { + jest + .spyOn(mainchainCCUUpdateCommand, 'verifyCommon' as never) + .mockResolvedValue(undefined as never); await expect( mainchainCCUUpdateCommand.verify({ ...verifyContext, params: { ...params, - certificateThreshold: BigInt(1), }, }), ).resolves.toEqual({ status: VerifyStatus.OK }); - expect( - MainchainInteroperabilityInternalMethod.prototype.verifyValidatorsUpdate, - ).toHaveBeenCalledTimes(1); - }); - - it('should verify partnerchain outbox root when inbox is not empty', async () => { - await expect( - mainchainCCUUpdateCommand.verify({ - ...verifyContext, - params: { - ...params, - inboxUpdate: { - crossChainMessages: [utils.getRandomBytes(100)], - messageWitnessHashes: [utils.getRandomBytes(32)], - outboxRootWitness: { - bitmap: utils.getRandomBytes(2), - siblingHashes: [utils.getRandomBytes(32)], - }, - }, - }, - }), - ).resolves.toEqual({ status: VerifyStatus.OK }); - - expect( - MainchainInteroperabilityInternalMethod.prototype.verifyPartnerChainOutboxRoot, - ).toHaveBeenCalledTimes(1); + expect(mainchainCCUUpdateCommand['verifyCommon']).toHaveBeenCalledTimes(1); }); }); @@ -402,19 +326,6 @@ describe('CrossChainUpdateCommand', () => { params: codec.encode(crossChainUpdateTransactionParams, params), }), }).createCommandExecuteContext(mainchainCCUUpdateCommand.schema); - jest.spyOn(interopMod.events.get(CcmProcessedEvent), 'log'); - jest - .spyOn(MainchainInteroperabilityInternalMethod.prototype, 'verifyCertificateSignature') - .mockResolvedValue(); - jest - .spyOn(MainchainInteroperabilityInternalMethod.prototype, 'terminateChainInternal') - .mockResolvedValue(); - jest - .spyOn(MainchainInteroperabilityInternalMethod.prototype, 'updateValidators') - .mockResolvedValue(); - jest - .spyOn(MainchainInteroperabilityInternalMethod.prototype, 'updateCertificate') - .mockResolvedValue(); jest .spyOn(MainchainInteroperabilityInternalMethod.prototype, 'appendToInboxTree') .mockResolvedValue(); @@ -424,172 +335,7 @@ describe('CrossChainUpdateCommand', () => { .mockResolvedValue(undefined as never); }); - it('should verify certificate signature', async () => { - executeContext = createTransactionContext({ - chainID, - stateStore, - transaction: new Transaction({ - ...defaultTransaction, - command: mainchainCCUUpdateCommand.name, - params: codec.encode(crossChainUpdateTransactionParams, { - ...params, - }), - }), - }).createCommandExecuteContext(mainchainCCUUpdateCommand.schema); - - await expect(mainchainCCUUpdateCommand.execute(executeContext)).resolves.toBeUndefined(); - expect( - MainchainInteroperabilityInternalMethod.prototype.verifyCertificateSignature, - ).toHaveBeenCalledTimes(1); - }); - - it('should initialize user account for message fee token ID when inboxUpdate is not empty', async () => { - executeContext = createTransactionContext({ - chainID, - stateStore, - transaction: new Transaction({ - ...defaultTransaction, - command: mainchainCCUUpdateCommand.name, - params: codec.encode(crossChainUpdateTransactionParams, { - ...params, - }), - }), - }).createCommandExecuteContext(mainchainCCUUpdateCommand.schema); - - await expect(mainchainCCUUpdateCommand.execute(executeContext)).resolves.toBeUndefined(); - expect( - mainchainCCUUpdateCommand['_interopsMethod'].getMessageFeeTokenID, - ).toHaveBeenCalledWith(expect.anything(), params.sendingChainID); - expect(mainchainCCUUpdateCommand['_tokenMethod'].initializeUserAccount).toHaveBeenCalledWith( - expect.anything(), - executeContext.transaction.senderAddress, - messageFeeTokenID, - expect.anything(), - expect.anything(), - ); - }); - - it('should not initialize user account for message fee token ID when inboxUpdate is empty', async () => { - executeContext = createTransactionContext({ - chainID, - stateStore, - transaction: new Transaction({ - ...defaultTransaction, - command: mainchainCCUUpdateCommand.name, - params: codec.encode(crossChainUpdateTransactionParams, { - ...params, - inboxUpdate: { - crossChainMessages: [], - messageWitnessHashes: [], - outboxRootWitness: { - bitmap: Buffer.alloc(0), - siblingHashes: [], - }, - }, - }), - }), - }).createCommandExecuteContext(mainchainCCUUpdateCommand.schema); - - await expect(mainchainCCUUpdateCommand.execute(executeContext)).resolves.toBeUndefined(); - expect( - mainchainCCUUpdateCommand['_interopsMethod'].getMessageFeeTokenID, - ).not.toHaveBeenCalled(); - expect( - mainchainCCUUpdateCommand['_tokenMethod'].initializeUserAccount, - ).not.toHaveBeenCalled(); - }); - - it('should reject terminate the chain and add an event when ccm format is invalid', async () => { - executeContext = createTransactionContext({ - chainID, - stateStore, - transaction: new Transaction({ - ...defaultTransaction, - command: mainchainCCUUpdateCommand.name, - params: codec.encode(crossChainUpdateTransactionParams, { - ...params, - inboxUpdate: { - ...defaultInboxUpdateValue, - crossChainMessages: [ - ...defaultCCMs, - { - crossChainCommand: CROSS_CHAIN_COMMAND_NAME_REGISTRATION, - fee: BigInt(0), - module: '___INVALID___NAME___', - nonce: BigInt(1), - params: utils.getRandomBytes(10), - receivingChainID: utils.intToBuffer(2, 4), - sendingChainID: defaultSendingChainIDBuffer, - status: CCMStatusCode.OK, - }, - ].map(ccm => codec.encode(ccmSchema, ccm)), - }, - }), - }), - }).createCommandExecuteContext(mainchainCCUUpdateCommand.schema); - - await expect(mainchainCCUUpdateCommand.execute(executeContext)).resolves.toBeUndefined(); - expect( - MainchainInteroperabilityInternalMethod.prototype.terminateChainInternal, - ).toHaveBeenCalledWith(params.sendingChainID, expect.anything()); - expect(interopMod.events.get(CcmProcessedEvent).log).toHaveBeenCalledWith( - expect.anything(), - params.sendingChainID, - chainID, - { - ccmID: expect.any(Buffer), - code: CCMProcessedCode.INVALID_CCM_VALIDATION_EXCEPTION, - result: CCMProcessedResult.DISCARDED, - }, - ); - }); - - it('should reject terminate the chain and add an event when CCM sending chain and ccu sending chain is not the same', async () => { - executeContext = createTransactionContext({ - chainID, - stateStore, - transaction: new Transaction({ - ...defaultTransaction, - command: mainchainCCUUpdateCommand.name, - params: codec.encode(crossChainUpdateTransactionParams, { - ...params, - inboxUpdate: { - ...defaultInboxUpdateValue, - crossChainMessages: [ - ...defaultCCMs, - { - crossChainCommand: CROSS_CHAIN_COMMAND_NAME_REGISTRATION, - fee: BigInt(0), - module: MODULE_NAME_INTEROPERABILITY, - nonce: BigInt(1), - params: utils.getRandomBytes(10), - receivingChainID: utils.intToBuffer(2, 4), - sendingChainID: Buffer.from([1, 2, 3, 4]), - status: CCMStatusCode.OK, - }, - ].map(ccm => codec.encode(ccmSchema, ccm)), - }, - }), - }), - }).createCommandExecuteContext(mainchainCCUUpdateCommand.schema); - - await expect(mainchainCCUUpdateCommand.execute(executeContext)).resolves.toBeUndefined(); - expect( - MainchainInteroperabilityInternalMethod.prototype.terminateChainInternal, - ).toHaveBeenCalledWith(params.sendingChainID, expect.anything()); - expect(interopMod.events.get(CcmProcessedEvent).log).toHaveBeenCalledWith( - expect.anything(), - params.sendingChainID, - chainID, - { - ccmID: expect.any(Buffer), - code: CCMProcessedCode.INVALID_CCM_VALIDATION_EXCEPTION, - result: CCMProcessedResult.DISCARDED, - }, - ); - }); - - it('should reject terminate the chain and add an event when receiving chain is the same as sending chain', async () => { + it('should call executeCommon', async () => { executeContext = createTransactionContext({ chainID, stateStore, @@ -598,175 +344,21 @@ describe('CrossChainUpdateCommand', () => { command: mainchainCCUUpdateCommand.name, params: codec.encode(crossChainUpdateTransactionParams, { ...params, - inboxUpdate: { - ...defaultInboxUpdateValue, - crossChainMessages: [ - ...defaultCCMs, - { - crossChainCommand: CROSS_CHAIN_COMMAND_NAME_REGISTRATION, - fee: BigInt(0), - module: MODULE_NAME_INTEROPERABILITY, - nonce: BigInt(1), - params: utils.getRandomBytes(10), - receivingChainID: defaultSendingChainIDBuffer, - sendingChainID: defaultSendingChainIDBuffer, - status: CCMStatusCode.OK, - }, - ].map(ccm => codec.encode(ccmSchema, ccm)), - }, - }), - }), - }).createCommandExecuteContext(mainchainCCUUpdateCommand.schema); - - await expect(mainchainCCUUpdateCommand.execute(executeContext)).resolves.toBeUndefined(); - expect( - MainchainInteroperabilityInternalMethod.prototype.terminateChainInternal, - ).toHaveBeenCalledWith(params.sendingChainID, expect.anything()); - expect(interopMod.events.get(CcmProcessedEvent).log).toHaveBeenCalledWith( - expect.anything(), - params.sendingChainID, - chainID, - { - ccmID: expect.any(Buffer), - code: CCMProcessedCode.INVALID_CCM_VALIDATION_EXCEPTION, - result: CCMProcessedResult.DISCARDED, - }, - ); - }); - - it('should reject terminate the chain and add an event when ccm status is CCMStatusCode.CHANNEL_UNAVAILABLE', async () => { - executeContext = createTransactionContext({ - chainID, - stateStore, - transaction: new Transaction({ - ...defaultTransaction, - command: mainchainCCUUpdateCommand.name, - params: codec.encode(crossChainUpdateTransactionParams, { - ...params, - inboxUpdate: { - ...defaultInboxUpdateValue, - crossChainMessages: [ - ...defaultCCMs, - { - crossChainCommand: CROSS_CHAIN_COMMAND_NAME_REGISTRATION, - fee: BigInt(0), - module: MODULE_NAME_INTEROPERABILITY, - nonce: BigInt(1), - params: utils.getRandomBytes(10), - receivingChainID: utils.intToBuffer(2, 4), - sendingChainID: defaultSendingChainIDBuffer, - status: CCMStatusCode.CHANNEL_UNAVAILABLE, - }, - ].map(ccm => codec.encode(ccmSchema, ccm)), - }, }), }), }).createCommandExecuteContext(mainchainCCUUpdateCommand.schema); + jest + .spyOn(mainchainCCUUpdateCommand, 'executeCommon' as never) + .mockResolvedValue([[], true] as never); await expect(mainchainCCUUpdateCommand.execute(executeContext)).resolves.toBeUndefined(); - expect( - MainchainInteroperabilityInternalMethod.prototype.terminateChainInternal, - ).toHaveBeenCalledWith(params.sendingChainID, expect.anything()); - expect(interopMod.events.get(CcmProcessedEvent).log).toHaveBeenCalledWith( + expect(mainchainCCUUpdateCommand['executeCommon']).toHaveBeenCalledTimes(1); + expect(mainchainCCUUpdateCommand['executeCommon']).toHaveBeenCalledWith( expect.anything(), - params.sendingChainID, - chainID, - { - ccmID: expect.any(Buffer), - code: CCMProcessedCode.INVALID_CCM_VALIDATION_EXCEPTION, - result: CCMProcessedResult.DISCARDED, - }, + true, ); }); - it('should update validators when activeValidatorsUpdate is not empty', async () => { - executeContext = createTransactionContext({ - chainID, - stateStore, - transaction: new Transaction({ - ...defaultTransaction, - command: mainchainCCUUpdateCommand.name, - params: codec.encode(crossChainUpdateTransactionParams, { - ...params, - inboxUpdate: { - ...defaultInboxUpdateValue, - activeValidatorsUpdate: [ - { bftWeight: BigInt(1), blsKey: utils.getRandomBytes(BLS_PUBLIC_KEY_LENGTH) }, - ], - }, - }), - }), - }).createCommandExecuteContext(mainchainCCUUpdateCommand.schema); - - await expect(mainchainCCUUpdateCommand.execute(executeContext)).resolves.toBeUndefined(); - expect( - MainchainInteroperabilityInternalMethod.prototype.updateValidators, - ).toHaveBeenCalledTimes(1); - }); - - it('should update validators when certificate threshold is different', async () => { - executeContext = createTransactionContext({ - chainID, - stateStore, - transaction: new Transaction({ - ...defaultTransaction, - command: mainchainCCUUpdateCommand.name, - params: codec.encode(crossChainUpdateTransactionParams, { - ...params, - inboxUpdate: { - ...defaultInboxUpdateValue, - certificateThreshold: BigInt(1), - }, - }), - }), - }).createCommandExecuteContext(mainchainCCUUpdateCommand.schema); - - await expect(mainchainCCUUpdateCommand.execute(executeContext)).resolves.toBeUndefined(); - expect( - MainchainInteroperabilityInternalMethod.prototype.updateValidators, - ).toHaveBeenCalledTimes(1); - }); - - it('should update certificate when certificate is not empty', async () => { - executeContext = createTransactionContext({ - chainID, - stateStore, - transaction: new Transaction({ - ...defaultTransaction, - command: mainchainCCUUpdateCommand.name, - params: codec.encode(crossChainUpdateTransactionParams, { - ...params, - certificate: encodedDefaultCertificate, - }), - }), - }).createCommandExecuteContext(mainchainCCUUpdateCommand.schema); - - await expect(mainchainCCUUpdateCommand.execute(executeContext)).resolves.toBeUndefined(); - expect( - MainchainInteroperabilityInternalMethod.prototype.updateCertificate, - ).toHaveBeenCalledTimes(1); - }); - - it('should not update certificate when certificate is empty', async () => { - executeContext = createTransactionContext({ - chainID, - stateStore, - transaction: new Transaction({ - ...defaultTransaction, - command: mainchainCCUUpdateCommand.name, - params: codec.encode(crossChainUpdateTransactionParams, { - ...params, - certificate: Buffer.alloc(0), - }), - }), - }).createCommandExecuteContext(mainchainCCUUpdateCommand.schema); - - await expect(mainchainCCUUpdateCommand.execute(executeContext)).resolves.toBeUndefined(); - expect( - MainchainInteroperabilityInternalMethod.prototype.updateCertificate, - ).not.toHaveBeenCalled(); - }); - it('should call apply for ccm and add to the inbox where receivign chain is the main chain', async () => { executeContext = createTransactionContext({ chainID, diff --git a/framework/test/unit/modules/interoperability/sidechain/commands/cc_update.spec.ts b/framework/test/unit/modules/interoperability/sidechain/commands/cc_update.spec.ts index 36089111945..9315419f85b 100644 --- a/framework/test/unit/modules/interoperability/sidechain/commands/cc_update.spec.ts +++ b/framework/test/unit/modules/interoperability/sidechain/commands/cc_update.spec.ts @@ -12,22 +12,19 @@ * Removal or modification of this copyright notice is prohibited. */ -import { utils } from '@liskhq/lisk-cryptography'; -import * as cryptography from '@liskhq/lisk-cryptography'; +import { bls, utils } from '@liskhq/lisk-cryptography'; import { codec } from '@liskhq/lisk-codec'; -import { BlockAssets } from '@liskhq/lisk-chain'; +import { Transaction } from '@liskhq/lisk-chain'; import { CommandExecuteContext, CommandVerifyContext, SidechainInteroperabilityModule, - testing, VerifyStatus, } from '../../../../../../src'; import { ActiveValidator, CCMsg, ChainAccount, - ChainValidators, ChannelData, CrossChainUpdateTransactionParams, } from '../../../../../../src/modules/interoperability/types'; @@ -35,17 +32,16 @@ import { SidechainCCUpdateCommand } from '../../../../../../src/modules/interope import { Certificate } from '../../../../../../src/engine/consensus/certificate_generation/types'; import { certificateSchema } from '../../../../../../src/engine/consensus/certificate_generation/schema'; import * as interopUtils from '../../../../../../src/modules/interoperability/utils'; -import { ccmSchema } from '../../../../../../src/modules/interoperability/schemas'; +import { + ccmSchema, + crossChainUpdateTransactionParams, +} from '../../../../../../src/modules/interoperability/schemas'; import { CCMStatusCode, CROSS_CHAIN_COMMAND_NAME_REGISTRATION, CROSS_CHAIN_COMMAND_NAME_SIDECHAIN_TERMINATED, - EMPTY_BYTES, - LIVENESS_LIMIT, - MAX_CCM_SIZE, MODULE_NAME_INTEROPERABILITY, } from '../../../../../../src/modules/interoperability/constants'; -import { BlockHeader, EventQueue } from '../../../../../../src/state_machine'; import { SidechainInteroperabilityInternalMethod } from '../../../../../../src/modules/interoperability/sidechain/store'; import { computeValidatorsHash } from '../../../../../../src/modules/interoperability/utils'; import { CROSS_CHAIN_COMMAND_NAME_FORWARD } from '../../../../../../src/modules/token/constants'; @@ -58,28 +54,26 @@ import { ChannelDataStore } from '../../../../../../src/modules/interoperability import { ChainValidatorsStore } from '../../../../../../src/modules/interoperability/stores/chain_validators'; import { InMemoryPrefixedStateDB } from '../../../../../../src/testing/in_memory_prefixed_state'; import { createStoreGetter } from '../../../../../../src/testing/utils'; -import { createTransientMethodContext } from '../../../../../../src/testing'; - -jest.mock('@liskhq/lisk-cryptography', () => ({ - ...jest.requireActual('@liskhq/lisk-cryptography'), -})); +import { createTransactionContext } from '../../../../../../src/testing'; describe('CrossChainUpdateCommand', () => { const interopMod = new SidechainInteroperabilityModule(); + const senderPublicKey = utils.getRandomBytes(32); + const messageFeeTokenID = Buffer.alloc(8, 0); - const chainID = cryptography.utils.getRandomBytes(32); + const chainID = Buffer.from([0, 0, 2, 0]); const defaultCertificateValues: Certificate = { - blockID: cryptography.utils.getRandomBytes(20), + blockID: utils.getRandomBytes(20), height: 21, timestamp: Math.floor(Date.now() / 1000), - stateRoot: cryptography.utils.getRandomBytes(38), - validatorsHash: cryptography.utils.getRandomBytes(48), - aggregationBits: cryptography.utils.getRandomBytes(38), - signature: cryptography.utils.getRandomBytes(32), + stateRoot: utils.getRandomBytes(38), + validatorsHash: utils.getRandomBytes(48), + aggregationBits: utils.getRandomBytes(38), + signature: utils.getRandomBytes(32), }; const defaultNewCertificateThreshold = BigInt(20); - const defaultSendingChainID = utils.intToBuffer(20, 4); + const defaultSendingChainID = Buffer.from([0, 0, 0, 0]); const defaultCCMs: CCMsg[] = [ { crossChainCommand: CROSS_CHAIN_COMMAND_NAME_SIDECHAIN_TERMINATED, @@ -121,7 +115,13 @@ describe('CrossChainUpdateCommand', () => { siblingHashes: [Buffer.alloc(32)], }, }; - const defaultTransaction = { module: MODULE_NAME_INTEROPERABILITY }; + const defaultTransaction = { + fee: BigInt(0), + module: interopMod.name, + nonce: BigInt(1), + senderPublicKey, + signatures: [], + }; let stateStore: PrefixedStateReadWriter; let encodedDefaultCertificate: Buffer; @@ -144,12 +144,20 @@ describe('CrossChainUpdateCommand', () => { new Map(), new Map(), ); + sidechainCCUUpdateCommand.init( + { + getMessageFeeTokenID: jest.fn().mockResolvedValue(messageFeeTokenID), + } as any, + { + initializeUserAccount: jest.fn(), + }, + ); stateStore = new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); activeValidatorsUpdate = [ - { blsKey: cryptography.utils.getRandomBytes(48), bftWeight: BigInt(1) }, - { blsKey: cryptography.utils.getRandomBytes(48), bftWeight: BigInt(3) }, - { blsKey: cryptography.utils.getRandomBytes(48), bftWeight: BigInt(4) }, - { blsKey: cryptography.utils.getRandomBytes(48), bftWeight: BigInt(3) }, + { blsKey: utils.getRandomBytes(48), bftWeight: BigInt(1) }, + { blsKey: utils.getRandomBytes(48), bftWeight: BigInt(3) }, + { blsKey: utils.getRandomBytes(48), bftWeight: BigInt(4) }, + { blsKey: utils.getRandomBytes(48), bftWeight: BigInt(3) }, ].sort((v1, v2) => v2.blsKey.compare(v1.blsKey)); // unsorted list const partnerValidators: any = { certificateThreshold: BigInt(10), @@ -177,9 +185,9 @@ describe('CrossChainUpdateCommand', () => { partnerChainAccount = { lastCertificate: { height: 10, - stateRoot: cryptography.utils.getRandomBytes(38), + stateRoot: utils.getRandomBytes(38), timestamp: Math.floor(Date.now() / 1000), - validatorsHash: cryptography.utils.getRandomBytes(48), + validatorsHash: utils.getRandomBytes(48), }, name: 'sidechain1', status: ChainStatus.ACTIVE, @@ -187,16 +195,16 @@ describe('CrossChainUpdateCommand', () => { partnerChannelAccount = { inbox: { appendPath: [Buffer.alloc(1), Buffer.alloc(1)], - root: cryptography.utils.getRandomBytes(38), + root: utils.getRandomBytes(38), size: 18, }, messageFeeTokenID: Buffer.from('0000000000000011', 'hex'), outbox: { appendPath: [Buffer.alloc(1), Buffer.alloc(1)], - root: cryptography.utils.getRandomBytes(38), + root: utils.getRandomBytes(38), size: 18, }, - partnerChainOutboxRoot: cryptography.utils.getRandomBytes(38), + partnerChainOutboxRoot: utils.getRandomBytes(38), }; params = { @@ -235,371 +243,128 @@ describe('CrossChainUpdateCommand', () => { jest.spyOn(SidechainInteroperabilityInternalMethod.prototype, 'isLive').mockResolvedValue(true); jest.spyOn(interopUtils, 'computeValidatorsHash').mockReturnValue(validatorsHash); - jest.spyOn(cryptography.bls, 'verifyWeightedAggSig').mockReturnValue(true); + jest.spyOn(bls, 'verifyWeightedAggSig').mockReturnValue(true); }); describe('verify', () => { beforeEach(() => { - verifyContext = { - header: { height: 20, timestamp: 10000 }, - getMethodContext: () => createTransientMethodContext({ stateStore }), - getStore: createStoreGetter(stateStore).getStore, - stateStore, - logger: testing.mocks.loggerMock, + verifyContext = createTransactionContext({ chainID, - params, - transaction: defaultTransaction as any, - }; + stateStore, + transaction: new Transaction({ + ...defaultTransaction, + command: sidechainCCUUpdateCommand.name, + params: codec.encode(crossChainUpdateTransactionParams, params), + }), + }).createCommandVerifyContext(sidechainCCUUpdateCommand.schema); jest .spyOn(SidechainInteroperabilityInternalMethod.prototype, 'isLive') .mockResolvedValue(true); - sidechainCCUUpdateCommand = new SidechainCCUpdateCommand( - interopMod.stores, - interopMod.events, - new Map(), - new Map(), - ); }); - it('should return error when ccu params validation fails', async () => { - const { status, error } = await sidechainCCUUpdateCommand.verify({ - ...verifyContext, - params: { ...params, sendingChainID: 2 } as any, - }); - - expect(status).toEqual(VerifyStatus.FAIL); - expect(error?.message).toContain('.sendingChainID'); + it('should reject when ccu params validation fails', async () => { + await expect( + sidechainCCUUpdateCommand.verify({ + ...verifyContext, + params: { ...params, sendingChainID: 2 } as any, + }), + ).rejects.toThrow('.sendingChainID'); }); - it('should return error when chain has terminated status', async () => { - await partnerChainStore.set(createStoreGetter(stateStore), defaultSendingChainID, { - ...partnerChainAccount, - status: ChainStatus.TERMINATED, - }); - - const { status, error } = await sidechainCCUUpdateCommand.verify(verifyContext); - - expect(status).toEqual(VerifyStatus.FAIL); - expect(error?.message).toContain('Sending partner chain 20 is terminated.'); + it('should reject when sending chain is not mainchain', async () => { + await expect( + sidechainCCUUpdateCommand.verify({ + ...verifyContext, + params: { ...params, sendingChainID: Buffer.from([0, 1, 1, 0]) } as any, + }), + ).rejects.toThrow('Only the mainchain can send a sidechain cross-chain update'); }); - it('should return error when chain is active but not live', async () => { + it('should return error when sending chain not live', async () => { jest .spyOn(SidechainInteroperabilityInternalMethod.prototype, 'isLive') .mockResolvedValue(false); - const { status, error } = await sidechainCCUUpdateCommand.verify(verifyContext); - - expect(status).toEqual(VerifyStatus.FAIL); - expect(error?.message).toContain('Sending partner chain 20 is not live'); - }); - - it('should return error checkLivenessRequirementFirstCCU fails', async () => { - await partnerChainStore.set(createStoreGetter(stateStore), defaultSendingChainID, { - ...partnerChainAccount, - status: ChainStatus.REGISTERED, - }); - - const { status, error } = await sidechainCCUUpdateCommand.verify({ - ...verifyContext, - params: { ...params, certificate: EMPTY_BYTES }, - }); - expect(status).toEqual(VerifyStatus.FAIL); - expect(error?.message).toContain( - `Sending partner chain ${defaultSendingChainID.readInt32BE( - 0, - )} has a registered status so certificate cannot be empty.`, - ); - }); - - it('should return error checkCertificateValidity fails when certificate height is less than lastCertificateHeight', async () => { - const encodedDefaultCertificateWithLowerheight = codec.encode(certificateSchema, { - ...defaultCertificateValues, - height: 9, - }); - - const { status, error } = await sidechainCCUUpdateCommand.verify({ - ...verifyContext, - params: { ...params, certificate: encodedDefaultCertificateWithLowerheight }, - }); - expect(status).toEqual(VerifyStatus.FAIL); - expect(error?.message).toContain( - 'Certificate height should be greater than last certificate height.', + await expect(sidechainCCUUpdateCommand.verify(verifyContext)).rejects.toThrow( + 'The sending chain is not live', ); }); - it('should return VerifyStatus.FAIL when checkValidatorsHashWithCertificate() throws error', async () => { - const certificateWithIncorrectValidatorHash = codec.encode(certificateSchema, { - ...defaultCertificateValues, - validatorsHash: cryptography.utils.getRandomBytes(48), - }); - - const { status, error } = await sidechainCCUUpdateCommand.verify({ - ...verifyContext, - params: { ...params, certificate: certificateWithIncorrectValidatorHash }, - }); - expect(status).toEqual(VerifyStatus.FAIL); - expect(error?.message).toContain('Validators hash given in the certificate is incorrect.'); - }); - - it('should return error verifyValidatorsUpdate fails when Validators blsKeys are not unique and lexicographically ordered', async () => { + it('should call verifyCommon', async () => { + jest + .spyOn(sidechainCCUUpdateCommand, 'verifyCommon' as never) + .mockResolvedValue(undefined as never); await expect( sidechainCCUUpdateCommand.verify({ ...verifyContext, - params: { ...params, activeValidatorsUpdate }, + params: { + ...params, + }, }), - ).rejects.toThrow('Keys are not sorted lexicographic order.'); - }); - - it('should reject when verifyCertificateSignature fails', async () => { - jest - .spyOn(SidechainInteroperabilityInternalMethod.prototype, 'verifyCertificateSignature') - .mockRejectedValue(new Error('Certificate is invalid due to invalid signature.')); - - await expect(sidechainCCUUpdateCommand.verify(verifyContext)).rejects.toThrow( - 'Certificate is invalid due to invalid signature', - ); - }); - - it('should return error checkInboxUpdateValidity fails', async () => { - jest.spyOn(interopUtils, 'checkInboxUpdateValidity').mockReturnValue({ - status: VerifyStatus.FAIL, - error: new Error( - 'Failed at verifying state root when messageWitnessHashes is non-empty and certificate is empty.', - ), - }); - - const { status, error } = await sidechainCCUUpdateCommand.verify(verifyContext); - - expect(status).toEqual(VerifyStatus.FAIL); - expect(error?.message).toContain( - 'Failed at verifying state root when messageWitnessHashes is non-empty and certificate is empty.', - ); - }); - - it('should return Verify.OK when all the checks pass', async () => { - const { status, error } = await sidechainCCUUpdateCommand.verify(verifyContext); + ).resolves.toEqual({ status: VerifyStatus.OK }); - expect(status).toEqual(VerifyStatus.OK); - expect(error).toBeUndefined(); + expect(sidechainCCUUpdateCommand['verifyCommon']).toHaveBeenCalledTimes(1); }); }); describe('execute', () => { - let blockHeader: any; - let partnerValidatorsDataVerify: ChainValidators; - let activeValidatorsVerify: ActiveValidator[]; - - beforeEach(async () => { - activeValidatorsVerify = [...activeValidatorsUpdate]; - blockHeader = { - height: 25, - timestamp: Math.floor(Date.now() / 1000) + 100000, - }; - - partnerValidatorsDataVerify = { - activeValidators: activeValidatorsVerify, - certificateThreshold: BigInt(10), - }; - executeContext = { - getMethodContext: () => createTransientMethodContext({ stateStore }), - getStore: createStoreGetter(stateStore).getStore, - logger: testing.mocks.loggerMock, - stateStore, + beforeEach(() => { + executeContext = createTransactionContext({ chainID, - params, - transaction: defaultTransaction as any, - assets: new BlockAssets(), - eventQueue: new EventQueue(0), - header: blockHeader as BlockHeader, - }; - - await partnerValidatorStore.set( - createStoreGetter(stateStore), - defaultSendingChainID, - partnerValidatorsDataVerify, - ); - - sidechainCCUUpdateCommand = new SidechainCCUpdateCommand( - interopMod.stores, - interopMod.events, - new Map(), - new Map(), - ); - }); - - it('should throw error when checkValidCertificateLiveness() throws error', async () => { - const blockHeaderWithInvalidTimestamp = { - height: 25, - timestamp: Math.floor(Date.now() / 1000) + LIVENESS_LIMIT, - }; - await expect( - sidechainCCUUpdateCommand.execute({ - ...executeContext, - header: { ...blockHeader, timestamp: blockHeaderWithInvalidTimestamp.timestamp }, - }), - ).rejects.toThrow('Certificate is not valid as it passed Liveness limit of 2592000 seconds.'); - }); - - it('should throw error when checkCertificateTimestamp() throws error', async () => { - const blockHeaderWithInvalidTimestamp = { - height: 25, - timestamp: Math.floor(Date.now() / 1000) - LIVENESS_LIMIT, - }; - await expect( - sidechainCCUUpdateCommand.execute({ - ...executeContext, - header: { ...blockHeader, timestamp: blockHeaderWithInvalidTimestamp.timestamp }, + stateStore, + transaction: new Transaction({ + ...defaultTransaction, + command: sidechainCCUUpdateCommand.name, + params: codec.encode(crossChainUpdateTransactionParams, params), }), - ).rejects.toThrow('Certificate is invalid due to invalid timestamp.'); - }); - - it('should throw error and calls terminateChainInternal() if CCM decoding fails', async () => { - const invalidCCM = Buffer.from([1]); + }).createCommandExecuteContext(sidechainCCUUpdateCommand.schema); jest - .spyOn(interopUtils, 'computeValidatorsHash') - .mockReturnValue(defaultCertificateValues.validatorsHash); - const terminateChainInternalMock = jest - .spyOn(SidechainInteroperabilityInternalMethod.prototype, 'terminateChainInternal') - .mockResolvedValue({} as never); - const invalidCCMContext = { - ...executeContext, - params: { - ...executeContext.params, - inboxUpdate: { ...executeContext.params.inboxUpdate, crossChainMessages: [invalidCCM] }, - }, - }; - await expect(sidechainCCUUpdateCommand.execute(invalidCCMContext)).rejects.toThrow( - 'Value yields unsupported wireType', - ); - expect(terminateChainInternalMock).toHaveBeenCalledTimes(1); + .spyOn(SidechainInteroperabilityInternalMethod.prototype, 'appendToInboxTree') + .mockResolvedValue(); + jest.spyOn(sidechainCCUUpdateCommand, 'apply' as never).mockResolvedValue(undefined as never); }); - it('should throw error when chain.status === ChainStatus.REGISTERED and inboxUpdate is non-empty and the first CCM is not a registration CCM', async () => { - await partnerChainStore.set(createStoreGetter(stateStore), defaultSendingChainID, { - ...partnerChainAccount, - status: ChainStatus.REGISTERED, - }); + it('should call executeCommon', async () => { + executeContext = createTransactionContext({ + chainID, + stateStore, + transaction: new Transaction({ + ...defaultTransaction, + command: sidechainCCUUpdateCommand.name, + params: codec.encode(crossChainUpdateTransactionParams, { + ...params, + }), + }), + }).createCommandExecuteContext(sidechainCCUUpdateCommand.schema); jest - .spyOn(interopUtils, 'computeValidatorsHash') - .mockReturnValue(defaultCertificateValues.validatorsHash); - const terminateChainInternalMock = jest - .spyOn(SidechainInteroperabilityInternalMethod.prototype, 'terminateChainInternal') - .mockResolvedValue({} as never); + .spyOn(sidechainCCUUpdateCommand, 'executeCommon' as never) + .mockResolvedValue([[], true] as never); await expect(sidechainCCUUpdateCommand.execute(executeContext)).resolves.toBeUndefined(); - expect(terminateChainInternalMock).toHaveBeenCalledTimes(1); - }); - - it('should call terminateChainInternal() for a ccm when txParams.sendingChainID !== ccm.deserilized.sendingChainID', async () => { - const invalidCCM = codec.encode(ccmSchema, { - crossChainCommand: CROSS_CHAIN_COMMAND_NAME_REGISTRATION, - fee: BigInt(0), - module: MODULE_NAME_INTEROPERABILITY, - nonce: BigInt(1), - params: Buffer.alloc(2), - receivingChainID: utils.intToBuffer(2, 4), - sendingChainID: utils.intToBuffer(50, 4), - status: CCMStatusCode.OK, - }); - jest - .spyOn(interopUtils, 'computeValidatorsHash') - .mockReturnValue(defaultCertificateValues.validatorsHash); - jest.spyOn(interopUtils, 'commonCCUExecutelogic').mockReturnValue({} as never); - const terminateChainInternalMock = jest - .spyOn(SidechainInteroperabilityInternalMethod.prototype, 'terminateChainInternal') - .mockResolvedValue({} as never); - - const invalidCCMContext = { - ...executeContext, - params: { - ...executeContext.params, - inboxUpdate: { ...executeContext.params.inboxUpdate, crossChainMessages: [invalidCCM] }, - }, - }; - await expect(sidechainCCUUpdateCommand.execute(invalidCCMContext)).resolves.toBeUndefined(); - expect(terminateChainInternalMock).toHaveBeenCalledTimes(1); - }); - - it('should call terminateChainInternal() for a ccm when it fails on validateFormat', async () => { - const invalidCCM = { - crossChainCommand: CROSS_CHAIN_COMMAND_NAME_REGISTRATION, - fee: BigInt(0), - module: MODULE_NAME_INTEROPERABILITY, - nonce: BigInt(1), - params: Buffer.alloc(MAX_CCM_SIZE + 10), - receivingChainID: utils.intToBuffer(2, 4), - sendingChainID: defaultSendingChainID, - status: CCMStatusCode.OK, - }; - const invalidCCMSerialized = codec.encode(ccmSchema, invalidCCM); - jest - .spyOn(interopUtils, 'computeValidatorsHash') - .mockReturnValue(defaultCertificateValues.validatorsHash); - jest.spyOn(interopUtils, 'commonCCUExecutelogic').mockReturnValue({} as never); - - const terminateChainInternalMock = jest - .spyOn(SidechainInteroperabilityInternalMethod.prototype, 'terminateChainInternal') - .mockResolvedValue({} as never); - - const invalidCCMContext = { - ...executeContext, - params: { - ...executeContext.params, - inboxUpdate: { - ...executeContext.params.inboxUpdate, - crossChainMessages: [invalidCCMSerialized], - }, - }, - }; - await expect(sidechainCCUUpdateCommand.execute(invalidCCMContext)).resolves.toBeUndefined(); - expect(terminateChainInternalMock).toHaveBeenCalledTimes(1); - expect(terminateChainInternalMock).toHaveBeenCalledWith( - invalidCCM.sendingChainID, - expect.any(Object), + expect(sidechainCCUUpdateCommand['executeCommon']).toHaveBeenCalledTimes(1); + expect(sidechainCCUUpdateCommand['executeCommon']).toHaveBeenCalledWith( + expect.anything(), + false, ); }); - it('should call apply() for all the valid CCMs', async () => { - const sidechainCCM = { - crossChainCommand: CROSS_CHAIN_COMMAND_NAME_REGISTRATION, - fee: BigInt(0), - module: MODULE_NAME_INTEROPERABILITY, - nonce: BigInt(1), - params: Buffer.alloc(10), - receivingChainID: utils.intToBuffer(80, 4), - sendingChainID: defaultSendingChainID, - status: CCMStatusCode.OK, - }; - const sidechainCCMSerialized = codec.encode(ccmSchema, sidechainCCM); - jest - .spyOn(interopUtils, 'computeValidatorsHash') - .mockReturnValue(defaultCertificateValues.validatorsHash); - const commonCCUExecutelogicMock = jest - .spyOn(interopUtils, 'commonCCUExecutelogic') - .mockReturnValue({} as never); - - const appendToInboxTreeMock = jest - .spyOn(SidechainInteroperabilityInternalMethod.prototype, 'appendToInboxTree') - .mockResolvedValue({} as never); - const applyMock = jest - .spyOn(sidechainCCUUpdateCommand, 'apply' as never) - .mockResolvedValue({} as never); + it('should call apply for ccm and add to the inbox where receivign chain is the main chain', async () => { + executeContext = createTransactionContext({ + chainID, + stateStore, + transaction: new Transaction({ + ...defaultTransaction, + command: sidechainCCUUpdateCommand.name, + params: codec.encode(crossChainUpdateTransactionParams, { + ...params, + }), + }), + }).createCommandExecuteContext(sidechainCCUUpdateCommand.schema); - const validCCMContext = { - ...executeContext, - params: { - ...executeContext.params, - inboxUpdate: { - ...executeContext.params.inboxUpdate, - crossChainMessages: [sidechainCCMSerialized], - }, - }, - }; - await expect(sidechainCCUUpdateCommand.execute(validCCMContext)).resolves.toBeUndefined(); - expect(appendToInboxTreeMock).toHaveBeenCalledTimes(1); - expect(applyMock).toHaveBeenCalledTimes(1); - expect(applyMock).toHaveBeenCalledTimes(1); - expect(commonCCUExecutelogicMock).toHaveBeenCalledTimes(1); + await expect(sidechainCCUUpdateCommand.execute(executeContext)).resolves.toBeUndefined(); + expect(sidechainCCUUpdateCommand['apply']).toHaveBeenCalledTimes(3); + expect( + SidechainInteroperabilityInternalMethod.prototype.appendToInboxTree, + ).toHaveBeenCalledTimes(3); }); }); });