From d04c5882c9aa54149bca7b04eeae1810295da8f2 Mon Sep 17 00:00:00 2001 From: Ishan Date: Mon, 12 Feb 2024 19:24:43 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20Improve=20chain=20conne?= =?UTF-8?q?ctor=20plugin=20-=20Improve=20storage=20to=20retrieve=20by=20ke?= =?UTF-8?q?y/val=20and=20delete=20efficiently=20instead=20of=20managing=20?= =?UTF-8?q?full=20array=20-=20Reduce=20number=20of=20RPC=20calls=20during?= =?UTF-8?q?=20new=20block=20event=20-=20Improve=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/active_validators_update.ts | 42 +- .../src/block_event_handler.ts | 408 ++++++++ .../src/ccu_handler.ts | 340 +++++++ .../src/certificate_generation.ts | 78 +- .../src/chain_api_client.ts | 190 ++++ .../src/chain_connector_plugin.ts | 914 +----------------- .../src/db.ts | 379 ++++++-- .../src/endpoint.ts | 59 +- .../src/inbox_update.ts | 43 +- .../src/schemas.ts | 44 + .../src/types.ts | 75 ++ .../src/utils.ts | 10 +- 12 files changed, 1490 insertions(+), 1092 deletions(-) create mode 100644 framework-plugins/lisk-framework-chain-connector-plugin/src/block_event_handler.ts create mode 100644 framework-plugins/lisk-framework-chain-connector-plugin/src/ccu_handler.ts create mode 100644 framework-plugins/lisk-framework-chain-connector-plugin/src/chain_api_client.ts diff --git a/framework-plugins/lisk-framework-chain-connector-plugin/src/active_validators_update.ts b/framework-plugins/lisk-framework-chain-connector-plugin/src/active_validators_update.ts index 4bf2e7f5b7a..8340ebdaf35 100644 --- a/framework-plugins/lisk-framework-chain-connector-plugin/src/active_validators_update.ts +++ b/framework-plugins/lisk-framework-chain-connector-plugin/src/active_validators_update.ts @@ -12,53 +12,31 @@ * Removal or modification of this copyright notice is prohibited. */ /* eslint-disable no-bitwise */ -import { - ActiveValidator, - Certificate, - LastCertificate, - utils, - ActiveValidatorsUpdate, -} from 'lisk-sdk'; +import { ActiveValidator, utils, ActiveValidatorsUpdate } from 'lisk-sdk'; import { ValidatorsData } from './types'; /** * @see https://github.com/LiskHQ/lips/blob/main/proposals/lip-0053.md#computing-the-validators-update */ + export const calculateActiveValidatorsUpdate = ( - certificate: Certificate, - validatorsHashPreimage: ValidatorsData[], - lastCertificate: LastCertificate, + validatorsDataAtLastCertificate: ValidatorsData, + validatorsDataAtNewCertificate: ValidatorsData, ): { activeValidatorsUpdate: ActiveValidatorsUpdate; certificateThreshold: bigint } => { let certificateThreshold; - const validatorDataAtCertificate = validatorsHashPreimage.find(validatorsData => - validatorsData.validatorsHash.equals(certificate.validatorsHash), - ); - - if (!validatorDataAtCertificate) { - throw new Error('No validators data found for the certificate height.'); - } - - const validatorDataAtLastCertificate = validatorsHashPreimage.find(validatorsData => - validatorsData.validatorsHash.equals(lastCertificate.validatorsHash), - ); - - if (!validatorDataAtLastCertificate) { - throw new Error('No validators data found for the given last certificate height.'); - } - // If the certificate threshold is not changed from last certificate then we assign zero if ( - validatorDataAtCertificate.certificateThreshold === - validatorDataAtLastCertificate.certificateThreshold + validatorsDataAtNewCertificate.certificateThreshold === + validatorsDataAtLastCertificate.certificateThreshold ) { - certificateThreshold = validatorDataAtLastCertificate.certificateThreshold; + certificateThreshold = validatorsDataAtLastCertificate.certificateThreshold; } else { - certificateThreshold = validatorDataAtCertificate.certificateThreshold; + certificateThreshold = validatorsDataAtNewCertificate.certificateThreshold; } const activeValidatorsUpdate = getActiveValidatorsUpdate( - validatorDataAtLastCertificate.validators, - validatorDataAtCertificate.validators, + validatorsDataAtLastCertificate.validators, + validatorsDataAtNewCertificate.validators, ); return { activeValidatorsUpdate, certificateThreshold }; diff --git a/framework-plugins/lisk-framework-chain-connector-plugin/src/block_event_handler.ts b/framework-plugins/lisk-framework-chain-connector-plugin/src/block_event_handler.ts new file mode 100644 index 00000000000..b912c516317 --- /dev/null +++ b/framework-plugins/lisk-framework-chain-connector-plugin/src/block_event_handler.ts @@ -0,0 +1,408 @@ +/* + * Copyright © 2024 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 { + chain, + CCMProcessedResult, + CcmProcessedEventData, + CcmSendSuccessEventData, + LastCertificate, + MODULE_NAME_INTEROPERABILITY, + codec, + ChainAccount, + EMPTY_BYTES, +} from 'lisk-sdk'; +import { ChainAPIClient } from './chain_api_client'; +import { ChainConnectorDB } from './db'; +import { BlockHeader, Logger, ModuleMetadata } from './types'; +import { CCM_PROCESSED, CCM_SEND_SUCCESS } from './constants'; +import { CCUHandler } from './ccu_handler'; + +export interface NewBlockHandlerConfig { + registrationHeight: number; + ownChainID: Buffer; + receivingChainID: Buffer; + maxCCUSize: number; + ccuFee: string; + isSaveCCU: boolean; + ccuSaveLimit: number; +} + +interface NewBlockHandlerInitArgs { + logger: Logger; + db: ChainConnectorDB; + sendingChainAPIClient: ChainAPIClient; + receivingChainAPIClient: ChainAPIClient; +} + +interface Data { + readonly blockHeader: chain.BlockHeaderJSON; +} +type FinalizedHeightInfo = { inboxSize: number; lastCertificateHeight: number }; + +export class BlockEventHandler { + private readonly _ownChainID!: Buffer; + private readonly _ccuHandler!: CCUHandler; + private _db!: ChainConnectorDB; + private _logger!: Logger; + private _sendingChainAPIClient!: ChainAPIClient; + private _receivingChainAPIClient!: ChainAPIClient; + private _lastCertificate!: LastCertificate; + private _interoperabilityMetadata!: ModuleMetadata; + private _heightToDeleteIndex!: Map; + private readonly _ccuFrequency!: number; + private _receivingChainFinalizedHeight!: number; + private readonly _ccuSaveLimit: number; + private readonly _receivingChainID: Buffer; + private _isReceivingChainRegistered = false; + + public constructor(config: NewBlockHandlerConfig) { + this._ownChainID = config.ownChainID; + this._ccuSaveLimit = config.ccuSaveLimit; + this._receivingChainFinalizedHeight = 0; + this._receivingChainID = config.receivingChainID; + this._ccuHandler = new CCUHandler({ + maxCCUSize: config.maxCCUSize, + ownChainID: config.ownChainID, + receivingChainID: config.receivingChainID, + registrationHeight: config.registrationHeight, + ccuFee: config.ccuFee, + isSaveCCU: config.isSaveCCU, + }); + } + + public async load(args: NewBlockHandlerInitArgs) { + this._logger = args.logger; + this._db = args.db; + this._sendingChainAPIClient = args.sendingChainAPIClient; + this._receivingChainAPIClient = args.receivingChainAPIClient; + await this._receivingChainAPIClient.connect(); + this._interoperabilityMetadata = await this._sendingChainAPIClient.getMetadataByModuleName( + MODULE_NAME_INTEROPERABILITY, + ); + this._lastCertificate = { + height: 0, + stateRoot: EMPTY_BYTES, + timestamp: 0, + validatorsHash: EMPTY_BYTES, + }; + + this._ccuHandler.load({ + db: args.db, + lastCertificate: this._lastCertificate, + logger: args.logger, + receivingChainAPIClient: args.receivingChainAPIClient, + sendingChainAPIClient: args.sendingChainAPIClient, + interoperabilityMetadata: this._interoperabilityMetadata, + }); + this._heightToDeleteIndex = new Map(); + // On a new block start with CCU creation process + this._sendingChainAPIClient.subscribe( + 'chain_newBlock', + async (data?: Record) => this.handleNewBlock(data), + ); + + this._sendingChainAPIClient.subscribe( + 'chain_deleteBlock', + async (data?: Record) => this._deleteBlockHandler(data), + ); + } + + public async handleNewBlock(data?: Record) { + const { blockHeader: receivedBlock } = data as unknown as Data; + + const newBlockHeader = chain.BlockHeader.fromJSON(receivedBlock).toObject(); + let chainAccount: ChainAccount | undefined; + // Save blockHeader, aggregateCommit, validatorsData and cross chain messages if any. + const nodeInfo = await this._sendingChainAPIClient.getNodeInfo(); + // Fetch last certificate from the receiving chain and update the _lastCertificate + try { + // eslint-disable-next-line no-console + console.log('this._ownChainID>>>', this._ownChainID); + + chainAccount = await this._receivingChainAPIClient.getChainAccount(this._ownChainID); + // eslint-disable-next-line no-console + console.log('chainAccount>>>>>>>>>>>', chainAccount); + } catch (error) { + // If receivingChainAPIClient is not ready then still save data on new block + await this._saveOnNewBlock(newBlockHeader); + await this._initializeReceivingChainClient(); + this._logger.error( + { err: error as Error }, + 'Error occurred while accessing receivingChainAPIClient but all data is saved on new block.', + ); + + return; + } + + // If sending chain is not registered with the receiving chain then only save data on new block and exit + if (!chainAccount || (chainAccount && !chainAccount.lastCertificate)) { + this._logger.info( + 'Sending chain is not registered to the receiving chain yet and has no chain data.', + ); + try { + await this._saveOnNewBlock(newBlockHeader); + } catch (error) { + this._logger.error( + { err: error as Error }, + 'Error occurred while saving data on new block.', + ); + } + + return; + } + + try { + this._lastCertificate = chainAccount.lastCertificate; + await this._saveOnNewBlock(newBlockHeader); + + const numOfBlocksSinceLastCertificate = newBlockHeader.height - this._lastCertificate.height; + if (nodeInfo.syncing || this._ccuFrequency > numOfBlocksSinceLastCertificate) { + this._logger.debug( + { + syncing: nodeInfo.syncing, + ccuFrequency: this._ccuFrequency, + nextPossibleCCUHeight: this._ccuFrequency - numOfBlocksSinceLastCertificate, + }, + 'No attempt to create CCU either due to ccuFrequency or the node is syncing', + ); + + return; + } + } catch (error) { + this._logger.error(error, 'Failed while saving on new block'); + return; + } + + // Check if receiving chain is registered yet or not + if (!this._isReceivingChainRegistered) { + const receivingChainAccount = await this._sendingChainAPIClient.getChainAccount( + this._receivingChainID, + ); + if (!receivingChainAccount) { + return; + } + this._isReceivingChainRegistered = true; + } + // When all the relevant data is saved successfully then try to create CCU + const computedCCUParams = await this._ccuHandler.computeCCU(); + + if (computedCCUParams) { + try { + await this._ccuHandler.submitCCU(computedCCUParams.ccuParams); + // If CCU was sent successfully then save the lastSentCCM if any + // TODO: Add function to check on the receiving chain whether last sent CCM was accepted or not + if (computedCCUParams.lastCCMToBeSent) { + await this._db.setLastSentCCM(computedCCUParams.lastCCMToBeSent); + } + } catch (error) { + this._logger.info( + { err: error }, + `Error occured while submitting CCU for the blockHeader at height: ${newBlockHeader.height}`, + ); + } + } + } + + public async _saveOnNewBlock(newBlockHeader: BlockHeader) { + // Save block header if a new block header arrives + await this._db.saveOnNewBlock(newBlockHeader); + + this._logger.info('Saved data on new block'); + // Check for events if any and store them + const events = await this._sendingChainAPIClient.getEvents(newBlockHeader.height); + + const ccmsFromEvents = []; + // eslint-disable-next-line no-restricted-syntax, no-labels + ccmInEventsCheck: if (events && events.length > 0) { + const ccmSendSuccessEvents = events.filter( + eventAttr => + eventAttr.name === CCM_SEND_SUCCESS && eventAttr.module === MODULE_NAME_INTEROPERABILITY, + ); + + const ccmProcessedEvents = events.filter( + eventAttr => + eventAttr.name === CCM_PROCESSED && eventAttr.module === MODULE_NAME_INTEROPERABILITY, + ); + + if (ccmSendSuccessEvents.length === 0 && ccmProcessedEvents.length === 0) { + // If there are no CCMs present in the events for the height then skip CCM saving part + // eslint-disable-next-line no-labels + break ccmInEventsCheck; + } + + // Save ccm send success events + if (ccmSendSuccessEvents.length > 0) { + const ccmSendSuccessEventInfo = this._interoperabilityMetadata.events.filter( + e => e.name === CCM_SEND_SUCCESS, + ); + + if (!ccmSendSuccessEventInfo?.[0]?.data) { + throw new Error('No schema found for "ccmSendSuccess" event data.'); + } + + for (const e of ccmSendSuccessEvents) { + const eventData = codec.decode( + ccmSendSuccessEventInfo[0].data, + Buffer.from(e.data, 'hex'), + ); + ccmsFromEvents.push(eventData.ccm); + } + } + // Save ccm processed events based on CCMProcessedResult FORWARDED = 1 + if (ccmProcessedEvents.length > 0) { + const ccmProcessedEventInfo = this._interoperabilityMetadata.events.filter( + e => e.name === CCM_PROCESSED, + ); + + if (!ccmProcessedEventInfo?.[0]?.data) { + throw new Error('No schema found for "ccmProcessed" event data.'); + } + + for (const e of ccmProcessedEvents) { + const eventData = codec.decode( + ccmProcessedEventInfo[0].data, + Buffer.from(e.data, 'hex'), + ); + if (eventData.result === CCMProcessedResult.FORWARDED) { + ccmsFromEvents.push(eventData.ccm); + } + } + } + } + this._logger.info('Collected CCMs'); + + await this._db.setCCMsByHeight( + ccmsFromEvents.map(ccm => ({ ...ccm, height: newBlockHeader.height })), + newBlockHeader.height, + ); + + this._logger.info('Set CCMs'); + + const validatorsData = await this._sendingChainAPIClient.getBFTParametersAtHeight( + newBlockHeader.height, + ); + + this._logger.info('get getBFTParametersAtHeight'); + + await this._db.setValidatorsDataByHash(validatorsData.validatorsHash, validatorsData); + this._logger.info('set getBFTParametersAtHeight'); + } + + private async _initializeReceivingChainClient() { + try { + await this._receivingChainAPIClient.connect(); + this._receivingChainAPIClient.subscribe( + 'chain_newBlock', + async (data?: Record) => this._newBlockReceivingChainHandler(data), + ); + } catch (error) { + this._logger.error( + error, + 'Not able to connect to receivingChainAPIClient. Trying again on next new block.', + ); + } + } + + private async _newBlockReceivingChainHandler(_?: Record) { + try { + const { finalizedHeight } = await this._receivingChainAPIClient.getNodeInfo(); + this._receivingChainFinalizedHeight = finalizedHeight; + const { inbox } = await this._receivingChainAPIClient.getChannelAccount(this._ownChainID); + if (!inbox) { + throw new Error('No channel data available on receiving chain.'); + } + const chainAccount = await this._receivingChainAPIClient.getChainAccount(this._ownChainID); + if (!chainAccount?.lastCertificate) { + throw new Error('No chain data available on receiving chain.'); + } + this._heightToDeleteIndex.set(finalizedHeight, { + inboxSize: inbox.size, + lastCertificateHeight: chainAccount.lastCertificate?.height, + }); + await this._cleanup(); + } catch (error) { + this._logger.debug( + error, + 'No Channel or Chain Data: Sending chain is not registered yet on receiving chain.', + ); + } + } + + private async _cleanup() { + // Delete CCUs + // When given -1 then there is no limit + if (this._ccuSaveLimit !== -1) { + const listOfCCUs = await this._db.getListOfCCUs(); + if (listOfCCUs.length > this._ccuSaveLimit) { + await this._db.setListOfCCUs( + // Takes the last ccuSaveLimit elements + listOfCCUs.slice(-this._ccuSaveLimit), + ); + } + let finalizedInfoAtHeight = this._heightToDeleteIndex.get( + this._receivingChainFinalizedHeight, + ); + if (!finalizedInfoAtHeight) { + for (let i = 1; i < this._heightToDeleteIndex.size; i += 1) { + if (this._heightToDeleteIndex.get(this._receivingChainFinalizedHeight - i)) { + finalizedInfoAtHeight = this._heightToDeleteIndex.get( + this._receivingChainFinalizedHeight - i, + ); + break; + } + } + } + + if (this._lastCertificate.height > 0) { + // Delete CCMs + await this._db.deleteCCMsBetweenHeight( + 1, + finalizedInfoAtHeight ? finalizedInfoAtHeight.inboxSize : 0, + ); + + // Delete blockHeaders + await this._db.deleteBlockHeadersBetweenHeight( + 1, + finalizedInfoAtHeight ? finalizedInfoAtHeight.lastCertificateHeight : 0, + ); + + // Delete aggregateCommits + await this._db.deleteAggregateCommitsBetweenHeight( + 1, + finalizedInfoAtHeight ? finalizedInfoAtHeight.lastCertificateHeight : 0, + ); + // Delete validatorsHashPreimage + } + + // Delete info less than finalized height + this._heightToDeleteIndex.forEach((_, key) => { + if (key < this._receivingChainFinalizedHeight) { + this._heightToDeleteIndex.delete(key); + } + }); + } + } + + private async _deleteBlockHandler(data?: Record) { + const { blockHeader: receivedBlock } = data as unknown as Data; + + const deletedBlockHeader = chain.BlockHeader.fromJSON(receivedBlock).toObject(); + + // Delete ccmEvents for the height of blockHeader + await this._db.deleteCCMsByHeight(deletedBlockHeader.height); + await this._db.deleteBlockHeaderByHeight(deletedBlockHeader.height); + await this._db.deleteAggregateCommitByHeight(deletedBlockHeader.height); + } +} diff --git a/framework-plugins/lisk-framework-chain-connector-plugin/src/ccu_handler.ts b/framework-plugins/lisk-framework-chain-connector-plugin/src/ccu_handler.ts new file mode 100644 index 00000000000..b7f4a39d118 --- /dev/null +++ b/framework-plugins/lisk-framework-chain-connector-plugin/src/ccu_handler.ts @@ -0,0 +1,340 @@ +import { + ActiveValidatorsUpdate, + CrossChainUpdateTransactionParams, + EMPTY_BYTES, + LastCertificate, + MODULE_NAME_INTEROPERABILITY, + Transaction, + ccuParamsSchema, + certificateSchema, + codec, + cryptography, + getMainchainID, + transactions, +} from 'lisk-sdk'; +import { ChainConnectorDB } from './db'; +import { ChainAPIClient } from './chain_api_client'; +import { + getCertificateFromAggregateCommitByBlockHeader, + getNextCertificateFromAggregateCommits, +} from './certificate_generation'; +import { + COMMAND_NAME_SUBMIT_MAINCHAIN_CCU, + COMMAND_NAME_SUBMIT_SIDECHAIN_CCU, + DEFAULT_LAST_CCM_SENT_NONCE, +} from './constants'; +import { calculateMessageWitnesses } from './inbox_update'; +import { LastSentCCM, Logger, ModuleMetadata, ValidatorsData } from './types'; +import { calculateActiveValidatorsUpdate } from './active_validators_update'; + +interface ComputeCCUConfig { + registrationHeight: number; + ownChainID: Buffer; + receivingChainID: Buffer; + maxCCUSize: number; + ccuFee: string; + isSaveCCU: boolean; +} + +interface ComputeCCUInitArgs { + logger: Logger; + db: ChainConnectorDB; + sendingChainAPIClient: ChainAPIClient; + receivingChainAPIClient: ChainAPIClient; + lastCertificate: LastCertificate; + interoperabilityMetadata: ModuleMetadata; +} + +export class CCUHandler { + private _db!: ChainConnectorDB; + private _logger!: Logger; + private _sendingChainAPIClient!: ChainAPIClient; + private _receivingChainAPIClient!: ChainAPIClient; + private _lastCertificate!: LastCertificate; + private readonly _registrationHeight: number; + private readonly _ownChainID!: Buffer; + private readonly _receivingChainID!: Buffer; + private readonly _maxCCUSize!: number; + private readonly _isReceivingChainMainchain!: boolean; + private _interoperabilityMetadata!: ModuleMetadata; + private _outboxKeyForInclusionProof!: Buffer; + private readonly _isSaveCCU!: boolean; + private readonly _ccuFee!: string; + + public constructor(config: ComputeCCUConfig) { + this._registrationHeight = config.registrationHeight; + this._ownChainID = config.ownChainID; + this._receivingChainID = config.receivingChainID; + this._maxCCUSize = config.maxCCUSize; + this._ccuFee = config.ccuFee; + this._isSaveCCU = config.isSaveCCU; + // If the running node is mainchain then receiving chain will be sidechain or vice verse. + this._isReceivingChainMainchain = !getMainchainID(this._ownChainID).equals(this._ownChainID); + } + + public load(args: ComputeCCUInitArgs) { + this._logger = args.logger; + this._db = args.db; + this._sendingChainAPIClient = args.sendingChainAPIClient; + this._receivingChainAPIClient = args.receivingChainAPIClient; + this._lastCertificate = args.lastCertificate; + this._interoperabilityMetadata = args.interoperabilityMetadata; + + const store = this._interoperabilityMetadata.stores.find( + s => s.data.$id === '/modules/interoperability/outbox', + ); + // Calculate the inclusion proof of the outbox root on state root + this._outboxKeyForInclusionProof = Buffer.concat([ + Buffer.from(store?.key as string, 'hex'), + cryptography.utils.hash(this._receivingChainID), + ]); + } + + public async computeCCU(): Promise< + | { + ccuParams: CrossChainUpdateTransactionParams; + lastCCMToBeSent: LastSentCCM | undefined; + } + | undefined + > { + const newCertificate = await this._findCertificate(); + if (!newCertificate && this._lastCertificate.height === 0) { + return undefined; + } + + const lastSentCCM = (await this._db.getLastSentCCM()) ?? { + nonce: DEFAULT_LAST_CCM_SENT_NONCE, + height: this._lastCertificate.height, + }; + + // Get range of CCMs and update the DB accordingly + const ccmsRange = await this._db.getCCMsBetweenHeight( + lastSentCCM.height, + newCertificate ? newCertificate.height : this._lastCertificate.height, + ); + const { inbox: inboxOnReceivingChain } = await this._receivingChainAPIClient.getChannelAccount( + this._ownChainID, + ); + const { outbox: outboxOnSendingChain } = await this._sendingChainAPIClient.getChannelAccount( + this._receivingChainID, + ); + + const { crossChainMessages, lastCCMToBeSent, messageWitnessHashes } = calculateMessageWitnesses( + inboxOnReceivingChain.size, + outboxOnSendingChain.size, + lastSentCCM, + ccmsRange, + this._maxCCUSize, + ); + let activeValidatorsUpdate: ActiveValidatorsUpdate = { + blsKeysUpdate: [], + bftWeightsUpdate: [], + bftWeightsUpdateBitmap: EMPTY_BYTES, + }; + let certificate = EMPTY_BYTES; + let certificateThreshold; + let outboxRootWitness; + + if (!newCertificate) { + if (crossChainMessages.length === 0) { + this._logger.info( + 'CCU cant be created as there are no pending CCMs for the last certificate.', + ); + return undefined; + } + // Empty outboxRootWitness for last certificate + outboxRootWitness = { + bitmap: EMPTY_BYTES, + siblingHashes: [], + }; + + // Use the old certificateThreshold + + const validatorsDataAtLastCertificate = await this._db.getValidatorsDataByHash( + this._lastCertificate.validatorsHash, + ); + certificateThreshold = validatorsDataAtLastCertificate?.certificateThreshold; + + return { + ccuParams: { + sendingChainID: this._ownChainID, + activeValidatorsUpdate, + certificate, + certificateThreshold, + inboxUpdate: { + crossChainMessages, + messageWitnessHashes, + outboxRootWitness, + }, + } as CrossChainUpdateTransactionParams, + lastCCMToBeSent, + }; + } + + const validatorsDataAtLastCertificate = await this._db.getValidatorsDataByHash( + this._lastCertificate.validatorsHash, + ); + if (!this._lastCertificate.validatorsHash.equals(newCertificate.validatorsHash)) { + const validatorsDataAtNewCertificate = await this._db.getValidatorsDataByHash( + newCertificate.validatorsHash, + ); + const validatorsUpdateResult = calculateActiveValidatorsUpdate( + validatorsDataAtLastCertificate as ValidatorsData, + validatorsDataAtNewCertificate as ValidatorsData, + ); + activeValidatorsUpdate = validatorsUpdateResult.activeValidatorsUpdate; + certificateThreshold = validatorsUpdateResult.certificateThreshold; + } else { + // If there was no activeValidatorsUpdate then use the old certificateThreshold + certificateThreshold = validatorsDataAtLastCertificate?.certificateThreshold; + } + + // Get the inclusionProof for outboxRoot on stateRoot + + if (crossChainMessages.length === 0) { + outboxRootWitness = { + bitmap: EMPTY_BYTES, + siblingHashes: [], + }; + } else { + const inclusionProofs = await this._sendingChainAPIClient.getSavedInclusionProofAtHeight( + newCertificate.height, + ); + const foundInclusionProof = inclusionProofs.proof.queries.find(q => + q.key.equals(this._outboxKeyForInclusionProof), + ); + if (!foundInclusionProof) { + throw new Error( + `No inclusion proof was found for key ${this._outboxKeyForInclusionProof.toString( + 'hex', + )}`, + ); + } + outboxRootWitness = { + bitmap: foundInclusionProof.bitmap, + siblingHashes: inclusionProofs.proof.siblingHashes, + }; + } + + certificate = codec.encode(certificateSchema, newCertificate); + + // eslint-disable-next-line consistent-return + return { + ccuParams: { + sendingChainID: this._ownChainID, + activeValidatorsUpdate, + certificate, + certificateThreshold, + inboxUpdate: { + crossChainMessages, + messageWitnessHashes, + outboxRootWitness, + }, + } as CrossChainUpdateTransactionParams, + lastCCMToBeSent, + }; + } + + private async _findCertificate() { + // Find certificate + if (this._lastCertificate.height === 0) { + const aggreggateCommits = await this._db.getAggregateCommitBetweenHeights( + this._registrationHeight, + 1000, + ); + for (const aggregateCommit of aggreggateCommits) { + const blockHeader = await this._db.getBlockHeaderByHeight(aggregateCommit.height); + if (!blockHeader) { + continue; + } + // When we receive the first aggregateCommit in the chain we can create certificate directly + const firstCertificate = getCertificateFromAggregateCommitByBlockHeader( + aggregateCommit, + blockHeader, + ); + + return firstCertificate; + } + + return undefined; + } + + const bftHeights = await this._sendingChainAPIClient.getBFTHeights(); + + return getNextCertificateFromAggregateCommits(this._db, bftHeights, this._lastCertificate); + } + + public async _getCcuFee(tx: Record): Promise { + let additionalFee = BigInt(0); + + const userBalance = await this._receivingChainAPIClient.hasUserTokenAccount( + cryptography.address.getLisk32AddressFromAddress( + cryptography.address.getAddressFromPublicKey(tx.senderPublicKey as Buffer), + ), + ); + + if (!userBalance.exists) { + const fee = await this._receivingChainAPIClient.getTokenInitializationFee(); + additionalFee += BigInt(fee.userAccount); + } + + const ccuFee = BigInt(this._ccuFee ?? '0') + additionalFee; + const computedMinFee = transactions.computeMinFee(tx, ccuParamsSchema, { + additionalFee, + }); + + if (ccuFee > computedMinFee) { + return ccuFee; + } + return computedMinFee; + } + + public async submitCCU(ccuParams: CrossChainUpdateTransactionParams): Promise { + if (!this._db.privateKey) { + throw new Error('There is no key enabled to submit CCU'); + } + const relayerPublicKey = cryptography.ed.getPublicKeyFromPrivateKey(this._db.privateKey); + const targetCommand = this._isReceivingChainMainchain + ? COMMAND_NAME_SUBMIT_MAINCHAIN_CCU + : COMMAND_NAME_SUBMIT_SIDECHAIN_CCU; + + const nonce = await this._receivingChainAPIClient.getAuthAccountNonceFromPublicKey( + relayerPublicKey, + ); + + const txWithoutFee = { + module: MODULE_NAME_INTEROPERABILITY, + command: targetCommand, + nonce: BigInt(nonce), + senderPublicKey: relayerPublicKey, + params: codec.encode(ccuParamsSchema, ccuParams), + signatures: [], + }; + + const tx = new Transaction({ + ...txWithoutFee, + fee: await this._getCcuFee({ + ...txWithoutFee, + params: ccuParams, + }), + }); + + tx.sign(this._receivingChainID, this._db.privateKey); + let result: { transactionId: string }; + if (this._isSaveCCU) { + result = { transactionId: tx.id.toString('hex') }; + } else { + result = await this._receivingChainAPIClient.postTransaction(tx.getBytes()); + } + /** + * TODO: As of now we save it in memory but going forward it should be saved in DB, + * as the array size can grow after sometime. + */ + // Save the sent CCU + const listOfCCUs = await this._db.getListOfCCUs(); + listOfCCUs.push(tx.toObject()); + await this._db.setListOfCCUs(listOfCCUs); + // Update logs + this._logger.info({ transactionID: result.transactionId }, 'Sent CCU transaction'); + } +} diff --git a/framework-plugins/lisk-framework-chain-connector-plugin/src/certificate_generation.ts b/framework-plugins/lisk-framework-chain-connector-plugin/src/certificate_generation.ts index da342b1a535..147e2ede7bb 100644 --- a/framework-plugins/lisk-framework-chain-connector-plugin/src/certificate_generation.ts +++ b/framework-plugins/lisk-framework-chain-connector-plugin/src/certificate_generation.ts @@ -20,42 +20,32 @@ import { computeUnsignedCertificateFromBlockHeader, LastCertificate, } from 'lisk-sdk'; -import { BlockHeader, ValidatorsData } from './types'; +import { BlockHeader } from './types'; +import { ChainConnectorDB } from './db'; /** * @see https://github.com/LiskHQ/lips/blob/main/proposals/lip-0061.md#getcertificatefromaggregatecommit */ -export const getCertificateFromAggregateCommit = ( +export const getCertificateFromAggregateCommitByBlockHeader = ( aggregateCommit: AggregateCommit, - blockHeaders: BlockHeader[], -): Certificate => { - const blockHeader = blockHeaders.find(header => header.height === aggregateCommit.height); - - if (!blockHeader) { - throw new Error( - `No block header found for the given aggregate height ${aggregateCommit.height} when calling getCertificateFromAggregateCommit.`, - ); - } - - return { - ...computeUnsignedCertificateFromBlockHeader(new chain.BlockHeader(blockHeader)), - aggregationBits: aggregateCommit.aggregationBits, - signature: aggregateCommit.certificateSignature, - }; -}; + blockHeader: BlockHeader, +): Certificate => ({ + ...computeUnsignedCertificateFromBlockHeader(new chain.BlockHeader(blockHeader)), + aggregationBits: aggregateCommit.aggregationBits, + signature: aggregateCommit.certificateSignature, +}); /** * @see https://github.com/LiskHQ/lips/blob/main/proposals/lip-0061.md#execution-8 */ -export const checkChainOfTrust = ( +export const checkChainOfTrust = async ( lastValidatorsHash: Buffer, blsKeyToBFTWeight: Record, lastCertificateThreshold: bigint, aggregateCommit: AggregateCommit, - blockHeaders: BlockHeader[], - validatorsHashPreimage: ValidatorsData[], -): boolean => { - const blockHeader = blockHeaders.find(header => header.height === aggregateCommit.height - 1); + db: ChainConnectorDB, +): Promise => { + const blockHeader = await db.getBlockHeaderByHeight(aggregateCommit.height - 1); if (!blockHeader) { throw new Error( `No block header found for the given the previous height ${ @@ -70,9 +60,7 @@ export const checkChainOfTrust = ( } let aggregateBFTWeight = BigInt(0); - const validatorData = validatorsHashPreimage.find(data => - data.validatorsHash.equals(blockHeader.validatorsHash), - ); + const validatorData = await db.getValidatorsDataByHash(blockHeader.validatorsHash); if (!validatorData) { throw new Error( `No validators data found for the given validatorsHash ${blockHeader.validatorsHash.toString( @@ -99,25 +87,23 @@ export const checkChainOfTrust = ( /** * @see https://github.com/LiskHQ/lips/blob/main/proposals/lip-0061.md#execution-8 */ -export const getNextCertificateFromAggregateCommits = ( - blockHeaders: BlockHeader[], - aggregateCommits: AggregateCommit[], - validatorsHashPreimage: ValidatorsData[], +export const getNextCertificateFromAggregateCommits = async ( + db: ChainConnectorDB, bftHeights: BFTHeights, lastCertificate: LastCertificate, -): Certificate | undefined => { - const blockHeaderAtLastCertifiedHeight = blockHeaders.find( - header => header.height === lastCertificate.height, - ); +): Promise => { + const blockHeaderAtLastCertifiedHeight = await db.getBlockHeaderByHeight(lastCertificate.height); + if (!blockHeaderAtLastCertifiedHeight) { throw new Error( `No block header found for the last certified height ${lastCertificate.height}.`, ); } - const validatorDataAtLastCertifiedHeight = validatorsHashPreimage.find(data => - data.validatorsHash.equals(blockHeaderAtLastCertifiedHeight?.validatorsHash), + const validatorDataAtLastCertifiedHeight = await db.getValidatorsDataByHash( + blockHeaderAtLastCertifiedHeight?.validatorsHash, ); + if (!validatorDataAtLastCertifiedHeight) { throw new Error( `No validatorsHash preimage data present for the given validatorsHash ${blockHeaderAtLastCertifiedHeight?.validatorsHash.toString( @@ -136,22 +122,32 @@ export const getNextCertificateFromAggregateCommits = ( while (height > lastCertificate.height) { // eslint-disable-next-line no-loop-func - const aggregateCommitAtHeight = aggregateCommits.find(a => a.height === height); + const aggregateCommitAtHeight = await db.getAggregateCommitByHeight(height); if (aggregateCommitAtHeight !== undefined) { // Verify whether the chain of trust is maintained, i.e., the certificate corresponding to // aggregateCommits[h] would be accepted by blockchain B. - const valid = checkChainOfTrust( + const valid = await checkChainOfTrust( blockHeaderAtLastCertifiedHeight.validatorsHash, blsKeyToBFTWeight, validatorDataAtLastCertifiedHeight.certificateThreshold, aggregateCommitAtHeight, - blockHeaders, - validatorsHashPreimage, + db, ); if (valid) { - return getCertificateFromAggregateCommit(aggregateCommitAtHeight, blockHeaders); + const blockHeaderAtAggregateCommitHeight = await db.getBlockHeaderByHeight( + aggregateCommitAtHeight.height, + ); + if (!blockHeaderAtAggregateCommitHeight) { + throw new Error( + `Block header not found for the given aggregate commit for height: ${aggregateCommitAtHeight.height}`, + ); + } + return getCertificateFromAggregateCommitByBlockHeader( + aggregateCommitAtHeight, + blockHeaderAtAggregateCommitHeight, + ); } } diff --git a/framework-plugins/lisk-framework-chain-connector-plugin/src/chain_api_client.ts b/framework-plugins/lisk-framework-chain-connector-plugin/src/chain_api_client.ts new file mode 100644 index 00000000000..5c95dbf368f --- /dev/null +++ b/framework-plugins/lisk-framework-chain-connector-plugin/src/chain_api_client.ts @@ -0,0 +1,190 @@ +/* + * Copyright © 2024 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 { + BFTHeights, + ChainAccount, + ChainAccountJSON, + ChannelData, + ChannelDataJSON, + EventCallback, + JSONObject, + apiClient, + cryptography, + chain, +} from 'lisk-sdk'; +import { + bftParametersJSONToObj, + chainAccountDataJSONToObj, + channelDataJSONToObj, + getTokenIDLSK, + proveResponseJSONToObj, +} from './utils'; +import { + BFTParametersJSON, + BFTParametersWithoutGeneratorKey, + Logger, + ModulesMetadata, + NodeInfo, + ProveResponseJSON, +} from './types'; + +const { address } = cryptography; +interface APIConfig { + wsConnectionString?: string; + ipcPath?: string; + logger: Logger; +} + +export class ChainAPIClient { + public chainID!: Buffer; + private readonly _config: APIConfig; + private _client!: apiClient.APIClient; + + public constructor(config: APIConfig) { + this._config = config; + } + + public async connect(client?: apiClient.APIClient) { + if (client) { + this._client = client; + + return; + } + if (!this._config.ipcPath && !this._config.wsConnectionString) { + throw new Error('IPC path and WS url are undefined in the configuration.'); + } + if (this._config.ipcPath) { + this._client = await apiClient.createIPCClient(this._config.ipcPath); + } else if (this._config.wsConnectionString) { + this._client = await apiClient.createWSClient(this._config.wsConnectionString); + } + + this.chainID = Buffer.from((await this.getNodeInfo()).chainID, 'hex'); + } + + public async disconnect() { + await this._client.disconnect(); + } + + public subscribe(eventName: string, cb: EventCallback): void { + this._client?.subscribe(eventName, cb); + } + + public async postTransaction(txBytes: Buffer): Promise<{ transactionId: string }> { + const result = await this._client?.invoke<{ + transactionId: string; + }>('txpool_postTransaction', { + transaction: txBytes.toString('hex'), + }); + + return result as { transactionId: string }; + } + + public async getAuthAccountNonceFromPublicKey(publicKey: Buffer): Promise { + return ( + await this._client.invoke<{ nonce: string }>('auth_getAuthAccount', { + address: address.getLisk32AddressFromPublicKey(publicKey), + }) + ).nonce; + } + + public async getNodeInfo(): Promise { + return this._client.node.getNodeInfo(); + } + + public async getChannelAccount(chainID: Buffer): Promise { + return channelDataJSONToObj( + await this._client.invoke('interoperability_getChannel', { + chainID: chainID.toString('hex'), + }), + ); + } + + public async getChainAccount(chainID: Buffer): Promise { + const chainAccount = await this._client.invoke( + 'interoperability_getChainAccount', + { + chainID: chainID.toString('hex'), + }, + ); + // eslint-disable-next-line no-console + console.log('?>>>>getChainAccount', chainAccount); + + if (!chainAccount || chainAccount?.lastCertificate === undefined) { + return undefined; + } + return chainAccountDataJSONToObj(chainAccount); + } + + public async hasUserTokenAccount(userAddress: string) { + return this._client.invoke<{ exists: boolean }>('token_hasUserAccount', { + address: userAddress, + // It is always LSK token + tokenID: `${getTokenIDLSK(this.chainID).toString('hex')}`, + }); + } + + public async getTokenInitializationFee() { + return this._client.invoke<{ + userAccount: string; + escrowAccount: string; + }>('token_getInitializationFees'); + } + + public async getBFTHeights() { + return this._client.invoke('consensus_getBFTHeights'); + } + + public async getEvents(height: number) { + return this._client.invoke>('chain_getEvents', { height }); + } + + public async getMetadataByModuleName(moduleName: string) { + const { modules: modulesMetadata } = await this._client.invoke<{ + modules: ModulesMetadata; + }>('system_getMetadata'); + const moduleMetadata = modulesMetadata.find(m => m.name === moduleName); + + if (!moduleMetadata) { + throw new Error(`No metadata found for ${moduleName} module.`); + } + + return moduleMetadata; + } + + public async getInclusionProof(queryKeys: Buffer[]) { + return proveResponseJSONToObj( + await this._client.invoke('state_prove', { + queryKeys: [...queryKeys].map(k => k.toString('hex')), + }), + ); + } + + public async getSavedInclusionProofAtHeight(height: number) { + return proveResponseJSONToObj( + await this._client.invoke('chain_getInclusionProofsAtHeight', { + height, + }), + ); + } + + public async getBFTParametersAtHeight(height: number): Promise { + return bftParametersJSONToObj( + await this._client.invoke('consensus_getBFTParametersActiveValidators', { + height, + }), + ); + } +} diff --git a/framework-plugins/lisk-framework-chain-connector-plugin/src/chain_connector_plugin.ts b/framework-plugins/lisk-framework-chain-connector-plugin/src/chain_connector_plugin.ts index 03d548f4c2c..3f748423db2 100644 --- a/framework-plugins/lisk-framework-chain-connector-plugin/src/chain_connector_plugin.ts +++ b/framework-plugins/lisk-framework-chain-connector-plugin/src/chain_connector_plugin.ts @@ -12,106 +12,26 @@ * Removal or modification of this copyright notice is prohibited. */ -import { - BasePlugin, - PluginInitContext, - apiClient, - BFTHeights, - db as liskDB, - codec, - chain, - OutboxRootWitness, - JSONObject, - Schema, - Transaction, - LastCertificate, - CcmSendSuccessEventData, - CcmProcessedEventData, - CCMProcessedResult, - CrossChainUpdateTransactionParams, - certificateSchema, - ccuParamsSchema, - cryptography, - ChainAccountJSON, - ActiveValidatorsUpdate, - AggregateCommit, - ChannelDataJSON, - Certificate, - transactions, -} from 'lisk-sdk'; -import { calculateActiveValidatorsUpdate } from './active_validators_update'; -import { - getCertificateFromAggregateCommit, - getNextCertificateFromAggregateCommits, -} from './certificate_generation'; -import { - CCU_FREQUENCY, - MODULE_NAME_INTEROPERABILITY, - CCM_SEND_SUCCESS, - COMMAND_NAME_SUBMIT_SIDECHAIN_CCU, - CCM_PROCESSED, - EMPTY_BYTES, - COMMAND_NAME_SUBMIT_MAINCHAIN_CCU, - CCU_TOTAL_CCM_SIZE, - DEFAULT_LAST_CCM_SENT_NONCE, -} from './constants'; -import { ChainConnectorStore, getDBInstance } from './db'; -import { Endpoint } from './endpoint'; +import { BasePlugin, PluginInitContext, db as liskDB } from 'lisk-sdk'; +import { CCU_TOTAL_CCM_SIZE } from './constants'; import { configSchema } from './schemas'; -import { - ChainConnectorPluginConfig, - BlockHeader, - ProveResponseJSON, - BFTParametersJSON, - ValidatorsData, - LastSentCCMWithHeight, - CCMsFromEvents, -} from './types'; -import { calculateMessageWitnesses } from './inbox_update'; -import { - bftParametersJSONToObj, - chainAccountDataJSONToObj, - channelDataJSONToObj, - getMainchainID, - getTokenIDLSK, - proveResponseJSONToObj, -} from './utils'; - -const { address, ed } = cryptography; - -interface Data { - readonly blockHeader: chain.BlockHeaderJSON; -} - -type ModulesMetadata = [ - { - stores: { key: string; data: Schema }[]; - events: { name: string; data: Schema }[]; - name: string; - }, -]; - -type FinalizedHeightInfo = { inboxSize: number; lastCertificateHeight: number }; +import { ChainConnectorPluginConfig } from './types'; +import { ChainAPIClient } from './chain_api_client'; +import { BlockEventHandler } from './block_event_handler'; +import { ChainConnectorDB, getDBInstance } from './db'; +import { ChainConnectorEndpoint } from './endpoint'; export class ChainConnectorPlugin extends BasePlugin { - public endpoint = new Endpoint(); + public endpoint = new ChainConnectorEndpoint(); public configSchema = configSchema; private _chainConnectorPluginDB!: liskDB.Database; - private _chainConnectorStore!: ChainConnectorStore; - private _lastCertificate!: LastCertificate; - private _ccuFrequency!: number; - private _maxCCUSize!: number; - private _isSaveCCU!: boolean; - private _receivingChainClient!: apiClient.APIClient; - private _sendingChainClient!: apiClient.APIClient; + private _chainConnectorStore!: ChainConnectorDB; + private _receivingChainClient!: ChainAPIClient; + private _sendingChainClient!: ChainAPIClient; private _ownChainID!: Buffer; private _receivingChainID!: Buffer; - private _isReceivingChainMainchain!: boolean; - private _registrationHeight!: number; - private _ccuSaveLimit!: number; - private _receivingChainFinalizedHeight!: number; - private _heightToDeleteIndex!: Map; + private _blockEventHandler!: BlockEventHandler; public get nodeModulePath(): string { return __filename; @@ -120,39 +40,46 @@ export class ChainConnectorPlugin extends BasePlugin // eslint-disable-next-line @typescript-eslint/require-await public async init(context: PluginInitContext): Promise { await super.init(context); - this._ccuFrequency = this.config.ccuFrequency ?? CCU_FREQUENCY; if (this.config.maxCCUSize > CCU_TOTAL_CCM_SIZE) { throw new Error(`maxCCUSize cannot be greater than ${CCU_TOTAL_CCM_SIZE} bytes.`); } this._receivingChainID = Buffer.from(this.config.receivingChainID, 'hex'); - this._maxCCUSize = this.config.maxCCUSize; - this._isSaveCCU = this.config.isSaveCCU; - this._registrationHeight = this.config.registrationHeight; - this._ccuSaveLimit = this.config.ccuSaveLimit; - this._receivingChainFinalizedHeight = 0; - this._heightToDeleteIndex = new Map(); + this._blockEventHandler = new BlockEventHandler({ + maxCCUSize: this.config.maxCCUSize, + ownChainID: Buffer.from(this.appConfig.genesis.chainID, 'hex'), + receivingChainID: Buffer.from(this.config.receivingChainID, 'hex'), + registrationHeight: this.config.registrationHeight, + ccuFee: this.config.ccuFee, + isSaveCCU: this.config.isSaveCCU, + ccuSaveLimit: this.config.ccuSaveLimit, + }); } public async load(): Promise { this._chainConnectorPluginDB = await getDBInstance(this.dataPath); - this._chainConnectorStore = new ChainConnectorStore(this._chainConnectorPluginDB); + this._chainConnectorStore = new ChainConnectorDB(this._chainConnectorPluginDB); this.endpoint.load(this.config, this._chainConnectorStore); - this._sendingChainClient = this.apiClient; + this._sendingChainClient = new ChainAPIClient({ + ipcPath: this.appConfig.system.dataPath, + logger: this.logger, + }); + await this._sendingChainClient.connect(this.apiClient); this._ownChainID = Buffer.from(this.appConfig.genesis.chainID, 'hex'); if (this._receivingChainID[0] !== this._ownChainID[0]) { throw new Error('Receiving Chain ID network does not match the sending chain network.'); } - // If the running node is mainchain then receiving chain will be sidechain or vice verse. - this._isReceivingChainMainchain = !getMainchainID(this._ownChainID).equals(this._ownChainID); - // On a new block start with CCU creation process - this._sendingChainClient.subscribe('chain_newBlock', async (data?: Record) => - this._newBlockHandler(data), - ); - this._sendingChainClient.subscribe( - 'chain_deleteBlock', - async (data?: Record) => this._deleteBlockHandler(data), - ); + this._receivingChainClient = new ChainAPIClient({ + logger: this.logger, + ipcPath: this.config.receivingChainIPCPath, + wsConnectionString: this.config.receivingChainWsURL, + }); + await this._blockEventHandler.load({ + db: this._chainConnectorStore, + logger: this.logger, + receivingChainAPIClient: this._receivingChainClient, + sendingChainAPIClient: this._sendingChainClient, + }); } public async unload(): Promise { @@ -165,769 +92,4 @@ export class ChainConnectorPlugin extends BasePlugin this._chainConnectorStore.close(); } - - public async _getCcuFee(tx: Record): Promise { - let additionalFee = BigInt(0); - - const userBalance = await this._receivingChainClient.invoke<{ exists: boolean }>( - 'token_hasUserAccount', - { - address: address.getLisk32AddressFromAddress( - address.getAddressFromPublicKey(tx.senderPublicKey as Buffer), - ), - // It is always LSK token - tokenID: `${getTokenIDLSK(this._receivingChainID).toString('hex')}`, - }, - ); - - if (!userBalance.exists) { - const fee = await this._receivingChainClient.invoke<{ - userAccount: string; - escrowAccount: string; - }>('token_getInitializationFees'); - additionalFee += BigInt(fee.userAccount); - } - - const ccuFee = BigInt(this.config.ccuFee ?? '0') + additionalFee; - const computedMinFee = transactions.computeMinFee(tx, ccuParamsSchema, { - additionalFee, - }); - - if (ccuFee > computedMinFee) { - return ccuFee; - } - return computedMinFee; - } - - private async _newBlockReceivingChainHandler(_?: Record) { - try { - const { finalizedHeight } = await this._receivingChainClient.invoke<{ - finalizedHeight: number; - }>('system_getNodeInfo'); - this._receivingChainFinalizedHeight = finalizedHeight; - const { inbox } = await this._receivingChainClient.invoke( - 'interoperability_getChannel', - { chainID: this._ownChainID.toString('hex') }, - ); - if (!inbox) { - throw new Error('No channel data available on receiving chain.'); - } - const { lastCertificate } = await this._receivingChainClient.invoke( - 'interoperability_getChainAccount', - { chainID: this._ownChainID.toString('hex') }, - ); - if (!lastCertificate) { - throw new Error('No chain data available on receiving chain.'); - } - this._heightToDeleteIndex.set(finalizedHeight, { - inboxSize: inbox.size, - lastCertificateHeight: lastCertificate.height, - }); - } catch (error) { - this.logger.debug( - error, - 'No Channel or Chain Data: Sending chain is not registered yet on receiving chain.', - ); - } - - await this._cleanup(); - } - - /** - * @see https://github.com/LiskHQ/lips/blob/main/proposals/lip-0053.md - * This function is a handler for a new block. It saves all the relevant needed to be stored for each block that will be used to calculate CCU params - * - Calls _computeCCUParams that calculates CCU params - * - Saves or sends a CCU if created - * - Updates the last certificate and does the cleanup - */ - private async _newBlockHandler(data?: Record) { - const { blockHeader: receivedBlock } = data as unknown as Data; - - const newBlockHeader = chain.BlockHeader.fromJSON(receivedBlock).toObject(); - let chainAccountJSON: ChainAccountJSON; - // Save blockHeader, aggregateCommit, validatorsData and cross chain messages if any. - try { - const nodeInfo = await this._sendingChainClient.node.getNodeInfo(); - // Fetch last certificate from the receiving chain and update the _lastCertificate - try { - chainAccountJSON = await this._receivingChainClient.invoke( - 'interoperability_getChainAccount', - { chainID: this._ownChainID.toString('hex') }, - ); - // If sending chain is not registered with the receiving chain then only save data on new block and exit - if (!chainAccountJSON || (chainAccountJSON && !chainAccountJSON.lastCertificate)) { - this.logger.info( - 'Sending chain is not registered to the receiving chain yet and has no chain data.', - ); - await this._saveDataOnNewBlock(newBlockHeader); - - return; - } - } catch (error) { - // If receivingChainAPIClient is not ready then still save data on new block - await this._saveDataOnNewBlock(newBlockHeader); - await this._initializeReceivingChainClient(); - this.logger.error( - { err: error as Error }, - 'Error occurred while accessing receivingChainAPIClient but all data is saved on newBlock.', - ); - - return; - } - - this._lastCertificate = chainAccountDataJSONToObj(chainAccountJSON).lastCertificate; - const { aggregateCommits, blockHeaders, validatorsHashPreimage, crossChainMessages } = - await this._saveDataOnNewBlock(newBlockHeader); - - const numOfBlocksSinceLastCertificate = newBlockHeader.height - this._lastCertificate.height; - if (nodeInfo.syncing || this._ccuFrequency > numOfBlocksSinceLastCertificate) { - this.logger.debug( - { - syncing: nodeInfo.syncing, - ccuFrequency: this._ccuFrequency, - nextPossibleCCUHeight: this._ccuFrequency - numOfBlocksSinceLastCertificate, - }, - 'No attempt to create CCU either due to ccuFrequency or the node is syncing', - ); - - return; - } - // When all the relevant data is saved successfully then try to create CCU - const computedCCUParams = await this._computeCCUParams( - blockHeaders, - aggregateCommits, - validatorsHashPreimage, - crossChainMessages, - ); - - if (computedCCUParams) { - try { - await this._submitCCU(computedCCUParams.ccuParams); - // If CCU was sent successfully then save the lastSentCCM if any - // TODO: Add function to check on the receiving chain whether last sent CCM was accepted or not - if (computedCCUParams.lastCCMToBeSent) { - await this._chainConnectorStore.setLastSentCCM(computedCCUParams.lastCCMToBeSent); - } - } catch (error) { - this.logger.info( - { err: error }, - `Error occured while submitting CCU for the blockHeader at height: ${newBlockHeader.height}`, - ); - return; - } - } - } catch (error) { - this.logger.error(error, 'Failed while handling the new block'); - } - } - - /** - * @see https://github.com/LiskHQ/lips/blob/main/proposals/lip-0053.md#cross-chain-update-transaction-properties - * This function computes CCU params especially, certificate, activeValidatorsUpdate and inboxUpdate - * - Uses either lastCertificate or newCertificate - * - When lastCertificate, it only computes pending CCMs if any else it skips CCU creation - * - When newCertificate it computes certificate, activeValidatorsUpdate and inboxUpdate - */ - - private async _computeCCUParams( - blockHeaders: BlockHeader[], - aggregateCommits: AggregateCommit[], - validatorsHashPreimage: ValidatorsData[], - ccmsFromEvents: CCMsFromEvents[], - ): Promise< - | { - ccuParams: CrossChainUpdateTransactionParams; - lastCCMToBeSent: LastSentCCMWithHeight | undefined; - } - | undefined - > { - const newCertificate = await this._findNextCertificate( - aggregateCommits, - blockHeaders, - validatorsHashPreimage, - ); - - if (!newCertificate && this._lastCertificate.height === 0) { - return; - } - /** - * If no lastSentCCM then assume that it's the first CCM to be sent - * and we can use the lastCertificate height - * which will be zero in case if this is the first CCU after registration - */ - const lastSentCCM = (await this._chainConnectorStore.getLastSentCCM()) ?? { - nonce: DEFAULT_LAST_CCM_SENT_NONCE, - height: this._lastCertificate.height, - }; - - let activeValidatorsUpdate: ActiveValidatorsUpdate = { - blsKeysUpdate: [], - bftWeightsUpdate: [], - bftWeightsUpdateBitmap: EMPTY_BYTES, - }; - let certificate = EMPTY_BYTES; - let certificateThreshold; - let outboxRootWitness; - - // Take range from lastSentCCM height until new or last certificate height - const ccmsToBeIncluded = ccmsFromEvents.filter( - record => - record.height >= lastSentCCM.height && - // If no newCertificate then use lastCertificate height - record.height <= (newCertificate ? newCertificate.height : this._lastCertificate.height), - ); - // Calculate messageWitnessHashes for pending CCMs if any - const channelDataOnReceivingChain = await this._receivingChainClient.invoke( - 'interoperability_getChannel', - { chainID: this._ownChainID.toString('hex') }, - ); - if (!channelDataOnReceivingChain?.inbox) { - this.logger.info('Receiving chain is not registered yet on the sending chain.'); - return; - } - const inboxSizeOnReceivingChain = channelDataJSONToObj(channelDataOnReceivingChain).inbox.size; - - const receivingChainChannelDataJSON = await this._sendingChainClient.invoke( - 'interoperability_getChannel', - { chainID: this._receivingChainID.toString('hex') }, - ); - - if (!receivingChainChannelDataJSON?.outbox) { - this.logger.info('Sending chain is not registered yet on the receiving chain.'); - return; - } - const outboxSizeOnSendingChain = channelDataJSONToObj(receivingChainChannelDataJSON).outbox - .size; - const messageWitnessHashesForCCMs = calculateMessageWitnesses( - inboxSizeOnReceivingChain, - outboxSizeOnSendingChain, - lastSentCCM, - ccmsToBeIncluded, - this._maxCCUSize, - ); - const { crossChainMessages, lastCCMToBeSent, messageWitnessHashes } = - messageWitnessHashesForCCMs; - /** - * If there is no new certificate then we calculate CCU params based on last certificate and pending ccms - */ - if (!newCertificate) { - if (crossChainMessages.length === 0) { - this.logger.info( - 'CCU cant be created as there are no pending CCMs for the last certificate.', - ); - return; - } - // Empty outboxRootWitness for last certificate - outboxRootWitness = { - bitmap: EMPTY_BYTES, - siblingHashes: [], - }; - - // Use the old certificateThreshold - const validatorsDataAtLastCertificate = validatorsHashPreimage.find(validatorsData => - validatorsData.validatorsHash.equals(this._lastCertificate.validatorsHash), - ); - if (!validatorsDataAtLastCertificate) { - throw new Error('No validatorsData found for the lastCertificate.'); - } - - certificateThreshold = validatorsDataAtLastCertificate.certificateThreshold; - } else { - if (!this._lastCertificate.validatorsHash.equals(newCertificate.validatorsHash)) { - const validatorsUpdateResult = calculateActiveValidatorsUpdate( - newCertificate, - validatorsHashPreimage, - this._lastCertificate, - ); - activeValidatorsUpdate = validatorsUpdateResult.activeValidatorsUpdate; - certificateThreshold = validatorsUpdateResult.certificateThreshold; - } else { - // If there was no activeValidatorsUpdate then use the old certificateThreshold - const validatorsDataAtLastCertificate = validatorsHashPreimage.find(validatorsData => - validatorsData.validatorsHash.equals(this._lastCertificate.validatorsHash), - ); - if (!validatorsDataAtLastCertificate) { - throw new Error('No validatorsData found for the lastCertificate.'); - } - certificateThreshold = validatorsDataAtLastCertificate.certificateThreshold; - } - - // Get the inclusionProof for outboxRoot on stateRoot - const ccmsDataAtCertificateHeight = ccmsToBeIncluded.find( - ccmsData => ccmsData.height === newCertificate.height, - ); - if (crossChainMessages.length === 0) { - outboxRootWitness = { - bitmap: EMPTY_BYTES, - siblingHashes: [], - }; - } else { - outboxRootWitness = ccmsDataAtCertificateHeight?.inclusionProof; - } - - certificate = codec.encode(certificateSchema, newCertificate); - } - - // eslint-disable-next-line consistent-return - return { - ccuParams: { - sendingChainID: this._ownChainID, - activeValidatorsUpdate, - certificate, - certificateThreshold, - inboxUpdate: { - crossChainMessages, - messageWitnessHashes, - outboxRootWitness, - }, - } as CrossChainUpdateTransactionParams, - lastCCMToBeSent, - }; - } - - private async _findNextCertificate( - aggregateCommits: AggregateCommit[], - blockHeaders: BlockHeader[], - validatorsHashPreimage: ValidatorsData[], - ): Promise { - if (aggregateCommits.length === 0) { - return undefined; - } - - if (this._lastCertificate.height === 0) { - for (const aggregateCommit of aggregateCommits) { - // If blockHeader corresponding to aggregateCommit height does not exist then try with the next aggregCommit. - const blockHeaderExist = blockHeaders.find( - header => header.height === aggregateCommit.height, - ); - if (!blockHeaderExist || aggregateCommit.height < this._registrationHeight) { - continue; - } - - // When we receive the first aggregateCommit in the chain we can create certificate directly - const firstCertificate = getCertificateFromAggregateCommit(aggregateCommit, blockHeaders); - - return firstCertificate; - } - - return undefined; - } - const bftHeights = await this._sendingChainClient.invoke('consensus_getBFTHeights'); - // Calculate certificate - return getNextCertificateFromAggregateCommits( - blockHeaders, - aggregateCommits, - validatorsHashPreimage, - bftHeights, - this._lastCertificate, - ); - } - - /** - * This function saves block header, aggregateCommit, validatorsHashPreimage and crossChainMessages for a new block - */ - private async _saveDataOnNewBlock(newBlockHeader: BlockHeader) { - // Save block header if a new block header arrives - const blockHeaders = await this._chainConnectorStore.getBlockHeaders(); - - const blockHeaderIndex = blockHeaders.findIndex( - header => header.height === newBlockHeader.height, - ); - if (blockHeaderIndex > -1) { - blockHeaders[blockHeaderIndex] = newBlockHeader; - } else { - blockHeaders.push(newBlockHeader); - } - - // Check for events if any and store them - const events = await this._sendingChainClient.invoke>( - 'chain_getEvents', - { height: newBlockHeader.height }, - ); - - const { modules: modulesMetadata } = await this._sendingChainClient.invoke<{ - modules: ModulesMetadata; - }>('system_getMetadata'); - const interoperabilityMetadata = modulesMetadata.find( - m => m.name === MODULE_NAME_INTEROPERABILITY, - ); - - if (!interoperabilityMetadata) { - throw new Error(`No metadata found for ${MODULE_NAME_INTEROPERABILITY} module.`); - } - - const ccmsFromEvents = []; - // eslint-disable-next-line no-restricted-syntax, no-labels - ccmInEventsCheck: if (events && events.length > 0) { - const ccmSendSuccessEvents = events.filter( - eventAttr => - eventAttr.name === CCM_SEND_SUCCESS && eventAttr.module === MODULE_NAME_INTEROPERABILITY, - ); - - const ccmProcessedEvents = events.filter( - eventAttr => - eventAttr.name === CCM_PROCESSED && eventAttr.module === MODULE_NAME_INTEROPERABILITY, - ); - - if (ccmSendSuccessEvents.length === 0 && ccmProcessedEvents.length === 0) { - // If there are no CCMs present in the events for the height then skip CCM saving part - // eslint-disable-next-line no-labels - break ccmInEventsCheck; - } - - // Save ccm send success events - if (ccmSendSuccessEvents.length > 0) { - const ccmSendSuccessEventInfo = interoperabilityMetadata.events.filter( - e => e.name === CCM_SEND_SUCCESS, - ); - - if (!ccmSendSuccessEventInfo?.[0]?.data) { - throw new Error('No schema found for "ccmSendSuccess" event data.'); - } - - for (const e of ccmSendSuccessEvents) { - const eventData = codec.decode( - ccmSendSuccessEventInfo[0].data, - Buffer.from(e.data, 'hex'), - ); - ccmsFromEvents.push(eventData.ccm); - } - } - // Save ccm processed events based on CCMProcessedResult FORWARDED = 1 - if (ccmProcessedEvents.length > 0) { - const ccmProcessedEventInfo = interoperabilityMetadata.events.filter( - e => e.name === CCM_PROCESSED, - ); - - if (!ccmProcessedEventInfo?.[0]?.data) { - throw new Error('No schema found for "ccmProcessed" event data.'); - } - - for (const e of ccmProcessedEvents) { - const eventData = codec.decode( - ccmProcessedEventInfo[0].data, - Buffer.from(e.data, 'hex'), - ); - if (eventData.result === CCMProcessedResult.FORWARDED) { - ccmsFromEvents.push(eventData.ccm); - } - } - } - } - // TODO: find a better way to find storeKey from metadata - const store = interoperabilityMetadata.stores.find( - s => s.data.$id === '/modules/interoperability/outbox', - ); - - // Calculate the inclusion proof of the outbox root on state root - const outboxKey = Buffer.concat([ - Buffer.from(store?.key as string, 'hex'), - cryptography.utils.hash(this._receivingChainID), - ]).toString('hex'); - - const proveResponseJSON = await this._sendingChainClient.invoke( - 'state_prove', - { - queryKeys: [outboxKey], - }, - ); - const proveResponseObj = proveResponseJSONToObj(proveResponseJSON); - const outboxRootWitness: OutboxRootWitness = { - bitmap: proveResponseObj.proof.queries[0].bitmap, - siblingHashes: proveResponseObj.proof.siblingHashes, - }; - const crossChainMessages = await this._chainConnectorStore.getCrossChainMessages(); - let receivingChainOutboxSize = 0; - try { - const receivingChainChannelDataJSON = await this._sendingChainClient.invoke( - 'interoperability_getChannel', - { chainID: this._receivingChainID.toString('hex') }, - ); - receivingChainOutboxSize = receivingChainChannelDataJSON.outbox.size; - } catch (error) { - this.logger.debug( - error, - 'No Channel Data: Receiving chain is not registered yet on sending chain', - ); - } - crossChainMessages.push({ - ccms: this._isReceivingChainMainchain - ? ccmsFromEvents - : ccmsFromEvents.filter(ccm => ccm.receivingChainID.equals(this._receivingChainID)), - height: newBlockHeader.height, - inclusionProof: outboxRootWitness, - // Add outbox size info to be used for cleanup - outboxSize: receivingChainOutboxSize, - }); - - await this._chainConnectorStore.setCrossChainMessages(crossChainMessages); - - // Save validatorsData for a new validatorsHash - const validatorsHashPreimage = await this._chainConnectorStore.getValidatorsHashPreimage(); - - // Get validatorsData at new block header height - const bftParametersJSON = await this._sendingChainClient.invoke( - 'consensus_getBFTParametersActiveValidators', - { height: newBlockHeader.height }, - ); - - const bftParametersObj = bftParametersJSONToObj(bftParametersJSON); - const validatorsDataIndex = validatorsHashPreimage.findIndex(v => - v.validatorsHash.equals(bftParametersObj.validatorsHash), - ); - // Save validatorsData if there is a new validatorsHash - if (validatorsDataIndex === -1) { - const activeValidators = bftParametersObj.validators; - validatorsHashPreimage.push({ - certificateThreshold: bftParametersObj.certificateThreshold, - validators: activeValidators, - validatorsHash: bftParametersObj.validatorsHash, - }); - } - - // Save aggregateCommit if present in the block header - const aggregateCommits = await this._chainConnectorStore.getAggregateCommits(); - if ( - !newBlockHeader.aggregateCommit.aggregationBits.equals(EMPTY_BYTES) || - !newBlockHeader.aggregateCommit.certificateSignature.equals(EMPTY_BYTES) - ) { - const aggregateCommitIndex = aggregateCommits.findIndex( - commit => commit.height === newBlockHeader.aggregateCommit.height, - ); - if (aggregateCommitIndex > -1) { - aggregateCommits[aggregateCommitIndex] = newBlockHeader.aggregateCommit; - } else { - aggregateCommits.push(newBlockHeader.aggregateCommit); - } - } - - // Save all the data - await this._chainConnectorStore.setBlockHeaders(blockHeaders); - await this._chainConnectorStore.setAggregateCommits(aggregateCommits); - await this._chainConnectorStore.setValidatorsHashPreimage(validatorsHashPreimage); - - return { - blockHeaders, - aggregateCommits, - validatorsHashPreimage, - crossChainMessages, - }; - } - - private async _deleteBlockHandler(data?: Record) { - const { blockHeader: receivedBlock } = data as unknown as Data; - - const deletedBlockHeader = chain.BlockHeader.fromJSON(receivedBlock).toObject(); - - // Delete ccmEvents for the height of blockHeader - const crossChainMessages = await this._chainConnectorStore.getCrossChainMessages(); - const indexForCCMEvents = crossChainMessages.findIndex( - ccm => ccm.height === deletedBlockHeader.height, - ); - crossChainMessages.splice(indexForCCMEvents, 1); - await this._chainConnectorStore.setCrossChainMessages(crossChainMessages); - - const findIndexByHeight = (someData: { height: number }[]): number => - someData.findIndex(datum => datum.height === deletedBlockHeader.height); - - const blockHeaders = await this._chainConnectorStore.getBlockHeaders(); - const blockHeaderIndex = findIndexByHeight(blockHeaders); - if (blockHeaderIndex !== -1) { - blockHeaders.splice(blockHeaderIndex, 1); - await this._chainConnectorStore.setBlockHeaders(blockHeaders); - } - - if ( - !deletedBlockHeader.aggregateCommit.aggregationBits.equals(EMPTY_BYTES) || - !deletedBlockHeader.aggregateCommit.certificateSignature.equals(EMPTY_BYTES) - ) { - const aggregateCommits = await this._chainConnectorStore.getAggregateCommits(); - const aggregateCommitIndex = aggregateCommits.findIndex( - commit => commit.height === deletedBlockHeader.aggregateCommit.height, - ); - if (aggregateCommitIndex > -1) { - aggregateCommits.splice(aggregateCommitIndex, 1); - } - await this._chainConnectorStore.setAggregateCommits(aggregateCommits); - } - - const validatorsHashPreimage = await this._chainConnectorStore.getValidatorsHashPreimage(); - const validatorsHashMap = blockHeaders.reduce((prev: Record, curr) => { - // eslint-disable-next-line no-param-reassign - prev[curr.validatorsHash.toString('hex')] = true; - return prev; - }, {}); - const updatedValidatorsHashPreimages = validatorsHashPreimage.filter( - vhp => validatorsHashMap[vhp.validatorsHash.toString('hex')], - ); - if (updatedValidatorsHashPreimages.length !== validatorsHashPreimage.length) { - await this._chainConnectorStore.setValidatorsHashPreimage(updatedValidatorsHashPreimages); - } - } - - private async _cleanup() { - // Delete CCUs - // When given -1 then there is no limit - if (this._ccuSaveLimit !== -1) { - const listOfCCUs = await this._chainConnectorStore.getListOfCCUs(); - if (listOfCCUs.length > this._ccuSaveLimit) { - await this._chainConnectorStore.setListOfCCUs( - // Takes the last ccuSaveLimit elements - listOfCCUs.slice(-this._ccuSaveLimit), - ); - } - let finalizedInfoAtHeight = this._heightToDeleteIndex.get( - this._receivingChainFinalizedHeight, - ); - if (!finalizedInfoAtHeight) { - for (let i = 1; i < this._heightToDeleteIndex.size; i += 1) { - if (this._heightToDeleteIndex.get(this._receivingChainFinalizedHeight - i)) { - finalizedInfoAtHeight = this._heightToDeleteIndex.get( - this._receivingChainFinalizedHeight - i, - ); - break; - } - } - } - - // Delete CCMs - const crossChainMessages = await this._chainConnectorStore.getCrossChainMessages(); - const ccmsAfterLastCertificate = crossChainMessages.filter( - ccm => - // Some extra ccms may be stored at the outbox size === finalizedheight.inboxSize - ccm.outboxSize >= (finalizedInfoAtHeight ? finalizedInfoAtHeight.inboxSize : 0), - ); - - await this._chainConnectorStore.setCrossChainMessages(ccmsAfterLastCertificate); - // Delete blockHeaders - const blockHeaders = await this._chainConnectorStore.getBlockHeaders(); - const updatedBlockHeaders = blockHeaders.filter( - blockHeader => - blockHeader.height >= - (finalizedInfoAtHeight ? finalizedInfoAtHeight.lastCertificateHeight : 0), - ); - await this._chainConnectorStore.setBlockHeaders(updatedBlockHeaders); - - // Delete aggregateCommits - const aggregateCommits = await this._chainConnectorStore.getAggregateCommits(); - - await this._chainConnectorStore.setAggregateCommits( - aggregateCommits.filter( - aggregateCommit => - aggregateCommit.height >= - (finalizedInfoAtHeight ? finalizedInfoAtHeight.lastCertificateHeight : 0), - ), - ); - // Delete validatorsHashPreimage - const validatorsHashPreimage = await this._chainConnectorStore.getValidatorsHashPreimage(); - const validatorsHashMap = updatedBlockHeaders.reduce( - (prev: Record, curr) => { - // eslint-disable-next-line no-param-reassign - prev[curr.validatorsHash.toString('hex')] = true; - return prev; - }, - {}, - ); - const updatedValidatorsHashPreimages = validatorsHashPreimage.filter( - vhp => validatorsHashMap[vhp.validatorsHash.toString('hex')], - ); - if (updatedValidatorsHashPreimages.length !== validatorsHashPreimage.length) { - await this._chainConnectorStore.setValidatorsHashPreimage(updatedValidatorsHashPreimages); - } - - // Delete info less than finalized height - this._heightToDeleteIndex.forEach((_, key) => { - if (key < this._receivingChainFinalizedHeight) { - this._heightToDeleteIndex.delete(key); - } - }); - } - } - - private async _submitCCU(ccuParams: CrossChainUpdateTransactionParams): Promise { - if (!this._chainConnectorStore.privateKey) { - throw new Error('There is no key enabled to submit CCU'); - } - const relayerPublicKey = ed.getPublicKeyFromPrivateKey(this._chainConnectorStore.privateKey); - const targetCommand = this._isReceivingChainMainchain - ? COMMAND_NAME_SUBMIT_MAINCHAIN_CCU - : COMMAND_NAME_SUBMIT_SIDECHAIN_CCU; - - const { nonce } = await this._receivingChainClient.invoke<{ nonce: string }>( - 'auth_getAuthAccount', - { - address: address.getLisk32AddressFromPublicKey(relayerPublicKey), - }, - ); - - const { chainID: chainIDStr } = await this._receivingChainClient.invoke<{ chainID: string }>( - 'system_getNodeInfo', - ); - const chainID = Buffer.from(chainIDStr, 'hex'); - - const txWithoutFee = { - module: MODULE_NAME_INTEROPERABILITY, - command: targetCommand, - nonce: BigInt(nonce), - senderPublicKey: relayerPublicKey, - params: codec.encode(ccuParamsSchema, ccuParams), - signatures: [], - }; - - const tx = new Transaction({ - ...txWithoutFee, - fee: await this._getCcuFee({ - ...txWithoutFee, - params: ccuParams, - }), - }); - - tx.sign(chainID, this._chainConnectorStore.privateKey); - let result: { transactionId: string }; - if (this._isSaveCCU) { - result = { transactionId: tx.id.toString('hex') }; - } else { - result = await this._receivingChainClient.invoke<{ - transactionId: string; - }>('txpool_postTransaction', { - transaction: tx.getBytes().toString('hex'), - }); - } - /** - * TODO: As of now we save it in memory but going forward it should be saved in DB, - * as the array size can grow after sometime. - */ - // Save the sent CCU - const listOfCCUs = await this._chainConnectorStore.getListOfCCUs(); - listOfCCUs.push(tx.toObject()); - await this._chainConnectorStore.setListOfCCUs(listOfCCUs); - // Update logs - this.logger.info({ transactionID: result.transactionId }, 'Sent CCU transaction'); - } - - private async _initializeReceivingChainClient() { - if (!this.config.receivingChainIPCPath && !this.config.receivingChainWsURL) { - throw new Error('IPC path and WS url are undefined in the configuration.'); - } - try { - if (this.config.receivingChainIPCPath) { - this._receivingChainClient = await apiClient.createIPCClient( - this.config.receivingChainIPCPath, - ); - } else if (this.config.receivingChainWsURL) { - this._receivingChainClient = await apiClient.createWSClient( - this.config.receivingChainWsURL, - ); - } - this._receivingChainClient.subscribe( - 'chain_newBlock', - async (data?: Record) => this._newBlockReceivingChainHandler(data), - ); - } catch (error) { - this.logger.error( - error, - 'Not able to connect to receivingChainAPIClient. Trying again on next new block.', - ); - } - } } diff --git a/framework-plugins/lisk-framework-chain-connector-plugin/src/db.ts b/framework-plugins/lisk-framework-chain-connector-plugin/src/db.ts index 9c65068b077..733bdb60ea0 100644 --- a/framework-plugins/lisk-framework-chain-connector-plugin/src/db.ts +++ b/framework-plugins/lisk-framework-chain-connector-plugin/src/db.ts @@ -12,46 +12,35 @@ * Removal or modification of this copyright notice is prohibited. */ -import { codec, db as liskDB, AggregateCommit, chain, cryptography } from 'lisk-sdk'; +import { + codec, + db as liskDB, + AggregateCommit, + chain, + cryptography, + EMPTY_BYTES, + aggregateCommitSchema, + CCMsg, +} from 'lisk-sdk'; import * as os from 'os'; import { join } from 'path'; import { ensureDir } from 'fs-extra'; +import { DB_KEY_CROSS_CHAIN_MESSAGES, DB_KEY_LAST_SENT_CCM, DB_KEY_LIST_OF_CCU } from './constants'; import { - DB_KEY_AGGREGATE_COMMITS, - DB_KEY_BLOCK_HEADERS, - DB_KEY_CROSS_CHAIN_MESSAGES, - DB_KEY_LAST_SENT_CCM, - DB_KEY_LIST_OF_CCU, - DB_KEY_VALIDATORS_HASH_PREIMAGE, -} from './constants'; -import { - aggregateCommitsInfoSchema, - blockHeadersInfoSchema, - ccmsFromEventsSchema, - lastSentCCMWithHeight, + blockHeaderSchemaWithID, + ccmsAtHeightSchema, + lastSentCCMSchema, listOfCCUsSchema, - validatorsHashPreimageInfoSchema, + validatorsDataSchema, } from './schemas'; -import { BlockHeader, CCMsFromEvents, LastSentCCMWithHeight, ValidatorsData } from './types'; +import { BlockHeader, CCMWithHeight, LastSentCCM, ValidatorsData } from './types'; const { Database } = liskDB; type KVStore = liskDB.Database; -interface BlockHeadersInfo { - blockHeaders: BlockHeader[]; -} - -interface AggregateCommitsInfo { - aggregateCommits: AggregateCommit[]; -} - -interface ValidatorsHashPreimage { - validatorsHashPreimage: ValidatorsData[]; -} - -interface CrossChainMessagesInfo { - ccmsFromEvents: CCMsFromEvents[]; -} +const DB_KEY_BLOCK_HEADER_BY_HEIGHT = Buffer.from([10]); +const DB_KEY_AGGREGATE_COMMIT_BY_HEIGHT = Buffer.from([20]); +const DB_KEY_VALIDATORS_DATA = Buffer.from([30]); export const getDBInstance = async ( dataPath: string, @@ -69,7 +58,14 @@ export const checkDBError = (error: Error | unknown) => { } }; -export class ChainConnectorStore { +export const concatDBKeys = (...keys: Buffer[]) => Buffer.concat(keys); +export const uint32BE = (val: number): Buffer => { + const result = Buffer.alloc(4); + result.writeUInt32BE(val, 0); + return result; +}; + +export class ChainConnectorDB { private readonly _db: KVStore; private _privateKey?: Buffer; @@ -85,98 +81,315 @@ export class ChainConnectorStore { return this._privateKey; } - public async getBlockHeaders(): Promise { - let blockHeaders: BlockHeader[] = []; + public async saveOnNewBlock(blockHeader: BlockHeader) { + const heightBuf = uint32BE(blockHeader.height); + const batch = new liskDB.Batch(); + const newBlockHeaderBytes = codec.encode(blockHeaderSchemaWithID, blockHeader); + + batch.set(concatDBKeys(DB_KEY_BLOCK_HEADER_BY_HEIGHT, heightBuf), newBlockHeaderBytes); + + if ( + !blockHeader.aggregateCommit.aggregationBits.equals(EMPTY_BYTES) || + !blockHeader.aggregateCommit.certificateSignature.equals(EMPTY_BYTES) + ) { + const aggregateCommitHeight = uint32BE(blockHeader.aggregateCommit.height); + const aggregateCommitBytes = codec.encode(aggregateCommitSchema, blockHeader.aggregateCommit); + batch.set( + concatDBKeys(DB_KEY_AGGREGATE_COMMIT_BY_HEIGHT, aggregateCommitHeight), + aggregateCommitBytes, + ); + } + + await this._db.write(batch); + } + + public async getBlockHeaderByHeight(height: number): Promise { try { - const encodedInfo = await this._db.get(DB_KEY_BLOCK_HEADERS); - blockHeaders = codec.decode( - blockHeadersInfoSchema, - encodedInfo, - ).blockHeaders; + const blockBytes = await this._db.get( + concatDBKeys(DB_KEY_BLOCK_HEADER_BY_HEIGHT, uint32BE(height)), + ); + + return codec.decode(blockHeaderSchemaWithID, blockBytes); } catch (error) { - checkDBError(error); + if (!(error instanceof liskDB.NotFoundError)) { + throw error; + } + + return undefined; } - return blockHeaders; } - public async setBlockHeaders(blockHeaders: BlockHeader[]) { - const encodedInfo = codec.encode(blockHeadersInfoSchema, { blockHeaders }); + public async getBlockHeadersBetweenHeights(fromHeight: number, toHeight: number) { + const stream = this._db.createReadStream({ + gte: concatDBKeys(DB_KEY_BLOCK_HEADER_BY_HEIGHT, uint32BE(fromHeight)), + lte: concatDBKeys(DB_KEY_BLOCK_HEADER_BY_HEIGHT, uint32BE(toHeight)), + reverse: true, + }); + const blockHeaders = await new Promise((resolve, reject) => { + const list: Buffer[] = []; + stream + .on('data', ({ value }: { value: Buffer }) => { + list.push(value); + }) + .on('error', error => { + reject(error); + }) + .on('end', () => { + resolve(list); + }); + }); + + return blockHeaders.map(b => codec.decode(blockHeaderSchemaWithID, b)); + } + + public async deleteBlockHeadersBetweenHeight( + fromHeight: number, + toHeight: number, + ): Promise { + const stream = this._db.createReadStream({ + gte: concatDBKeys(DB_KEY_BLOCK_HEADER_BY_HEIGHT, uint32BE(fromHeight)), + lte: concatDBKeys(DB_KEY_BLOCK_HEADER_BY_HEIGHT, uint32BE(toHeight)), + reverse: true, + }); + const blockHeaderIndexes = await new Promise((resolve, reject) => { + const list: Buffer[] = []; + stream + .on('data', ({ key }: { key: Buffer }) => { + list.push(key); + }) + .on('error', error => { + reject(error); + }) + .on('end', () => { + resolve(list); + }); + }); + const batch = new liskDB.Batch(); + for (const key of blockHeaderIndexes) { + batch.del(key); + } + + await this._db.write(batch); + } - await this._db.set(DB_KEY_BLOCK_HEADERS, encodedInfo); + public async deleteBlockHeaderByHeight(height: number): Promise { + const heightBuf = uint32BE(height); + await this._db.del(concatDBKeys(DB_KEY_BLOCK_HEADER_BY_HEIGHT, heightBuf)); } - public async getAggregateCommits(): Promise { - let aggregateCommits: AggregateCommit[] = []; + public async getAggregateCommitByHeight(height: number) { try { - const encodedInfo = await this._db.get(DB_KEY_AGGREGATE_COMMITS); - aggregateCommits = codec.decode( - aggregateCommitsInfoSchema, - encodedInfo, - ).aggregateCommits; + const bytes = await this._db.get( + concatDBKeys(DB_KEY_AGGREGATE_COMMIT_BY_HEIGHT, uint32BE(height)), + ); + + return codec.decode(aggregateCommitSchema, bytes); } catch (error) { - checkDBError(error); + if (!(error instanceof liskDB.NotFoundError)) { + throw error; + } + + return undefined; } - return aggregateCommits; } - public async setAggregateCommits(aggregateCommits: AggregateCommit[]) { - const encodedInfo = codec.encode(aggregateCommitsInfoSchema, { aggregateCommits }); - await this._db.set(DB_KEY_AGGREGATE_COMMITS, encodedInfo); + public async getAggregateCommitBetweenHeights(fromHeight: number, toHeight: number) { + const stream = this._db.createReadStream({ + gte: concatDBKeys(DB_KEY_AGGREGATE_COMMIT_BY_HEIGHT, uint32BE(fromHeight)), + lte: concatDBKeys(DB_KEY_AGGREGATE_COMMIT_BY_HEIGHT, uint32BE(toHeight)), + reverse: true, + }); + const aggregateCommits = await new Promise((resolve, reject) => { + const list: Buffer[] = []; + stream + .on('data', ({ value }: { value: Buffer }) => { + list.push(value); + }) + .on('error', error => { + reject(error); + }) + .on('end', () => { + resolve(list); + }); + }); + + return aggregateCommits.map(a => codec.decode(aggregateCommitSchema, a)); + } + + public async deleteAggregateCommitsBetweenHeight( + fromHeight: number, + toHeight: number, + ): Promise { + const stream = this._db.createReadStream({ + gte: concatDBKeys(DB_KEY_AGGREGATE_COMMIT_BY_HEIGHT, uint32BE(fromHeight)), + lte: concatDBKeys(DB_KEY_AGGREGATE_COMMIT_BY_HEIGHT, uint32BE(toHeight)), + reverse: true, + }); + const aggregateCommitIndexes = await new Promise((resolve, reject) => { + const list: Buffer[] = []; + stream + .on('data', ({ key }: { key: Buffer }) => { + list.push(key); + }) + .on('error', error => { + reject(error); + }) + .on('end', () => { + resolve(list); + }); + }); + const batch = new liskDB.Batch(); + for (const key of aggregateCommitIndexes) { + batch.del(key); + } + + await this._db.write(batch); + } + + public async deleteAggregateCommitByHeight(height: number): Promise { + const heightBuf = uint32BE(height); + await this._db.del(concatDBKeys(DB_KEY_AGGREGATE_COMMIT_BY_HEIGHT, heightBuf)); } - public async getValidatorsHashPreimage(): Promise { - let validatorsHashPreimage: ValidatorsData[] = []; + public async getValidatorsDataByHash(validatorsHash: Buffer) { try { - const encodedInfo = await this._db.get(DB_KEY_VALIDATORS_HASH_PREIMAGE); - validatorsHashPreimage = codec.decode( - validatorsHashPreimageInfoSchema, - encodedInfo, - ).validatorsHashPreimage; + const bytes = await this._db.get(concatDBKeys(DB_KEY_VALIDATORS_DATA, validatorsHash)); + + return codec.decode(validatorsDataSchema, bytes); } catch (error) { - checkDBError(error); + if (!(error instanceof liskDB.NotFoundError)) { + throw error; + } + + return undefined; } - return validatorsHashPreimage; } - public async setValidatorsHashPreimage(validatorsHashInput: ValidatorsData[]) { - const encodedInfo = codec.encode(validatorsHashPreimageInfoSchema, { - validatorsHashPreimage: validatorsHashInput, + public async setValidatorsDataByHash(validatorsHash: Buffer, validatorsData: ValidatorsData) { + const bytes = codec.encode(validatorsDataSchema, validatorsData); + await this._db.set(concatDBKeys(DB_KEY_VALIDATORS_DATA, validatorsHash), bytes); + } + + public async getAllValidatorsData() { + const stream = this._db.createReadStream({ + gte: concatDBKeys(DB_KEY_VALIDATORS_DATA, Buffer.alloc(4, 0)), + lte: concatDBKeys(DB_KEY_VALIDATORS_DATA, Buffer.alloc(4, 255)), + reverse: true, + }); + const validatorsData = await new Promise((resolve, reject) => { + const list: Buffer[] = []; + stream + .on('data', ({ value }: { value: Buffer }) => { + list.push(value); + }) + .on('error', error => { + reject(error); + }) + .on('end', () => { + resolve(list); + }); }); - await this._db.set(DB_KEY_VALIDATORS_HASH_PREIMAGE, encodedInfo); + + return validatorsData.map(v => codec.decode(validatorsDataSchema, v)); } - public async getCrossChainMessages(): Promise { - let crossChainMessages: CCMsFromEvents[] = []; + public async getCCMsByHeight(height: number): Promise { + const heightBuf = uint32BE(height); + let crossChainMessages: CCMsg[] = []; try { - const encodedInfo = await this._db.get(DB_KEY_CROSS_CHAIN_MESSAGES); - crossChainMessages = codec.decode( - ccmsFromEventsSchema, - encodedInfo, - ).ccmsFromEvents; + const encodedInfo = await this._db.get(concatDBKeys(DB_KEY_CROSS_CHAIN_MESSAGES, heightBuf)); + crossChainMessages = codec.decode<{ ccms: CCMsg[] }>(ccmsAtHeightSchema, encodedInfo).ccms; } catch (error) { checkDBError(error); } return crossChainMessages; } - public async setCrossChainMessages(ccms: CCMsFromEvents[]) { - const encodedInfo = codec.encode(ccmsFromEventsSchema, { ccmsFromEvents: ccms }); - await this._db.set(DB_KEY_CROSS_CHAIN_MESSAGES, encodedInfo); + public async deleteCCMsByHeight(height: number): Promise { + const heightBuf = uint32BE(height); + await this._db.del(concatDBKeys(DB_KEY_CROSS_CHAIN_MESSAGES, heightBuf)); + } + + public async getCCMsBetweenHeight( + fromHeight: number, + toHeight: number, + ): Promise { + const stream = this._db.createReadStream({ + gte: concatDBKeys(DB_KEY_CROSS_CHAIN_MESSAGES, uint32BE(fromHeight)), + lte: concatDBKeys(DB_KEY_CROSS_CHAIN_MESSAGES, uint32BE(toHeight)), + reverse: true, + }); + const ccmArrayAtEachHeight = await new Promise((resolve, reject) => { + const list: Buffer[] = []; + stream + .on('data', ({ value }: { value: Buffer }) => { + list.push(value); + }) + .on('error', error => { + reject(error); + }) + .on('end', () => { + resolve(list); + }); + }); + + const flatCCMList = []; + + for (const ccms of ccmArrayAtEachHeight) { + flatCCMList.push(...codec.decode<{ ccms: CCMWithHeight[] }>(ccmsAtHeightSchema, ccms).ccms); + } + + return flatCCMList; + } + + public async deleteCCMsBetweenHeight(fromHeight: number, toHeight: number): Promise { + const stream = this._db.createReadStream({ + gte: concatDBKeys(DB_KEY_CROSS_CHAIN_MESSAGES, uint32BE(fromHeight)), + lte: concatDBKeys(DB_KEY_CROSS_CHAIN_MESSAGES, uint32BE(toHeight)), + reverse: true, + }); + const ccmsListIndexes = await new Promise((resolve, reject) => { + const list: Buffer[] = []; + stream + .on('data', ({ key }: { key: Buffer }) => { + list.push(key); + }) + .on('error', error => { + reject(error); + }) + .on('end', () => { + resolve(list); + }); + }); + const batch = new liskDB.Batch(); + for (const key of ccmsListIndexes) { + batch.del(key); + } + + await this._db.write(batch); + } + + public async setCCMsByHeight(ccms: CCMWithHeight[], height: number) { + const heightBuf = uint32BE(height); + + const encodedInfo = codec.encode(ccmsAtHeightSchema, { ccms }); + + await this._db.set(concatDBKeys(DB_KEY_CROSS_CHAIN_MESSAGES, heightBuf), encodedInfo); } - public async getLastSentCCM(): Promise { - let lastSentCCM: LastSentCCMWithHeight | undefined; + public async getLastSentCCM(): Promise { + let lastSentCCM: LastSentCCM | undefined; try { const encodedInfo = await this._db.get(DB_KEY_LAST_SENT_CCM); - lastSentCCM = codec.decode(lastSentCCMWithHeight, encodedInfo); + lastSentCCM = codec.decode(lastSentCCMSchema, encodedInfo); } catch (error) { checkDBError(error); } return lastSentCCM; } - public async setLastSentCCM(lastSentCCM: LastSentCCMWithHeight) { - await this._db.set(DB_KEY_LAST_SENT_CCM, codec.encode(lastSentCCMWithHeight, lastSentCCM)); + public async setLastSentCCM(ccm: LastSentCCM) { + await this._db.set(DB_KEY_LAST_SENT_CCM, codec.encode(lastSentCCMSchema, ccm)); } public async getListOfCCUs(): Promise { diff --git a/framework-plugins/lisk-framework-chain-connector-plugin/src/endpoint.ts b/framework-plugins/lisk-framework-chain-connector-plugin/src/endpoint.ts index 3aaf8b36455..d11c45c96b4 100644 --- a/framework-plugins/lisk-framework-chain-connector-plugin/src/endpoint.ts +++ b/framework-plugins/lisk-framework-chain-connector-plugin/src/endpoint.ts @@ -20,61 +20,66 @@ import { BlockHeaderJSON, validator as liskValidator, } from 'lisk-sdk'; -import { ChainConnectorStore } from './db'; import { AggregateCommitJSON, - CCMsFromEventsJSON, + CCMWithHeight, ChainConnectorPluginConfig, LastSentCCMWithHeightJSON, SentCCUsJSON, ValidatorsDataJSON, } from './types'; -import { aggregateCommitToJSON, ccmsFromEventsToJSON, validatorsHashPreimagetoJSON } from './utils'; +import { aggregateCommitToJSON, validatorsHashPreimagetoJSON } from './utils'; import { authorizeRequestSchema } from './schemas'; +import { ChainConnectorDB } from './db'; // disabled for type annotation // eslint-disable-next-line prefer-destructuring const validator: liskValidator.LiskValidator = liskValidator.validator; -export class Endpoint extends BasePluginEndpoint { - private _chainConnectorStore!: ChainConnectorStore; +export class ChainConnectorEndpoint extends BasePluginEndpoint { + private db!: ChainConnectorDB; private _config!: ChainConnectorPluginConfig; - public load(config: ChainConnectorPluginConfig, store: ChainConnectorStore) { + public load(config: ChainConnectorPluginConfig, store: ChainConnectorDB) { this._config = config; - this._chainConnectorStore = store; + this.db = store; } // eslint-disable-next-line @typescript-eslint/require-await public async getSentCCUs(_context: PluginEndpointContext): Promise { - const sentCCUs = await this._chainConnectorStore.getListOfCCUs(); + const sentCCUs = await this.db.getListOfCCUs(); return sentCCUs.map(transaction => new chain.Transaction(transaction).toJSON()); } - public async getAggregateCommits( - _context: PluginEndpointContext, - ): Promise { - const aggregateCommits = await this._chainConnectorStore.getAggregateCommits(); + public async getAggregateCommits(context: PluginEndpointContext): Promise { + const { from, to } = context.params as { from: number; to: number }; + const aggregateCommits = await this.db.getAggregateCommitBetweenHeights(from, to); return aggregateCommits.map(aggregateCommit => aggregateCommitToJSON(aggregateCommit)); } - public async getBlockHeaders(_context: PluginEndpointContext): Promise { - const blockHeaders = await this._chainConnectorStore.getBlockHeaders(); + public async getBlockHeaders(context: PluginEndpointContext): Promise { + const { from, to } = context.params as { from: number; to: number }; + const blockHeaders = await this.db.getBlockHeadersBetweenHeights(from, to); return blockHeaders.map(blockHeader => new BlockHeader(blockHeader).toJSON()); } - public async getCrossChainMessages( - _context: PluginEndpointContext, - ): Promise { - const ccmsAndInclusionProofs = await this._chainConnectorStore.getCrossChainMessages(); - return ccmsAndInclusionProofs.map(ccmsAndInclusionProof => - ccmsFromEventsToJSON(ccmsAndInclusionProof), - ); + public async getBlockHeaderByHeight(context: PluginEndpointContext): Promise { + const { height } = context.params as { height: number }; + const blockHeader = await this.db.getBlockHeaderByHeight(height); + + return new BlockHeader(blockHeader as BlockHeader).toJSON(); + } + public async getCrossChainMessages(context: PluginEndpointContext): Promise { + const { from, to } = context.params as { from: number; to: number }; + + const ccms = await this.db.getCCMsBetweenHeight(from, to); + + return ccms; } public async getLastSentCCM(_context: PluginEndpointContext): Promise { - const lastSentCCM = await this._chainConnectorStore.getLastSentCCM(); + const lastSentCCM = await this.db.getLastSentCCM(); if (!lastSentCCM) { throw new Error('No CCM was sent so far.'); } @@ -89,10 +94,8 @@ export class Endpoint extends BasePluginEndpoint { }; } - public async getValidatorsInfoFromPreimage( - _context: PluginEndpointContext, - ): Promise { - const validatorsHashPreimage = await this._chainConnectorStore.getValidatorsHashPreimage(); + public async getValidatorsdata(_context: PluginEndpointContext): Promise { + const validatorsHashPreimage = await this.db.getAllValidatorsData(); return validatorsHashPreimagetoJSON(validatorsHashPreimage); } @@ -107,13 +110,13 @@ export class Endpoint extends BasePluginEndpoint { const result = `Successfully ${enable ? 'enabled' : 'disabled'} the chain connector plugin.`; if (!enable) { - await this._chainConnectorStore.deletePrivateKey(this._config.encryptedPrivateKey, password); + await this.db.deletePrivateKey(this._config.encryptedPrivateKey, password); return { result, }; } - await this._chainConnectorStore.setPrivateKey(this._config.encryptedPrivateKey, password); + await this.db.setPrivateKey(this._config.encryptedPrivateKey, password); return { result, }; diff --git a/framework-plugins/lisk-framework-chain-connector-plugin/src/inbox_update.ts b/framework-plugins/lisk-framework-chain-connector-plugin/src/inbox_update.ts index b01d920e742..43332ef359e 100644 --- a/framework-plugins/lisk-framework-chain-connector-plugin/src/inbox_update.ts +++ b/framework-plugins/lisk-framework-chain-connector-plugin/src/inbox_update.ts @@ -13,15 +13,15 @@ */ import { ccmSchema, codec, tree } from 'lisk-sdk'; -import { CCMsFromEvents, LastSentCCMWithHeight } from './types'; +import { CCMWithHeight, LastSentCCM } from './types'; /** * @see https://github.com/LiskHQ/lips/blob/main/proposals/lip-0053.md#messagewitnesshashes - * + * * Calculates messageWitnessHashes if there are any pending ccms as well as it filters out ccms * based on last sent ccm nonce. * Also, it checks whether a list of ccm can fit into a CCU based on maxCCUSize - * + * * @param sendingChainChannelInfo Channel info of the sendingChain stored on receivingChain * @param ccmsToBeIncluded Filtered list of CCMs that can be included for a given certificate * @param lastSentCCMInfo Last send CCM info which is used to filter out ccms @@ -36,38 +36,35 @@ export const calculateMessageWitnesses = ( inboxSizeOnReceivingChain: number, outboxSizeOnSendingChain: number, lastSentCCM: { - height: number; nonce: bigint; + height: number; }, - ccmsToBeIncluded: CCMsFromEvents[], + ccmsToBeIncluded: CCMWithHeight[], maxCCUSize: number, ): { crossChainMessages: Buffer[]; messageWitnessHashes: Buffer[]; - lastCCMToBeSent: LastSentCCMWithHeight | undefined; + lastCCMToBeSent: LastSentCCM | undefined; } => { const allSerializedCCMs = []; const includedSerializedCCMs = []; let lastCCMWithHeight; let totalSize = 0; // Make an array of ccms with nonce greater than last sent ccm nonce - for (const ccmsFromEvents of ccmsToBeIncluded) { - const { ccms, height } = ccmsFromEvents; - for (const ccm of ccms) { - if (height !== 0 && lastSentCCM.height === height) { - if (ccm.nonce === lastSentCCM.nonce) { - continue; - } + for (const ccm of ccmsToBeIncluded) { + if (ccm.height !== 0 && lastSentCCM.height === ccm.height) { + if (ccm.nonce === lastSentCCM.nonce) { + continue; } - if (inboxSizeOnReceivingChain < outboxSizeOnSendingChain) { - const ccmBytes = codec.encode(ccmSchema, ccm); - totalSize += ccmBytes.length; - if (totalSize < maxCCUSize) { - includedSerializedCCMs.push(ccmBytes); - lastCCMWithHeight = { ...ccm, height: ccmsFromEvents.height }; - } - allSerializedCCMs.push(ccmBytes); + } + if (inboxSizeOnReceivingChain < outboxSizeOnSendingChain) { + const ccmBytes = codec.encode(ccmSchema, ccm); + totalSize += ccmBytes.length; + if (totalSize < maxCCUSize) { + includedSerializedCCMs.push(ccmBytes); + lastCCMWithHeight = { ...ccm, height: ccm.height }; } + allSerializedCCMs.push(ccmBytes); } } @@ -85,7 +82,7 @@ export const calculateMessageWitnesses = ( return { crossChainMessages: includedSerializedCCMs, messageWitnessHashes: [], - lastCCMToBeSent: lastCCMWithHeight, + lastCCMToBeSent: { ...(lastCCMWithHeight as LastSentCCM), outboxSize: -1 }, }; } @@ -99,6 +96,6 @@ export const calculateMessageWitnesses = ( return { crossChainMessages: includedSerializedCCMs, messageWitnessHashes, - lastCCMToBeSent: lastCCMWithHeight, + lastCCMToBeSent: { ...(lastCCMWithHeight as LastSentCCM), outboxSize: -1 }, }; }; diff --git a/framework-plugins/lisk-framework-chain-connector-plugin/src/schemas.ts b/framework-plugins/lisk-framework-chain-connector-plugin/src/schemas.ts index b3c4418830a..9e0c9bd3dc3 100644 --- a/framework-plugins/lisk-framework-chain-connector-plugin/src/schemas.ts +++ b/framework-plugins/lisk-framework-chain-connector-plugin/src/schemas.ts @@ -107,6 +107,19 @@ export const validatorsDataSchema = { }, }; +export const blockHeaderSchemaWithID = { + $id: `${pluginSchemaIDPrefix}/blockHeaderWithID`, + type: 'object', + required: [...chain.blockHeaderSchema.required, 'id'], + properties: { + ...chain.blockHeaderSchema.properties, + id: { + dataType: 'bytes', + fieldNumber: Object.keys(chain.blockHeaderSchema.properties).length + 1, + }, + }, +}; + export const blockHeadersInfoSchema = { $id: `${pluginSchemaIDPrefix}/blockHeaders`, type: 'object', @@ -159,6 +172,17 @@ export const lastSentCCMWithHeight = { }, }; +export const lastSentCCMSchema = { + $id: `${pluginSchemaIDPrefix}/lastSentCCM`, + type: 'object', + required: [...ccmSchema.required, 'height', 'outboxSize'], + properties: { + ...ccmSchema.properties, + height: { dataType: 'uint32', fieldNumber: Object.keys(ccmSchema.properties).length + 1 }, + outboxSize: { dataType: 'uint32', fieldNumber: Object.keys(ccmSchema.properties).length + 2 }, + }, +}; + export const listOfCCUsSchema = { $id: `${pluginSchemaIDPrefix}/listOfCCUs`, type: 'object', @@ -173,6 +197,26 @@ export const listOfCCUsSchema = { }, }; +export const ccmsAtHeightSchema = { + $id: `${pluginSchemaIDPrefix}/ccmsAtHeight`, + type: 'object', + required: ['ccms'], + properties: { + ccms: { + type: 'array', + fieldNumber: 1, + items: { + type: 'object', + required: [...ccmSchema.required, 'height'], + properties: { + ...ccmSchema.properties, + height: { dataType: 'uint32', fieldNumber: Object.keys(ccmSchema.properties).length + 1 }, + }, + }, + }, + }, +}; + export const ccmsFromEventsSchema = { $id: `${pluginSchemaIDPrefix}/ccmsFromEvents`, type: 'object', diff --git a/framework-plugins/lisk-framework-chain-connector-plugin/src/types.ts b/framework-plugins/lisk-framework-chain-connector-plugin/src/types.ts index a8ac15541c3..cb378c424f3 100644 --- a/framework-plugins/lisk-framework-chain-connector-plugin/src/types.ts +++ b/framework-plugins/lisk-framework-chain-connector-plugin/src/types.ts @@ -23,12 +23,71 @@ import { Proof, ProveResponse, BFTValidator, + Schema, } from 'lisk-sdk'; export interface BlockHeader extends chain.BlockHeaderAttrs { validatorsHash: Buffer; } +export interface Logger { + readonly trace: (data?: Record | unknown, message?: string) => void; + readonly debug: (data?: Record | unknown, message?: string) => void; + readonly info: (data?: Record | unknown, message?: string) => void; + readonly warn: (data?: Record | unknown, message?: string) => void; + readonly error: (data?: Record | unknown, message?: string) => void; + readonly fatal: (data?: Record | unknown, message?: string) => void; + readonly level: () => number; +} + +export interface GenesisConfig { + [key: string]: unknown; + readonly bftBatchSize: number; + readonly chainID: string; + readonly blockTime: number; + readonly maxTransactionsSize: number; +} + +export interface NodeInfo { + readonly version: string; + readonly networkVersion: string; + readonly chainID: string; + readonly lastBlockID: string; + readonly height: number; + readonly genesisHeight: number; + readonly finalizedHeight: number; + readonly syncing: boolean; + readonly unconfirmedTransactions: number; + readonly genesis: GenesisConfig; + readonly network: { + readonly port: number; + readonly hostIp?: string; + readonly seedPeers: { + readonly ip: string; + readonly port: number; + }[]; + readonly blacklistedIPs?: string[]; + readonly fixedPeers?: string[]; + readonly whitelistedPeers?: { + readonly ip: string; + readonly port: number; + }[]; + }; +} + +export type ModuleMetadata = { + stores: { + key: string; + data: Schema; + }[]; + events: { + name: string; + data: Schema; + }[]; + name: string; +}; +export type ModulesMetadata = ModuleMetadata[]; + export interface ChainConnectorPluginConfig { receivingChainID: string; receivingChainWsURL?: string; @@ -49,6 +108,14 @@ export interface ActiveValidatorWithAddress extends ActiveValidator { address: Buffer; } +export interface BFTParametersWithoutGeneratorKey extends Omit { + validators: { + address: Buffer; + bftWeight: bigint; + blsKey: Buffer; + }[]; +} + export interface ValidatorsData { certificateThreshold: bigint; validators: ActiveValidatorWithAddress[]; @@ -59,6 +126,14 @@ export interface LastSentCCMWithHeight extends CCMsg { height: number; } +export interface LastSentCCM extends CCMsg { + height: number; + outboxSize: number; +} + +export interface CCMWithHeight extends CCMsg { + height: number; +} export interface CCMsFromEvents { ccms: CCMsg[]; height: number; diff --git a/framework-plugins/lisk-framework-chain-connector-plugin/src/utils.ts b/framework-plugins/lisk-framework-chain-connector-plugin/src/utils.ts index 39b17c450ce..28973add9f5 100644 --- a/framework-plugins/lisk-framework-chain-connector-plugin/src/utils.ts +++ b/framework-plugins/lisk-framework-chain-connector-plugin/src/utils.ts @@ -22,12 +22,12 @@ import { InboxJSON, Outbox, OutboxJSON, - BFTParameters, ProveResponse, } from 'lisk-sdk'; import { BFTParametersJSON, + BFTParametersWithoutGeneratorKey, CCMsFromEvents, CCMsFromEventsJSON, ProveResponseJSON, @@ -36,14 +36,6 @@ import { import { CHAIN_ID_LENGTH } from './constants'; -interface BFTParametersWithoutGeneratorKey extends Omit { - validators: { - address: Buffer; - bftWeight: bigint; - blsKey: Buffer; - }[]; -} - export const getMainchainID = (chainID: Buffer): Buffer => { const networkID = chainID.slice(0, 1); // 3 bytes for remaining chainID bytes