Skip to content

Commit

Permalink
Merge pull request #7142 from LiskHQ/7023-interop-msg-rec
Browse files Browse the repository at this point in the history
Implement message recovery commands - Closes #7023
  • Loading branch information
ishantiw committed May 18, 2022
2 parents d1d46eb + 1beeb27 commit ee59db7
Show file tree
Hide file tree
Showing 12 changed files with 1,285 additions and 2 deletions.
Expand Up @@ -13,9 +13,9 @@
*/

import { BaseCommand } from '../base_command';
import { BaseCCCommand } from './base_cc_command';
import { BaseInteroperabilityStore } from './base_interoperability_store';
import { BaseInteroperableAPI } from './base_interoperable_api';
import { BaseCCCommand } from './base_cc_command';
import { StoreCallback } from './types';

export abstract class BaseInteroperabilityCommand extends BaseCommand {
Expand Down
Expand Up @@ -54,6 +54,7 @@ import {
OwnChainAccount,
CCMApplyContext,
StoreCallback,
TerminatedOutboxAccount as TerminatedOutbox,
} from './types';
import { getCCMSize, getIDAsKeyForStore } from './utils';
import {
Expand Down Expand Up @@ -212,6 +213,65 @@ export abstract class BaseInteroperabilityStore {
await terminatedOutboxSubstore.setWithSchema(chainID, terminatedOutbox, terminatedOutboxSchema);
}

public async setTerminatedOutboxAccount(
chainID: Buffer,
params: Partial<TerminatedOutbox>,
): Promise<boolean> {
// Passed params is empty, no need to call this method
if (Object.keys(params).length === 0) {
return false;
}

const terminatedOutboxSubstore = this.getStore(
MODULE_ID_INTEROPERABILITY,
STORE_PREFIX_TERMINATED_OUTBOX,
);

const doesOutboxExist = await terminatedOutboxSubstore.has(chainID);

if (!doesOutboxExist) {
return false;
}

const account = await terminatedOutboxSubstore.getWithSchema<TerminatedOutbox>(
chainID,
terminatedOutboxSchema,
);

const terminatedOutbox = {
...account,
...params,
};

await terminatedOutboxSubstore.setWithSchema(chainID, terminatedOutbox, terminatedOutboxSchema);

return true;
}

public async terminatedOutboxAccountExist(chainID: Buffer) {
const terminatedOutboxSubstore = this.getStore(
MODULE_ID_INTEROPERABILITY,
STORE_PREFIX_TERMINATED_OUTBOX,
);

const doesOutboxExist = await terminatedOutboxSubstore.has(chainID);

return doesOutboxExist;
}

public async getTerminatedOutboxAccount(chainID: Buffer) {
const terminatedOutboxSubstore = this.getStore(
MODULE_ID_INTEROPERABILITY,
STORE_PREFIX_TERMINATED_OUTBOX,
);

const terminatedOutboxAccount = await terminatedOutboxSubstore.getWithSchema<TerminatedOutbox>(
chainID,
terminatedOutboxSchema,
);
return terminatedOutboxAccount;
}

public async createTerminatedStateAccount(chainID: number, stateRoot?: Buffer): Promise<boolean> {
const chainIDAsStoreKey = getIDAsKeyForStore(chainID);
const chainSubstore = this.getStore(MODULE_ID_INTEROPERABILITY, STORE_PREFIX_CHAIN_DATA);
Expand Down
1 change: 1 addition & 0 deletions framework/src/modules/interoperability/constants.ts
Expand Up @@ -71,3 +71,4 @@ export const COMMAND_ID_SIDECHAIN_REG = 0;
export const COMMAND_ID_MAINCHAIN_REG = 1;
export const COMMAND_ID_SIDECHAIN_CCU = 2;
export const COMMAND_ID_MAINCHAIN_CCU = 3;
export const COMMAND_ID_MESSAGE_RECOVERY = 5;
@@ -0,0 +1,162 @@
/*
* Copyright © 2022 Lisk Foundation
*
* See the LICENSE file at the top-level directory of this distribution
* for licensing information.
*
* Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation,
* no part of this software, including this file, may be copied, modified,
* propagated, or distributed except according to the terms contained in the
* LICENSE file.
*
* Removal or modification of this copyright notice is prohibited.
*/

import { codec } from '@liskhq/lisk-codec';
import { regularMerkleTree } from '@liskhq/lisk-tree';
import { hash } from '@liskhq/lisk-cryptography';
import {
CCM_STATUS_RECOVERED,
CHAIN_ACTIVE,
COMMAND_ID_MESSAGE_RECOVERY,
EMPTY_FEE_ADDRESS,
} from '../../constants';
import { ccmSchema, messageRecoveryParams } from '../../schema';
import { CommandExecuteContext, VerificationResult } from '../../../../node/state_machine';
import { CCMsg, StoreCallback, MessageRecoveryParams } from '../../types';
import { BaseInteroperabilityCommand } from '../../base_interoperability_command';
import { MainchainInteroperabilityStore } from '../store';
import { getIDAsKeyForStore, swapReceivingAndSendingChainIDs } from '../../utils';
import { BaseInteroperableAPI } from '../../base_interoperable_api';
import { createCCCommandExecuteContext } from '../../context';

export class MessageRecoveryCommand extends BaseInteroperabilityCommand {
public id = COMMAND_ID_MESSAGE_RECOVERY;
public name = 'messageRecovery';
public schema = messageRecoveryParams;

// TODO
// eslint-disable-next-line @typescript-eslint/require-await
public async verify(): Promise<VerificationResult> {
throw new Error('Method not implemented.');
}

public async execute(context: CommandExecuteContext<MessageRecoveryParams>): Promise<void> {
const { transaction, params, getAPIContext, logger, networkIdentifier, getStore } = context;
const apiContext = getAPIContext();
const { eventQueue } = apiContext;

const chainIdAsBuffer = getIDAsKeyForStore(params.chainID);

const updatedCCMs: Buffer[] = [];
const deserializedCCMs = params.crossChainMessages.map(serializedCCMsg =>
codec.decode<CCMsg>(ccmSchema, serializedCCMsg),
);
for (const ccm of deserializedCCMs) {
const apisWithBeforeRecoverCCM = [...this.interoperableCCAPIs.values()].filter(api =>
Reflect.has(api, 'beforeRecoverCCM'),
) as Pick<Required<BaseInteroperableAPI>, 'beforeRecoverCCM'>[];
for (const api of apisWithBeforeRecoverCCM) {
await api.beforeRecoverCCM({
ccm,
trsSender: transaction.senderAddress,
eventQueue: apiContext.eventQueue,
getAPIContext,
logger,
networkIdentifier,
getStore,
feeAddress: EMPTY_FEE_ADDRESS,
});
}

const recoveryCCM: CCMsg = {
...ccm,
fee: BigInt(0),
status: CCM_STATUS_RECOVERED,
};
const encodedUpdatedCCM = codec.encode(ccmSchema, recoveryCCM);
updatedCCMs.push(encodedUpdatedCCM);
}

const interoperabilityStore = this.getInteroperabilityStore(getStore);

const doesTerminatedOutboxAccountExist = await interoperabilityStore.terminatedOutboxAccountExist(
chainIdAsBuffer,
);

if (!doesTerminatedOutboxAccountExist) {
throw new Error('Terminated outbox account does not exist.');
}

const terminatedChainOutboxAccount = await interoperabilityStore.getTerminatedOutboxAccount(
chainIdAsBuffer,
);
const terminatedChainOutboxSize = terminatedChainOutboxAccount.outboxSize;

const proof = {
size: terminatedChainOutboxSize,
indexes: params.idxs,
siblingHashes: params.siblingHashes,
};

const hashedUpdatedCCMs = updatedCCMs.map(ccm => hash(ccm));

const outboxRoot = regularMerkleTree.calculateRootFromUpdateData(hashedUpdatedCCMs, proof);

await interoperabilityStore.setTerminatedOutboxAccount(chainIdAsBuffer, {
outboxRoot,
});

const ownChainAccount = await interoperabilityStore.getOwnChainAccount();
for (const ccm of deserializedCCMs) {
const newCcm = swapReceivingAndSendingChainIDs(ccm);

if (ownChainAccount.id === ccm.receivingChainID) {
const ccCommands = this.ccCommands.get(newCcm.moduleID);

if (!ccCommands) {
continue;
}

const ccCommand = ccCommands.find(command => command.ID === newCcm.crossChainCommandID);

if (!ccCommand) {
continue;
}

const ccCommandExecuteContext = createCCCommandExecuteContext({
ccm: newCcm,
eventQueue,
feeAddress: EMPTY_FEE_ADDRESS,
getAPIContext,
getStore,
logger,
networkIdentifier,
});

await ccCommand.execute(ccCommandExecuteContext);
continue;
}

const ccmChainIdAsBuffer = getIDAsKeyForStore(newCcm.receivingChainID);
const chainAccountExist = await interoperabilityStore.chainAccountExist(ccmChainIdAsBuffer);
const isLive = await interoperabilityStore.isLive(ccmChainIdAsBuffer, Date.now());

if (!chainAccountExist || !isLive) {
continue;
}

const chainAccount = await interoperabilityStore.getChainAccount(ccmChainIdAsBuffer);

if (chainAccount.status !== CHAIN_ACTIVE) {
continue;
}

await interoperabilityStore.addToOutbox(ccmChainIdAsBuffer, newCcm);
}
}

protected getInteroperabilityStore(getStore: StoreCallback): MainchainInteroperabilityStore {
return new MainchainInteroperabilityStore(this.moduleID, getStore, this.interoperableCCAPIs);
}
}
34 changes: 34 additions & 0 deletions framework/src/modules/interoperability/schema.ts
Expand Up @@ -429,6 +429,40 @@ export const crossChainUpdateTransactionParams = {
},
};

export const messageRecoveryParams = {
$id: '/modules/interoperability/mainchain/messageRecovery',
type: 'object',
required: ['chainID', 'crossChainMessages', 'idxs', 'siblingHashes'],
properties: {
chainID: {
dataType: 'uint32',
fieldNumber: 1,
},
crossChainMessages: {
type: 'array',
minItems: 1,
items: {
dataType: 'bytes',
},
fieldNumber: 2,
},
idxs: {
type: 'array',
items: {
dataType: 'uint32',
},
fieldNumber: 3,
},
siblingHashes: {
type: 'array',
items: {
dataType: 'bytes',
},
fieldNumber: 4,
},
},
};

// Cross chain commands schemas
export const registrationCCMParamsSchema = {
$id: 'modules/interoperability/ccCommand/registration',
Expand Down

0 comments on commit ee59db7

Please sign in to comment.