diff --git a/framework/src/modules/interoperability/mainchain/commands/recover_message.ts b/framework/src/modules/interoperability/mainchain/commands/recover_message.ts index ebcac70eae..3051240220 100644 --- a/framework/src/modules/interoperability/mainchain/commands/recover_message.ts +++ b/framework/src/modules/interoperability/mainchain/commands/recover_message.ts @@ -78,6 +78,7 @@ export class RecoverMessageCommand extends BaseInteroperabilityCommand idxs[i + 1]) { return { @@ -96,8 +98,10 @@ export class RecoverMessageCommand extends BaseInteroperabilityCommand { expect(result.error?.message).toInclude(`Cross-chain message does not have a valid index.`); }); - it('should return error if idxs[0] <= 1', async () => { + it('should return error if idxs[0] === 1', async () => { transactionParams.idxs = [1]; ccms = [ccms[0]]; ccmsEncoded = ccms.map(ccm => codec.encode(ccmSchema, ccm)); @@ -366,6 +369,42 @@ describe('MessageRecoveryCommand', () => { expect(result.error?.message).toInclude(`Cross-chain message was never in the outbox.`); }); + it('should return error if ccm has invalid schema', async () => { + ccms = [ + { + nonce: BigInt(0), + module: MODULE_NAME_INTEROPERABILITY, + crossChainCommand: CROSS_CHAIN_COMMAND_REGISTRATION, + sendingChainID: utils.intToBuffer(0, 2), // *** + receivingChainID: utils.intToBuffer(3, 4), + fee: BigInt(1), + status: CCMStatusCode.FAILED_CCM, + params: Buffer.alloc(0), + }, + ]; + ccmsEncoded = ccms.map(ccm => codec.encode(ccmSchema, ccm)); + transactionParams.crossChainMessages = [...ccmsEncoded]; + transactionParams.idxs = appendPrecedingToIndices([1], terminatedChainOutboxSize); + + commandVerifyContext = createCommandVerifyContext(transaction, transactionParams); + + await interopModule.stores + .get(TerminatedOutboxStore) + .set(createStoreGetter(commandVerifyContext.stateStore as any), chainID, { + outboxRoot, + outboxSize: terminatedChainOutboxSize, + partnerChainInboxSize: 0, + }); + + try { + await command.verify(commandVerifyContext); + } catch (err: any) { + expect((err as Error).message).toInclude( + `Property '.sendingChainID' minLength not satisfied`, + ); + } + }); + it('should return error if ccm.status !== CCMStatusCode.OK', async () => { ccms = [ { @@ -396,7 +435,9 @@ describe('MessageRecoveryCommand', () => { const result = await command.verify(commandVerifyContext); expect(result.status).toBe(VerifyStatus.FAIL); - expect(result.error?.message).toInclude(`Cross-chain message status is not valid.`); + expect(result.error?.message).toInclude( + `Cross-chain message status must be equal to value ${CCMStatusCode.OK}.`, + ); }); it('should return error if cross-chain message receiving chain ID is not valid', async () => { @@ -536,62 +577,125 @@ describe('MessageRecoveryCommand', () => { await expect(command.execute(commandExecuteContext)).resolves.toBeUndefined(); + expect(commandExecuteContext.contextStore.set).toHaveBeenNthCalledWith( + 1, + CONTEXT_STORE_KEY_CCM_PROCESSING, + true, + ); + + const recoveredCCMs: Buffer[] = []; for (const crossChainMessage of commandExecuteContext.params.crossChainMessages) { - const ccm = codec.decode(ccmSchema, crossChainMessage); + const { decodedCCM: ccm, ccmID } = getDecodedCCMAndID(crossChainMessage); const ctx: CrossChainMessageContext = { ...commandExecuteContext, ccm, eventQueue: commandExecuteContext.eventQueue.getChildQueue( - Buffer.concat([EVENT_TOPIC_CCM_EXECUTION, utils.hash(crossChainMessage)]), + Buffer.concat([EVENT_TOPIC_CCM_EXECUTION, ccmID]), ), }; expect(command['_applyRecovery']).toHaveBeenCalledWith(ctx); - expect(commandExecuteContext.contextStore.set).toHaveBeenNthCalledWith( - 1, - CONTEXT_STORE_KEY_CCM_PROCESSING, - true, - ); - expect(commandExecuteContext.contextStore.set).toHaveBeenNthCalledWith( - 2, - CONTEXT_STORE_KEY_CCM_PROCESSING, - false, - ); + + const recoveredCCM: CCMsg = { + ...ccm, + status: CCMStatusCode.RECOVERED, + sendingChainID: ccm.receivingChainID, + receivingChainID: ccm.sendingChainID, + }; + recoveredCCMs.push(codec.encode(ccmSchema, recoveredCCM)); } - expect(interopModule.stores.get(TerminatedOutboxStore).set).toHaveBeenCalledTimes(1); + expect(commandExecuteContext.contextStore.set).toHaveBeenNthCalledWith( + 2, + CONTEXT_STORE_KEY_CCM_PROCESSING, + false, + ); + + const terminatedOutboxSubstore = interopModule.stores.get(TerminatedOutboxStore); + jest.spyOn(terminatedOutboxSubstore, 'set'); + + expect(terminatedOutboxSubstore.set).toHaveBeenCalledTimes(1); + + const terminatedOutboxAccount = await terminatedOutboxSubstore.get( + commandExecuteContext, + commandExecuteContext.params.chainID, + ); + const proofLocal = { + size: terminatedOutboxAccount.outboxSize, + idxs: commandExecuteContext.params.idxs, + siblingHashes: commandExecuteContext.params.siblingHashes, + }; + terminatedOutboxAccount.outboxRoot = regularMerkleTree.calculateRootFromUpdateData( + recoveredCCMs.map(ccm => utils.hash(ccm)), + { ...proofLocal, indexes: proofLocal.idxs }, + ); + expect(terminatedOutboxSubstore.set).toHaveBeenCalledWith( + commandExecuteContext, + commandExecuteContext.params.chainID, + terminatedOutboxAccount, + ); }); it('should call forwardRecovery when sending chain is not mainchain', async () => { await expect(command.execute(commandExecuteContext)).resolves.toBeUndefined(); + expect(commandExecuteContext.contextStore.set).toHaveBeenNthCalledWith( + 1, + CONTEXT_STORE_KEY_CCM_PROCESSING, + true, + ); + + const recoveredCCMs: Buffer[] = []; for (const crossChainMessage of commandExecuteContext.params.crossChainMessages) { - const ccm = codec.decode(ccmSchema, crossChainMessage); + const { decodedCCM: ccm, ccmID } = getDecodedCCMAndID(crossChainMessage); const ctx: CrossChainMessageContext = { ...commandExecuteContext, ccm, eventQueue: commandExecuteContext.eventQueue.getChildQueue( - Buffer.concat([EVENT_TOPIC_CCM_EXECUTION, utils.hash(crossChainMessage)]), + Buffer.concat([EVENT_TOPIC_CCM_EXECUTION, ccmID]), ), }; expect(command['_forwardRecovery']).toHaveBeenCalledWith(ctx); - } - const terminatedOutboxStore = command['stores'].get(TerminatedOutboxStore); - jest.spyOn(terminatedOutboxStore, 'set'); + const recoveredCCM: CCMsg = { + ...ctx.ccm, + status: CCMStatusCode.RECOVERED, + sendingChainID: ctx.ccm.receivingChainID, + receivingChainID: ctx.ccm.sendingChainID, + }; + recoveredCCMs.push(codec.encode(ccmSchema, recoveredCCM)); + } - expect(terminatedOutboxStore.set).toHaveBeenCalledTimes(1); - expect(commandExecuteContext.contextStore.set).toHaveBeenNthCalledWith( - 1, - CONTEXT_STORE_KEY_CCM_PROCESSING, - true, - ); expect(commandExecuteContext.contextStore.set).toHaveBeenNthCalledWith( 2, CONTEXT_STORE_KEY_CCM_PROCESSING, false, ); + + const terminatedOutboxSubstore = command['stores'].get(TerminatedOutboxStore); + const terminatedOutboxAccount = await terminatedOutboxSubstore.get( + commandExecuteContext, + commandExecuteContext.params.chainID, + ); + jest.spyOn(terminatedOutboxSubstore, 'set'); + + expect(terminatedOutboxSubstore.set).toHaveBeenCalledTimes(1); + + const proofLocal = { + size: terminatedOutboxAccount.outboxSize, + idxs: commandExecuteContext.params.idxs, + siblingHashes: commandExecuteContext.params.siblingHashes, + }; + terminatedOutboxAccount.outboxRoot = regularMerkleTree.calculateRootFromUpdateData( + recoveredCCMs.map(ccm => utils.hash(ccm)), + { ...proofLocal, indexes: proofLocal.idxs }, + ); + expect(terminatedOutboxSubstore.set).toHaveBeenCalledWith( + commandExecuteContext, + commandExecuteContext.params.chainID, + terminatedOutboxAccount, + ); }); });