diff --git a/framework/src/engine/engine.ts b/framework/src/engine/engine.ts index aa427d092a..7234953878 100644 --- a/framework/src/engine/engine.ts +++ b/framework/src/engine/engine.ts @@ -137,6 +137,7 @@ export class Engine { await this._network.stop(); await this._generator.stop(); await this._consensus.stop(); + this._legacyChainHandler.stop(); this._rpcServer.stop(); this._closeDB(); this._logger.info('Engine cleanup completed'); @@ -228,6 +229,7 @@ export class Engine { const legacyEndpoint = new LegacyEndpoint({ db: this._legacyDB, + legacyConfig: this._config.legacy, }); const chainEndpoint = new ChainEndpoint({ diff --git a/framework/src/engine/legacy/codec.ts b/framework/src/engine/legacy/codec.ts index 0aa0e24e7d..b1f41f5f60 100644 --- a/framework/src/engine/legacy/codec.ts +++ b/framework/src/engine/legacy/codec.ts @@ -29,6 +29,7 @@ import { LegacyBlockHeaderWithID, LegacyTransaction, LegacyTransactionJSON, + LegacyBlockHeader, } from './types'; interface LegacyBlockSchema { @@ -43,6 +44,14 @@ export const blockSchemaMap: Record = { }, }; +export const getBlockSchema = (version: number) => { + const blockSchema = blockSchemaMap[version]; + if (!blockSchema) { + throw new Error(`Legacy block version ${version} is not registered.`); + } + return blockSchema; +}; + // Implement read version logic when adding more versions const readVersion = (): number => 2; @@ -50,10 +59,7 @@ export const decodeBlock = ( data: Buffer, ): { block: LegacyBlockWithID; schema: LegacyBlockSchema } => { const version = readVersion(); - const blockSchema = blockSchemaMap[version]; - if (!blockSchema) { - throw new Error(`Legacy block version ${version} is not registered.`); - } + const blockSchema = getBlockSchema(version); const rawBlock = codec.decode(blockSchema.block, data); const id = utils.hash(rawBlock.header); return { @@ -68,6 +74,17 @@ export const decodeBlock = ( }; }; +export const decodeBlockHeader = (blockHeader: Buffer): LegacyBlockHeaderWithID => { + const version = readVersion(); + const blockSchema = getBlockSchema(version); + const id = utils.hash(blockHeader); + + return { + ...codec.decode(blockSchema.header, blockHeader), + id, + }; +}; + export const decodeBlockJSON = ( data: Buffer, ): { block: LegacyBlockJSON; schema: LegacyBlockSchema } => { @@ -100,10 +117,7 @@ export const getLegacyTransactionJSONWithSchema = ( }; export const encodeBlock = (data: LegacyBlock): Buffer => { - const blockSchema = blockSchemaMap[data.header.version]; - if (!blockSchema) { - throw new Error(`Legacy block version ${data.header.version} is not registered.`); - } + const blockSchema = getBlockSchema(data.header.version); const headerBytes = codec.encode(blockSchema.header, data.header); return codec.encode(blockSchema.block, { @@ -112,5 +126,13 @@ export const encodeBlock = (data: LegacyBlock): Buffer => { }); }; +export const encodeBlockHeader = (blockHeader: LegacyBlockHeader): Buffer => { + const blockSchema = getBlockSchema(blockHeader.version); + return codec.encode(blockSchema.header, blockHeader); +}; + export const encodeLegacyChainBracketInfo = (data: LegacyChainBracketInfo): Buffer => codec.encode(legacyChainBracketInfoSchema, data); + +export const decodeLegacyChainBracketInfo = (data: Buffer): LegacyChainBracketInfo => + codec.decode(legacyChainBracketInfoSchema, data); diff --git a/framework/src/engine/legacy/constants.ts b/framework/src/engine/legacy/constants.ts index 128aae0ae9..2367fe6056 100644 --- a/framework/src/engine/legacy/constants.ts +++ b/framework/src/engine/legacy/constants.ts @@ -12,8 +12,16 @@ * Removal or modification of this copyright notice is prohibited. */ -export const DB_KEY_BLOCKS_ID = Buffer.from('blocks:id'); -export const DB_KEY_BLOCKS_HEIGHT = Buffer.from('blocks:height'); -export const DB_KEY_TRANSACTIONS_BLOCK_ID = Buffer.from('transactions:blockID'); -export const DB_KEY_TRANSACTIONS_ID = Buffer.from('transactions:id'); +export const DB_KEY_BLOCKS_ID = 'blocks:id'; +export const DB_KEY_BLOCKS_HEIGHT = 'blocks:height'; +export const DB_KEY_TRANSACTIONS_BLOCK_ID = 'transactions:blockID'; +export const DB_KEY_TRANSACTIONS_ID = 'transactions:id'; export const DB_KEY_LEGACY_BRACKET = Buffer.from([2]); + +// When no peer was found then resyncing after 12 seconds, 1000 * 12 ms +export const FAILED_SYNC_RETRY_TIMEOUT = 12000; +// To avoid syncing with the same peer frequently and get banned due to RPC limit, resync after 5 seconds, 5 * 1000 ms +export const SUCCESS_SYNC_RETRY_TIMEOUT = 5000; +export const MAX_FAILED_ATTEMPTS = 10; +export const ENGINE_LEGACY_MODULE_NAME = 'legacy'; +export const LOG_OBJECT_ENGINE_LEGACY_MODULE = { engineModule: ENGINE_LEGACY_MODULE_NAME }; diff --git a/framework/src/engine/legacy/endpoint.ts b/framework/src/engine/legacy/endpoint.ts index 8caf10c451..153d06cede 100644 --- a/framework/src/engine/legacy/endpoint.ts +++ b/framework/src/engine/legacy/endpoint.ts @@ -15,21 +15,29 @@ import { Database } from '@liskhq/lisk-db'; import { isHexString } from '@liskhq/lisk-validator'; import { RequestContext } from '../rpc/rpc_server'; -import { LegacyBlockJSON, LegacyTransactionJSON } from './types'; +import { + LegacyBlockJSON, + LegacyChainBracketInfoWithSnapshotBlockID, + LegacyTransactionJSON, +} from './types'; import { Storage } from './storage'; import { decodeBlockJSON, getLegacyTransactionJSONWithSchema } from './codec'; +import { LegacyConfig } from '../../types'; interface EndpointArgs { db: Database; + legacyConfig: LegacyConfig; } export class LegacyEndpoint { [key: string]: unknown; public readonly storage: Storage; + private readonly _legacyConfig: LegacyConfig; public constructor(args: EndpointArgs) { this.storage = new Storage(args.db); + this._legacyConfig = args.legacyConfig; } public async getTransactionByID(context: RequestContext): Promise { @@ -77,4 +85,21 @@ export class LegacyEndpoint { return decodeBlockJSON(await this.storage.getBlockByHeight(height)).block; } + + public async getLegacyBrackets( + _context: RequestContext, + ): Promise { + return Promise.all( + this._legacyConfig.brackets.map(async bracket => { + const bracketInfo = await this.storage.getBracketInfo( + Buffer.from(bracket.snapshotBlockID, 'hex'), + ); + + return { + ...bracketInfo, + snapshotBlockID: bracket.snapshotBlockID, + }; + }), + ); + } } diff --git a/framework/src/engine/legacy/errors.ts b/framework/src/engine/legacy/errors.ts index 5fac908499..856777e97a 100644 --- a/framework/src/engine/legacy/errors.ts +++ b/framework/src/engine/legacy/errors.ts @@ -12,9 +12,11 @@ * Removal or modification of this copyright notice is prohibited. */ -export class PeerNotFoundWithLegacyInfo extends Error { +import { FAILED_SYNC_RETRY_TIMEOUT } from './constants'; + +export class FailSyncError extends Error { public constructor(message: string) { - super(message); + super(`${message}: Attempting to sync again after ${FAILED_SYNC_RETRY_TIMEOUT} ms`); this.name = this.constructor.name; } } diff --git a/framework/src/engine/legacy/legacy_chain_handler.ts b/framework/src/engine/legacy/legacy_chain_handler.ts index 4ebbe2d66e..80aae02904 100644 --- a/framework/src/engine/legacy/legacy_chain_handler.ts +++ b/framework/src/engine/legacy/legacy_chain_handler.ts @@ -19,12 +19,19 @@ import { LegacyConfig } from '../../types'; import { Network } from '../network'; import { getBlocksFromIdResponseSchema } from '../consensus/schema'; import { Storage } from './storage'; -import { LegacyBlock, LegacyBlockBracket, Peer, LegacyChainBracketInfo } from './types'; -import { decodeBlock, encodeBlock } from './codec'; -import { PeerNotFoundWithLegacyInfo } from './errors'; +import { LegacyBlock, LegacyBlockBracket, Peer } from './types'; +import { decodeBlock, encodeBlockHeader } from './codec'; +import { FailSyncError } from './errors'; import { validateLegacyBlock } from './validate'; -import { legacyChainBracketInfoSchema } from './schemas'; import { Logger } from '../../logger'; +import { + FAILED_SYNC_RETRY_TIMEOUT, + LOG_OBJECT_ENGINE_LEGACY_MODULE, + MAX_FAILED_ATTEMPTS, + SUCCESS_SYNC_RETRY_TIMEOUT, +} from './constants'; +import { getLegacyBlocksFromIdRequestSchema } from './schemas'; +import { NETWORK_LEGACY_GET_BLOCKS_FROM_ID } from '../consensus/constants'; interface LegacyChainHandlerArgs { legacyConfig: LegacyConfig; @@ -36,11 +43,19 @@ interface LegacyHandlerInitArgs { db: Database; } +const wait = async (duration: number): Promise => + new Promise(resolve => { + const timeout = setTimeout(() => { + resolve(timeout); + }, duration); + }); + export class LegacyChainHandler { private readonly _network: Network; private _storage!: Storage; private readonly _legacyConfig: LegacyConfig; private readonly _logger: Logger; + private readonly _syncedBrackets: Buffer[] = []; private _syncTimeout!: NodeJS.Timeout; public constructor(args: LegacyChainHandlerArgs) { @@ -52,67 +67,105 @@ export class LegacyChainHandler { public async init(args: LegacyHandlerInitArgs): Promise { this._storage = new Storage(args.db); - for (const bracket of this._legacyConfig.brackets) { + for (const bracketInfo of this._legacyConfig.brackets) { try { - await this._storage.getLegacyChainBracketInfo(Buffer.from(bracket.snapshotBlockID, 'hex')); - } catch (err) { - if (!(err instanceof NotFoundError)) { - throw err; + const bracketStorageKey = Buffer.from(bracketInfo.snapshotBlockID, 'hex'); + const bracketExists = await this._storage.hasBracketInfo(bracketStorageKey); + + if (!bracketExists) { + await this._storage.setBracketInfo(bracketStorageKey, { + startHeight: bracketInfo.startHeight, + snapshotBlockHeight: bracketInfo.snapshotHeight, + // if start block already exists then assign to lastBlockHeight + lastBlockHeight: bracketInfo.snapshotHeight, + }); + continue; } - // Save config brackets in advance, these will be used in next step (`sync`) - await this._storage.setLegacyChainBracketInfo(Buffer.from(bracket.snapshotBlockID), { - startHeight: bracket.startHeight, - snapshotBlockHeight: bracket.snapshotHeight, - lastBlockHeight: bracket.snapshotHeight, + + const storedBracketInfo = await this._storage.getBracketInfo(bracketStorageKey); + const startBlock = await this._storage.getBlockByHeight(bracketInfo.startHeight); + + // In case a user wants to indirectly update the bracketInfo stored in legacyDB + await this._storage.setBracketInfo(bracketStorageKey, { + ...storedBracketInfo, + startHeight: bracketInfo.startHeight, + snapshotBlockHeight: bracketInfo.snapshotHeight, + // if start block already exists then assign to lastBlockHeight + lastBlockHeight: startBlock ? bracketInfo.startHeight : bracketInfo.snapshotHeight, }); + } catch (error) { + if (!(error instanceof NotFoundError)) { + throw error; + } } } } + public stop() { + clearTimeout(this._syncTimeout); + } + public async sync() { for (const bracket of this._legacyConfig.brackets) { - const encodedBracketInfo = await this._storage.getLegacyChainBracketInfo( + const bracketInfo = await this._storage.getBracketInfo( Buffer.from(bracket.snapshotBlockID, 'hex'), ); - const bracketInfo = codec.decode( - legacyChainBracketInfoSchema, - encodedBracketInfo, - ); // means this bracket is already synced/parsed (in next `syncBlocks` step) if (bracket.startHeight === bracketInfo.lastBlockHeight) { + this._syncedBrackets.push(Buffer.from(bracket.snapshotBlockID, 'hex')); + this._network.applyNodeInfo({ + legacy: [...this._syncedBrackets], + }); continue; } - const lastBlock = decodeBlock( - await this._storage.getBlockByHeight(bracketInfo.lastBlockHeight), - ).block; + let lastBlockID; + try { + const lastBlock = decodeBlock( + await this._storage.getBlockByHeight(bracketInfo.lastBlockHeight), + ).block; + lastBlockID = lastBlock.header.id; + } catch (error) { + if (!(error instanceof NotFoundError)) { + throw error; + } + // If lastBlock does not exist then sync from the beginning + lastBlockID = Buffer.from(bracket.snapshotBlockID, 'hex'); + } + this._logger.info( + LOG_OBJECT_ENGINE_LEGACY_MODULE, + `Started syncing legacy blocks for bracket with snapshotBlockID ${bracket.snapshotBlockID}`, + ); // start parsing bracket from `lastBlock` height` - await this._trySyncBlocks(bracket, lastBlock); + this._trySyncBlocks(bracket, lastBlockID).catch((err: Error) => + this._logger.error({ err }, 'Failed to sync block with error'), + ); } - - // when ALL brackets are synced/parsed, finally update node with it's `legacy` property - this._network.applyNodeInfo({ - legacy: this._legacyConfig.brackets.map(bracket => - Buffer.from(bracket.snapshotBlockID, 'hex'), - ), - }); - - clearTimeout(this._syncTimeout); } - private async _trySyncBlocks(bracket: LegacyBlockBracket, lastBlock: LegacyBlock) { + private async _trySyncBlocks( + bracket: LegacyBlockBracket, + lastBlockID: Buffer, + syncRetryCounter = 0, + ) { try { - await this.syncBlocks(bracket, lastBlock); - } catch (err) { - if (err instanceof PeerNotFoundWithLegacyInfo) { - // eslint-disable-next-line @typescript-eslint/no-misused-promises - this._syncTimeout = setTimeout(async () => { - await this._trySyncBlocks(bracket, lastBlock); - }, 120000); // 2 mints = (60 * 2) * 1000 + await this._syncBlocks(bracket, lastBlockID, syncRetryCounter); + } catch (error) { + if (error instanceof FailSyncError) { + this._logger.debug( + LOG_OBJECT_ENGINE_LEGACY_MODULE, + `Retrying syncing legacy blocks for bracket with snapshotBlockID ${bracket.snapshotBlockID}`, + ); + clearTimeout(this._syncTimeout); + this._syncTimeout = await wait(FAILED_SYNC_RETRY_TIMEOUT); } else { - throw err; + this._logger.debug( + { ...LOG_OBJECT_ENGINE_LEGACY_MODULE, error: (error as Error).message }, + `Retrying syncing legacy blocks for bracket with snapshotBlockID ${bracket.snapshotBlockID}`, + ); } + await this._trySyncBlocks(bracket, lastBlockID); } } @@ -132,89 +185,134 @@ export class LegacyChainHandler { * If last block height equals bracket.startHeight, simply save bracket with `lastBlockHeight: lastBlock?.header.height` */ // eslint-disable-next-line @typescript-eslint/member-ordering - public async syncBlocks(bracket: LegacyBlockBracket, legacyBlock: LegacyBlock): Promise { + private async _syncBlocks( + bracket: LegacyBlockBracket, + lastBlockID: Buffer, + failedAttempts = 0, + ): Promise { const connectedPeers = this._network.getConnectedPeers() as unknown as Peer[]; const peersWithLegacyInfo = connectedPeers.filter( peer => - !!(peer.options as { legacy: Buffer[] }).legacy.find(snapshotBlockID => - snapshotBlockID.equals(Buffer.from(bracket.snapshotBlockID, 'hex')), + !!(peer.options as { legacy: string[] }).legacy.find( + snapshotBlockID => snapshotBlockID === bracket.snapshotBlockID, ), ); - if (!peersWithLegacyInfo) { - throw new PeerNotFoundWithLegacyInfo('No peer found with legacy info.'); + if (peersWithLegacyInfo.length === 0) { + const errorMessage = 'No peer found with legacy info.'; + this._logger.warn({ ...LOG_OBJECT_ENGINE_LEGACY_MODULE, method: 'syncBlocks' }, errorMessage); + throw new FailSyncError(errorMessage); } const randomPeerIndex = Math.trunc(Math.random() * peersWithLegacyInfo.length - 1); const { peerId } = peersWithLegacyInfo[randomPeerIndex]; + const requestData = codec.encode(getLegacyBlocksFromIdRequestSchema, { + blockID: lastBlockID, + snapshotBlockID: Buffer.from(bracket.snapshotBlockID, 'hex'), + }); const p2PRequestPacket: P2PRequestPacket = { - procedure: 'getLegacyBlocksFromId', - data: legacyBlock.header.id, + procedure: NETWORK_LEGACY_GET_BLOCKS_FROM_ID, + data: requestData, }; - const response = await this._network.requestFromPeer({ ...p2PRequestPacket, peerId }); + let syncRetryCounter = failedAttempts; + let response; + try { + response = await this._network.requestFromPeer({ ...p2PRequestPacket, peerId }); + // Reset counter on success + syncRetryCounter = 0; + } catch (error) { + // eslint-disable-next-line no-param-reassign + syncRetryCounter += 1; + if (syncRetryCounter > MAX_FAILED_ATTEMPTS) { + const errorMessage = `Failed ${MAX_FAILED_ATTEMPTS} times to request from peer.`; + this._logger.warn( + { ...LOG_OBJECT_ENGINE_LEGACY_MODULE, peerId, method: 'requestFromPeer' }, + errorMessage, + ); + + throw new FailSyncError(errorMessage); + } + return this._trySyncBlocks(bracket, lastBlockID, syncRetryCounter); + } // `data` is expected to hold blocks in DESC order - const { data } = response; + const { data } = response as { data: Buffer }; let legacyBlocks: LegacyBlock[]; - const applyPenaltyAndRepeat = async (msg: string) => { - this._logger.warn({ peerId }, `${msg} Applying a penalty to the peer`); - this._network.applyPenaltyOnPeer({ peerId, penalty: 100 }); - await this.syncBlocks(bracket, legacyBlock); - }; - try { // this part is needed to make sure `data` returns ONLY `{ blocks: Buffer[] }` & not any extra field(s) - const { blocks } = codec.decode<{ blocks: Buffer[] }>( - getBlocksFromIdResponseSchema, - data as Buffer, - ); + const { blocks } = codec.decode<{ blocks: Buffer[] }>(getBlocksFromIdResponseSchema, data); if (blocks.length === 0) { - await applyPenaltyAndRepeat('Received empty response'); + this.applyPenaltyOnSyncFailure('Received empty response', peerId); + + return this._trySyncBlocks(bracket, lastBlockID, syncRetryCounter); } this._applyValidation(blocks); legacyBlocks = blocks.map(block => decodeBlock(block).block); - if (legacyBlocks.length === 0) { - await applyPenaltyAndRepeat('received empty blocks'); - } } catch (err) { - await applyPenaltyAndRepeat((err as Error).message); // catch validation error + this.applyPenaltyOnSyncFailure((err as Error).message, peerId); + + return this._trySyncBlocks(bracket, lastBlockID, syncRetryCounter); } - // @ts-expect-error Variable 'legacyBlocks' is used before being assigned. for (const block of legacyBlocks) { - if (block.header.height > bracket.startHeight) { + if (block.header.height >= bracket.startHeight) { const payload = block.payload.length ? block.payload : []; await this._storage.saveBlock( block.header.id as Buffer, block.header.height, - encodeBlock(block), + encodeBlockHeader(block.header), payload, ); } } - // @ts-expect-error Variable 'legacyBlocks' is used before being assigned. const lastBlock = legacyBlocks[legacyBlocks.length - 1]; if (lastBlock && lastBlock.header.height > bracket.startHeight) { + this._logger.debug( + LOG_OBJECT_ENGINE_LEGACY_MODULE, + `Saved blocks from ${legacyBlocks[0].header.height} to ${lastBlock.header.height}`, + ); await this._updateBracketInfo(lastBlock, bracket); - await this.syncBlocks(bracket, lastBlock); + clearTimeout(this._syncTimeout); + this._syncTimeout = await wait(SUCCESS_SYNC_RETRY_TIMEOUT); + await this._trySyncBlocks(bracket, lastBlock.header.id as Buffer, syncRetryCounter); + } else { + // Syncing is finished + this._logger.info( + LOG_OBJECT_ENGINE_LEGACY_MODULE, + `Finished syncing legacy blocks for bracket with snapshotBlockID ${bracket.snapshotBlockID}`, + ); + + // After successful sync of a bracket, communicate to the network + this._syncedBrackets.push(Buffer.from(bracket.snapshotBlockID, 'hex')); + this._network.applyNodeInfo({ + legacy: [...this._syncedBrackets], + }); } - await this._updateBracketInfo(lastBlock, bracket); + return this._updateBracketInfo(lastBlock, bracket); } private async _updateBracketInfo(lastBlock: LegacyBlock, bracket: LegacyBlockBracket) { - await this._storage.setLegacyChainBracketInfo(Buffer.from(bracket.snapshotBlockID, 'hex'), { + await this._storage.setBracketInfo(Buffer.from(bracket.snapshotBlockID, 'hex'), { startHeight: bracket.startHeight, lastBlockHeight: lastBlock?.header.height, snapshotBlockHeight: bracket.snapshotHeight, }); } + private applyPenaltyOnSyncFailure(msg: string, peerId: string) { + this._logger.warn( + { ...LOG_OBJECT_ENGINE_LEGACY_MODULE, peerId }, + `${msg}: Applying a penalty to the peer`, + ); + this._network.applyPenaltyOnPeer({ peerId, penalty: 100 }); + } + private _applyValidation(blocks: Buffer[]) { const sortedBlocks = []; for (let i = blocks.length - 1; i >= 0; i -= 1) { diff --git a/framework/src/engine/legacy/network_endpoint.ts b/framework/src/engine/legacy/network_endpoint.ts index c127182fe2..985b023cee 100644 --- a/framework/src/engine/legacy/network_endpoint.ts +++ b/framework/src/engine/legacy/network_endpoint.ts @@ -12,20 +12,18 @@ * Removal or modification of this copyright notice is prohibited. */ -import { Database } from '@liskhq/lisk-db'; +import { Database, NotFoundError } from '@liskhq/lisk-db'; import { codec } from '@liskhq/lisk-codec'; import { validator } from '@liskhq/lisk-validator'; import { Logger } from '../../logger'; import { Network } from '../network'; import { BaseNetworkEndpoint } from '../network/base_network_endpoint'; import { NETWORK_LEGACY_GET_BLOCKS_FROM_ID } from '../consensus/constants'; -import { - getBlocksFromIdRequestSchema, - getBlocksFromIdResponseSchema, - RPCBlocksByIdData, -} from '../consensus/schema'; +import { getBlocksFromIdResponseSchema } from '../consensus/schema'; import { Storage } from './storage'; import { decodeBlock } from './codec'; +import { getLegacyBlocksFromIdRequestSchema } from './schemas'; +import { RPCLegacyBlocksByIdData } from './types'; const LEGACY_BLOCKS_FROM_IDS_RATE_LIMIT_FREQUENCY = 100; @@ -49,17 +47,17 @@ export class LegacyNetworkEndpoint extends BaseNetworkEndpoint { // return 100 blocks desc starting from the id // eslint-disable-next-line @typescript-eslint/require-await - public async handleRPCGetLegacyBlocksFromID(data: unknown, peerID: string): Promise { + public async handleRPCGetLegacyBlocksFromID(data: unknown, peerId: string): Promise { this.addRateLimit( NETWORK_LEGACY_GET_BLOCKS_FROM_ID, - peerID, + peerId, LEGACY_BLOCKS_FROM_IDS_RATE_LIMIT_FREQUENCY, ); - let rpcBlocksByIdData: RPCBlocksByIdData; + let rpcBlocksByIdData: RPCLegacyBlocksByIdData; try { - rpcBlocksByIdData = codec.decode( - getBlocksFromIdRequestSchema, + rpcBlocksByIdData = codec.decode( + getLegacyBlocksFromIdRequestSchema, data as never, ); } catch (error) { @@ -67,50 +65,87 @@ export class LegacyNetworkEndpoint extends BaseNetworkEndpoint { { err: error as Error, req: data, - peerID, + peerId, }, `${NETWORK_LEGACY_GET_BLOCKS_FROM_ID} response failed on decoding. Applying a penalty to the peer`, ); this._network.applyPenaltyOnPeer({ - peerId: peerID, + peerId, penalty: 100, }); throw error; } try { - validator.validate(getBlocksFromIdRequestSchema, rpcBlocksByIdData); + validator.validate(getLegacyBlocksFromIdRequestSchema, rpcBlocksByIdData); } catch (error) { this._logger.warn( { err: error as Error, req: data, - peerID, + peerId, }, `${NETWORK_LEGACY_GET_BLOCKS_FROM_ID} response failed on validation. Applying a penalty to the peer`, ); this._network.applyPenaltyOnPeer({ - peerId: peerID, + peerId, penalty: 100, }); throw error; } - const { blockId } = rpcBlocksByIdData; + const { blockID: lastBlockID, snapshotBlockID } = rpcBlocksByIdData; - let lastBlockHeader; + let bracketInfo; try { - const block = await this._storage.getBlockByID(blockId); - lastBlockHeader = decodeBlock(block).block.header; + bracketInfo = await this._storage.getBracketInfo(snapshotBlockID); + } catch (error) { + if (!(error instanceof NotFoundError)) { + throw error; + } + // Peer should be banned if the request is coming for invalid snapshotBlockID which does not exist + // Peers should always choose peers with snapshotBlockID present in their nodeInfo + this._logger.warn( + { peerId }, + `Received invalid snapshotBlockID: Applying a penalty to the peer`, + ); + this._network.applyPenaltyOnPeer({ peerId, penalty: 100 }); + + throw error; + } + + let fromBlockHeight; + try { + // if the requested blockID is the same as snapshotBlockID then start from a block before snapshotBlock + if (snapshotBlockID.equals(lastBlockID)) { + fromBlockHeight = bracketInfo.snapshotBlockHeight; + } else { + const { + block: { + header: { height }, + }, + } = decodeBlock(await this._storage.getBlockByID(lastBlockID)); + fromBlockHeight = height; + } } catch (errors) { return codec.encode(getBlocksFromIdResponseSchema, { blocks: [] }); } - const lastBlockHeight = lastBlockHeader.height; - const fetchUntilHeight = lastBlockHeight + 100; + // we have to sync backwards so if lastBlockHeight is 171, then node responds with blocks from [71, 170] + // so lastBlockHeight = 170 and fetchFromHeight should be (lastBlockHeight - 99) = 71 + // where blocks at 71 and 170 are inclusive so in total 100 blocks + const lastBlockHeight = fromBlockHeight - 1; + const fetchFromHeight = + bracketInfo.startHeight >= lastBlockHeight - 99 + ? bracketInfo.startHeight + : lastBlockHeight - 100; + this._logger.debug( + { peerId, engineModule: 'legacy' }, + `Responding to "${NETWORK_LEGACY_GET_BLOCKS_FROM_ID}" with blocks from height ${fetchFromHeight} to ${lastBlockHeight}`, + ); const encodedBlocks = await this._storage.getBlocksByHeightBetween( + fetchFromHeight, lastBlockHeight, - fetchUntilHeight, ); return codec.encode(getBlocksFromIdResponseSchema, { blocks: encodedBlocks }); diff --git a/framework/src/engine/legacy/schemas.ts b/framework/src/engine/legacy/schemas.ts index f7867317a6..b00d2bc874 100644 --- a/framework/src/engine/legacy/schemas.ts +++ b/framework/src/engine/legacy/schemas.ts @@ -135,3 +135,24 @@ export const legacyChainBracketInfoSchema = { }, required: ['startHeight', 'snapshotBlockHeight', 'lastBlockHeight'], }; + +export const getLegacyBlocksFromIdRequestSchema = { + $id: '/legacy/getBlocksFromIdRequest', + title: 'Get Blocks From Id Request', + type: 'object', + required: ['blockID', 'snapshotBlockID'], + properties: { + blockID: { + fieldNumber: 1, + dataType: 'bytes', + minLength: 32, + maxLength: 32, + }, + snapshotBlockID: { + fieldNumber: 2, + dataType: 'bytes', + minLength: 32, + maxLength: 32, + }, + }, +}; diff --git a/framework/src/engine/legacy/storage.ts b/framework/src/engine/legacy/storage.ts index 36d42e09e4..8ce020d415 100644 --- a/framework/src/engine/legacy/storage.ts +++ b/framework/src/engine/legacy/storage.ts @@ -12,9 +12,10 @@ * Removal or modification of this copyright notice is prohibited. */ -import { Batch, Database } from '@liskhq/lisk-db'; +import { Batch, Database, NotFoundError } from '@liskhq/lisk-db'; import { utils } from '@liskhq/lisk-cryptography'; -import { encodeLegacyChainBracketInfo } from './codec'; +import { codec } from '@liskhq/lisk-codec'; +import { decodeLegacyChainBracketInfo, encodeLegacyChainBracketInfo } from './codec'; import { LegacyChainBracketInfo } from './types'; import { buildBlockIDDbKey, @@ -23,6 +24,7 @@ import { buildLegacyBracketDBKey, buildTxsBlockIDDbKey, } from './utils'; +import { blockSchemaV2 } from './schemas'; export class Storage { private readonly _db: Database; @@ -37,7 +39,20 @@ export class Storage { } public async getBlockByID(id: Buffer): Promise { - return this._db.get(buildBlockIDDbKey(id)); + const blockHeader = await this._db.get(buildBlockIDDbKey(id)); + let payload: Buffer[] = []; + try { + payload = await this.getTransactionsByBlockID(id); + } catch (error) { + if (!(error instanceof NotFoundError)) { + throw error; + } + } + + return codec.encode(blockSchemaV2, { + header: blockHeader, + payload, + }); } public async getBlockByHeight(height: number): Promise { @@ -70,7 +85,7 @@ export class Storage { // each txID is hashed value of 32 length const idLength = 32; for (let i = 0; i < txIdsBuffer.length; i += idLength) { - const txId = txIdsBuffer.subarray(i, (i += idLength)); + const txId = txIdsBuffer.subarray(i, i + idLength); txIds.push(txId); } @@ -102,11 +117,13 @@ export class Storage { await this._db.write(batch); } - public async getLegacyChainBracketInfo(snapshotBlockID: Buffer): Promise { - return this._db.get(buildLegacyBracketDBKey(snapshotBlockID)); + public async getBracketInfo(snapshotBlockID: Buffer): Promise { + const encodedBracketInfo = await this._db.get(buildLegacyBracketDBKey(snapshotBlockID)); + + return decodeLegacyChainBracketInfo(encodedBracketInfo); } - public async setLegacyChainBracketInfo( + public async setBracketInfo( snapshotBlockID: Buffer, bracketInfo: LegacyChainBracketInfo, ): Promise { @@ -116,6 +133,20 @@ export class Storage { ); } + public async hasBracketInfo(snapshotBlockID: Buffer): Promise { + try { + const bracketInfo = await this.getBracketInfo(snapshotBlockID); + + return !!bracketInfo; + } catch (error) { + if (!(error instanceof NotFoundError)) { + throw error; + } + + return false; + } + } + private async _getBlockIDsBetweenHeights( fromHeight: number, toHeight: number, diff --git a/framework/src/engine/legacy/types.ts b/framework/src/engine/legacy/types.ts index 1e7d7d99e5..a6070c47ad 100644 --- a/framework/src/engine/legacy/types.ts +++ b/framework/src/engine/legacy/types.ts @@ -73,9 +73,18 @@ export interface LegacyChainBracketInfo { lastBlockHeight: number; } +export interface LegacyChainBracketInfoWithSnapshotBlockID extends LegacyChainBracketInfo { + snapshotBlockID: string; +} + export interface Peer { readonly peerId: string; readonly options: { - readonly legacy: Buffer[]; + readonly legacy: string[]; }; } + +export interface RPCLegacyBlocksByIdData { + readonly blockID: Buffer; + readonly snapshotBlockID: Buffer; +} diff --git a/framework/src/engine/legacy/utils.ts b/framework/src/engine/legacy/utils.ts index 5550722577..adf61e67a0 100644 --- a/framework/src/engine/legacy/utils.ts +++ b/framework/src/engine/legacy/utils.ts @@ -21,15 +21,17 @@ import { } from './constants'; // INFO: Here ID refers to hashed value of 32 length -export const buildTxIDDbKey = (id: Buffer): Buffer => Buffer.concat([DB_KEY_TRANSACTIONS_ID, id]); +export const buildTxIDDbKey = (id: Buffer): Buffer => + Buffer.from(`${DB_KEY_TRANSACTIONS_ID}:${id.toString('binary')}`); + +export const buildBlockIDDbKey = (id: Buffer): Buffer => + Buffer.from(`${DB_KEY_BLOCKS_ID}:${id.toString('binary')}`); -export const buildBlockIDDbKey = (id: Buffer): Buffer => Buffer.concat([DB_KEY_BLOCKS_ID, id]); export const buildTxsBlockIDDbKey = (id: Buffer): Buffer => - Buffer.concat([DB_KEY_TRANSACTIONS_BLOCK_ID, id]); + Buffer.from(`${DB_KEY_TRANSACTIONS_BLOCK_ID}:${id.toString('binary')}`); -// INFO: Generated Buffer is further used as `ID` for ```getBlockByID (ID:Buffer)``` export const buildBlockHeightDbKey = (height: number): Buffer => - Buffer.concat([DB_KEY_BLOCKS_HEIGHT, utils.intToBuffer(height, 4)]); + Buffer.from(`${DB_KEY_BLOCKS_HEIGHT}:${utils.intToBuffer(height, 4).toString('binary')}`); export const buildLegacyBracketDBKey = (snapshotBlockID: Buffer): Buffer => Buffer.concat([DB_KEY_LEGACY_BRACKET, snapshotBlockID]); diff --git a/framework/test/unit/engine/legacy/endpoint.spec.ts b/framework/test/unit/engine/legacy/endpoint.spec.ts index 70ee718940..0a6af6282e 100644 --- a/framework/test/unit/engine/legacy/endpoint.spec.ts +++ b/framework/test/unit/engine/legacy/endpoint.spec.ts @@ -24,18 +24,39 @@ import { transactionSchemaV2, } from '../../../../src/engine/legacy/schemas'; import { LegacyBlockJSON, LegacyTransactionJSON } from '../../../../src/engine/legacy/types'; +import { Storage } from '../../../../src/engine/legacy/storage'; const bufferToHex = (b: Buffer) => Buffer.from(b).toString('hex'); +const randomSnapshotBlockID = utils.getRandomBytes(20); +const expectedSnapshotBlockID = utils.getRandomBytes(20); describe('Legacy endpoint', () => { + const { header, payload } = blockFixtures[0]; let encodedBlock: Buffer; let legacyEndpoint: LegacyEndpoint; beforeEach(() => { - legacyEndpoint = new LegacyEndpoint({ db: new InMemoryDatabase() as any }); + legacyEndpoint = new LegacyEndpoint({ + db: new InMemoryDatabase() as any, + legacyConfig: { + sync: true, + brackets: [ + { + startHeight: 0, + snapshotBlockID: randomSnapshotBlockID.toString('hex'), + snapshotHeight: 100, + }, + { + startHeight: 16270306, + snapshotBlockID: expectedSnapshotBlockID.toString('hex'), + snapshotHeight: 16270316, + }, + ], + }, + }); encodedBlock = codec.encode(blockSchemaV2, { - header: codec.encode(blockHeaderSchemaV2, blockFixtures[0].header), - payload: blockFixtures[0].payload, + header: codec.encode(blockHeaderSchemaV2, header), + payload, }); jest.spyOn(legacyEndpoint.storage, 'getBlockByID').mockResolvedValue(encodedBlock); @@ -44,25 +65,19 @@ describe('Legacy endpoint', () => { describe('LegacyEndpoint', () => { const matchBlockExpectations = (block: LegacyBlockJSON) => { - expect(block.header.id).toEqual(bufferToHex(blockFixtures[0].header.id)); - expect(block.header.version).toEqual(blockFixtures[0].header.version); - expect(block.header.timestamp).toEqual(blockFixtures[0].header.timestamp); - expect(block.header.height).toEqual(blockFixtures[0].header.height); - expect(block.header.previousBlockID).toEqual( - bufferToHex(blockFixtures[0].header.previousBlockID), - ); - expect(block.header.transactionRoot).toEqual( - bufferToHex(blockFixtures[0].header.transactionRoot), - ); - expect(block.header.generatorPublicKey).toEqual( - bufferToHex(blockFixtures[0].header.generatorPublicKey), - ); - expect(BigInt(block.header.reward as number)).toEqual(blockFixtures[0].header.reward); - expect(block.header.asset).toEqual(bufferToHex(blockFixtures[0].header.asset)); - expect(block.header.signature).toEqual(bufferToHex(blockFixtures[0].header.signature)); - - expect(block.payload).toHaveLength(blockFixtures[0].payload.length); - expect(block.payload[0]).toEqual(bufferToHex(blockFixtures[0].payload[0])); + expect(block.header.id).toEqual(bufferToHex(header.id)); + expect(block.header.version).toEqual(header.version); + expect(block.header.timestamp).toEqual(header.timestamp); + expect(block.header.height).toEqual(header.height); + expect(block.header.previousBlockID).toEqual(bufferToHex(header.previousBlockID)); + expect(block.header.transactionRoot).toEqual(bufferToHex(header.transactionRoot)); + expect(block.header.generatorPublicKey).toEqual(bufferToHex(header.generatorPublicKey)); + expect(BigInt(block.header.reward as number)).toEqual(header.reward); + expect(block.header.asset).toEqual(bufferToHex(header.asset)); + expect(block.header.signature).toEqual(bufferToHex(header.signature)); + + expect(block.payload).toHaveLength(payload.length); + expect(block.payload[0]).toEqual(bufferToHex(payload[0])); }; const matchTxExpectations = ( @@ -83,7 +98,7 @@ describe('Legacy endpoint', () => { it('getBlockByID', async () => { const block = await legacyEndpoint.getBlockByID({ - params: { id: bufferToHex(blockFixtures[0].header.id) }, + params: { id: bufferToHex(header.id) }, } as any); matchBlockExpectations(block); @@ -91,14 +106,14 @@ describe('Legacy endpoint', () => { it('getBlockByHeight', async () => { const block = await legacyEndpoint.getBlockByHeight({ - params: { height: blockFixtures[0].header.height }, + params: { height: header.height }, } as any); matchBlockExpectations(block); }); it('getTransactionByID', async () => { - const tx = blockFixtures[0].payload[0]; + const tx = payload[0]; jest.spyOn(legacyEndpoint['storage'], 'getTransactionByID').mockResolvedValue(tx); const txId = utils.hash(tx).toString('hex'); @@ -110,8 +125,8 @@ describe('Legacy endpoint', () => { }); it('getTransactionsByBlockID', async () => { - const blockId = blockFixtures[0].header.id; - const tx = blockFixtures[0].payload[0]; + const blockId = header.id; + const tx = payload[0]; const txId = utils.hash(tx).toString('hex'); jest.spyOn(legacyEndpoint['storage'], 'getTransactionsByBlockID').mockResolvedValue([tx]); @@ -123,5 +138,43 @@ describe('Legacy endpoint', () => { expect(transactions).toBeArray(); matchTxExpectations(transactions[0], tx, txId); }); + + it('getLegacyBrackets', async () => { + const blockId = header.id; + const legacyConfig = { + sync: true, + brackets: [ + { + startHeight: header.height - 200, + snapshotBlockID: blockId.toString('hex'), + snapshotHeight: header.height, + }, + ], + }; + + const legacyStorage = new Storage(new InMemoryDatabase() as any); + await legacyStorage.setBracketInfo(blockId, { + startHeight: header.height - 200, + lastBlockHeight: header.height - 100, + snapshotBlockHeight: header.height, + }); + legacyEndpoint = new LegacyEndpoint({ + db: legacyStorage as any, + legacyConfig, + }); + + (legacyEndpoint as any)['storage'] = legacyStorage; + + const brackets = await legacyEndpoint.getLegacyBrackets({} as any); + + expect(brackets).toEqual([ + { + startHeight: legacyConfig.brackets[0].startHeight, + snapshotBlockID: legacyConfig.brackets[0].snapshotBlockID, + snapshotBlockHeight: header.height, + lastBlockHeight: header.height - 100, + }, + ]); + }); }); }); diff --git a/framework/test/unit/engine/legacy/fixtures.ts b/framework/test/unit/engine/legacy/fixtures.ts index 1b5e0baf1c..5534e25e4b 100644 --- a/framework/test/unit/engine/legacy/fixtures.ts +++ b/framework/test/unit/engine/legacy/fixtures.ts @@ -14,7 +14,7 @@ import { utils } from '@liskhq/lisk-cryptography'; import { regularMerkleTree } from '@liskhq/lisk-tree'; -import { encodeBlock } from '../../../../src/engine/legacy/codec'; +import { encodeBlock, encodeBlockHeader } from '../../../../src/engine/legacy/codec'; import { LegacyBlockHeader, LegacyBlockWithID } from '../../../../src/engine/legacy/types'; // Version 2 blocks @@ -354,18 +354,23 @@ export const createFakeLegacyBlockHeaderV2 = ( * @params start: Start height of the block range going backwards * @params numberOfBlocks: Number of blocks to be generated with decreasing height */ -export const getLegacyBlocksRangeV2 = (startHeight: number, numberOfBlocks: number): Buffer[] => { - const blocks: LegacyBlockWithID[] = []; +export const getLegacyBlockHeadersRangeV2 = ( + startHeight: number, + numberOfBlocks: number, +): Buffer[] => { + const blockHeaders: LegacyBlockHeader[] = []; for (let i = startHeight; i >= startHeight - numberOfBlocks; i -= 1) { // After the startHeight, all the blocks are generated with previousBlockID as previous height block ID const block = createFakeLegacyBlockHeaderV2({ height: i, previousBlockID: - i === startHeight ? utils.getRandomBytes(32) : blocks[startHeight - i - 1].header.id, + i === startHeight + ? utils.getRandomBytes(32) + : (blockHeaders[startHeight - i - 1].id as Buffer), }); - blocks.push(block); + blockHeaders.push(block.header); } - return blocks.map(b => encodeBlock(b)); + return blockHeaders.map(b => encodeBlockHeader(b)); }; diff --git a/framework/test/unit/engine/legacy/legacy_chain_handler.spec.ts b/framework/test/unit/engine/legacy/legacy_chain_handler.spec.ts index 15c52b8c85..c82dcf092d 100644 --- a/framework/test/unit/engine/legacy/legacy_chain_handler.spec.ts +++ b/framework/test/unit/engine/legacy/legacy_chain_handler.spec.ts @@ -15,10 +15,11 @@ import { utils } from '@liskhq/lisk-cryptography'; import { codec } from '@liskhq/lisk-codec'; +import { InMemoryDatabase } from '@liskhq/lisk-db'; import { LegacyConfig } from '../../../../src'; import { LegacyChainHandler } from '../../../../src/engine/legacy/legacy_chain_handler'; import { Network } from '../../../../src/engine/network'; -import { encodeBlock, encodeLegacyChainBracketInfo } from '../../../../src/engine/legacy/codec'; +import { encodeBlock } from '../../../../src/engine/legacy/codec'; import { Peer, LegacyBlock } from '../../../../src/engine/legacy/types'; import { getBlocksFromIdResponseSchema } from '../../../../src/engine/consensus/schema'; import { blockFixtures } from './fixtures'; @@ -27,7 +28,6 @@ import { fakeLogger } from '../../../utils/mocks'; const randomSnapshotBlockID = utils.getRandomBytes(20); const expectedSnapshotBlockID = utils.getRandomBytes(20); -// https://lisk.observer/block/19583716 describe('Legacy Chain Handler', () => { let legacyChainHandler: LegacyChainHandler; let legacyConfig: LegacyConfig; @@ -39,11 +39,6 @@ describe('Legacy Chain Handler', () => { legacyConfig = { sync: true, brackets: [ - { - startHeight: 0, - snapshotBlockID: randomSnapshotBlockID.toString('hex'), - snapshotHeight: 100, - }, { startHeight: 16270306, snapshotBlockID: expectedSnapshotBlockID.toString('hex'), @@ -55,13 +50,13 @@ describe('Legacy Chain Handler', () => { { peerId: 'peerId-1', options: { - legacy: [expectedSnapshotBlockID], + legacy: [expectedSnapshotBlockID.toString('hex')], }, }, { peerId: 'peerId-2', options: { - legacy: [randomSnapshotBlockID, expectedSnapshotBlockID], + legacy: [randomSnapshotBlockID.toString('hex'), expectedSnapshotBlockID.toString('hex')], }, }, ]; @@ -74,48 +69,16 @@ describe('Legacy Chain Handler', () => { legacyChainHandler = new LegacyChainHandler({ legacyConfig, network, logger: fakeLogger }); await legacyChainHandler.init({ - db: { get: jest.fn(), write: jest.fn(), set: jest.fn() } as never, + db: new InMemoryDatabase() as never, }); jest.spyOn(legacyChainHandler['_network'], 'getConnectedPeers').mockImplementation(() => { return peers as any; }); - jest - .spyOn(legacyChainHandler['_storage'], 'getLegacyChainBracketInfo') - .mockReturnValueOnce( - encodeLegacyChainBracketInfo({ - startHeight: 0, - snapshotBlockHeight: 0, - lastBlockHeight: 0, - }) as any, // this means this bracket is already synced, since it's lastBlockHeight equals bracket's startHeight - ) - .mockReturnValueOnce( - encodeLegacyChainBracketInfo({ - startHeight: 16270306, - snapshotBlockHeight: 16270316, - lastBlockHeight: 16270316, - }) as any, - ); - jest .spyOn(legacyChainHandler['_storage'], 'getBlockByHeight') .mockReturnValueOnce(encodeBlock(legacyBlock16270316) as any); // we want to return blocks from this height ONCE - - // `getLegacyBlocksFromId` should return blocks in DESC order (starting from 16270316 (excluding) till 16270306) - const reversedFixtures = blockFixtures - .slice(0, blockFixtures.length - 1) - .sort((a, b) => b.header.height - a.header.height); - const encodedBlocks = reversedFixtures.map(block => encodeBlock(block)); - - jest - .spyOn(network, 'requestFromPeer') - .mockReturnValueOnce({ - data: codec.encode(getBlocksFromIdResponseSchema, { blocks: encodedBlocks }), - } as any) - .mockReturnValueOnce({ - data: [], - } as any); }); describe('constructor', () => { @@ -125,20 +88,106 @@ describe('Legacy Chain Handler', () => { }); describe('sync', () => { + beforeEach(() => { + // `getLegacyBlocksFromId` should return blocks in DESC order (starting from 16270316 (excluding) till 16270306) + const reversedFixtures = blockFixtures + .slice(0, blockFixtures.length - 1) + .sort((a, b) => b.header.height - a.header.height); + const encodedBlocks = reversedFixtures.map(block => encodeBlock(block)); + + jest + .spyOn(network, 'requestFromPeer') + .mockReturnValueOnce({ + data: codec.encode(getBlocksFromIdResponseSchema, { blocks: encodedBlocks }), + } as any) + .mockReturnValueOnce({ + data: codec.encode(getBlocksFromIdResponseSchema, { blocks: [] }), + } as any) + .mockReturnValueOnce({ + data: codec.encode(getBlocksFromIdResponseSchema, { blocks: [] }), + } as any); + }); it('should sync blocks in range for given config brackets', async () => { jest.spyOn(legacyChainHandler['_storage'], 'saveBlock'); - jest.spyOn(legacyChainHandler['_storage'], 'setLegacyChainBracketInfo'); + jest.spyOn(legacyChainHandler['_storage'], 'setBracketInfo'); jest.spyOn(legacyChainHandler['_network'], 'applyNodeInfo'); + jest.spyOn(legacyChainHandler as any, '_trySyncBlocks'); await legacyChainHandler.sync(); - // starting from 16270316 (excluding) till 16270306 = 10, // but we save blocks only if ```block.header.height > bracket.startHeight``` - expect(legacyChainHandler['_storage'].saveBlock).toHaveBeenCalledTimes(9); + expect(legacyChainHandler['_trySyncBlocks']).toHaveBeenCalledTimes(1); + }); + }); + + describe('_syncBlocks', () => { + let reversedFixtures; + let encodedBlocks: any[]; + beforeEach(() => { + reversedFixtures = blockFixtures + .slice(0, blockFixtures.length - 1) + .sort((a, b) => b.header.height - a.header.height); + encodedBlocks = reversedFixtures.map(block => encodeBlock(block)); + }); + it('should sync blocks in range for given config brackets', async () => { + jest + .spyOn(network, 'requestFromPeer') + .mockReturnValueOnce({ + data: codec.encode(getBlocksFromIdResponseSchema, { blocks: encodedBlocks }), + } as any) + .mockReturnValueOnce({ + data: codec.encode(getBlocksFromIdResponseSchema, { blocks: [] }), + } as any) + .mockReturnValueOnce({ + data: codec.encode(getBlocksFromIdResponseSchema, { blocks: [] }), + } as any); + jest.spyOn(legacyChainHandler['_storage'], 'saveBlock'); + jest.spyOn(legacyChainHandler['_storage'], 'setBracketInfo'); + jest.spyOn(legacyChainHandler['_network'], 'applyNodeInfo'); + jest.spyOn(legacyChainHandler as any, '_trySyncBlocks'); + await legacyChainHandler['_syncBlocks']( + legacyConfig.brackets[0], + legacyBlock16270316.header.id as Buffer, + 0, + ); + + expect(legacyChainHandler['_storage'].saveBlock).toHaveBeenCalledTimes(10); // should be 1, since if `lastBlock.header.height > bracket.startHeight` is skipped // & only the final `_updateBracketInfo(...)` is called - expect(legacyChainHandler['_storage'].setLegacyChainBracketInfo).toHaveBeenCalledTimes(1); + expect(legacyChainHandler['_storage'].setBracketInfo).toHaveBeenCalledTimes(1); + }); + + it('should throw error when no peers are found', async () => { + jest.spyOn(legacyChainHandler['_network'], 'getConnectedPeers').mockImplementation(() => []); + await expect( + legacyChainHandler['_syncBlocks']( + legacyConfig.brackets[0], + legacyBlock16270316.header.id as Buffer, + 0, + ), + ).rejects.toThrow('No peer found with legacy info.: Attempting to sync again after 12000 ms'); + }); + + it('should throw error when no peers are found with legacy data', async () => { + jest.spyOn(legacyChainHandler['_network'], 'getConnectedPeers').mockImplementation( + () => + [ + { + peerId: 'peerId-1', + options: { + legacy: [randomSnapshotBlockID.toString('hex')], + }, + }, + ] as any, + ); + await expect( + legacyChainHandler['_syncBlocks']( + legacyConfig.brackets[0], + legacyBlock16270316.header.id as Buffer, + 0, + ), + ).rejects.toThrow('No peer found with legacy info.: Attempting to sync again after 12000 ms'); }); }); }); diff --git a/framework/test/unit/engine/legacy/network_endpoint.spec.ts b/framework/test/unit/engine/legacy/network_endpoint.spec.ts index fb85b62f61..0a98e673ec 100644 --- a/framework/test/unit/engine/legacy/network_endpoint.spec.ts +++ b/framework/test/unit/engine/legacy/network_endpoint.spec.ts @@ -19,13 +19,11 @@ import { codec } from '@liskhq/lisk-codec'; import { LegacyNetworkEndpoint } from '../../../../src/engine/legacy/network_endpoint'; import { loggerMock } from '../../../../src/testing/mocks'; import { Network } from '../../../../src/engine/network'; -import { - getBlocksFromIdRequestSchema, - getBlocksFromIdResponseSchema, -} from '../../../../src/engine/consensus/schema'; +import { getBlocksFromIdResponseSchema } from '../../../../src/engine/consensus/schema'; -import { getLegacyBlocksRangeV2 } from './fixtures'; -import { decodeBlock, encodeBlock } from '../../../../src/engine/legacy/codec'; +import { getLegacyBlockHeadersRangeV2 } from './fixtures'; +import { decodeBlockHeader } from '../../../../src/engine/legacy/codec'; +import { getLegacyBlocksFromIdRequestSchema } from '../../../../src/engine/legacy/schemas'; describe('Legacy P2P network endpoint', () => { const defaultPeerID = 'peer-id'; @@ -65,40 +63,51 @@ describe('Legacy P2P network endpoint', () => { }); it("should return empty list if ID doesn't exist", async () => { - const blockId = utils.getRandomBytes(32); - const blockIds = codec.encode(getBlocksFromIdRequestSchema, { - blockId, + const blockID = utils.getRandomBytes(32); + const snapshotBlockID = utils.getRandomBytes(32); + const requestPayload = codec.encode(getLegacyBlocksFromIdRequestSchema, { + blockID, + snapshotBlockID, }); - const blocks = await endpoint.handleRPCGetLegacyBlocksFromID(blockIds, defaultPeerID); + await endpoint._storage.setBracketInfo(snapshotBlockID, { + lastBlockHeight: 100, + snapshotBlockHeight: 200, + startHeight: 50, + }); + const blocks = await endpoint.handleRPCGetLegacyBlocksFromID(requestPayload, defaultPeerID); expect(blocks).toEqual(codec.encode(getBlocksFromIdResponseSchema, { blocks: [] })); }); it('should return 100 blocks from the requested ID', async () => { - const startHeight = 110; + const requestedHeight = 110; // 100 blocks including the requested block ID - const blocks = getLegacyBlocksRangeV2(startHeight, 99); - - const requestedBlock = decodeBlock(blocks[0]).block; + const blockHeaders = getLegacyBlockHeadersRangeV2(requestedHeight, 100); - const { - header: { id, ...blockHeader }, - payload, - } = requestedBlock; + const requestedBlockHeader = decodeBlockHeader(blockHeaders[0]); - const requestedBlockWithoutID = { header: { ...blockHeader }, payload }; - - const encodedBlockWithoutID = encodeBlock(requestedBlockWithoutID); - const requestedBlockID = utils.hash(encodedBlockWithoutID); + const { id: requestedBlockID } = requestedBlockHeader; // Save blocks to the database - for (let i = 0; i < blocks.length; i += 1) { - const block = blocks[i]; - await endpoint['_storage'].saveBlock(utils.hash(block), startHeight + i, block, []); + for (let i = 0; i < blockHeaders.length; i += 1) { + const blockHeader = blockHeaders[i]; + await endpoint['_storage'].saveBlock( + utils.hash(blockHeader), + requestedHeight - i, + blockHeader, + [], + ); } - const encodedRequest = codec.encode(getBlocksFromIdRequestSchema, { - blockId: requestedBlockID, + const snapshotBlockID = utils.getRandomBytes(32); + const encodedRequest = codec.encode(getLegacyBlocksFromIdRequestSchema, { + blockID: requestedBlockID, + snapshotBlockID, } as never); + await endpoint._storage.setBracketInfo(snapshotBlockID, { + lastBlockHeight: 100, + snapshotBlockHeight: 200, + startHeight: requestedHeight - 101, + }); const blocksReceived = await endpoint.handleRPCGetLegacyBlocksFromID( encodedRequest, defaultPeerID, diff --git a/framework/test/unit/engine/legacy/storage.spec.ts b/framework/test/unit/engine/legacy/storage.spec.ts index d18c7961f8..1d4f59a6e8 100644 --- a/framework/test/unit/engine/legacy/storage.spec.ts +++ b/framework/test/unit/engine/legacy/storage.spec.ts @@ -15,7 +15,7 @@ import { Batch, Database, InMemoryDatabase } from '@liskhq/lisk-db'; import { utils } from '@liskhq/lisk-cryptography'; -import { encodeBlock, encodeLegacyChainBracketInfo } from '../../../../src/engine/legacy/codec'; +import { encodeBlock, encodeBlockHeader } from '../../../../src/engine/legacy/codec'; import { Storage } from '../../../../src/engine/legacy/storage'; import { blockFixtures } from './fixtures'; import { buildBlockHeightDbKey, buildBlockIDDbKey } from '../../../../src/engine/legacy/utils'; @@ -36,8 +36,8 @@ describe('Legacy storage', () => { for (const block of blocks) { const { header, payload } = block; - batch.set(buildBlockIDDbKey(header.id), encodeBlock({ header, payload })); - batch.set(buildBlockHeightDbKey(header.height), header.id); + const encodedHeader = encodeBlockHeader(header); + await storage.saveBlock(header.id, header.height, encodedHeader, payload); } await db.write(batch); @@ -60,8 +60,9 @@ describe('Legacy storage', () => { }); it('should throw error if block with given id does not exist', async () => { - await expect(storage.getBlockByID(Buffer.alloc(0))).rejects.toThrow( - `Specified key 626c6f636b733a6964 does not exist`, + const randomBlockID = utils.hash(utils.getRandomBytes(1)); + await expect(storage.getBlockByID(randomBlockID)).rejects.toThrow( + `Specified key ${buildBlockIDDbKey(randomBlockID).toString('hex')} does not exist`, ); }); }); @@ -76,7 +77,7 @@ describe('Legacy storage', () => { it('should throw an error if the block is not found', async () => { await expect(storage.getBlockByHeight(100)).rejects.toThrow( - `Specified key 626c6f636b733a68656967687400000064 does not exist`, + `Specified key ${buildBlockHeightDbKey(100).toString('hex')} does not exist`, ); }); }); @@ -124,7 +125,7 @@ describe('Legacy storage', () => { describe('saveBlock', () => { it("should save the block along with it's transactions", async () => { const { header, payload } = blockFixtures[0]; - await storage.saveBlock(header.id, header.height, encodeBlock({ header, payload }), payload); + await storage.saveBlock(header.id, header.height, encodeBlockHeader(header), payload); const result = await storage.getBlockByID(header.id); expect(result).toEqual(encodeBlock({ header, payload })); @@ -138,7 +139,7 @@ describe('Legacy storage', () => { it("should save the block without it's transactions", async () => { const { header, payload } = blockFixtures[0]; - await storage.saveBlock(header.id, header.height, encodeBlock({ header, payload }), []); + await storage.saveBlock(header.id, header.height, encodeBlockHeader(header), payload); const result = await storage.getBlockByID(header.id); expect(result).toEqual(encodeBlock({ header, payload })); @@ -166,15 +167,15 @@ describe('Legacy storage', () => { lastBlockHeight: header.height, }; - await storage.setLegacyChainBracketInfo(header.id, bracketInfo); + await storage.setBracketInfo(header.id, bracketInfo); - const result = await storage.getLegacyChainBracketInfo(header.id); + const result = await storage.getBracketInfo(header.id); - expect(result).toEqual(encodeLegacyChainBracketInfo(bracketInfo)); + expect(result).toEqual(bracketInfo); }); it('should throw error if block with given id does not exist', async () => { - await expect(storage.getLegacyChainBracketInfo(Buffer.alloc(0))).rejects.toThrow( + await expect(storage.getBracketInfo(Buffer.alloc(0))).rejects.toThrow( `Specified key 02 does not exist`, ); });