Skip to content
This repository has been archived by the owner on Jun 11, 2024. It is now read-only.

Commit

Permalink
Missing test(s) for message recovery (#9137)
Browse files Browse the repository at this point in the history
* Update message recovery code & tests

* Add missing test for schema verification

* Revert check, since this check would be needed only in case message recovery gets enabled in sidechains

* Revert test for valid params

* Rearrange expects per code & cover recoveredCCMs part

* Simplify tests

* Better use `.toHaveBeenCalledWith(ctx)`

---------

Co-authored-by: !shan <ishantiw.quasar@gmail.com>
  • Loading branch information
sitetester and ishantiw authored Nov 23, 2023
1 parent 7d8301a commit 8c7aba6
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export class RecoverMessageCommand extends BaseInteroperabilityCommand<Mainchain
};
}

// Check that there is exactly one index per cross-chain message.
if (idxs.length !== crossChainMessages.length) {
return {
status: VerifyStatus.FAIL,
Expand All @@ -87,6 +88,7 @@ export class RecoverMessageCommand extends BaseInteroperabilityCommand<Mainchain
};
}

// Check that the idxs are strictly increasing
for (let i = 0; i < idxs.length - 1; i += 1) {
if (idxs[i] > idxs[i + 1]) {
return {
Expand All @@ -96,20 +98,17 @@ export class RecoverMessageCommand extends BaseInteroperabilityCommand<Mainchain
}
}

// Ensure that there are at least two bits, i.e. the value must be larger than 1.
// It's sufficient to check only the first one due the ascending order.
// Check that the CCMs are still pending. We check only the first one,
// as the idxs are sorted in ascending order. Note that one must unset the most significant
// bit of an encoded index in idxs in order to get the position in the tree. To do this
// we must ensure that there are at least two bits, i.e. the value must be larger than 1.
// See https://github.com/LiskHQ/lips/blob/main/proposals/lip-0031.md#proof-serialization.
if (idxs[0] <= 1) {
return {
status: VerifyStatus.FAIL,
error: new Error('Cross-chain message does not have a valid index.'),
};
}

// Check that the CCMs are still pending. We can check only the first one,
// as the idxs are sorted in ascending order. Note that one must unset the most significant
// bit a of an encoded index in idxs in order to get the position in the tree.
// See https://github.com/LiskHQ/lips/blob/main/proposals/lip-0031.md#proof-serialization.
const firstPosition = parseInt(idxs[0].toString(2).slice(1), 2);
if (firstPosition < terminatedOutboxAccount.partnerChainInboxSize) {
return {
Expand Down Expand Up @@ -138,7 +137,9 @@ export class RecoverMessageCommand extends BaseInteroperabilityCommand<Mainchain
if (ccm.status !== CCMStatusCode.OK) {
return {
status: VerifyStatus.FAIL,
error: new Error('Cross-chain message status is not valid.'),
error: new Error(
`Cross-chain message status must be equal to value ${CCMStatusCode.OK}.`,
),
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import { codec } from '@liskhq/lisk-codec';
import { Transaction } from '@liskhq/lisk-chain';
import { utils } from '@liskhq/lisk-cryptography';
import { MerkleTree } from '@liskhq/lisk-tree';
import { MerkleTree, regularMerkleTree } from '@liskhq/lisk-tree';
import { Proof } from '@liskhq/lisk-tree/dist-node/merkle_tree/types';
import {
CROSS_CHAIN_COMMAND_NAME_TRANSFER,
Expand All @@ -31,8 +31,8 @@ import {
CONTEXT_STORE_KEY_CCM_PROCESSING,
CROSS_CHAIN_COMMAND_CHANNEL_TERMINATED,
CROSS_CHAIN_COMMAND_REGISTRATION,
EVENT_TOPIC_CCM_EXECUTION,
MODULE_NAME_INTEROPERABILITY,
EVENT_TOPIC_CCM_EXECUTION,
} from '../../../../../../src/modules/interoperability/constants';
import { RecoverMessageCommand } from '../../../../../../src/modules/interoperability/mainchain/commands/recover_message';
import {
Expand All @@ -49,7 +49,10 @@ import {
createCrossChainMessageContext,
createTransactionContext,
} from '../../../../../../src/testing';
import { getMainchainID } from '../../../../../../src/modules/interoperability/utils';
import {
getMainchainID,
getDecodedCCMAndID,
} from '../../../../../../src/modules/interoperability/utils';
import { TerminatedOutboxStore } from '../../../../../../src/modules/interoperability/stores/terminated_outbox';
import { createStoreGetter } from '../../../../../../src/testing/utils';
import {
Expand Down Expand Up @@ -286,7 +289,7 @@ describe('MessageRecoveryCommand', () => {
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));
Expand Down Expand Up @@ -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 = [
{
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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<CCMsg>(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<CCMsg>(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,
);
});
});

Expand Down

0 comments on commit 8c7aba6

Please sign in to comment.