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

Commit

Permalink
Align init message recovery command with LIP0054 (#9117)
Browse files Browse the repository at this point in the history
♻️ Align init message recovery command with LIP0054
  • Loading branch information
ishantiw authored Oct 23, 2023
1 parent 5745085 commit ce8329b
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 73 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { ChainAccountStore } from '../../stores/chain_account';
import { TerminatedStateStore } from '../../stores/terminated_state';
import { ChannelDataStore, channelSchema } from '../../stores/channel_data';
import { TerminatedOutboxStore } from '../../stores/terminated_outbox';
import { InvalidSMTVerificationEvent } from '../../events/invalid_smt_verification';

export interface MessageRecoveryInitializationParams {
chainID: Buffer;
Expand All @@ -50,8 +51,8 @@ export class InitializeMessageRecoveryCommand extends BaseInteroperabilityComman
): Promise<VerificationResult> {
const { params } = context;

// The command fails if the channel parameter is not a valid serialized channel.
const deserializedChannel = codec.decode<ChannelData>(channelSchema, params.channel);

validator.validate(channelSchema, deserializedChannel);

const ownchainAccount = await this.stores.get(OwnChainAccountStore).get(context, EMPTY_BYTES);
Expand All @@ -63,6 +64,7 @@ export class InitializeMessageRecoveryCommand extends BaseInteroperabilityComman
};
}

// The command fails if the chain is not registered.
const chainAccountExist = await this.stores.get(ChainAccountStore).has(context, params.chainID);
if (!chainAccountExist) {
return {
Expand All @@ -71,6 +73,7 @@ export class InitializeMessageRecoveryCommand extends BaseInteroperabilityComman
};
}

// The command fails if the chain is not terminated.
const terminatedAccountExists = await this.stores
.get(TerminatedStateStore)
.has(context, params.chainID);
Expand All @@ -81,10 +84,7 @@ export class InitializeMessageRecoveryCommand extends BaseInteroperabilityComman
};
}

const terminatedAccount = await this.stores
.get(TerminatedStateStore)
.get(context, params.chainID);

// The command fails if there exist already a terminated outbox account.
const terminatedOutboxAccountExists = await this.stores
.get(TerminatedOutboxStore)
.has(context, params.chainID);
Expand All @@ -95,40 +95,41 @@ export class InitializeMessageRecoveryCommand extends BaseInteroperabilityComman
};
}

const ownChainAccount = await this.stores.get(OwnChainAccountStore).get(context, EMPTY_BYTES);
return {
status: VerifyStatus.OK,
};
}

public async execute(
context: CommandExecuteContext<MessageRecoveryInitializationParams>,
): Promise<void> {
const { params } = context;
const terminatedAccount = await this.stores
.get(TerminatedStateStore)
.get(context, params.chainID);

const queryKey = Buffer.concat([
// key contains both module and store key
this.stores.get(ChannelDataStore).key,
utils.hash(ownChainAccount.chainID),
utils.hash(context.chainID),
]);
const query = {
key: queryKey,
value: utils.hash(params.channel),
bitmap: params.bitmap,
};

// The SMT verification step is computationally expensive. Therefore, it is done in the
// execution step such that the transaction fee must be paid.
const smt = new SparseMerkleTree();
const valid = await smt.verifyInclusionProof(terminatedAccount.stateRoot, [queryKey], {
siblingHashes: params.siblingHashes,
queries: [query],
});

if (!valid) {
return {
status: VerifyStatus.FAIL,
error: new Error('Message recovery initialization proof of inclusion is not valid.'),
};
this.events.get(InvalidSMTVerificationEvent).error(context);
throw new Error('Message recovery initialization proof of inclusion is not valid.');
}

return {
status: VerifyStatus.OK,
};
}

public async execute(
context: CommandExecuteContext<MessageRecoveryInitializationParams>,
): Promise<void> {
const { params } = context;
const partnerChannel = codec.decode<ChannelData>(channelSchema, params.channel);
const channel = await this.stores.get(ChannelDataStore).get(context, params.chainID);
await this.internalMethod.createTerminatedOutboxAccount(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { codec } from '@liskhq/lisk-codec';
import { utils } from '@liskhq/lisk-cryptography';
import { SparseMerkleTree } from '@liskhq/lisk-db';
import {
CommandExecuteContext,
CommandVerifyContext,
MainchainInteroperabilityModule,
Transaction,
Expand Down Expand Up @@ -45,6 +46,7 @@ import { TerminatedStateStore } from '../../../../../../src/modules/interoperabi
import { OwnChainAccount } from '../../../../../../src/modules/interoperability/types';
import { PrefixedStateReadWriter } from '../../../../../../src/state_machine/prefixed_state_read_writer';
import { createTransactionContext, InMemoryPrefixedStateDB } from '../../../../../../src/testing';
import { InvalidSMTVerificationEvent } from '../../../../../../src/modules/interoperability/events/invalid_smt_verification';

describe('InitializeMessageRecoveryCommand', () => {
const interopMod = new MainchainInteroperabilityModule();
Expand Down Expand Up @@ -240,70 +242,23 @@ describe('InitializeMessageRecoveryCommand', () => {
expect(result.error?.message).toInclude(`Terminated outbox account already exists.`);
});

it('should reject when proof of inclusion is not valid', async () => {
jest.spyOn(SparseMerkleTree.prototype, 'verify').mockResolvedValue(false);

const result = await command.verify(defaultContext);

expect(result.status).toBe(VerifyStatus.FAIL);
expect(result.error?.message).toInclude(
'Message recovery initialization proof of inclusion is not valid.',
);
});

it('should resolve when ownchainID !== mainchainID', async () => {
await interopMod.stores
.get(OwnChainAccountStore)
.set(stateStore, EMPTY_BYTES, { ...ownChainAccount, chainID: Buffer.from([2, 2, 2, 2]) });
const queryKey = Buffer.concat([
interopMod.stores.get(ChannelDataStore).key,
utils.hash(Buffer.from([2, 2, 2, 2])),
]);

await expect(command.verify(defaultContext)).resolves.toEqual({ status: VerifyStatus.OK });
expect(SparseMerkleTree.prototype.verify).toHaveBeenCalledWith(
terminatedState.stateRoot,
[queryKey],
{
siblingHashes: defaultParams.siblingHashes,
queries: [
{
key: queryKey,
value: utils.hash(defaultParams.channel),
bitmap: defaultParams.bitmap,
},
],
},
);
});

it('should resolve when params is valid', async () => {
const queryKey = Buffer.concat([
interopMod.stores.get(ChannelDataStore).key,
utils.hash(ownChainAccount.chainID),
]);

await expect(command.verify(defaultContext)).resolves.toEqual({ status: VerifyStatus.OK });
expect(SparseMerkleTree.prototype.verify).toHaveBeenCalledWith(
terminatedState.stateRoot,
[queryKey],
{
siblingHashes: defaultParams.siblingHashes,
queries: [
{
key: queryKey,
value: utils.hash(defaultParams.channel),
bitmap: defaultParams.bitmap,
},
],
},
);
});
});

describe('execute', () => {
it('should create terminated outbox account', async () => {
const context = createTransactionContext({
let executeContext: CommandExecuteContext<MessageRecoveryInitializationParams>;
beforeEach(() => {
executeContext = createTransactionContext({
stateStore,
transaction: new Transaction({
...defaultTx,
Expand All @@ -313,7 +268,33 @@ describe('InitializeMessageRecoveryCommand', () => {
}),
}),
}).createCommandExecuteContext<MessageRecoveryInitializationParams>(command.schema);
await expect(command.execute(context)).resolves.toBeUndefined();
});

it('should reject when proof of inclusion is not valid and log SMT verification event', async () => {
jest.spyOn(SparseMerkleTree.prototype, 'verifyInclusionProof').mockResolvedValue(false);
jest.spyOn(command['events'].get(InvalidSMTVerificationEvent), 'error');

await expect(command.execute(executeContext)).rejects.toThrow(
'Message recovery initialization proof of inclusion is not valid',
);
expect(command['events'].get(InvalidSMTVerificationEvent).error).toHaveBeenCalledOnceWith(
executeContext,
);
});

it('should create terminated outbox account', async () => {
await interopMod.stores.get(TerminatedOutboxStore).set(stateStore, targetChainID, {
outboxRoot: utils.getRandomBytes(32),
outboxSize: 10,
partnerChainInboxSize: 20,
});
jest.spyOn(SparseMerkleTree.prototype, 'verifyInclusionProof').mockResolvedValue(true);
const queryKey = Buffer.concat([
interopMod.stores.get(ChannelDataStore).key,
utils.hash(executeContext.chainID),
]);

await expect(command.execute(executeContext)).resolves.toBeUndefined();

expect(command['internalMethod'].createTerminatedOutboxAccount).toHaveBeenCalledWith(
expect.anything(),
Expand All @@ -322,6 +303,21 @@ describe('InitializeMessageRecoveryCommand', () => {
storedChannel.outbox.size,
paramsChannel.inbox.size,
);

expect(SparseMerkleTree.prototype.verifyInclusionProof).toHaveBeenCalledWith(
terminatedState.stateRoot,
[queryKey],
{
siblingHashes: defaultParams.siblingHashes,
queries: [
{
key: queryKey,
value: utils.hash(defaultParams.channel),
bitmap: defaultParams.bitmap,
},
],
},
);
});
});
});

0 comments on commit ce8329b

Please sign in to comment.