lisk-framework
Version:
Lisk blockchain application platform
393 lines • 18.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseCrossChainUpdateCommand = void 0;
const lisk_codec_1 = require("@liskhq/lisk-codec");
const lisk_cryptography_1 = require("@liskhq/lisk-cryptography");
const lisk_validator_1 = require("@liskhq/lisk-validator");
const base_interoperability_command_1 = require("./base_interoperability_command");
const constants_1 = require("./constants");
const ccm_processed_1 = require("./events/ccm_processed");
const ccm_send_success_1 = require("./events/ccm_send_success");
const schemas_1 = require("./schemas");
const chain_account_1 = require("./stores/chain_account");
const utils_1 = require("./utils");
const chain_validators_1 = require("./stores/chain_validators");
const own_chain_account_1 = require("./stores/own_chain_account");
class BaseCrossChainUpdateCommand extends base_interoperability_command_1.BaseInteroperabilityCommand {
constructor() {
super(...arguments);
this.schema = schemas_1.crossChainUpdateTransactionParams;
}
init(interopsMethod, tokenMethod) {
this._tokenMethod = tokenMethod;
this._interopsMethod = interopsMethod;
}
async verifyCommon(context, isMainchain) {
const { params } = context;
lisk_validator_1.validator.validate(schemas_1.crossChainUpdateTransactionParams, params);
const ownChainAccount = await this.stores.get(own_chain_account_1.OwnChainAccountStore).get(context, constants_1.EMPTY_BYTES);
if (params.sendingChainID.equals(ownChainAccount.chainID)) {
throw new Error('The sending chain cannot be the same as the receiving chain.');
}
if (params.certificate.length === 0 && (0, utils_1.isInboxUpdateEmpty)(params.inboxUpdate)) {
throw new Error('A cross-chain update must contain a non-empty certificate and/or a non-empty inbox update.');
}
const sendingChainAccount = await this.stores
.get(chain_account_1.ChainAccountStore)
.getOrUndefined(context, params.sendingChainID);
if (!sendingChainAccount) {
throw new Error('The sending chain is not registered.');
}
let live;
if (isMainchain) {
live = await this.internalMethod.isLive(context, params.sendingChainID, context.header.timestamp);
}
else {
live = await this.internalMethod.isLive(context, params.sendingChainID);
}
if (!live) {
throw new Error('The sending chain is not live.');
}
if (sendingChainAccount.status === 0 && params.certificate.length === 0) {
throw new Error(`Cross-chain updates from chains with status ${0} must contain a non-empty certificate.`);
}
if (params.certificate.length > 0) {
await this.internalMethod.verifyCertificate(context, params, context.header.timestamp);
}
const sendingChainValidators = await this.stores
.get(chain_validators_1.ChainValidatorsStore)
.get(context, params.sendingChainID);
if (!(0, utils_1.emptyActiveValidatorsUpdate)(params.activeValidatorsUpdate) ||
params.certificateThreshold !== sendingChainValidators.certificateThreshold) {
await this.internalMethod.verifyValidatorsUpdate(context, params);
}
if (!(0, utils_1.isInboxUpdateEmpty)(params.inboxUpdate)) {
this.internalMethod.verifyOutboxRootWitness(context, params);
}
}
async beforeCrossChainMessagesExecution(context, isMainchain) {
const { params } = context;
const { inboxUpdate } = params;
const ccms = [];
let ccm;
for (const ccmBytes of inboxUpdate.crossChainMessages) {
const ccmID = (0, utils_1.getIDFromCCMBytes)(ccmBytes);
const ccmContext = {
...context,
eventQueue: context.eventQueue.getChildQueue(Buffer.concat([constants_1.EVENT_TOPIC_CCM_EXECUTION, ccmID])),
};
try {
ccm = lisk_codec_1.codec.decode(schemas_1.ccmSchema, ccmBytes);
}
catch (error) {
await this.internalMethod.terminateChainInternal(ccmContext, params.sendingChainID);
this.events
.get(ccm_processed_1.CcmProcessedEvent)
.log(ccmContext, params.sendingChainID, ccmContext.chainID, {
ccm: constants_1.EmptyCCM,
result: 3,
code: 5,
});
return [[], false];
}
try {
(0, utils_1.validateFormat)(ccm);
}
catch (error) {
await this.internalMethod.terminateChainInternal(ccmContext, params.sendingChainID);
ccm = { ...ccm, params: constants_1.EMPTY_BYTES };
this.events
.get(ccm_processed_1.CcmProcessedEvent)
.log(ccmContext, params.sendingChainID, ccm.receivingChainID, {
ccm,
result: 3,
code: 6,
});
return [[], false];
}
try {
this.verifyRoutingRules(ccm, params, ccmContext.chainID, isMainchain);
ccms.push(ccm);
}
catch (error) {
await this.internalMethod.terminateChainInternal(ccmContext, params.sendingChainID);
this.events
.get(ccm_processed_1.CcmProcessedEvent)
.log(ccmContext, params.sendingChainID, ccm.receivingChainID, {
ccm,
result: 3,
code: 7,
});
return [[], false];
}
}
return [ccms, true];
}
verifyRoutingRules(ccm, ccuParams, ownChainID, isMainchain) {
if (isMainchain) {
if (!ccm.sendingChainID.equals(ccuParams.sendingChainID)) {
throw new Error('CCM is not from the sending chain.');
}
if (ccm.status === 1) {
throw new Error('CCM status channel unavailable can only be set on the mainchain.');
}
}
else if (!ownChainID.equals(ccm.receivingChainID)) {
throw new Error('CCM is not directed to the sidechain.');
}
if (ccm.receivingChainID.equals(ccm.sendingChainID)) {
throw new Error('Sending and receiving chains must differ.');
}
}
async afterCrossChainMessagesExecute(context) {
const { params } = context;
const sendingChainValidators = await this.stores
.get(chain_validators_1.ChainValidatorsStore)
.get(context, params.sendingChainID);
if (!(0, utils_1.emptyActiveValidatorsUpdate)(params.activeValidatorsUpdate) ||
params.certificateThreshold !== sendingChainValidators.certificateThreshold) {
await this.internalMethod.updateValidators(context, params);
}
if (params.certificate.length > 0) {
await this.internalMethod.updateCertificate(context, params);
}
if (!(0, utils_1.isInboxUpdateEmpty)(params.inboxUpdate) && params.certificate.length > 0) {
await this.internalMethod.updatePartnerChainOutboxRoot(context, params);
}
}
async _beforeCrossChainCommandExecute(context, baseEventSnapshotID, baseStateSnapshotID) {
const { ccm, logger } = context;
const { ccmID } = (0, utils_1.getEncodedCCMAndID)(ccm);
try {
for (const [module, method] of this.interoperableCCMethods.entries()) {
if (method.beforeCrossChainCommandExecute) {
logger.debug({
moduleName: module,
commandName: ccm.crossChainCommand,
ccmID: ccmID.toString('hex'),
}, 'Execute beforeCrossChainCommandExecute');
await method.beforeCrossChainCommandExecute(context);
}
}
}
catch (error) {
context.eventQueue.restoreSnapshot(baseEventSnapshotID);
context.stateStore.restoreSnapshot(baseStateSnapshotID);
logger.info({ err: error, moduleName: ccm.module, commandName: ccm.crossChainCommand }, 'Fail to execute beforeCrossChainCommandExecute.');
await this.internalMethod.terminateChainInternal(context, ccm.sendingChainID);
this.events.get(ccm_processed_1.CcmProcessedEvent).log(context, ccm.sendingChainID, ccm.receivingChainID, {
ccm,
result: 3,
code: 10,
});
return false;
}
return true;
}
async _afterCrossChainCommandExecute(context, baseEventSnapshotID, baseStateSnapshotID) {
const { ccm, logger } = context;
const { ccmID } = (0, utils_1.getEncodedCCMAndID)(ccm);
try {
for (const [module, method] of this.interoperableCCMethods.entries()) {
if (method.afterCrossChainCommandExecute) {
logger.debug({
moduleName: module,
commandName: ccm.crossChainCommand,
ccmID: ccmID.toString('hex'),
}, 'Execute afterCrossChainCommandExecute');
await method.afterCrossChainCommandExecute(context);
}
}
}
catch (error) {
context.eventQueue.restoreSnapshot(baseEventSnapshotID);
context.stateStore.restoreSnapshot(baseStateSnapshotID);
logger.info({ err: error, moduleName: ccm.module, commandName: ccm.crossChainCommand }, 'Fail to execute afterCrossChainCommandExecute.');
await this.internalMethod.terminateChainInternal(context, ccm.sendingChainID);
this.events.get(ccm_processed_1.CcmProcessedEvent).log(context, ccm.sendingChainID, ccm.receivingChainID, {
ccm,
result: 3,
code: 11,
});
return false;
}
return true;
}
async apply(context) {
const { ccm, logger } = context;
const { ccmID, encodedCCM } = (0, utils_1.getEncodedCCMAndID)(ccm);
const valid = await this.verifyCCM(context, ccmID);
if (!valid) {
return;
}
const baseEventSnapshotID = context.eventQueue.createSnapshot();
const baseStateSnapshotID = context.stateStore.createSnapshot();
let statusCode;
let processedCode;
let crossChainCommand;
const crossChainCommands = this.ccCommands.get(ccm.module);
if (!crossChainCommands) {
statusCode = 2;
processedCode = 2;
}
else {
crossChainCommand = crossChainCommands.find(com => com.name === ccm.crossChainCommand);
if (!crossChainCommand) {
statusCode = 3;
processedCode = 3;
}
}
if (!crossChainCommands || !crossChainCommand) {
if (!(await this._beforeCrossChainCommandExecute(context, baseEventSnapshotID, baseStateSnapshotID))) {
return;
}
if (!(await this._afterCrossChainCommandExecute(context, baseEventSnapshotID, baseStateSnapshotID))) {
return;
}
await this.bounce(context, encodedCCM.length, statusCode, processedCode);
return;
}
let decodedParams;
try {
decodedParams = lisk_codec_1.codec.decode(crossChainCommand.schema, context.ccm.params);
lisk_validator_1.validator.validate(crossChainCommand.schema, decodedParams);
}
catch (error) {
logger.info({ err: error, moduleName: ccm.module, commandName: ccm.crossChainCommand }, 'Invalid CCM params.');
await this.internalMethod.terminateChainInternal(context, ccm.sendingChainID);
this.events.get(ccm_processed_1.CcmProcessedEvent).log(context, ccm.sendingChainID, ccm.receivingChainID, {
ccm,
result: 3,
code: 8,
});
return;
}
if (crossChainCommand.verify) {
try {
await crossChainCommand.verify(context);
}
catch (error) {
logger.info({ err: error, moduleName: ccm.module, commandName: ccm.crossChainCommand }, 'Fail to verify cross chain command.');
await this.internalMethod.terminateChainInternal(context, ccm.sendingChainID);
this.events.get(ccm_processed_1.CcmProcessedEvent).log(context, ccm.sendingChainID, ccm.receivingChainID, {
ccm,
result: 3,
code: 8,
});
return;
}
}
if (!(await this._beforeCrossChainCommandExecute(context, baseEventSnapshotID, baseStateSnapshotID))) {
return;
}
const execEventSnapshotID = context.eventQueue.createSnapshot();
const execStateSnapshotID = context.stateStore.createSnapshot();
try {
const sendingChainExists = await this.stores
.get(chain_account_1.ChainAccountStore)
.has(context, ccm.sendingChainID);
const ccuParams = lisk_codec_1.codec.decode(schemas_1.crossChainUpdateTransactionParams, context.transaction.params);
if (sendingChainExists && !ccuParams.sendingChainID.equals(ccm.sendingChainID)) {
throw new Error('Cannot receive forwarded messages for a direct channel.');
}
await crossChainCommand.execute({ ...context, params: decodedParams });
this.events.get(ccm_processed_1.CcmProcessedEvent).log(context, ccm.sendingChainID, ccm.receivingChainID, {
ccm,
result: 0,
code: 0,
});
}
catch (error) {
context.eventQueue.restoreSnapshot(execEventSnapshotID);
context.stateStore.restoreSnapshot(execStateSnapshotID);
if (!(await this._afterCrossChainCommandExecute(context, baseEventSnapshotID, baseStateSnapshotID))) {
return;
}
await this.bounce(context, encodedCCM.length, 4, 4);
return;
}
await this._afterCrossChainCommandExecute(context, baseEventSnapshotID, baseStateSnapshotID);
}
async bounce(context, ccmSize, ccmStatusCode, ccmProcessedCode) {
const { ccm } = context;
const minReturnFeePerByte = await this._interopsMethod.getMinReturnFeePerByte(context, ccm.sendingChainID);
const minFee = minReturnFeePerByte * BigInt(ccmSize);
if (ccm.status !== 0 || ccm.fee < minFee) {
this.events.get(ccm_processed_1.CcmProcessedEvent).log(context, ccm.sendingChainID, ccm.receivingChainID, {
code: ccmProcessedCode,
result: 3,
ccm,
});
return;
}
this.events.get(ccm_processed_1.CcmProcessedEvent).log(context, ccm.sendingChainID, ccm.receivingChainID, {
code: ccmProcessedCode,
result: 2,
ccm,
});
const bouncedCCM = {
...ccm,
status: ccmStatusCode,
sendingChainID: ccm.receivingChainID,
receivingChainID: ccm.sendingChainID,
fee: BigInt(0),
};
let partnerChainID;
const mainchainID = (0, utils_1.getMainchainID)(bouncedCCM.receivingChainID);
const ownChainAccount = await this.stores.get(own_chain_account_1.OwnChainAccountStore).get(context, constants_1.EMPTY_BYTES);
if (ownChainAccount.chainID.equals(mainchainID)) {
partnerChainID = bouncedCCM.receivingChainID;
}
else {
const receivingChainExists = await this.stores
.get(chain_account_1.ChainAccountStore)
.has(context, bouncedCCM.receivingChainID);
partnerChainID = !receivingChainExists ? mainchainID : bouncedCCM.receivingChainID;
}
await this.internalMethod.addToOutbox(context, partnerChainID, bouncedCCM);
const newCcmID = lisk_cryptography_1.utils.hash(lisk_codec_1.codec.encode(schemas_1.ccmSchema, bouncedCCM));
this.events
.get(ccm_send_success_1.CcmSendSuccessEvent)
.log(context, bouncedCCM.sendingChainID, bouncedCCM.receivingChainID, newCcmID, {
ccm: bouncedCCM,
});
}
async verifyCCM(context, ccmID) {
const { ccm, logger } = context;
try {
const isLive = await this.internalMethod.isLive(context, ccm.sendingChainID);
if (!isLive) {
throw new Error(`Sending chain ${ccm.sendingChainID.toString('hex')} is not live.`);
}
for (const [module, method] of this.interoperableCCMethods.entries()) {
if (method.verifyCrossChainMessage) {
logger.debug({ module, ccmID: ccmID.toString('hex') }, 'verifying cross chain message');
await method.verifyCrossChainMessage(context);
}
}
return true;
}
catch (error) {
logger.info({ err: error, moduleName: ccm.module, commandName: ccm.crossChainCommand }, 'Fail to verify cross chain message.');
await this.internalMethod.terminateChainInternal(context, ccm.sendingChainID);
this.events.get(ccm_processed_1.CcmProcessedEvent).log(context, ccm.sendingChainID, ccm.receivingChainID, {
code: 8,
result: 3,
ccm,
});
return false;
}
}
async verifyCertificateSignatureAndPartnerChainOutboxRoot(context) {
const { params, transaction } = context;
const { inboxUpdate } = params;
await this.internalMethod.verifyCertificateSignature(context, params);
if (!(0, utils_1.isInboxUpdateEmpty)(inboxUpdate)) {
await this.internalMethod.verifyPartnerChainOutboxRoot(context, params);
const messageFeeTokenID = await this._interopsMethod.getMessageFeeTokenID(context, params.sendingChainID);
await this._tokenMethod.initializeUserAccount(context, transaction.senderAddress, messageFeeTokenID);
}
}
}
exports.BaseCrossChainUpdateCommand = BaseCrossChainUpdateCommand;
//# sourceMappingURL=base_cross_chain_update_command.js.map