diff --git a/elements/lisk-chain/src/block.ts b/elements/lisk-chain/src/block.ts index 6106b96f381..dfe3259d7fc 100644 --- a/elements/lisk-chain/src/block.ts +++ b/elements/lisk-chain/src/block.ts @@ -13,6 +13,7 @@ */ import { codec } from '@liskhq/lisk-codec'; +import { BlockAssets } from './block_assets'; import { BlockHeader } from './block_header'; import { blockSchema } from './schema'; import { Transaction } from './transaction'; @@ -20,35 +21,45 @@ import { Transaction } from './transaction'; interface BlockAttrs { header: Buffer; payload: Buffer[]; + assets: Buffer[]; } export class Block { // eslint-disable-next-line no-useless-constructor - public constructor(public readonly header: BlockHeader, public readonly payload: Transaction[]) { + public constructor( + public readonly header: BlockHeader, + public readonly payload: Transaction[], + public readonly assets: BlockAssets, + ) { // No body necessary } public static fromBytes(value: Buffer): Block { - const { header, payload } = codec.decode(blockSchema, value); + const { header, payload, assets } = codec.decode(blockSchema, value); return new Block( BlockHeader.fromBytes(header), payload.map(v => Transaction.fromBytes(v)), + BlockAssets.fromBytes(assets), ); } public static fromJSON(value: Record): Block { - const { header, payload } = value; + const { header, payload, assets } = value; if (typeof header !== 'object') { throw new Error('Invalid block format. header must be an object.'); } if (!Array.isArray(payload)) { throw new Error('Invalid block format. payload must be an array.'); } + if (!Array.isArray(assets)) { + throw new Error('Invalid block format. assets must be an array.'); + } return new Block( BlockHeader.fromJSON(value.header as Record), payload.map(v => Transaction.fromBytes(v)), + BlockAssets.fromJSON(assets), ); } @@ -56,6 +67,7 @@ export class Block { return codec.encode(blockSchema, { header: this.header.getBytes(), payload: this.payload.map(p => p.getBytes()), + assets: this.assets.getBytes(), }); } diff --git a/elements/lisk-chain/src/block_assets.ts b/elements/lisk-chain/src/block_assets.ts new file mode 100644 index 00000000000..dccd54c59cd --- /dev/null +++ b/elements/lisk-chain/src/block_assets.ts @@ -0,0 +1,61 @@ +/* + * Copyright © 2021 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { codec } from '@liskhq/lisk-codec'; +import { blockAssetSchema } from './schema'; + +export interface BlockAsset { + moduleID: number; + data: Buffer; +} + +export class BlockAssets { + private readonly _assets: BlockAsset[] = []; + + public constructor(assets: BlockAsset[] = []) { + this._assets = assets; + } + + public static fromBytes(values: ReadonlyArray): BlockAssets { + const assets = values.map(val => codec.decode(blockAssetSchema, val)); + const blockAssets = new BlockAssets(assets); + return blockAssets; + } + + public static fromJSON(values: Record[]): BlockAssets { + const assets = values.map(val => codec.fromJSON(blockAssetSchema, val)); + return new BlockAssets(assets); + } + + public getBytes(): Buffer[] { + return this._assets.map(asset => codec.encode(blockAssetSchema, asset)); + } + + public getAsset(moduleID: number): Buffer | undefined { + return this._assets.find(a => a.moduleID === moduleID)?.data; + } + + public setAsset(moduleID: number, value: Buffer): void { + const asset = this.getAsset(moduleID); + if (asset) { + throw new Error(`Module asset for "${moduleID}" is already set.`); + } + + this._assets.push({ moduleID, data: value }); + } + + public sort(): void { + this._assets.sort((a1, a2) => a1.moduleID - a2.moduleID); + } +} diff --git a/elements/lisk-chain/src/block_header.ts b/elements/lisk-chain/src/block_header.ts index b683dc44f60..944bcdda3c4 100644 --- a/elements/lisk-chain/src/block_header.ts +++ b/elements/lisk-chain/src/block_header.ts @@ -12,27 +12,29 @@ * Removal or modification of this copyright notice is prohibited. */ -import { objects } from '@liskhq/lisk-utils'; import { signDataWithPrivateKey, hash, verifyData } from '@liskhq/lisk-cryptography'; import { codec } from '@liskhq/lisk-codec'; import { validator, LiskValidationError } from '@liskhq/lisk-validator'; import { TAG_BLOCK_HEADER } from './constants'; import { blockHeaderSchema, blockHeaderSchemaWithId, signingBlockHeaderSchema } from './schema'; -export interface BlockHeaderAsset { - moduleID: number; - data: Buffer; -} - export interface BlockHeaderAttrs { readonly version: number; readonly height: number; readonly generatorAddress: Buffer; readonly previousBlockID: Buffer; readonly timestamp: number; + readonly maxHeightPrevoted: number; + readonly maxHeightGenerated: number; + readonly aggregateCommit: { + readonly height: number; + readonly aggregationBits: Buffer; + readonly certificateSignature: Buffer; + }; + readonly validatorsHash?: Buffer; readonly stateRoot?: Buffer; readonly transactionRoot?: Buffer; - readonly assets: ReadonlyArray; + readonly assetsRoot?: Buffer; signature?: Buffer; id?: Buffer; } @@ -43,9 +45,17 @@ export interface BlockHeaderJSON { readonly height: number; readonly generatorAddress: string; readonly previousBlockID: string; + readonly maxHeightPrevoted: number; + readonly maxHeightGenerated: number; + readonly aggregateCommit: { + readonly height: number; + readonly aggregationBits: string; + readonly certificateSignature: string; + }; + readonly validatorsHash: string; readonly stateRoot: string; readonly transactionRoot: string; - readonly assets: ReadonlyArray; + readonly assetsRoot: string; readonly signature: string; readonly id: string; } @@ -56,9 +66,17 @@ export class BlockHeader { public readonly generatorAddress: Buffer; public readonly previousBlockID: Buffer; public readonly timestamp: number; + public readonly maxHeightPrevoted: number; + public readonly maxHeightGenerated: number; + public readonly aggregateCommit: { + readonly height: number; + readonly aggregationBits: Buffer; + readonly certificateSignature: Buffer; + }; + private _validatorsHash?: Buffer; private _stateRoot?: Buffer; private _transactionRoot?: Buffer; - private readonly _assets: BlockHeaderAsset[]; + private _assetsRoot?: Buffer; private _signature?: Buffer; private _id?: Buffer; @@ -68,20 +86,28 @@ export class BlockHeader { height, generatorAddress, previousBlockID, + maxHeightPrevoted, + maxHeightGenerated, + aggregateCommit, + validatorsHash, stateRoot, + assetsRoot, transactionRoot, signature, id, - assets, }: BlockHeaderAttrs) { this.version = version; this.height = height; this.generatorAddress = generatorAddress; this.previousBlockID = previousBlockID; this.timestamp = timestamp; + this.maxHeightPrevoted = maxHeightPrevoted; + this.maxHeightGenerated = maxHeightGenerated; + this.aggregateCommit = aggregateCommit; + this._validatorsHash = validatorsHash; this._stateRoot = stateRoot; this._transactionRoot = transactionRoot; - this._assets = objects.cloneDeep([...assets]); + this._assetsRoot = assetsRoot; this._signature = signature; this._id = id; @@ -104,6 +130,15 @@ export class BlockHeader { this._resetComputedValues(); } + public get assetsRoot() { + return this._assetsRoot; + } + + public set assetsRoot(val) { + this._assetsRoot = val; + this._resetComputedValues(); + } + public get transactionRoot() { return this._transactionRoot; } @@ -113,6 +148,15 @@ export class BlockHeader { this._resetComputedValues(); } + public get validatorsHash() { + return this._validatorsHash; + } + + public set validatorsHash(val) { + this._validatorsHash = val; + this._resetComputedValues(); + } + public getBytes(): Buffer { return codec.encode(blockHeaderSchema, this._getBlockHeaderProps()); } @@ -121,7 +165,7 @@ export class BlockHeader { return codec.toJSON(blockHeaderSchemaWithId, this._getAllProps()); } - public toObject(): BlockHeaderAttrs { + public toObject(): Required { return this._getAllProps(); } @@ -165,20 +209,6 @@ export class BlockHeader { ); } - public getAsset(moduleID: number): Buffer | undefined { - return this._assets.find(a => a.moduleID === moduleID)?.data; - } - - public setAsset(moduleID: number, value: Buffer): void { - const asset = this.getAsset(moduleID); - if (asset) { - throw new Error(`Module asset for "${moduleID}" is already set.`); - } - - this._assets.push({ moduleID, data: value }); - this._resetComputedValues(); - } - public get signature(): Buffer { if (!this._signature) { throw new Error('Block header is not signed.'); @@ -210,15 +240,31 @@ export class BlockHeader { } private _getSigningProps() { + if (!this.assetsRoot) { + throw new Error('Asset root is empty.'); + } + if (!this.stateRoot) { + throw new Error('State root is empty.'); + } + if (!this.transactionRoot) { + throw new Error('Transaction root is empty.'); + } + if (!this.validatorsHash) { + throw new Error('Validators hash is empty.'); + } return { version: this.version, timestamp: this.timestamp, height: this.height, previousBlockID: this.previousBlockID, stateRoot: this.stateRoot, + assetsRoot: this.assetsRoot, transactionRoot: this.transactionRoot, + validatorsHash: this.validatorsHash, + aggregateCommit: this.aggregateCommit, generatorAddress: this.generatorAddress, - assets: this._assets, + maxHeightPrevoted: this.maxHeightPrevoted, + maxHeightGenerated: this.maxHeightGenerated, }; } diff --git a/elements/lisk-chain/src/data_access/data_access.ts b/elements/lisk-chain/src/data_access/data_access.ts index 3705fe3a7b9..bde8994c14f 100644 --- a/elements/lisk-chain/src/data_access/data_access.ts +++ b/elements/lisk-chain/src/data_access/data_access.ts @@ -20,6 +20,7 @@ import { Block } from '../block'; import { BlockCache } from './cache'; import { Storage as StorageAccess } from './storage'; import { StateStore } from '../state_store'; +import { BlockAssets } from '../block_assets'; interface DAConstructor { readonly db: KVStore; @@ -299,6 +300,7 @@ export class DataAccess { finalizedHeight, encodedHeader, encodedPayload, + block.assets.getBytes(), stateStore, removeFromTemp, ); @@ -312,12 +314,21 @@ export class DataAccess { const { id: blockID, height } = block.header; const txIDs = block.payload.map(tx => tx.id); const encodedBlock = block.getBytes(); - await this._storage.deleteBlock(blockID, height, txIDs, encodedBlock, stateStore, saveToTemp); + await this._storage.deleteBlock( + blockID, + height, + txIDs, + block.assets.getBytes(), + encodedBlock, + stateStore, + saveToTemp, + ); } private _decodeRawBlock(block: RawBlock): Block { const header = BlockHeader.fromBytes(block.header); const transactions = block.payload.map(txBytes => Transaction.fromBytes(txBytes)); - return new Block(header, transactions); + const assets = BlockAssets.fromBytes(block.assets); + return new Block(header, transactions, assets); } } diff --git a/elements/lisk-chain/src/data_access/storage.ts b/elements/lisk-chain/src/data_access/storage.ts index ddaadea1858..ea3e2cca876 100644 --- a/elements/lisk-chain/src/data_access/storage.ts +++ b/elements/lisk-chain/src/data_access/storage.ts @@ -25,10 +25,40 @@ import { DB_KEY_TEMPBLOCKS_HEIGHT, DB_KEY_DIFF_STATE, DB_KEY_FINALIZED_HEIGHT, + DB_KEY_BLOCK_ASSETS_BLOCK_ID, } from '../db_keys'; import { concatDBKeys } from '../utils'; import { stateDiffSchema } from '../schema'; +const bytesArraySchema = { + $id: 'lisk-chain/bytesarray', + type: 'object', + required: ['list'], + properties: { + list: { + type: 'array', + fieldNumber: 1, + items: { + dataType: 'bytes', + }, + }, + }, +}; + +interface ByteArrayContainer { + list: Buffer[]; +} + +const decodeByteArray = (val: Buffer): Buffer[] => { + const decoded = codec.decode(bytesArraySchema, val); + return decoded.list; +}; + +const encodeByteArray = (val: Buffer[]): Buffer => { + const encoded = codec.encode(bytesArraySchema, { list: val }); + return codec.encode(bytesArraySchema, encoded); +}; + export class Storage { private readonly _db: KVStore; @@ -141,10 +171,12 @@ export class Storage { public async getBlockByID(id: Buffer): Promise { const blockHeader = await this.getBlockHeaderByID(id); const transactions = await this._getTransactions(id); + const assets = await this._getBlockAssets(id); return { header: blockHeader, payload: transactions, + assets, }; } @@ -170,10 +202,12 @@ export class Storage { const header = await this.getBlockHeaderByHeight(height); const blockID = hash(header); const transactions = await this._getTransactions(blockID); + const assets = await this._getBlockAssets(blockID); return { header, payload: transactions, + assets, }; } @@ -183,7 +217,8 @@ export class Storage { for (const header of headers) { const blockID = hash(header); const transactions = await this._getTransactions(blockID); - blocks.push({ header, payload: transactions }); + const assets = await this._getBlockAssets(blockID); + blocks.push({ header, payload: transactions, assets }); } return blocks; @@ -193,10 +228,12 @@ export class Storage { const header = await this.getLastBlockHeader(); const blockID = hash(header); const transactions = await this._getTransactions(blockID); + const assets = await this._getBlockAssets(blockID); return { header, payload: transactions, + assets, }; } @@ -303,6 +340,7 @@ export class Storage { finalizedHeight: number, header: Buffer, payload: { id: Buffer; value: Buffer }[], + assets: Buffer[], stateStore: StateStore, removeFromTemp = false, ): Promise { @@ -318,6 +356,10 @@ export class Storage { } batch.put(concatDBKeys(DB_KEY_TRANSACTIONS_BLOCK_ID, id), Buffer.concat(ids)); } + if (assets.length > 0) { + const encodedAsset = encodeByteArray(assets); + batch.put(concatDBKeys(DB_KEY_BLOCK_ASSETS_BLOCK_ID, id), encodedAsset); + } if (removeFromTemp) { batch.del(concatDBKeys(DB_KEY_TEMPBLOCKS_HEIGHT, heightBuf)); } @@ -336,6 +378,7 @@ export class Storage { id: Buffer, height: number, txIDs: Buffer[], + assets: Buffer[], fullBlock: Buffer, stateStore: StateStore, saveToTemp = false, @@ -350,6 +393,9 @@ export class Storage { } batch.del(concatDBKeys(DB_KEY_TRANSACTIONS_BLOCK_ID, id)); } + if (assets.length > 0) { + batch.del(concatDBKeys(DB_KEY_BLOCK_ASSETS_BLOCK_ID, id)); + } if (saveToTemp) { batch.put(concatDBKeys(DB_KEY_TEMPBLOCKS_HEIGHT, heightBuf), fullBlock); } @@ -398,6 +444,18 @@ export class Storage { }); } + private async _getBlockAssets(blockID: Buffer): Promise { + try { + const encodedAssets = await this._db.get(concatDBKeys(DB_KEY_BLOCK_ASSETS_BLOCK_ID, blockID)); + return decodeByteArray(encodedAssets); + } catch (error) { + if (!(error instanceof NotFoundError)) { + throw error; + } + return []; + } + } + private async _getTransactions(blockID: Buffer): Promise { const txIDs: Buffer[] = []; try { diff --git a/elements/lisk-chain/src/db_keys.ts b/elements/lisk-chain/src/db_keys.ts index bf858be31fe..9666e88315e 100644 --- a/elements/lisk-chain/src/db_keys.ts +++ b/elements/lisk-chain/src/db_keys.ts @@ -18,6 +18,7 @@ export const DB_KEY_BLOCKS_HEIGHT = Buffer.from([4]); export const DB_KEY_TRANSACTIONS_BLOCK_ID = Buffer.from([5]); export const DB_KEY_TRANSACTIONS_ID = Buffer.from([6]); export const DB_KEY_TEMPBLOCKS_HEIGHT = Buffer.from([7]); +export const DB_KEY_BLOCK_ASSETS_BLOCK_ID = Buffer.from([8]); export const DB_KEY_STATE_STORE = Buffer.from([10]); diff --git a/elements/lisk-chain/src/index.ts b/elements/lisk-chain/src/index.ts index 244d41128d1..cfb043c5dc4 100644 --- a/elements/lisk-chain/src/index.ts +++ b/elements/lisk-chain/src/index.ts @@ -33,5 +33,6 @@ export { concatDBKeys } from './utils'; export { StateStore } from './state_store'; export { Block } from './block'; -export { BlockHeader, BlockHeaderAttrs, BlockHeaderAsset, BlockHeaderJSON } from './block_header'; +export { BlockAsset, BlockAssets } from './block_assets'; +export { BlockHeader, BlockHeaderAttrs, BlockHeaderJSON } from './block_header'; export { DataAccess } from './data_access'; diff --git a/elements/lisk-chain/src/schema.ts b/elements/lisk-chain/src/schema.ts index 3f96359b929..e59a57ed72b 100644 --- a/elements/lisk-chain/src/schema.ts +++ b/elements/lisk-chain/src/schema.ts @@ -26,30 +26,48 @@ export const blockSchema = { }, fieldNumber: 2, }, + assets: { + type: 'array', + items: { + dataType: 'bytes', + }, + fieldNumber: 3, + }, }, - required: ['header', 'payload'], + required: ['header', 'payload', 'assets'], }; export const signingBlockHeaderSchema = { - $id: '/block/header/signing/2', + $id: '/block/header/signing/3', type: 'object', properties: { version: { dataType: 'uint32', fieldNumber: 1 }, timestamp: { dataType: 'uint32', fieldNumber: 2 }, height: { dataType: 'uint32', fieldNumber: 3 }, previousBlockID: { dataType: 'bytes', fieldNumber: 4 }, - stateRoot: { dataType: 'bytes', fieldNumber: 5 }, + generatorAddress: { dataType: 'bytes', fieldNumber: 5 }, transactionRoot: { dataType: 'bytes', fieldNumber: 6 }, - generatorAddress: { dataType: 'bytes', fieldNumber: 7 }, - assets: { - type: 'array', - fieldNumber: 8, - items: { - type: 'object', - required: ['moduleID', 'data'], - properties: { - moduleID: { dataType: 'uint32', fieldNumber: 1 }, - data: { dataType: 'bytes', fieldNumber: 2 }, + assetsRoot: { dataType: 'bytes', fieldNumber: 7 }, + stateRoot: { dataType: 'bytes', fieldNumber: 8 }, + maxHeightPrevoted: { dataType: 'uint32', fieldNumber: 9 }, + maxHeightGenerated: { dataType: 'uint32', fieldNumber: 10 }, + validatorsHash: { dataType: 'bytes', fieldNumber: 11 }, + aggregateCommit: { + type: 'object', + fieldNumber: 12, + required: ['height', 'aggregationBits', 'certificateSignature'], + properties: { + height: { + dataType: 'uint32', + fieldNumber: 1, + }, + aggregationBits: { + dataType: 'bytes', + fieldNumber: 2, + }, + certificateSignature: { + dataType: 'bytes', + fieldNumber: 3, }, }, }, @@ -59,30 +77,50 @@ export const signingBlockHeaderSchema = { 'timestamp', 'height', 'previousBlockID', - 'stateRoot', - 'transactionRoot', 'generatorAddress', - 'assets', + 'transactionRoot', + 'assetsRoot', + 'stateRoot', + 'maxHeightPrevoted', + 'maxHeightGenerated', + 'validatorsHash', + 'aggregateCommit', ], }; export const blockHeaderSchema = { ...signingBlockHeaderSchema, - $id: '/block/header/2', + $id: '/block/header/3', required: [...signingBlockHeaderSchema.required, 'signature'], properties: { ...signingBlockHeaderSchema.properties, - signature: { dataType: 'bytes', fieldNumber: 9 }, + signature: { dataType: 'bytes', fieldNumber: 13 }, }, }; export const blockHeaderSchemaWithId = { ...blockHeaderSchema, - $id: '/block/header/2', + $id: '/block/header/3', required: [...blockHeaderSchema.required, 'id'], properties: { ...blockHeaderSchema.properties, - id: { dataType: 'bytes', fieldNumber: 10 }, + id: { dataType: 'bytes', fieldNumber: 14 }, + }, +}; + +export const blockAssetSchema = { + $id: '/block/asset/3', + type: 'object', + required: ['moduleID', 'data'], + properties: { + moduleID: { + dataType: 'uint32', + fieldNumber: 1, + }, + data: { + dataType: 'bytes', + fieldNumber: 2, + }, }, }; diff --git a/elements/lisk-chain/src/types.ts b/elements/lisk-chain/src/types.ts index 5ffcde9f69d..b105e4e1596 100644 --- a/elements/lisk-chain/src/types.ts +++ b/elements/lisk-chain/src/types.ts @@ -21,6 +21,7 @@ export interface BlockRewardOptions { export interface RawBlock { header: Buffer; payload: ReadonlyArray; + assets: ReadonlyArray; } export interface DiffHistory { diff --git a/elements/lisk-chain/test/integration/data_access/blocks.spec.ts b/elements/lisk-chain/test/integration/data_access/blocks.spec.ts index 40fd8a42115..435c45211e1 100644 --- a/elements/lisk-chain/test/integration/data_access/blocks.spec.ts +++ b/elements/lisk-chain/test/integration/data_access/blocks.spec.ts @@ -16,10 +16,11 @@ import * as path from 'path'; import * as fs from 'fs-extra'; import { KVStore, formatInt, NotFoundError } from '@liskhq/lisk-db'; import { codec } from '@liskhq/lisk-codec'; +import { getRandomBytes } from '@liskhq/lisk-cryptography'; import { Storage } from '../../../src/data_access/storage'; import { createValidDefaultBlock } from '../../utils/block'; import { getTransaction } from '../../utils/transaction'; -import { Block, StateStore, Transaction } from '../../../src'; +import { Block, BlockAssets, StateStore, Transaction } from '../../../src'; import { DataAccess } from '../../../src/data_access'; import { stateDiffSchema } from '../../../src/schema'; import { @@ -67,9 +68,13 @@ describe('dataAccess.blocks', () => { const block302 = await createValidDefaultBlock({ header: { height: 302 }, payload: [getTransaction({ nonce: BigInt(1) }), getTransaction({ nonce: BigInt(2) })], + assets: new BlockAssets([{ moduleID: 3, data: getRandomBytes(64) }]), }); - const block303 = await createValidDefaultBlock({ header: { height: 303 } }); + const block303 = await createValidDefaultBlock({ + header: { height: 303 }, + assets: new BlockAssets([{ moduleID: 3, data: getRandomBytes(64) }]), + }); blocks = [block300, block301, block302, block303]; const batch = db.batch(); @@ -276,6 +281,7 @@ describe('dataAccess.blocks', () => { expect(block.header.toObject()).toStrictEqual(blocks[0].header.toObject()); expect(block.payload[0]).toBeInstanceOf(Transaction); expect(block.payload[0].id).toStrictEqual(blocks[0].payload[0].id); + expect(block.assets.getAsset(3)).toEqual(blocks[0].assets.getAsset(3)); }); }); diff --git a/elements/lisk-chain/test/unit/__snapshots__/block_header.spec.ts.snap b/elements/lisk-chain/test/unit/__snapshots__/block_header.spec.ts.snap index 454a7f618e0..cc2085383de 100644 --- a/elements/lisk-chain/test/unit/__snapshots__/block_header.spec.ts.snap +++ b/elements/lisk-chain/test/unit/__snapshots__/block_header.spec.ts.snap @@ -2,48 +2,62 @@ exports[`block_header blockHeaderSchema should be valid schema with signature but without id 1`] = ` Object { - "$id": "/block/header/2", + "$id": "/block/header/3", "properties": Object { - "assets": Object { - "fieldNumber": 8, - "items": Object { - "properties": Object { - "data": Object { - "dataType": "bytes", - "fieldNumber": 2, - }, - "moduleID": Object { - "dataType": "uint32", - "fieldNumber": 1, - }, + "aggregateCommit": Object { + "fieldNumber": 12, + "properties": Object { + "aggregationBits": Object { + "dataType": "bytes", + "fieldNumber": 2, + }, + "certificateSignature": Object { + "dataType": "bytes", + "fieldNumber": 3, + }, + "height": Object { + "dataType": "uint32", + "fieldNumber": 1, }, - "required": Array [ - "moduleID", - "data", - ], - "type": "object", }, - "type": "array", + "required": Array [ + "height", + "aggregationBits", + "certificateSignature", + ], + "type": "object", }, - "generatorAddress": Object { + "assetsRoot": Object { "dataType": "bytes", "fieldNumber": 7, }, + "generatorAddress": Object { + "dataType": "bytes", + "fieldNumber": 5, + }, "height": Object { "dataType": "uint32", "fieldNumber": 3, }, + "maxHeightGenerated": Object { + "dataType": "uint32", + "fieldNumber": 10, + }, + "maxHeightPrevoted": Object { + "dataType": "uint32", + "fieldNumber": 9, + }, "previousBlockID": Object { "dataType": "bytes", "fieldNumber": 4, }, "signature": Object { "dataType": "bytes", - "fieldNumber": 9, + "fieldNumber": 13, }, "stateRoot": Object { "dataType": "bytes", - "fieldNumber": 5, + "fieldNumber": 8, }, "timestamp": Object { "dataType": "uint32", @@ -53,6 +67,10 @@ Object { "dataType": "bytes", "fieldNumber": 6, }, + "validatorsHash": Object { + "dataType": "bytes", + "fieldNumber": 11, + }, "version": Object { "dataType": "uint32", "fieldNumber": 1, @@ -63,10 +81,14 @@ Object { "timestamp", "height", "previousBlockID", - "stateRoot", - "transactionRoot", "generatorAddress", - "assets", + "transactionRoot", + "assetsRoot", + "stateRoot", + "maxHeightPrevoted", + "maxHeightGenerated", + "validatorsHash", + "aggregateCommit", "signature", ], "type": "object", @@ -75,52 +97,66 @@ Object { exports[`block_header blockHeaderSchemaWithId should be valid schema with signature and id 1`] = ` Object { - "$id": "/block/header/2", + "$id": "/block/header/3", "properties": Object { - "assets": Object { - "fieldNumber": 8, - "items": Object { - "properties": Object { - "data": Object { - "dataType": "bytes", - "fieldNumber": 2, - }, - "moduleID": Object { - "dataType": "uint32", - "fieldNumber": 1, - }, + "aggregateCommit": Object { + "fieldNumber": 12, + "properties": Object { + "aggregationBits": Object { + "dataType": "bytes", + "fieldNumber": 2, + }, + "certificateSignature": Object { + "dataType": "bytes", + "fieldNumber": 3, + }, + "height": Object { + "dataType": "uint32", + "fieldNumber": 1, }, - "required": Array [ - "moduleID", - "data", - ], - "type": "object", }, - "type": "array", + "required": Array [ + "height", + "aggregationBits", + "certificateSignature", + ], + "type": "object", }, - "generatorAddress": Object { + "assetsRoot": Object { "dataType": "bytes", "fieldNumber": 7, }, + "generatorAddress": Object { + "dataType": "bytes", + "fieldNumber": 5, + }, "height": Object { "dataType": "uint32", "fieldNumber": 3, }, "id": Object { "dataType": "bytes", + "fieldNumber": 14, + }, + "maxHeightGenerated": Object { + "dataType": "uint32", "fieldNumber": 10, }, + "maxHeightPrevoted": Object { + "dataType": "uint32", + "fieldNumber": 9, + }, "previousBlockID": Object { "dataType": "bytes", "fieldNumber": 4, }, "signature": Object { "dataType": "bytes", - "fieldNumber": 9, + "fieldNumber": 13, }, "stateRoot": Object { "dataType": "bytes", - "fieldNumber": 5, + "fieldNumber": 8, }, "timestamp": Object { "dataType": "uint32", @@ -130,6 +166,10 @@ Object { "dataType": "bytes", "fieldNumber": 6, }, + "validatorsHash": Object { + "dataType": "bytes", + "fieldNumber": 11, + }, "version": Object { "dataType": "uint32", "fieldNumber": 1, @@ -140,10 +180,14 @@ Object { "timestamp", "height", "previousBlockID", - "stateRoot", - "transactionRoot", "generatorAddress", - "assets", + "transactionRoot", + "assetsRoot", + "stateRoot", + "maxHeightPrevoted", + "maxHeightGenerated", + "validatorsHash", + "aggregateCommit", "signature", "id", ], @@ -153,44 +197,58 @@ Object { exports[`block_header signingBlockHeaderSchema should be valid schema without signature and id 1`] = ` Object { - "$id": "/block/header/signing/2", + "$id": "/block/header/signing/3", "properties": Object { - "assets": Object { - "fieldNumber": 8, - "items": Object { - "properties": Object { - "data": Object { - "dataType": "bytes", - "fieldNumber": 2, - }, - "moduleID": Object { - "dataType": "uint32", - "fieldNumber": 1, - }, + "aggregateCommit": Object { + "fieldNumber": 12, + "properties": Object { + "aggregationBits": Object { + "dataType": "bytes", + "fieldNumber": 2, + }, + "certificateSignature": Object { + "dataType": "bytes", + "fieldNumber": 3, + }, + "height": Object { + "dataType": "uint32", + "fieldNumber": 1, }, - "required": Array [ - "moduleID", - "data", - ], - "type": "object", }, - "type": "array", + "required": Array [ + "height", + "aggregationBits", + "certificateSignature", + ], + "type": "object", }, - "generatorAddress": Object { + "assetsRoot": Object { "dataType": "bytes", "fieldNumber": 7, }, + "generatorAddress": Object { + "dataType": "bytes", + "fieldNumber": 5, + }, "height": Object { "dataType": "uint32", "fieldNumber": 3, }, + "maxHeightGenerated": Object { + "dataType": "uint32", + "fieldNumber": 10, + }, + "maxHeightPrevoted": Object { + "dataType": "uint32", + "fieldNumber": 9, + }, "previousBlockID": Object { "dataType": "bytes", "fieldNumber": 4, }, "stateRoot": Object { "dataType": "bytes", - "fieldNumber": 5, + "fieldNumber": 8, }, "timestamp": Object { "dataType": "uint32", @@ -200,6 +258,10 @@ Object { "dataType": "bytes", "fieldNumber": 6, }, + "validatorsHash": Object { + "dataType": "bytes", + "fieldNumber": 11, + }, "version": Object { "dataType": "uint32", "fieldNumber": 1, @@ -210,10 +272,14 @@ Object { "timestamp", "height", "previousBlockID", - "stateRoot", - "transactionRoot", "generatorAddress", - "assets", + "transactionRoot", + "assetsRoot", + "stateRoot", + "maxHeightPrevoted", + "maxHeightGenerated", + "validatorsHash", + "aggregateCommit", ], "type": "object", } diff --git a/elements/lisk-chain/test/unit/block_assets.spec.ts b/elements/lisk-chain/test/unit/block_assets.spec.ts new file mode 100644 index 00000000000..3531802140a --- /dev/null +++ b/elements/lisk-chain/test/unit/block_assets.spec.ts @@ -0,0 +1,75 @@ +/* + * Copyright © 2021 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 { getRandomBytes } from '@liskhq/lisk-cryptography'; +import { BlockAssets } from '../../src'; + +describe('block assets', () => { + let assets: BlockAssets; + + beforeEach(() => { + assets = new BlockAssets([ + { + moduleID: 6, + data: getRandomBytes(64), + }, + { + moduleID: 3, + data: getRandomBytes(64), + }, + ]); + }); + + describe('sort', () => { + it('should sort the assets in ascending order by module ID', () => { + assets.sort(); + expect(assets['_assets'][0].moduleID).toEqual(3); + }); + }); + + describe('getAsset', () => { + it('it should return undefined if no matching asset exists ', () => { + expect(assets.getAsset(5)).toBeUndefined(); + }); + + it('it should return asset data if matching asset exists ', () => { + expect(assets.getAsset(3)).toBeInstanceOf(Buffer); + }); + }); + + describe('setAsset', () => { + it('it should not overwrite existing asset', () => { + const data = getRandomBytes(32); + expect(() => assets.setAsset(3, data)).toThrow(); + }); + + it('it should add asset data if matching asset does not exist ', () => { + const data = getRandomBytes(32); + assets.setAsset(4, data); + expect(assets['_assets']).toHaveLength(3); + }); + }); + + describe('fromJSON', () => { + it('should create BlockAssets from JSON format', () => { + assets = BlockAssets.fromJSON([ + { + moduleID: 4, + data: getRandomBytes(30).toString('hex'), + }, + ]); + expect(assets['_assets']).toHaveLength(1); + expect(assets.getAsset(4)).toBeInstanceOf(Buffer); + }); + }); +}); diff --git a/elements/lisk-chain/test/unit/block_header.spec.ts b/elements/lisk-chain/test/unit/block_header.spec.ts index 476d3a28fc1..26be7a42e46 100644 --- a/elements/lisk-chain/test/unit/block_header.spec.ts +++ b/elements/lisk-chain/test/unit/block_header.spec.ts @@ -11,6 +11,7 @@ * * Removal or modification of this copyright notice is prohibited. */ +import { hash } from '@liskhq/lisk-cryptography'; import { BlockHeader } from '../../src/block_header'; import { blockHeaderSchema, @@ -25,21 +26,26 @@ const getBlockAttrs = () => ({ previousBlockID: Buffer.from('4a462ea57a8c9f72d866c09770e5ec70cef18727', 'hex'), stateRoot: Buffer.from('7f9d96a09a3fd17f3478eb7bef3a8bda00e1238b', 'hex'), transactionRoot: Buffer.from('b27ca21f40d44113c2090ca8f05fb706c54e87dd', 'hex'), + assetsRoot: Buffer.from('b27ca21f40d44113c2090ca8f05fb706c54e87dd', 'hex'), generatorAddress: Buffer.from('be63fb1c0426573352556f18b21efd5b6183c39c', 'hex'), - assets: [ - { moduleID: 1, data: Buffer.from('aab2fe3588d6f4ff3d735fb998faf5e80a4da5d4', 'hex') }, - { moduleID: 2, data: Buffer.from('b19531270ec52845bd51ed897d8d14d86bd5a1f8', 'hex') }, - ], + maxHeightPrevoted: 1000988, + maxHeightGenerated: 1000988, + validatorsHash: hash(Buffer.alloc(0)), + aggregateCommit: { + height: 0, + aggregationBits: Buffer.alloc(0), + certificateSignature: Buffer.alloc(0), + }, signature: Buffer.from('6da88e2fd4435e26e02682435f108002ccc3ddd5', 'hex'), }); const blockId = Buffer.from( - '9c80710aed045e039a9d6ddd638de52bfc69c722891c0f28654e3f69ed2b139a', + '097ce5adc1a34680d6c939287011dec9b70a3bc1f5f896a3f9024fc9bed59992', 'hex', ); const blockHeaderBytes = Buffer.from( - '080110c4d23d18c4d23d22144a462ea57a8c9f72d866c09770e5ec70cef187272a147f9d96a09a3fd17f3478eb7bef3a8bda00e1238b3214b27ca21f40d44113c2090ca8f05fb706c54e87dd3a14be63fb1c0426573352556f18b21efd5b6183c39c421808011214aab2fe3588d6f4ff3d735fb998faf5e80a4da5d4421808021214b19531270ec52845bd51ed897d8d14d86bd5a1f84a146da88e2fd4435e26e02682435f108002ccc3ddd5', + '080110c4d23d18c4d23d22144a462ea57a8c9f72d866c09770e5ec70cef187272a14be63fb1c0426573352556f18b21efd5b6183c39c3214b27ca21f40d44113c2090ca8f05fb706c54e87dd3a14b27ca21f40d44113c2090ca8f05fb706c54e87dd42147f9d96a09a3fd17f3478eb7bef3a8bda00e1238b489c8c3d509c8c3d5a20e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b8556206080012001a006a146da88e2fd4435e26e02682435f108002ccc3ddd5', 'hex', ); @@ -48,10 +54,14 @@ const blockHeaderProps = [ 'timestamp', 'height', 'previousBlockID', - 'stateRoot', - 'transactionRoot', 'generatorAddress', - 'assets', + 'transactionRoot', + 'assetsRoot', + 'stateRoot', + 'maxHeightPrevoted', + 'maxHeightGenerated', + 'validatorsHash', + 'aggregateCommit', ]; describe('block_header', () => { @@ -104,9 +114,12 @@ describe('block_header', () => { expect(blockHeader.generatorAddress).toEqual(data.generatorAddress); expect(blockHeader.previousBlockID).toEqual(data.previousBlockID); expect(blockHeader.stateRoot).toEqual(data.stateRoot); + expect(blockHeader.validatorsHash).toEqual(data.validatorsHash); + expect(blockHeader.aggregateCommit).toEqual(data.aggregateCommit); + expect(blockHeader.maxHeightPrevoted).toEqual(data.maxHeightPrevoted); + expect(blockHeader.maxHeightGenerated).toEqual(data.maxHeightGenerated); + expect(blockHeader.assetsRoot).toEqual(data.assetsRoot); expect(blockHeader.transactionRoot).toEqual(data.transactionRoot); - expect(blockHeader.getAsset(1)).toEqual(data.assets[0].data); - expect(blockHeader.getAsset(2)).toEqual(data.assets[1].data); expect(blockHeader.id).toEqual(blockId); expect(blockHeader.signature).toEqual(data.signature); }); @@ -160,21 +173,5 @@ describe('block_header', () => { expect(blockHeader.id).toEqual(blockId); }); }); - - describe('getAsset', () => { - it('should get relevant module asset if exists', () => { - const data = getBlockAttrs(); - const blockHeader = new BlockHeader(data); - - expect(blockHeader.getAsset(1)).toEqual(data.assets[0].data); - }); - - it('should return undefined if module asset does not exists', () => { - const data = getBlockAttrs(); - const blockHeader = new BlockHeader(data); - - expect(blockHeader.getAsset(3)).toBeUndefined(); - }); - }); }); }); diff --git a/elements/lisk-chain/test/unit/data_access/data_access.spec.ts b/elements/lisk-chain/test/unit/data_access/data_access.spec.ts index c0494d8c310..b9e6b0f5ff8 100644 --- a/elements/lisk-chain/test/unit/data_access/data_access.spec.ts +++ b/elements/lisk-chain/test/unit/data_access/data_access.spec.ts @@ -25,7 +25,7 @@ import { DB_KEY_TEMPBLOCKS_HEIGHT, } from '../../../src/db_keys'; import { Block } from '../../../src/block'; -import { BlockHeader } from '../../../src'; +import { BlockAssets, BlockHeader } from '../../../src'; jest.mock('@liskhq/lisk-db'); @@ -340,7 +340,7 @@ describe('data_access', () => { // Assert expect(db.createReadStream).toHaveBeenCalledTimes(1); - expect(db.get).toHaveBeenCalledTimes(2); + expect(db.get).toHaveBeenCalledTimes(3); }); }); @@ -363,7 +363,7 @@ describe('data_access', () => { // Assert expect(db.createReadStream).toHaveBeenCalledTimes(1); - expect(db.get).toHaveBeenCalledTimes(2); + expect(db.get).toHaveBeenCalledTimes(3); }); }); @@ -490,7 +490,7 @@ describe('data_access', () => { const blocks = []; for (let i = 0; i < 5; i += 1) { - block = new Block(createFakeBlockHeader({ height: i + 10 }), []); + block = new Block(createFakeBlockHeader({ height: i + 10 }), [], new BlockAssets()); blocks.push(block); dataAccess.addBlockHeader(block.header); } diff --git a/elements/lisk-chain/test/utils/block.ts b/elements/lisk-chain/test/utils/block.ts index b6fd420bb53..ef7fb787933 100644 --- a/elements/lisk-chain/test/utils/block.ts +++ b/elements/lisk-chain/test/utils/block.ts @@ -22,7 +22,7 @@ import { Mnemonic } from '@liskhq/lisk-passphrase'; import { MerkleTree } from '@liskhq/lisk-tree'; import * as genesis from '../fixtures/genesis_block.json'; import { Transaction } from '../../src/transaction'; -import { Block, BlockHeader } from '../../src'; +import { Block, BlockAssets, BlockHeader } from '../../src'; import { BlockHeaderAttrs } from '../../src/block_header'; export const defaultNetworkIdentifier = Buffer.from( @@ -43,7 +43,16 @@ export const createFakeBlockHeader = (header?: Partial): Block previousBlockID: header?.previousBlockID ?? hash(getRandomBytes(4)), transactionRoot: header?.transactionRoot ?? hash(getRandomBytes(4)), generatorAddress: header?.generatorAddress ?? getRandomBytes(32), - assets: [], + maxHeightGenerated: header?.maxHeightGenerated ?? 0, + maxHeightPrevoted: header?.maxHeightPrevoted ?? 0, + stateRoot: header?.stateRoot ?? hash(getRandomBytes(32)), + assetsRoot: header?.assetsRoot ?? hash(getRandomBytes(32)), + validatorsHash: header?.validatorsHash ?? hash(getRandomBytes(32)), + aggregateCommit: header?.aggregateCommit ?? { + height: 0, + aggregationBits: Buffer.alloc(0), + certificateSignature: Buffer.alloc(0), + }, signature: header?.signature ?? getRandomBytes(64), }); @@ -52,7 +61,7 @@ export const createFakeBlockHeader = (header?: Partial): Block * Calculates the signature, transactionRoot etc. internally. Facilitating the creation of block with valid signature and other properties */ export const createValidDefaultBlock = async ( - block?: { header?: Partial; payload?: Transaction[] }, + block?: { header?: Partial; payload?: Transaction[]; assets?: BlockAssets }, networkIdentifier: Buffer = defaultNetworkIdentifier, ): Promise => { const keypair = getKeyPair(); @@ -67,11 +76,21 @@ export const createValidDefaultBlock = async ( timestamp: genesis.header.timestamp + 10, transactionRoot: txTree.root, stateRoot: getRandomBytes(32), + assetsRoot: getRandomBytes(32), + validatorsHash: hash(getRandomBytes(32)), + maxHeightGenerated: 0, + maxHeightPrevoted: 0, + aggregateCommit: { + height: 0, + aggregationBits: Buffer.alloc(0), + certificateSignature: Buffer.alloc(0), + }, generatorAddress: getAddressFromPublicKey(keypair.publicKey), - assets: [], ...block?.header, }); + const blockAssets = block?.assets ?? new BlockAssets(); + blockHeader.sign(networkIdentifier, keypair.privateKey); - return new Block(blockHeader, payload); + return new Block(blockHeader, payload, blockAssets); }; diff --git a/framework/src/modules/liskbft/api.ts b/framework/src/modules/liskbft/api.ts index 821fa8e5e2d..34aa3e30b3e 100644 --- a/framework/src/modules/liskbft/api.ts +++ b/framework/src/modules/liskbft/api.ts @@ -12,13 +12,10 @@ * Removal or modification of this copyright notice is prohibited. */ -import { codec } from '@liskhq/lisk-codec'; -import { BlockHeader } from '@liskhq/lisk-chain'; import { BaseAPI } from '../base_api'; import { GeneratorStore } from '../../node/generator'; import { APIContext, ImmutableAPIContext } from '../../node/state_machine'; -import { BFTHeader, BFTVotes } from '../../node/consensus'; -import { liskBFTAssetSchema, liskBFTModuleID } from './constants'; +import { BFTVotes } from '../../node/consensus'; interface Validator { address: Buffer; @@ -49,23 +46,8 @@ export class LiskBFTAPI extends BaseAPI { return []; } - public getBFTHeader(header: BlockHeader): BFTHeader { - const asset = header.getAsset(liskBFTModuleID); - const decodedAsset = asset - ? codec.decode(liskBFTAssetSchema, asset) - : { maxHeightPrevoted: 0, maxHeightPreviouslyForged: 0 }; - return { - generatorAddress: header.generatorAddress, - height: header.height, - id: header.id, - timestamp: header.timestamp, - previousBlockID: header.previousBlockID, - ...decodedAsset, - }; - } - // eslint-disable-next-line @typescript-eslint/require-await - public async getBFTVotes(_apiClient: ImmutableAPIContext): Promise { + public async getBFTHeights(_apiClient: ImmutableAPIContext): Promise { return { maxHeightPrecommited: 0, maxHeightPrevoted: 0, diff --git a/framework/src/node/consensus/consensus.ts b/framework/src/node/consensus/consensus.ts index d6ee64dddc5..2bab81372fd 100644 --- a/framework/src/node/consensus/consensus.ts +++ b/framework/src/node/consensus/consensus.ts @@ -32,7 +32,6 @@ import { GenesisBlockContext } from '../state_machine/genesis_block_context'; import { Network } from '../network'; import { NetworkEndpoint } from './network_endpoint'; import { EventPostBlockData, postBlockEventSchema } from './schema'; -import { BlockHeader } from '../state_machine/types'; import { CONSENSUS_EVENT_BLOCK_BROADCAST, CONSENSUS_EVENT_BLOCK_DELETE, @@ -46,8 +45,9 @@ import { } from './constants'; import { GenesisConfig } from '../../types'; import { ValidatorAPI, LiskBFTAPI } from './types'; -import { APIContext } from '../state_machine'; +import { createAPIContext } from '../state_machine'; import { forkChoice, ForkStatus } from './fork_choice/fork_choice_rule'; +import { createNewAPIContext } from '../state_machine/api_context'; interface ConsensusArgs { stateMachine: StateMachine; @@ -127,7 +127,6 @@ export class Consensus { logger: this._logger, network: this._network, blockExecutor, - liskBFTAPI: this._liskBFTAPI, }); const fastChainSwitchMechanism = new FastChainSwitchingMechanism({ chain: this._chain, @@ -139,7 +138,6 @@ export class Consensus { chainModule: this._chain, logger: this._logger, blockExecutor, - liskBFTAPI: this._liskBFTAPI, mechanisms: [blockSyncMechanism, fastChainSwitchMechanism], }); @@ -176,7 +174,8 @@ export class Consensus { const eventQueue = new EventQueue(); const ctx = new GenesisBlockContext({ eventQueue, - header: (args.genesisBlock.header as unknown) as BlockHeader, + header: args.genesisBlock.header, + assets: args.genesisBlock.assets, logger: this._logger, stateStore: (stateStore as unknown) as StateStore, }); @@ -295,10 +294,9 @@ export class Consensus { if (lastBlockHeader.version === 0) { return height <= lastBlockHeader.height && maxHeightPrevoted <= lastBlockHeader.height; } - const lastBFTHeader = this._liskBFTAPI.getBFTHeader(lastBlockHeader); return ( - maxHeightPrevoted < lastBFTHeader.maxHeightPrevoted || - (maxHeightPrevoted === lastBFTHeader.maxHeightPrevoted && height < lastBlockHeader.height) + maxHeightPrevoted < lastBlockHeader.maxHeightPrevoted || + (maxHeightPrevoted === lastBlockHeader.maxHeightPrevoted && height < lastBlockHeader.height) ); } @@ -319,7 +317,6 @@ export class Consensus { genesisBlockTimestamp: this._genesisBlockTimestamp ?? 0, interval: this._genesisConfig.blockTime, }), - this._liskBFTAPI, ); if (!forkStatusList.includes(forkStatus)) { @@ -427,13 +424,14 @@ export class Consensus { ): Promise { const stateStore = new StateStore(this._db); const eventQueue = new EventQueue(); - const apiContext = new APIContext({ stateStore, eventQueue }); + const apiContext = createAPIContext({ stateStore, eventQueue }); const ctx = new BlockContext({ stateStore: (stateStore as unknown) as StateStore, eventQueue, networkIdentifier: Buffer.alloc(0), logger: this._logger, header: block.header, + assets: block.assets, transactions: block.payload, }); await this._stateMachine.verifyBlock(ctx); @@ -446,7 +444,7 @@ export class Consensus { } await this._stateMachine.executeBlock(ctx); - const bftVotes = await this._liskBFTAPI.getBFTVotes(apiContext); + const bftVotes = await this._liskBFTAPI.getBFTHeights(apiContext); let { finalizedHeight } = this._chain; if (bftVotes.maxHeightPrecommited > finalizedHeight) { @@ -527,10 +525,7 @@ export class Consensus { } private _createBlockExecutor(): BlockExecutor { - const apiContext = new APIContext({ - stateStore: new StateStore(this._db), - eventQueue: new EventQueue(), - }); + const apiContext = createNewAPIContext(this._db); return { deleteLastBlock: async (options: DeleteOptions = {}) => this._deleteLastBlock(options), executeValidated: async (block: Block, options?: ExecuteOptions) => diff --git a/framework/src/node/consensus/fork_choice/fork_choice_rule.ts b/framework/src/node/consensus/fork_choice/fork_choice_rule.ts index b37abe9408e..233a4272aa0 100644 --- a/framework/src/node/consensus/fork_choice/fork_choice_rule.ts +++ b/framework/src/node/consensus/fork_choice/fork_choice_rule.ts @@ -13,7 +13,7 @@ */ import { BlockHeader, Slots } from '@liskhq/lisk-chain'; -import { BFTHeader, LiskBFTAPI } from '../types'; +import { BFTHeader } from '../types'; export enum ForkStatus { IDENTICAL_BLOCK = 1, @@ -96,30 +96,28 @@ export const forkChoice = ( blockHeader: BlockHeader, lastBlockHeader: BlockHeader, slots: Slots, - liskBFTAPI: LiskBFTAPI, ): ForkStatus => { // Current time since Lisk Epoch const receivedBFTHeader = { - ...liskBFTAPI.getBFTHeader(blockHeader), + ...blockHeader.toObject(), receivedAt: slots.timeSinceGenesis(), }; - const lastBFTHeader = liskBFTAPI.getBFTHeader(lastBlockHeader); /* Cases are numbered following LIP-0014 Fork choice rule. See: https://github.com/LiskHQ/lips/blob/master/proposals/lip-0014.md#applying-blocks-according-to-fork-choice-rule Case 2 and 1 have flipped execution order for better readability. Behavior is still the same */ - if (isValidBlock(lastBFTHeader, receivedBFTHeader)) { + if (isValidBlock(lastBlockHeader, blockHeader)) { // Case 2: correct block received return ForkStatus.VALID_BLOCK; } - if (isIdenticalBlock(lastBFTHeader, receivedBFTHeader)) { + if (isIdenticalBlock(lastBlockHeader, blockHeader)) { // Case 1: same block received twice return ForkStatus.IDENTICAL_BLOCK; } - if (isDoubleForging(lastBFTHeader, receivedBFTHeader)) { + if (isDoubleForging(lastBlockHeader, blockHeader)) { // Delegates are the same // Case 3: double forging different blocks in the same slot. // Last Block stands. @@ -129,7 +127,7 @@ export const forkChoice = ( if ( isTieBreak({ slots, - lastAppliedBlock: lastBFTHeader, + lastAppliedBlock: lastBlockHeader, receivedBlock: receivedBFTHeader, }) ) { @@ -138,7 +136,7 @@ export const forkChoice = ( return ForkStatus.TIE_BREAK; } - if (isDifferentChain(lastBFTHeader, receivedBFTHeader)) { + if (isDifferentChain(lastBlockHeader, blockHeader)) { // Case 5: received block has priority. Move to a different chain. return ForkStatus.DIFFERENT_CHAIN; } diff --git a/framework/src/node/consensus/index.ts b/framework/src/node/consensus/index.ts index dfba7a6a330..d4f7a120843 100644 --- a/framework/src/node/consensus/index.ts +++ b/framework/src/node/consensus/index.ts @@ -19,4 +19,4 @@ export { CONSENSUS_EVENT_BLOCK_DELETE, CONSENSUS_EVENT_BLOCK_NEW, } from './constants'; -export { BFTHeader, LiskBFTAPI, ValidatorAPI, BFTVotes } from './types'; +export { LiskBFTAPI, ValidatorAPI, BFTVotes } from './types'; diff --git a/framework/src/node/consensus/synchronizer/block_synchronization_mechanism.ts b/framework/src/node/consensus/synchronizer/block_synchronization_mechanism.ts index 05a0db6d197..f230747a9fb 100644 --- a/framework/src/node/consensus/synchronizer/block_synchronization_mechanism.ts +++ b/framework/src/node/consensus/synchronizer/block_synchronization_mechanism.ts @@ -32,7 +32,6 @@ import { BlockExecutor } from './type'; import { Logger } from '../../../logger'; import { Network } from '../../network'; import { isDifferentChain } from '../fork_choice/fork_choice_rule'; -import { LiskBFTAPI } from '../types'; interface Peer { readonly peerId: string; @@ -49,7 +48,6 @@ interface BlockSynchronizationMechanismInput { readonly chain: Chain; readonly blockExecutor: BlockExecutor; readonly network: Network; - readonly liskBFTAPI: LiskBFTAPI; } const groupByPeer = (peers: Peer[]): dataStructures.BufferMap => { @@ -67,18 +65,15 @@ const groupByPeer = (peers: Peer[]): dataStructures.BufferMap => { export class BlockSynchronizationMechanism extends BaseSynchronizer { private readonly blockExecutor: BlockExecutor; - private readonly _liskBFTAPI: LiskBFTAPI; public constructor({ logger, chain, - liskBFTAPI, blockExecutor, network: networkModule, }: BlockSynchronizationMechanismInput) { super(logger, chain, networkModule); this._chain = chain; - this._liskBFTAPI = liskBFTAPI; this.blockExecutor = blockExecutor; } @@ -193,8 +188,8 @@ export class BlockSynchronizationMechanism extends BaseSynchronizer { // Check if the new tip has priority over the last tip we had before applying const newTipHasPreference = isDifferentChain( - this._liskBFTAPI.getBFTHeader(tipBeforeApplying.header), - this._liskBFTAPI.getBFTHeader(this._chain.lastBlock.header), + tipBeforeApplying.header, + this._chain.lastBlock.header, ); if (!newTipHasPreference) { @@ -413,10 +408,8 @@ export class BlockSynchronizationMechanism extends BaseSynchronizer { const { valid: validBlock } = await this._blockDetachedStatus(networkLastBlock); const inDifferentChain = - isDifferentChain( - this._liskBFTAPI.getBFTHeader(this._chain.lastBlock.header), - this._liskBFTAPI.getBFTHeader(networkLastBlock.header), - ) || networkLastBlock.header.id.equals(this._chain.lastBlock.header.id); + isDifferentChain(this._chain.lastBlock.header, networkLastBlock.header) || + networkLastBlock.header.id.equals(this._chain.lastBlock.header.id); if (!validBlock || !inDifferentChain) { throw new ApplyPenaltyAndRestartError( peerId, @@ -512,10 +505,7 @@ export class BlockSynchronizationMechanism extends BaseSynchronizer { maxHeightPrevoted: selectedPeers[randomPeerIndex].options.maxHeightPrevoted, }; - const tipHasPreference = isDifferentChain( - this._liskBFTAPI.getBFTHeader(this._chain.lastBlock.header), - peersTip, - ); + const tipHasPreference = isDifferentChain(this._chain.lastBlock.header, peersTip); if (!tipHasPreference) { throw new AbortError('Peer tip does not have preference over current tip.'); diff --git a/framework/src/node/consensus/synchronizer/synchronizer.ts b/framework/src/node/consensus/synchronizer/synchronizer.ts index c563fc0d014..3dd3189d742 100644 --- a/framework/src/node/consensus/synchronizer/synchronizer.ts +++ b/framework/src/node/consensus/synchronizer/synchronizer.ts @@ -19,12 +19,10 @@ import { BaseSynchronizer } from './base_synchronizer'; import { Logger } from '../../../logger'; import { BlockExecutor } from './type'; import * as utils from './utils'; -import { LiskBFTAPI } from '../types'; interface SynchronizerInput { readonly logger: Logger; readonly chainModule: Chain; - readonly liskBFTAPI: LiskBFTAPI; readonly blockExecutor: BlockExecutor; readonly mechanisms: BaseSynchronizer[]; } @@ -32,24 +30,16 @@ interface SynchronizerInput { export class Synchronizer { protected logger: Logger; private readonly chainModule: Chain; - private readonly _liskBFTAPI: LiskBFTAPI; private readonly _mutex: jobHandlers.Mutex; private readonly blockExecutor: BlockExecutor; private readonly mechanisms: BaseSynchronizer[]; - public constructor({ - logger, - chainModule, - blockExecutor, - liskBFTAPI, - mechanisms = [], - }: SynchronizerInput) { + public constructor({ logger, chainModule, blockExecutor, mechanisms = [] }: SynchronizerInput) { assert(Array.isArray(mechanisms), 'mechanisms should be an array of mechanisms'); this.mechanisms = mechanisms; this.logger = logger; this.chainModule = chainModule; this.blockExecutor = blockExecutor; - this._liskBFTAPI = liskBFTAPI; this._checkMechanismsInterfaces(); this._mutex = new jobHandlers.Mutex(); @@ -59,12 +49,7 @@ export class Synchronizer { const isEmpty = await this.chainModule.dataAccess.isTempBlockEmpty(); if (!isEmpty) { try { - await utils.restoreBlocksUponStartup( - this.logger, - this.chainModule, - this.blockExecutor, - this._liskBFTAPI, - ); + await utils.restoreBlocksUponStartup(this.logger, this.chainModule, this.blockExecutor); } catch (err) { this.logger.error( { err: err as Error }, diff --git a/framework/src/node/consensus/synchronizer/utils.ts b/framework/src/node/consensus/synchronizer/utils.ts index 08585bb4f1e..206f8b2d815 100644 --- a/framework/src/node/consensus/synchronizer/utils.ts +++ b/framework/src/node/consensus/synchronizer/utils.ts @@ -15,7 +15,6 @@ import { Chain } from '@liskhq/lisk-chain'; import { BlockExecutor } from './type'; import { Logger } from '../../../logger'; import { isDifferentChain, isValidBlock } from '../fork_choice/fork_choice_rule'; -import { LiskBFTAPI } from '../types'; export const restoreBlocks = async ( chainModule: Chain, @@ -77,19 +76,15 @@ export const restoreBlocksUponStartup = async ( logger: Logger, chainModule: Chain, blockExecutor: BlockExecutor, - liskBFTAPI: LiskBFTAPI, ): Promise => { // Get all blocks and find lowest height (next one to be applied), as it should return in height desc const tempBlocks = await chainModule.dataAccess.getTempBlocks(); const blockLowestHeight = tempBlocks[tempBlocks.length - 1]; const blockHighestHeight = tempBlocks[0]; - const lastBFTHeader = liskBFTAPI.getBFTHeader(chainModule.lastBlock.header); - const highestBFTHeader = liskBFTAPI.getBFTHeader(blockHighestHeight.header); - const blockHasPriority = - isDifferentChain(lastBFTHeader, highestBFTHeader) || - isValidBlock(lastBFTHeader, highestBFTHeader); + isDifferentChain(chainModule.lastBlock.header, blockHighestHeight.header) || + isValidBlock(chainModule.lastBlock.header, blockHighestHeight.header); // Block in the temp table has preference over current tip of the chain if (blockHasPriority) { diff --git a/framework/src/node/consensus/types.ts b/framework/src/node/consensus/types.ts index 683dd1cd13a..3329eff2fce 100644 --- a/framework/src/node/consensus/types.ts +++ b/framework/src/node/consensus/types.ts @@ -12,7 +12,6 @@ * Removal or modification of this copyright notice is prohibited. */ -import { BlockHeader } from '@liskhq/lisk-chain'; import { GeneratorStore } from '../generator'; import { APIContext, ImmutableAPIContext } from '../state_machine'; @@ -23,7 +22,7 @@ export interface BFTHeader { timestamp: number; height: number; maxHeightPrevoted: number; - maxHeightPreviouslyForged: number; + maxHeightGenerated: number; receivedAt?: number; } @@ -56,6 +55,5 @@ export interface LiskBFTAPI { }, ) => Promise; getValidators: (_apiContext: ImmutableAPIContext) => Promise; - getBFTHeader: (header: BlockHeader) => BFTHeader; - getBFTVotes: (apiClient: ImmutableAPIContext) => Promise; + getBFTHeights: (apiClient: ImmutableAPIContext) => Promise; } diff --git a/framework/src/node/generator/constants.ts b/framework/src/node/generator/constants.ts index 9bd3078d27a..ee02b73729d 100644 --- a/framework/src/node/generator/constants.ts +++ b/framework/src/node/generator/constants.ts @@ -20,3 +20,5 @@ export const LOAD_TRANSACTION_RETRIES = 5; export const NETWORK_RPC_GET_TRANSACTIONS = 'getTransactions'; export const NETWORK_EVENT_POST_TRANSACTIONS_ANNOUNCEMENT = 'postTransactionsAnnouncement'; + +export const GENERATOR_STORE_RESERVED_PREFIX = 0; diff --git a/framework/src/node/generator/context.ts b/framework/src/node/generator/context.ts index 05d70e4b4ad..41295375a10 100644 --- a/framework/src/node/generator/context.ts +++ b/framework/src/node/generator/context.ts @@ -12,16 +12,17 @@ * Removal or modification of this copyright notice is prohibited. */ -import { StateStore } from '@liskhq/lisk-chain/dist-node/state_store'; +import { BlockAssets, BlockHeader, StateStore } from '@liskhq/lisk-chain'; import { Logger } from '../../logger'; -import { BlockGenerateContext, WritableBlockHeader } from './types'; +import { BlockGenerateContext } from './types'; import { GeneratorStore } from './generator_store'; -import { APIContext, EventQueue } from '../state_machine'; +import { createAPIContext, EventQueue } from '../state_machine'; interface GenerationContextArgs { logger: Logger; stateStore: StateStore; - header: WritableBlockHeader; + header: BlockHeader; + assets: BlockAssets; generatorStore: GeneratorStore; networkIdentifier: Buffer; } @@ -30,7 +31,8 @@ export class GenerationContext { private readonly _logger: Logger; private readonly _networkIdentifier: Buffer; private readonly _stateStore: StateStore; - private readonly _header: WritableBlockHeader; + private readonly _header: BlockHeader; + private readonly _assets: BlockAssets; private readonly _generatorStore: GeneratorStore; public constructor(args: GenerationContextArgs) { @@ -39,9 +41,10 @@ export class GenerationContext { this._header = args.header; this._stateStore = args.stateStore; this._generatorStore = args.generatorStore; + this._assets = args.assets; } - public get blockHeader(): WritableBlockHeader { + public get blockHeader(): BlockHeader { return this._header; } @@ -49,11 +52,12 @@ export class GenerationContext { return { logger: this._logger, getAPIContext: () => - new APIContext({ stateStore: this._stateStore, eventQueue: new EventQueue() }), + createAPIContext({ stateStore: this._stateStore, eventQueue: new EventQueue() }), getStore: (moduleID: number, storePrefix: number) => this._stateStore.getStore(moduleID, storePrefix), getGeneratorStore: (moduleID: number) => this._generatorStore.getGeneratorStore(moduleID), header: this._header, + assets: this._assets, networkIdentifier: this._networkIdentifier, }; } diff --git a/framework/src/node/generator/endpoint.ts b/framework/src/node/generator/endpoint.ts index 3c9ee516980..02810fd7ee9 100644 --- a/framework/src/node/generator/endpoint.ts +++ b/framework/src/node/generator/endpoint.ts @@ -26,17 +26,18 @@ import { dataStructures } from '@liskhq/lisk-utils'; import { LiskValidationError, validator } from '@liskhq/lisk-validator'; import { Logger } from '../../logger'; import { Generator, ModuleEndpointContext } from '../../types'; -import { - APIContext, - EventQueue, - StateMachine, - TransactionContext, - VerifyStatus, -} from '../state_machine'; +import { EventQueue, StateMachine, TransactionContext, VerifyStatus } from '../state_machine'; import { Broadcaster } from './broadcaster'; import { InvalidTransactionError } from './errors'; import { GeneratorStore } from './generator_store'; import { + getLastGeneratedInfo, + isEqualGeneratedInfo, + isZeroValueGeneratedInfo, + setLastGeneratedInfo, +} from './generated_info'; +import { + GeneratedInfo, GetForgingStatusResponse, PostTransactionRequest, postTransactionRequestSchema, @@ -45,13 +46,12 @@ import { updateForgingStatusRequestSchema, UpdateForgingStatusResponse, } from './schemas'; -import { Consensus, Keypair, LiskBFTAPI } from './types'; +import { Consensus, Keypair } from './types'; interface EndpointArgs { keypair: dataStructures.BufferMap; generators: Generator[]; consensus: Consensus; - liskBFTAPI: LiskBFTAPI; chain: Chain; stateMachine: StateMachine; pool: TransactionPool; @@ -69,7 +69,6 @@ export class Endpoint { private readonly _keypairs: dataStructures.BufferMap; private readonly _generators: Generator[]; private readonly _consensus: Consensus; - private readonly _liskBFTAPI: LiskBFTAPI; private readonly _stateMachine: StateMachine; private readonly _pool: TransactionPool; private readonly _broadcaster: Broadcaster; @@ -83,7 +82,6 @@ export class Endpoint { this._keypairs = args.keypair; this._generators = args.generators; this._consensus = args.consensus; - this._liskBFTAPI = args.liskBFTAPI; this._chain = args.chain; this._stateMachine = args.stateMachine; this._pool = args.pool; @@ -212,10 +210,6 @@ export class Endpoint { enabled: false, }; } - const apiClient = new APIContext({ - stateStore: new StateStore(this._blockchainDB), - eventQueue: new EventQueue(), - }); const synced = this._consensus.isSynced(req.height, req.maxHeightPrevoted); if (!synced) { @@ -223,10 +217,34 @@ export class Endpoint { } const generatorStore = new GeneratorStore(this._generatorDB); - await this._liskBFTAPI.verifyGeneratorInfo(apiClient, generatorStore, { - ...req, - address, - }); + // check + let lastGeneratedInfo: GeneratedInfo | undefined; + try { + lastGeneratedInfo = await getLastGeneratedInfo( + generatorStore, + Buffer.from(req.address, 'hex'), + ); + } catch (error) { + ctx.logger.debug(`Last generated information does not exist for address: ${req.address}`); + } + + if (req.overwrite !== true) { + if (lastGeneratedInfo !== undefined && !isEqualGeneratedInfo(req, lastGeneratedInfo)) { + throw new Error('Request does not match last generated information.'); + } + if (lastGeneratedInfo === undefined && !isZeroValueGeneratedInfo(req)) { + throw new Error('Last generated information does not exist.'); + } + } + + if ( + lastGeneratedInfo === undefined || + (req.overwrite === true && + lastGeneratedInfo !== undefined && + !isEqualGeneratedInfo(req, lastGeneratedInfo)) + ) { + await setLastGeneratedInfo(generatorStore, Buffer.from(req.address, 'hex'), req); + } const batch = this._generatorDB.batch(); generatorStore.finalize(batch); diff --git a/framework/src/node/generator/errors.ts b/framework/src/node/generator/errors.ts index 9231510f4bb..09fa00ebc79 100644 --- a/framework/src/node/generator/errors.ts +++ b/framework/src/node/generator/errors.ts @@ -11,6 +11,7 @@ * * Removal or modification of this copyright notice is prohibited. */ +/* eslint-disable max-classes-per-file */ export class InvalidTransactionError extends Error { public readonly message: string; @@ -22,3 +23,5 @@ export class InvalidTransactionError extends Error { this.id = id; } } + +export class NotFoundError extends Error {} diff --git a/framework/src/node/generator/generated_info.ts b/framework/src/node/generator/generated_info.ts new file mode 100644 index 00000000000..67211538482 --- /dev/null +++ b/framework/src/node/generator/generated_info.ts @@ -0,0 +1,70 @@ +/* + * Copyright © 2021 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { codec } from '@liskhq/lisk-codec'; +import { GENERATOR_STORE_RESERVED_PREFIX } from './constants'; +import { NotFoundError } from './errors'; +import { GeneratorStore } from './generator_store'; +import { GeneratedInfo, previouslyGeneratedInfoSchema } from './schemas'; + +export const setLastGeneratedInfo = async ( + store: GeneratorStore, + generatorAddress: Buffer, + header: GeneratedInfo, +): Promise => { + const subStore = store.getGeneratorStore(GENERATOR_STORE_RESERVED_PREFIX); + const generatedInfo: GeneratedInfo = { + height: header.height, + maxHeightGenerated: header.maxHeightGenerated, + maxHeightPrevoted: header.maxHeightPrevoted, + }; + const encodedGeneratedInfo = codec.encode(previouslyGeneratedInfoSchema, generatedInfo); + await subStore.set(generatorAddress, encodedGeneratedInfo); +}; + +export const getLastGeneratedInfo = async ( + store: GeneratorStore, + generatorAddress: Buffer, +): Promise => { + const subStore = store.getGeneratorStore(GENERATOR_STORE_RESERVED_PREFIX); + const encodedGeneratedInfo = await subStore.get(generatorAddress); + return codec.decode(previouslyGeneratedInfoSchema, encodedGeneratedInfo); +}; + +export const getOrDefaultLastGeneratedInfo = async ( + store: GeneratorStore, + generatorAddress: Buffer, +): Promise => { + try { + const info = await getLastGeneratedInfo(store, generatorAddress); + return info; + } catch (error) { + if (!(error instanceof NotFoundError)) { + throw error; + } + return { + height: 0, + maxHeightGenerated: 0, + maxHeightPrevoted: 0, + }; + } +}; + +export const isZeroValueGeneratedInfo = (info: GeneratedInfo): boolean => + info.height === 0 && info.maxHeightGenerated === 0 && info.maxHeightPrevoted === 0; + +export const isEqualGeneratedInfo = (g1: GeneratedInfo, g2: GeneratedInfo): boolean => + g1.height === g2.height && + g1.maxHeightGenerated === g2.maxHeightGenerated && + g1.maxHeightPrevoted === g2.maxHeightPrevoted; diff --git a/framework/src/node/generator/generator.ts b/framework/src/node/generator/generator.ts index bd8f9aa6693..65fa5fa8ffb 100644 --- a/framework/src/node/generator/generator.ts +++ b/framework/src/node/generator/generator.ts @@ -12,14 +12,14 @@ * Removal or modification of this copyright notice is prohibited. */ -import { Chain, Transaction, BlockHeader } from '@liskhq/lisk-chain'; -import { Block } from '@liskhq/lisk-chain/dist-node/block'; +import { Chain, Block, Transaction, BlockHeader, BlockAssets } from '@liskhq/lisk-chain'; import { StateStore } from '@liskhq/lisk-chain/dist-node/state_store'; import { codec } from '@liskhq/lisk-codec'; import { decryptPassphraseWithPassword, getAddressFromPublicKey, getPrivateAndPublicKeyFromPassphrase, + hash, parseEncryptedPassphrase, } from '@liskhq/lisk-cryptography'; import { KVStore } from '@liskhq/lisk-db'; @@ -32,7 +32,6 @@ import { Logger } from '../../logger'; import { GenerationConfig, GenesisConfig } from '../../types'; import { Network } from '../network'; import { - APIContext, EventQueue, StateMachine, TransactionContext, @@ -55,6 +54,8 @@ import { NetworkEndpoint } from './network_endpoint'; import { GetTransactionResponse, getTransactionsResponseSchema } from './schemas'; import { HighFeeGenerationStrategy } from './strategies'; import { Consensus, GeneratorModule, LiskBFTAPI, ValidatorAPI } from './types'; +import { createAPIContext, createNewAPIContext } from '../state_machine/api_context'; +import { getOrDefaultLastGeneratedInfo, setLastGeneratedInfo } from './generated_info'; interface GeneratorArgs { genesisConfig: GenesisConfig; @@ -136,7 +137,6 @@ export class Generator { keypair: this._keypairs, chain: this._chain, consensus: this._consensus, - liskBFTAPI: this._liskBFTAPI, broadcaster: this._broadcaster, pool: this._pool, stateMachine: this._stateMachine, @@ -373,10 +373,7 @@ export class Generator { } private async _generateLoop(): Promise { - const apiContext = new APIContext({ - stateStore: new StateStore(this._blockchainDB), - eventQueue: new EventQueue(), - }); + const apiContext = createNewAPIContext(this._blockchainDB); const MS_IN_A_SEC = 1000; const currentTime = Math.floor(new Date().getTime() / MS_IN_A_SEC); @@ -436,20 +433,32 @@ export class Generator { timestamp: number, ): Promise { const stateStore = new StateStore(this._blockchainDB); + const generatorStore = new GeneratorStore(this._generatorDB); + const apiContext = createAPIContext({ stateStore, eventQueue: new EventQueue() }); + + const { maxHeightPrevoted } = await this._liskBFTAPI.getBFTHeights(apiContext); + const { height } = await getOrDefaultLastGeneratedInfo(generatorStore, generatorAddress); const blockHeader = new BlockHeader({ generatorAddress, height: this._chain.lastBlock.header.height + 1, previousBlockID: this._chain.lastBlock.header.id, version: BLOCK_VERSION, + maxHeightPrevoted, + maxHeightGenerated: height, + aggregateCommit: { + height: 0, + aggregationBits: Buffer.alloc(0), + certificateSignature: Buffer.alloc(0), + }, timestamp, - assets: [], }); + const blockAssets = new BlockAssets(); - const generatorStore = new GeneratorStore(this._generatorDB); const genContext = new GenerationContext({ generatorStore, header: blockHeader, + assets: blockAssets, logger: this._logger, networkIdentifier: this._chain.networkIdentifier, stateStore, @@ -465,6 +474,7 @@ export class Generator { const blockCtx = new BlockContext({ eventQueue, header: blockHeader, + assets: blockAssets, logger: this._logger, networkIdentifier: this._chain.networkIdentifier, stateStore, @@ -489,9 +499,21 @@ export class Generator { await txTree.init(transactions.map(tx => tx.id)); const transactionRoot = txTree.root; blockHeader.transactionRoot = transactionRoot; + // TODO: Update the assetsRoot with proper calculation + blockHeader.assetsRoot = hash(Buffer.concat(blockAssets.getBytes())); + // TODO: Update the stateRoot with proper calculation + blockHeader.stateRoot = hash(Buffer.alloc(0)); + // TODO: Update validatorsHash with proper calculation + blockHeader.validatorsHash = hash(Buffer.alloc(0)); blockHeader.sign(this._chain.networkIdentifier, keypair.privateKey); - const generatedBlock = new Block(blockHeader, transactions); + const generatedBlock = new Block(blockHeader, transactions, blockAssets); + + await setLastGeneratedInfo(generatorStore, blockHeader.generatorAddress, blockHeader); + + const batch = this._generatorDB.batch(); + generatorStore.finalize(batch); + await batch.write(); return generatedBlock; } diff --git a/framework/src/node/generator/generator_store.ts b/framework/src/node/generator/generator_store.ts index 9b270ae034f..ad224095340 100644 --- a/framework/src/node/generator/generator_store.ts +++ b/framework/src/node/generator/generator_store.ts @@ -12,8 +12,9 @@ * Removal or modification of this copyright notice is prohibited. */ -import { BatchChain, KVStore } from '@liskhq/lisk-db'; +import { BatchChain, KVStore, NotFoundError as DBNotFoundError } from '@liskhq/lisk-db'; import { dataStructures } from '@liskhq/lisk-utils'; +import { NotFoundError } from './errors'; export class GeneratorStore { private readonly _db: KVStore; @@ -38,9 +39,16 @@ export class GeneratorStore { if (cachedValue) { return cachedValue; } - const storedValue = await this._db.get(prefixedKey); - this._data.set(prefixedKey, storedValue); - return storedValue; + try { + const storedValue = await this._db.get(prefixedKey); + this._data.set(prefixedKey, storedValue); + return storedValue; + } catch (error) { + if (error instanceof DBNotFoundError) { + throw new NotFoundError(); + } + throw error; + } } // eslint-disable-next-line @typescript-eslint/require-await diff --git a/framework/src/node/generator/index.ts b/framework/src/node/generator/index.ts index 2a3bff069a1..2fe0be8d123 100644 --- a/framework/src/node/generator/index.ts +++ b/framework/src/node/generator/index.ts @@ -13,4 +13,4 @@ */ export { Generator } from './generator'; -export { BlockGenerateContext, WritableBlockHeader, Keypair, GeneratorStore } from './types'; +export { BlockGenerateContext, Keypair, GeneratorStore } from './types'; diff --git a/framework/src/node/generator/schemas.ts b/framework/src/node/generator/schemas.ts index f982fcad0af..8a88d7e2c08 100644 --- a/framework/src/node/generator/schemas.ts +++ b/framework/src/node/generator/schemas.ts @@ -36,7 +36,7 @@ export interface UpdateForgingStatusRequest { password: string; height: number; maxHeightPrevoted: number; - maxHeightPreviouslyForged: number; + maxHeightGenerated: number; overwrite?: boolean; } @@ -54,14 +54,7 @@ export const updateForgingStatusRequestSchema = { $id: 'lisk/updateForgingStatusRequest', title: 'Update forging status', type: 'object', - required: [ - 'address', - 'password', - 'enable', - 'height', - 'maxHeightPreviouslyForged', - 'maxHeightPrevoted', - ], + required: ['address', 'password', 'enable', 'height', 'maxHeightGenerated', 'maxHeightPrevoted'], properties: { address: { type: 'string', @@ -75,7 +68,7 @@ export const updateForgingStatusRequestSchema = { height: { type: 'integer', }, - maxHeightPreviouslyForged: { + maxHeightGenerated: { type: 'integer', }, maxHeightPrevoted: { @@ -171,3 +164,30 @@ export const postTransactionsAnnouncementSchema = { export interface PostTransactionsAnnouncement { transactionIds: Buffer[]; } + +export interface GeneratedInfo { + height: number; + maxHeightPrevoted: number; + maxHeightGenerated: number; +} + +export const previouslyGeneratedInfoSchema = { + title: 'Previously Generated Info', + $id: '/node/generator/previously_generated_info', + type: 'object', + required: ['height', 'maxHeightPrevoted', 'maxHeightGenerated'], + properties: { + height: { + dataType: 'uint32', + fieldNumber: 1, + }, + maxHeightPrevoted: { + dataType: 'uint32', + fieldNumber: 2, + }, + maxHeightGenerated: { + dataType: 'uint32', + fieldNumber: 3, + }, + }, +}; diff --git a/framework/src/node/generator/strategies.ts b/framework/src/node/generator/strategies.ts index b4b21aabf16..15531bc35cc 100644 --- a/framework/src/node/generator/strategies.ts +++ b/framework/src/node/generator/strategies.ts @@ -17,9 +17,14 @@ import { dataStructures } from '@liskhq/lisk-utils'; import { Chain, Transaction } from '@liskhq/lisk-chain'; import { getAddressFromPublicKey } from '@liskhq/lisk-cryptography'; import { KVStore } from '@liskhq/lisk-db'; -import { StateMachine, VerifyStatus, EventQueue, TransactionContext } from '../state_machine'; +import { + StateMachine, + VerifyStatus, + EventQueue, + TransactionContext, + BlockHeader, +} from '../state_machine'; import { Logger } from '../../logger'; -import { WritableBlockHeader } from './types'; export class HighFeeGenerationStrategy { private readonly _chain: Chain; @@ -56,7 +61,7 @@ export class HighFeeGenerationStrategy { this._logger = args.logger; } - public async getTransactionsForBlock(header: WritableBlockHeader): Promise { + public async getTransactionsForBlock(header: BlockHeader): Promise { // Initialize array to select transactions const readyTransactions = []; diff --git a/framework/src/node/generator/types.ts b/framework/src/node/generator/types.ts index 76e16664c5f..b6915c03c21 100644 --- a/framework/src/node/generator/types.ts +++ b/framework/src/node/generator/types.ts @@ -14,7 +14,8 @@ import { Block } from '@liskhq/lisk-chain'; import { Logger } from '../../logger'; -import { APIContext, ImmutableSubStore } from '../state_machine'; +import { BFTVotes } from '../consensus'; +import { APIContext, BlockHeader, ImmutableAPIContext, ImmutableSubStore } from '../state_machine'; export interface Keypair { publicKey: Buffer; @@ -32,6 +33,7 @@ export interface Consensus { } export interface LiskBFTAPI { + getBFTHeights: (_apiClient: ImmutableAPIContext) => Promise; verifyGeneratorInfo: ( apiContext: APIContext, generatorStore: GeneratorStore, @@ -51,12 +53,7 @@ export interface ValidatorAPI { getSlotTime: (apiContext: APIContext, slot: number) => number; } -export interface WritableBlockHeader { - version: number; - height: number; - timestamp: number; - previousBlockID: Buffer; - generatorAddress: Buffer; +export interface WritableBlockAssets { getAsset: (moduleID: number) => Buffer | undefined; setAsset: (moduleID: number, value: Buffer) => void; } @@ -66,7 +63,8 @@ export interface BlockGenerateContext { networkIdentifier: Buffer; getAPIContext: () => APIContext; getStore: (moduleID: number, storePrefix: number) => ImmutableSubStore; - header: WritableBlockHeader; + header: BlockHeader; + assets: WritableBlockAssets; getGeneratorStore: (moduleID: number) => GeneratorStore; } diff --git a/framework/src/node/state_machine/api_context.ts b/framework/src/node/state_machine/api_context.ts index 8382b8fcc83..2bbda712066 100644 --- a/framework/src/node/state_machine/api_context.ts +++ b/framework/src/node/state_machine/api_context.ts @@ -12,14 +12,21 @@ * Removal or modification of this copyright notice is prohibited. */ +import { StateStore } from '@liskhq/lisk-chain'; +import { InMemoryKVStore, KVStore } from '@liskhq/lisk-db'; import { EventQueue } from './event_queue'; -import { StateStore, SubStore } from './types'; +import { SubStore } from './types'; interface Params { stateStore: StateStore; eventQueue: EventQueue; } +export const createAPIContext = (params: Params) => new APIContext(params); + +export const createNewAPIContext = (db: KVStore | InMemoryKVStore) => + new APIContext({ stateStore: new StateStore(db), eventQueue: new EventQueue() }); + export class APIContext { private readonly _stateStore: StateStore; private readonly _eventQueue: EventQueue; diff --git a/framework/src/node/state_machine/block_context.ts b/framework/src/node/state_machine/block_context.ts index eda6e2d3da1..d92d168f78f 100644 --- a/framework/src/node/state_machine/block_context.ts +++ b/framework/src/node/state_machine/block_context.ts @@ -12,9 +12,9 @@ * Removal or modification of this copyright notice is prohibited. */ -import { Transaction } from '@liskhq/lisk-chain'; +import { Transaction, StateStore } from '@liskhq/lisk-chain'; import { Logger } from '../../logger'; -import { APIContext } from './api_context'; +import { createAPIContext } from './api_context'; import { EventQueue } from './event_queue'; import { TransactionContext } from './transaction_context'; import { @@ -22,13 +22,14 @@ import { BlockExecuteContext, BlockVerifyContext, BlockHeader, - StateStore, + BlockAssets, } from './types'; export interface ContextParams { networkIdentifier: Buffer; stateStore: StateStore; header: BlockHeader; + assets: BlockAssets; logger: Logger; eventQueue: EventQueue; transactions?: ReadonlyArray; @@ -40,6 +41,7 @@ export class BlockContext { private readonly _logger: Logger; private readonly _eventQueue: EventQueue; private readonly _header: BlockHeader; + private readonly _assets: BlockAssets; private _transactions?: ReadonlyArray; public constructor(params: ContextParams) { @@ -48,6 +50,7 @@ export class BlockContext { this._stateStore = params.stateStore; this._eventQueue = params.eventQueue; this._header = params.header; + this._assets = params.assets; this._transactions = params.transactions; } @@ -68,10 +71,11 @@ export class BlockContext { networkIdentifier: this._networkIdentifier, eventQueue: this._eventQueue, getAPIContext: () => - new APIContext({ stateStore: this._stateStore, eventQueue: this._eventQueue }), + createAPIContext({ stateStore: this._stateStore, eventQueue: this._eventQueue }), getStore: (moduleID: number, storePrefix: number) => this._stateStore.getStore(moduleID, storePrefix), header: this._header, + assets: this._assets, }; } @@ -81,10 +85,11 @@ export class BlockContext { networkIdentifier: this._networkIdentifier, eventQueue: this._eventQueue, getAPIContext: () => - new APIContext({ stateStore: this._stateStore, eventQueue: this._eventQueue }), + createAPIContext({ stateStore: this._stateStore, eventQueue: this._eventQueue }), getStore: (moduleID: number, storePrefix: number) => this._stateStore.getStore(moduleID, storePrefix), header: this._header, + assets: this._assets, }; } @@ -97,10 +102,11 @@ export class BlockContext { networkIdentifier: this._networkIdentifier, eventQueue: this._eventQueue, getAPIContext: () => - new APIContext({ stateStore: this._stateStore, eventQueue: this._eventQueue }), + createAPIContext({ stateStore: this._stateStore, eventQueue: this._eventQueue }), getStore: (moduleID: number, storePrefix: number) => this._stateStore.getStore(moduleID, storePrefix), header: this._header, + assets: this._assets, transactions: this._transactions, }; } @@ -113,6 +119,7 @@ export class BlockContext { transaction: tx, eventQueue: this._eventQueue, header: this._header, + assets: this._assets, }); } } diff --git a/framework/src/node/state_machine/genesis_block_context.ts b/framework/src/node/state_machine/genesis_block_context.ts index 812252310f4..340929837d2 100644 --- a/framework/src/node/state_machine/genesis_block_context.ts +++ b/framework/src/node/state_machine/genesis_block_context.ts @@ -12,15 +12,17 @@ * Removal or modification of this copyright notice is prohibited. */ +import { StateStore } from '@liskhq/lisk-chain'; import { Logger } from '../../logger'; import { APIContext } from './api_context'; import { EventQueue } from './event_queue'; -import { BlockHeader, GenesisBlockExecuteContext, StateStore } from './types'; +import { BlockAssets, BlockHeader, GenesisBlockExecuteContext } from './types'; export interface ContextParams { logger: Logger; stateStore: StateStore; header: BlockHeader; + assets: BlockAssets; eventQueue: EventQueue; } @@ -28,6 +30,7 @@ export class GenesisBlockContext { private readonly _stateStore: StateStore; private readonly _logger: Logger; private readonly _header: BlockHeader; + private readonly _assets: BlockAssets; private readonly _eventQueue: EventQueue; public constructor(params: ContextParams) { @@ -35,6 +38,7 @@ export class GenesisBlockContext { this._stateStore = params.stateStore; this._eventQueue = params.eventQueue; this._header = params.header; + this._assets = params.assets; } public createGenesisBlockExecuteContext(): GenesisBlockExecuteContext { @@ -46,6 +50,7 @@ export class GenesisBlockContext { this._stateStore.getStore(moduleID, storePrefix), header: this._header, logger: this._logger, + assets: this._assets, }; } } diff --git a/framework/src/node/state_machine/index.ts b/framework/src/node/state_machine/index.ts index 18ac17664d1..94d0a89f730 100644 --- a/framework/src/node/state_machine/index.ts +++ b/framework/src/node/state_machine/index.ts @@ -16,11 +16,11 @@ export { StateMachine } from './state_machine'; export { TransactionContext } from './transaction_context'; export { BlockContext } from './block_context'; export { GenesisBlockContext } from './genesis_block_context'; -export { APIContext } from './api_context'; export { EventQueue } from './event_queue'; +export { createAPIContext } from './api_context'; export { + APIContext, BlockHeader, - StateStore, VerifyStatus, ImmutableSubStore, ImmutableAPIContext, diff --git a/framework/src/node/state_machine/transaction_context.ts b/framework/src/node/state_machine/transaction_context.ts index 4226077d0bc..ca124840ab5 100644 --- a/framework/src/node/state_machine/transaction_context.ts +++ b/framework/src/node/state_machine/transaction_context.ts @@ -12,10 +12,10 @@ * Removal or modification of this copyright notice is prohibited. */ -import { Transaction } from '@liskhq/lisk-chain'; +import { Transaction, StateStore } from '@liskhq/lisk-chain'; import { codec, Schema } from '@liskhq/lisk-codec'; import { Logger } from '../../logger'; -import { APIContext } from './api_context'; +import { createAPIContext } from './api_context'; import { EventQueue } from './event_queue'; import { CommandExecuteContext, @@ -23,7 +23,7 @@ import { TransactionExecuteContext, TransactionVerifyContext, BlockHeader, - StateStore, + BlockAssets, } from './types'; interface ContextParams { @@ -33,6 +33,7 @@ interface ContextParams { eventQueue: EventQueue; transaction: Transaction; header?: BlockHeader; + assets?: BlockAssets; } export class TransactionContext { @@ -42,6 +43,7 @@ export class TransactionContext { private readonly _eventQueue: EventQueue; private readonly _transaction: Transaction; private readonly _header?: BlockHeader; + private readonly _assets?: BlockAssets; public constructor(params: ContextParams) { this._stateStore = params.stateStore; @@ -50,6 +52,7 @@ export class TransactionContext { this._eventQueue = params.eventQueue; this._networkIdentifier = params.networkIdentifier; this._transaction = params.transaction; + this._assets = params.assets; } public createTransactionVerifyContext(): TransactionVerifyContext { @@ -57,7 +60,7 @@ export class TransactionContext { logger: this._logger, networkIdentifier: this._networkIdentifier, getAPIContext: () => - new APIContext({ stateStore: this._stateStore, eventQueue: this._eventQueue }), + createAPIContext({ stateStore: this._stateStore, eventQueue: this._eventQueue }), getStore: (moduleID: number, storePrefix: number) => this._stateStore.getStore(moduleID, storePrefix), transaction: this._transaction, @@ -66,18 +69,22 @@ export class TransactionContext { public createTransactionExecuteContext(): TransactionExecuteContext { if (!this._header) { - throw new Error('Transaction Execution requires block header in the context'); + throw new Error('Transaction Execution requires block header in the context.'); + } + if (!this._assets) { + throw new Error('Transaction Execution requires block assets in the context.'); } return { logger: this._logger, networkIdentifier: this._networkIdentifier, eventQueue: this._eventQueue, getAPIContext: () => - new APIContext({ stateStore: this._stateStore, eventQueue: this._eventQueue }), + createAPIContext({ stateStore: this._stateStore, eventQueue: this._eventQueue }), getStore: (moduleID: number, storePrefix: number) => this._stateStore.getStore(moduleID, storePrefix), header: this._header, transaction: this._transaction, + assets: this._assets, }; } @@ -86,7 +93,7 @@ export class TransactionContext { logger: this._logger, networkIdentifier: this._networkIdentifier, getAPIContext: () => - new APIContext({ stateStore: this._stateStore, eventQueue: this._eventQueue }), + createAPIContext({ stateStore: this._stateStore, eventQueue: this._eventQueue }), getStore: (moduleID: number, storePrefix: number) => this._stateStore.getStore(moduleID, storePrefix), transaction: this._transaction, @@ -99,7 +106,7 @@ export class TransactionContext { logger: this._logger, networkIdentifier: this._networkIdentifier, getAPIContext: () => - new APIContext({ stateStore: this._stateStore, eventQueue: this._eventQueue }), + createAPIContext({ stateStore: this._stateStore, eventQueue: this._eventQueue }), getStore: (moduleID: number, storePrefix: number) => this._stateStore.getStore(moduleID, storePrefix), transaction: this._transaction, diff --git a/framework/src/node/state_machine/types.ts b/framework/src/node/state_machine/types.ts index d5127c62a3b..9ea5950599a 100644 --- a/framework/src/node/state_machine/types.ts +++ b/framework/src/node/state_machine/types.ts @@ -17,12 +17,6 @@ import { Schema } from '@liskhq/lisk-codec'; import { Logger } from '../../logger'; import { EventQueue } from './event_queue'; -export interface StateStore { - getStore: (moduleID: number, storePrefix: number) => SubStore; - createSnapshot(): void; - restoreSnapshot(): void; -} - export interface ImmutableSubStore { get(key: Buffer): Promise; getWithSchema(key: Buffer, schema: Schema): Promise; @@ -59,6 +53,16 @@ export interface BlockHeader { timestamp: number; previousBlockID: Buffer; generatorAddress: Buffer; + maxHeightPrevoted: number; + maxHeightGenerated: number; + aggregateCommit: { + height: number; + aggregationBits: Buffer; + certificateSignature: Buffer; + }; +} + +export interface BlockAssets { getAsset: (moduleID: number) => Buffer | undefined; } @@ -99,6 +103,7 @@ export interface GenesisBlockExecuteContext { getAPIContext: () => APIContext; getStore: (moduleID: number, storePrefix: number) => SubStore; header: BlockHeader; + assets: BlockAssets; } export interface TransactionExecuteContext { @@ -108,6 +113,7 @@ export interface TransactionExecuteContext { getAPIContext: () => APIContext; getStore: (moduleID: number, storePrefix: number) => SubStore; header: BlockHeader; + assets: BlockAssets; transaction: Transaction; } @@ -118,6 +124,7 @@ export interface BlockVerifyContext { getAPIContext: () => ImmutableAPIContext; getStore: (moduleID: number, storePrefix: number) => ImmutableSubStore; header: BlockHeader; + assets: BlockAssets; } export interface BlockExecuteContext { @@ -127,6 +134,7 @@ export interface BlockExecuteContext { getAPIContext: () => APIContext; getStore: (moduleID: number, storePrefix: number) => SubStore; header: BlockHeader; + assets: BlockAssets; } export interface BlockAfterExecuteContext extends BlockExecuteContext { diff --git a/framework/src/testing/block_processing_env.ts b/framework/src/testing/block_processing_env.ts index da3653501b8..21e91b6d38b 100644 --- a/framework/src/testing/block_processing_env.ts +++ b/framework/src/testing/block_processing_env.ts @@ -14,7 +14,7 @@ * */ -import { Block, Chain, DataAccess, BlockHeader, Transaction, StateStore } from '@liskhq/lisk-chain'; +import { Block, Chain, DataAccess, BlockHeader, Transaction } from '@liskhq/lisk-chain'; import { getNetworkIdentifier, getKeys } from '@liskhq/lisk-cryptography'; import { InMemoryKVStore, KVStore } from '@liskhq/lisk-db'; import { objects } from '@liskhq/lisk-utils'; @@ -27,8 +27,9 @@ import { createDB, removeDB } from './utils'; import { ApplicationConfig, GenesisConfig } from '../types'; import { createGenesisBlock } from './create_genesis_block'; import { Consensus } from '../node/consensus'; -import { APIContext, EventQueue } from '../node/state_machine'; +import { APIContext } from '../node/state_machine'; import { Node } from '../node'; +import { createNewAPIContext } from '../node/state_machine/api_context'; type Options = { genesisConfig?: GenesisConfig; @@ -82,10 +83,7 @@ const createProcessableBlock = async ( timestamp?: number, ): Promise => { // Get previous block and generate valid timestamp, seed reveal, maxHeightPrevoted, reward and maxHeightPreviouslyForged - const apiContext = new APIContext({ - eventQueue: new EventQueue(), - stateStore: new StateStore(node['_blockchainDB']), - }); + const apiContext = createNewAPIContext(node['_blockchainDB']); const previousBlockHeader = node['_chain'].lastBlock.header; const nextTimestamp = timestamp ?? getNextTimestamp(node, apiContext, previousBlockHeader); const validator = await node.validatorAPI.getGenerator(apiContext, nextTimestamp); @@ -141,10 +139,7 @@ export const getBlockProcessingEnv = async ( }, getLastBlock: () => node['_chain'].lastBlock, getNextValidatorPassphrase: async (previousBlockHeader: BlockHeader): Promise => { - const apiContext = new APIContext({ - eventQueue: new EventQueue(), - stateStore: new StateStore(blockchainDB), - }); + const apiContext = createNewAPIContext(blockchainDB); const nextTimestamp = getNextTimestamp(node, apiContext, previousBlockHeader); const validator = await node.validatorAPI.getGenerator(apiContext, nextTimestamp); const passphrase = getPassphraseFromDefaultConfig(validator); diff --git a/framework/src/testing/create_block.ts b/framework/src/testing/create_block.ts index d9e981ac511..f0702aa9998 100644 --- a/framework/src/testing/create_block.ts +++ b/framework/src/testing/create_block.ts @@ -13,13 +13,7 @@ * */ /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */ -import { - Block, - BlockHeader, - Transaction, - BlockHeaderAsset, - BlockHeaderAttrs, -} from '@liskhq/lisk-chain'; +import { Block, BlockHeader, Transaction, BlockHeaderAttrs, BlockAssets } from '@liskhq/lisk-chain'; import { getAddressFromPublicKey, getPrivateAndPublicKeyFromPassphrase, @@ -35,7 +29,7 @@ interface CreateBlock { previousBlockID: Buffer; payload?: Transaction[]; header?: Partial; - assets?: BlockHeaderAsset[]; + assets?: BlockAssets; } export const createBlockHeaderWithDefaults = (header?: Partial): BlockHeader => @@ -47,7 +41,15 @@ export const createBlockHeaderWithDefaults = (header?: Partial transactionRoot: header?.transactionRoot ?? hash(getRandomBytes(4)), stateRoot: header?.stateRoot ?? hash(getRandomBytes(4)), generatorAddress: header?.generatorAddress ?? getRandomBytes(32), - assets: header?.assets ?? [], + aggregateCommit: header?.aggregateCommit ?? { + height: 0, + aggregationBits: Buffer.alloc(0), + certificateSignature: Buffer.alloc(0), + }, + maxHeightGenerated: header?.maxHeightGenerated ?? 0, + maxHeightPrevoted: header?.maxHeightPrevoted ?? 0, + assetsRoot: header?.assetsRoot ?? hash(getRandomBytes(4)), + validatorsHash: header?.validatorsHash ?? hash(getRandomBytes(4)), }); export const createFakeBlockHeader = (header?: Partial): BlockHeader => { @@ -63,6 +65,7 @@ export const createBlock = async ({ timestamp, previousBlockID, payload, + assets, header, }: CreateBlock): Promise => { const { publicKey, privateKey } = getPrivateAndPublicKeyFromPassphrase(passphrase); @@ -80,5 +83,5 @@ export const createBlock = async ({ blockHeader.sign(networkIdentifier, privateKey); - return new Block(blockHeader, payload ?? []); + return new Block(blockHeader, payload ?? [], assets ?? new BlockAssets()); }; diff --git a/framework/src/testing/create_contexts.ts b/framework/src/testing/create_contexts.ts index 7e41bee3c40..7836f006a7e 100644 --- a/framework/src/testing/create_contexts.ts +++ b/framework/src/testing/create_contexts.ts @@ -13,7 +13,7 @@ * */ -import { BlockHeader, StateStore, Transaction } from '@liskhq/lisk-chain'; +import { BlockAssets, BlockHeader, StateStore, Transaction } from '@liskhq/lisk-chain'; import { getRandomBytes, hash } from '@liskhq/lisk-cryptography'; import { InMemoryKVStore } from '@liskhq/lisk-db'; import { Logger } from '../logger'; @@ -39,18 +39,27 @@ export const createGenesisBlockContext = (params: { params.header ?? new BlockHeader({ height: 0, - assets: [], generatorAddress: getRandomBytes(20), previousBlockID: Buffer.alloc(0), timestamp: Math.floor(Date.now() / 1000), version: 0, transactionRoot: hash(Buffer.alloc(0)), stateRoot: hash(Buffer.alloc(0)), + maxHeightGenerated: 0, + maxHeightPrevoted: 0, + assetsRoot: hash(Buffer.alloc(0)), + aggregateCommit: { + height: 0, + aggregationBits: Buffer.alloc(0), + certificateSignature: Buffer.alloc(0), + }, + validatorsHash: hash(Buffer.alloc(0)), }); const ctx = new GenesisBlockContext({ eventQueue, stateStore, header, + assets: new BlockAssets(), logger, }); return ctx; @@ -61,6 +70,7 @@ export const createBlockContext = (params: { eventQueue?: EventQueue; logger?: Logger; header: BlockHeader; + assets?: BlockAssets; transactions?: Transaction[]; }): BlockContext => { const logger = params.logger ?? loggerMock; @@ -72,6 +82,7 @@ export const createBlockContext = (params: { eventQueue, transactions: params.transactions ?? [], header: params.header, + assets: params.assets ?? new BlockAssets(), networkIdentifier: getRandomBytes(32), }); return ctx; @@ -82,6 +93,7 @@ export const createTransactionContext = (params: { eventQueue?: EventQueue; logger?: Logger; header?: BlockHeader; + assets?: BlockAssets; transaction: Transaction; }): TransactionContext => { const logger = params.logger ?? loggerMock; @@ -91,19 +103,28 @@ export const createTransactionContext = (params: { params.header ?? new BlockHeader({ height: 0, - assets: [], generatorAddress: getRandomBytes(20), previousBlockID: Buffer.alloc(0), timestamp: Math.floor(Date.now() / 1000), version: 0, transactionRoot: hash(Buffer.alloc(0)), stateRoot: hash(Buffer.alloc(0)), + maxHeightGenerated: 0, + maxHeightPrevoted: 0, + assetsRoot: hash(Buffer.alloc(0)), + aggregateCommit: { + height: 0, + aggregationBits: Buffer.alloc(0), + certificateSignature: Buffer.alloc(0), + }, + validatorsHash: hash(Buffer.alloc(0)), }); const ctx = new TransactionContext({ stateStore, logger, eventQueue, header, + assets: params.assets ?? new BlockAssets(), networkIdentifier: getRandomBytes(32), transaction: params.transaction, }); @@ -116,9 +137,6 @@ export const createAPIContext = (params: { }): APIContext => { const stateStore = params.stateStore ?? new StateStore(new InMemoryKVStore()); const eventQueue = params.eventQueue ?? new EventQueue(); - const ctx = new APIContext({ - stateStore, - eventQueue, - }); + const ctx = createAPIContext({ stateStore, eventQueue }); return ctx; }; diff --git a/framework/src/testing/create_genesis_block.ts b/framework/src/testing/create_genesis_block.ts index 9856d10c988..09d83828d77 100644 --- a/framework/src/testing/create_genesis_block.ts +++ b/framework/src/testing/create_genesis_block.ts @@ -15,8 +15,8 @@ import { Block, + BlockAssets, BlockHeader, - BlockHeaderAsset, BlockHeaderJSON, TransactionJSON, } from '@liskhq/lisk-chain'; @@ -29,7 +29,7 @@ interface CreateGenesisBlock { height?: number; timestamp?: number; previousBlockID?: Buffer; - assets?: BlockHeaderAsset[]; + assets?: BlockAssets; } export const createGenesisBlock = ( @@ -55,10 +55,18 @@ export const createGenesisBlock = ( transactionRoot: hash(Buffer.alloc(0)), stateRoot: hash(Buffer.alloc(0)), signature: Buffer.alloc(0), - assets: params.assets ?? [], + maxHeightGenerated: 0, + maxHeightPrevoted: 0, + assetsRoot: hash(Buffer.alloc(0)), + aggregateCommit: { + height: 0, + aggregationBits: Buffer.alloc(0), + certificateSignature: Buffer.alloc(0), + }, + validatorsHash: hash(Buffer.alloc(0)), }); - const genesisBlock = new Block(header, []); + const genesisBlock = new Block(header, [], params.assets ?? new BlockAssets()); return { genesisBlock, diff --git a/framework/test/fixtures/blocks.ts b/framework/test/fixtures/blocks.ts index 6165424476f..cf2130cb6f8 100644 --- a/framework/test/fixtures/blocks.ts +++ b/framework/test/fixtures/blocks.ts @@ -20,7 +20,7 @@ import { } from '@liskhq/lisk-cryptography'; import { Mnemonic } from '@liskhq/lisk-passphrase'; import { MerkleTree } from '@liskhq/lisk-tree'; -import { Block, BlockHeader, BlockHeaderAttrs, Transaction } from '@liskhq/lisk-chain'; +import { Block, BlockAssets, BlockHeader, BlockHeaderAttrs, Transaction } from '@liskhq/lisk-chain'; export const defaultNetworkIdentifier = Buffer.from( '93d00fe5be70d90e7ae247936a2e7d83b50809c79b73fa14285f02c842348b3e', @@ -30,17 +30,25 @@ export const defaultNetworkIdentifier = Buffer.from( export const genesisBlock = (): Block => { const header = new BlockHeader({ generatorAddress: Buffer.alloc(0), - assets: [], height: 0, version: 0, previousBlockID: Buffer.alloc(0), timestamp: Math.floor(Date.now() / 1000 - 24 * 60 * 60), stateRoot: hash(Buffer.alloc(0)), + maxHeightGenerated: 0, + maxHeightPrevoted: 0, + assetsRoot: hash(Buffer.alloc(0)), + validatorsHash: getRandomBytes(32), + aggregateCommit: { + height: 0, + aggregationBits: Buffer.alloc(0), + certificateSignature: Buffer.alloc(0), + }, transactionRoot: hash(Buffer.alloc(0)), signature: Buffer.alloc(0), }); - return new Block(header, []); + return new Block(header, [], new BlockAssets()); }; const getKeyPair = (): { publicKey: Buffer; privateKey: Buffer } => { @@ -55,9 +63,17 @@ export const createFakeBlockHeader = (header?: Partial): Block height: header?.height ?? 0, previousBlockID: header?.previousBlockID ?? hash(getRandomBytes(4)), transactionRoot: header?.transactionRoot ?? hash(getRandomBytes(4)), + maxHeightGenerated: header?.maxHeightGenerated ?? 0, + maxHeightPrevoted: header?.maxHeightPrevoted ?? 0, + assetsRoot: header?.assetsRoot ?? hash(getRandomBytes(4)), + aggregateCommit: header?.aggregateCommit ?? { + height: 0, + aggregationBits: Buffer.alloc(0), + certificateSignature: Buffer.alloc(0), + }, + validatorsHash: header?.validatorsHash ?? getRandomBytes(32), stateRoot: header?.stateRoot ?? hash(getRandomBytes(4)), generatorAddress: header?.generatorAddress ?? getRandomBytes(32), - assets: [], signature: header?.signature ?? getRandomBytes(64), }); @@ -82,7 +98,15 @@ export const createValidDefaultBlock = async ( transactionRoot: txTree.root, stateRoot: getRandomBytes(32), generatorAddress: getAddressFromPublicKey(keypair.publicKey), - assets: [], + aggregateCommit: { + height: 0, + aggregationBits: Buffer.alloc(0), + certificateSignature: Buffer.alloc(0), + }, + assetsRoot: getRandomBytes(32), + maxHeightPrevoted: 0, + maxHeightGenerated: 0, + validatorsHash: getRandomBytes(32), ...block?.header, }); @@ -91,5 +115,5 @@ export const createValidDefaultBlock = async ( // Assigning the id ahead // eslint-disable-next-line @typescript-eslint/no-unused-expressions blockHeader.id; - return new Block(blockHeader, payload); + return new Block(blockHeader, payload, new BlockAssets()); }; diff --git a/framework/test/unit/application.spec.ts b/framework/test/unit/application.spec.ts index a7c21b8349c..0202e6d7f74 100644 --- a/framework/test/unit/application.spec.ts +++ b/framework/test/unit/application.spec.ts @@ -69,7 +69,7 @@ describe('Application', () => { fatal: jest.fn(), }; const { id, ...header } = genesisBlock().header.toJSON(); - const genesisBlockJSON = { header, payload: [] }; + const genesisBlockJSON = { header, payload: [], assets: [] }; (createLogger as jest.Mock).mockReturnValue(loggerMock); diff --git a/framework/test/unit/node/consensus/consensus.spec.ts b/framework/test/unit/node/consensus/consensus.spec.ts index cf5e2b86c6b..111b592ba57 100644 --- a/framework/test/unit/node/consensus/consensus.spec.ts +++ b/framework/test/unit/node/consensus/consensus.spec.ts @@ -76,7 +76,7 @@ describe('consensus', () => { executeBlock: jest.fn(), } as unknown) as StateMachine; liskBFTAPI = { - getBFTVotes: jest.fn().mockResolvedValue({ maxHeghgtPrevoted: 0, maxHeightPrecommited: 0 }), + getBFTHeights: jest.fn().mockResolvedValue({ maxHeghgtPrevoted: 0, maxHeightPrecommited: 0 }), } as never; validatorAPI = {} as never; consensus = new Consensus({ diff --git a/framework/test/unit/node/consensus/synchronizer/block_synchronization_mechanism/block_synchronization_mechanism.spec.ts b/framework/test/unit/node/consensus/synchronizer/block_synchronization_mechanism/block_synchronization_mechanism.spec.ts index 128798f22bc..a22609344df 100644 --- a/framework/test/unit/node/consensus/synchronizer/block_synchronization_mechanism/block_synchronization_mechanism.spec.ts +++ b/framework/test/unit/node/consensus/synchronizer/block_synchronization_mechanism/block_synchronization_mechanism.spec.ts @@ -36,11 +36,6 @@ import { getBlocksFromIdRequestSchema, getBlocksFromIdResponseSchema, } from '../../../../../../src/node/consensus/schema'; -import { - liskBFTAssetSchema, - liskBFTModuleID, -} from '../../../../../../src/modules/liskbft/constants'; -import { LiskBFTAPI } from '../../../../../../src/modules/liskbft/api'; describe('block_synchronization_mechanism', () => { const genesisBlock = getGenesisBlock(); @@ -132,7 +127,6 @@ describe('block_synchronization_mechanism', () => { logger: loggerMock, chain: chainModule, blockExecutor, - liskBFTAPI: new LiskBFTAPI(liskBFTModuleID), network: networkMock, }); }); @@ -147,15 +141,8 @@ describe('block_synchronization_mechanism', () => { aBlock = await createValidDefaultBlock({ header: { height: 10, - assets: [ - { - moduleID: liskBFTModuleID, - data: codec.encode(liskBFTAssetSchema, { - maxHeightPrevoted: 0, - maxHeightPreviouslyForged: 0, - }), - }, - ], + maxHeightPrevoted: 0, + maxHeightGenerated: 0, }, }); // chainModule.init will check whether the genesisBlock in storage matches the genesisBlock in @@ -465,21 +452,21 @@ describe('block_synchronization_mechanism', () => { peerId, }) .mockResolvedValue({ - data: receivedBlock.getBytes().toString('hex'), + data: receivedBlock.getBytes(), } as never); when(networkMock.requestFromPeer) .calledWith({ procedure: 'getBlocksFromId', peerId, data: { - blockId: highestCommonBlock.id.toString('hex'), + blockId: highestCommonBlock.id, }, }) .mockResolvedValue({ data: objects .cloneDeep(requestedBlocks) .reverse() - .map(b => b.getBytes().toString('hex')), + .map(b => b.getBytes()), } as never); } @@ -958,15 +945,8 @@ describe('block_synchronization_mechanism', () => { const previousTip = await createValidDefaultBlock({ header: { height: aBlock.header.height - 1, // So it doesn't have preference over new tip (height >) - assets: [ - { - moduleID: liskBFTModuleID, - data: codec.encode(liskBFTAssetSchema, { - maxHeightPrevoted: 0, - maxHeightPreviouslyForged: 0, - }), - }, - ], + maxHeightPrevoted: 0, + maxHeightGenerated: 0, }, }); diff --git a/framework/test/unit/node/consensus/synchronizer/synchronizer.spec.ts b/framework/test/unit/node/consensus/synchronizer/synchronizer.spec.ts index 44d38b99926..431549ffd93 100644 --- a/framework/test/unit/node/consensus/synchronizer/synchronizer.spec.ts +++ b/framework/test/unit/node/consensus/synchronizer/synchronizer.spec.ts @@ -16,7 +16,6 @@ import { when } from 'jest-when'; import { Block, Chain } from '@liskhq/lisk-chain'; import { InMemoryKVStore } from '@liskhq/lisk-db'; -import { codec } from '@liskhq/lisk-codec'; import { Synchronizer } from '../../../../../src/node/consensus/synchronizer/synchronizer'; import { createValidDefaultBlock, @@ -25,8 +24,6 @@ import { import * as synchronizerUtils from '../../../../../src/node/consensus/synchronizer/utils'; import { BlockExecutor } from '../../../../../src/node/consensus/synchronizer/type'; import { applicationConfigSchema } from '../../../../../src/schema'; -import { liskBFTAssetSchema, liskBFTModuleID } from '../../../../../src/modules/liskbft/constants'; -import { LiskBFTAPI } from '../../../../../src/modules/liskbft/api'; jest.mock('@liskhq/lisk-db'); @@ -99,7 +96,6 @@ describe('Synchronizer', () => { logger: loggerMock, blockExecutor, chainModule, - liskBFTAPI: new LiskBFTAPI(liskBFTModuleID), mechanisms: [syncMechanism1, syncMechanism2], }; @@ -211,15 +207,8 @@ describe('Synchronizer', () => { height: genesisBlock.header.height + 1, previousBlockID: genesisBlock.header.id, version: 9, - assets: [ - { - moduleID: liskBFTModuleID, - data: codec.encode(liskBFTAssetSchema, { - maxHeightPrevoted: 0, - maxHeightPreviouslyForged: 0, - }), - }, - ], + maxHeightPrevoted: 0, + maxHeightGenerated: 0, }, }); const blocksTempTableEntries = [ @@ -228,15 +217,8 @@ describe('Synchronizer', () => { height: genesisBlock.header.height + 2, version: 2, previousBlockID: initialLastBlock.header.id, - assets: [ - { - moduleID: liskBFTModuleID, - data: codec.encode(liskBFTAssetSchema, { - maxHeightPrevoted: 3, - maxHeightPreviouslyForged: 0, - }), - }, - ], + maxHeightPrevoted: 3, + maxHeightGenerated: 0, }, }), ]; @@ -363,7 +345,6 @@ describe('Synchronizer', () => { logger: loggerMock, blockExecutor, chainModule, - liskBFTAPI: new LiskBFTAPI(liskBFTModuleID), mechanisms: [aSyncingMechanism, anotherSyncingMechanism] as any, }); @@ -382,7 +363,6 @@ describe('Synchronizer', () => { logger: loggerMock, blockExecutor, chainModule, - liskBFTAPI: new LiskBFTAPI(liskBFTModuleID), mechanisms: [aSyncingMechanism] as any, }), ).toThrow('Mechanism Object should implement "isValidFor" method'); @@ -399,7 +379,6 @@ describe('Synchronizer', () => { logger: loggerMock, blockExecutor, chainModule, - liskBFTAPI: new LiskBFTAPI(liskBFTModuleID), mechanisms: [aSyncingMechanism] as any, }), ).toThrow('Mechanism Object should implement "run" method'); diff --git a/framework/test/unit/node/consensus/synchronizer/utils.spec.ts b/framework/test/unit/node/consensus/synchronizer/utils.spec.ts index 739e105bfe3..1e556d7ae5c 100644 --- a/framework/test/unit/node/consensus/synchronizer/utils.spec.ts +++ b/framework/test/unit/node/consensus/synchronizer/utils.spec.ts @@ -13,9 +13,6 @@ */ import { Block } from '@liskhq/lisk-chain'; -import { codec } from '@liskhq/lisk-codec'; -import { LiskBFTAPI } from '../../../../../src/modules/liskbft/api'; -import { liskBFTAssetSchema, liskBFTModuleID } from '../../../../../src/modules/liskbft/constants'; import { restoreBlocks, restoreBlocksUponStartup, @@ -32,15 +29,8 @@ describe('#synchronizer/utils', () => { lastBlock = await createValidDefaultBlock({ header: { height: 1, - assets: [ - { - moduleID: liskBFTModuleID, - data: codec.encode(liskBFTAssetSchema, { - maxHeightPrevoted: 0, - maxHeightPreviouslyForged: 0, - }), - }, - ], + maxHeightPrevoted: 0, + maxHeightGenerated: 0, }, }); chainMock = { @@ -118,29 +108,15 @@ describe('#synchronizer/utils', () => { header: { height: 11, previousBlockID: Buffer.from('height-10'), - assets: [ - { - moduleID: liskBFTModuleID, - data: codec.encode(liskBFTAssetSchema, { - maxHeightPrevoted: 7, - maxHeightPreviouslyForged: 0, - }), - }, - ], + maxHeightPrevoted: 7, + maxHeightGenerated: 0, }, }), await createValidDefaultBlock({ header: { height: 10, - assets: [ - { - moduleID: liskBFTModuleID, - data: codec.encode(liskBFTAssetSchema, { - maxHeightPrevoted: 6, - maxHeightPreviouslyForged: 0, - }), - }, - ], + maxHeightPrevoted: 6, + maxHeightGenerated: 0, }, }), ]; @@ -152,25 +128,13 @@ describe('#synchronizer/utils', () => { chainMock.lastBlock = await createValidDefaultBlock({ header: { height: 1, - assets: [ - { - moduleID: liskBFTModuleID, - data: codec.encode(liskBFTAssetSchema, { - maxHeightPrevoted: 0, - maxHeightPreviouslyForged: 0, - }), - }, - ], + maxHeightPrevoted: 0, + maxHeightGenerated: 0, }, }); // Act - await restoreBlocksUponStartup( - loggerMock, - chainMock, - blockExecutor, - new LiskBFTAPI(liskBFTModuleID), - ); + await restoreBlocksUponStartup(loggerMock, chainMock, blockExecutor); // Assert expect(chainMock.dataAccess.getTempBlocks).toHaveBeenCalled(); @@ -183,15 +147,8 @@ describe('#synchronizer/utils', () => { header: { previousBlockID: Buffer.from('height-9'), height: 9, - assets: [ - { - moduleID: liskBFTModuleID, - data: codec.encode(liskBFTAssetSchema, { - maxHeightPrevoted: 0, - maxHeightPreviouslyForged: 0, - }), - }, - ], + maxHeightPrevoted: 0, + maxHeightGenerated: 0, }, }); }); @@ -199,26 +156,14 @@ describe('#synchronizer/utils', () => { header: { previousBlockID: Buffer.from('height-9'), height: 10, - assets: [ - { - moduleID: liskBFTModuleID, - data: codec.encode(liskBFTAssetSchema, { - maxHeightPrevoted: 0, - maxHeightPreviouslyForged: 0, - }), - }, - ], + maxHeightPrevoted: 0, + maxHeightGenerated: 0, }, }); chainMock.lastBlock.header._id = Buffer.from('height-10'); // Act - await restoreBlocksUponStartup( - loggerMock, - chainMock, - blockExecutor, - new LiskBFTAPI(liskBFTModuleID), - ); + await restoreBlocksUponStartup(loggerMock, chainMock, blockExecutor); // Assert expect(chainMock.dataAccess.getTempBlocks).toHaveBeenCalled(); @@ -231,25 +176,13 @@ describe('#synchronizer/utils', () => { chainMock.lastBlock = await createValidDefaultBlock({ header: { height: 1, - assets: [ - { - moduleID: liskBFTModuleID, - data: codec.encode(liskBFTAssetSchema, { - maxHeightPrevoted: 0, - maxHeightPreviouslyForged: 0, - }), - }, - ], + maxHeightPrevoted: 0, + maxHeightGenerated: 0, }, }); // Act - await restoreBlocksUponStartup( - loggerMock, - chainMock, - blockExecutor, - new LiskBFTAPI(liskBFTModuleID), - ); + await restoreBlocksUponStartup(loggerMock, chainMock, blockExecutor); // Assert expect(chainMock.dataAccess.getTempBlocks).toHaveBeenCalled(); diff --git a/framework/test/unit/node/generator/endpoint.spec.ts b/framework/test/unit/node/generator/endpoint.spec.ts index 3af966ec7fe..28161783520 100644 --- a/framework/test/unit/node/generator/endpoint.spec.ts +++ b/framework/test/unit/node/generator/endpoint.spec.ts @@ -13,16 +13,19 @@ */ import { Chain, Transaction } from '@liskhq/lisk-chain'; +import { codec } from '@liskhq/lisk-codec'; import { getRandomBytes } from '@liskhq/lisk-cryptography'; -import { InMemoryKVStore } from '@liskhq/lisk-db'; +import { InMemoryKVStore, KVStore } from '@liskhq/lisk-db'; import { TransactionPool } from '@liskhq/lisk-transaction-pool'; import { dataStructures } from '@liskhq/lisk-utils'; import { LiskValidationError } from '@liskhq/lisk-validator'; import { Logger } from '../../../../src/logger'; -import { LiskBFTAPI } from '../../../../src/node/consensus'; import { Broadcaster } from '../../../../src/node/generator/broadcaster'; +import { GENERATOR_STORE_RESERVED_PREFIX } from '../../../../src/node/generator/constants'; import { Endpoint } from '../../../../src/node/generator/endpoint'; import { InvalidTransactionError } from '../../../../src/node/generator/errors'; +import { GeneratorStore } from '../../../../src/node/generator/generator_store'; +import { previouslyGeneratedInfoSchema } from '../../../../src/node/generator/schemas'; import { Consensus, Keypair } from '../../../../src/node/generator/types'; import { StateMachine, VerifyStatus } from '../../../../src/node/state_machine'; import { fakeLogger } from '../../../utils/node'; @@ -55,7 +58,6 @@ describe('generator endpoint', () => { let consensus: Consensus; let pool: TransactionPool; let stateMachine: StateMachine; - let liskBFTAPI: LiskBFTAPI; beforeEach(() => { broadcaster = { @@ -72,9 +74,6 @@ describe('generator endpoint', () => { consensus = { isSynced: jest.fn().mockResolvedValue(true), } as never; - liskBFTAPI = { - verifyGeneratorInfo: jest.fn(), - } as never; pool = { contains: jest.fn().mockReturnValue(false), add: jest.fn().mockResolvedValue({}), @@ -88,7 +87,6 @@ describe('generator endpoint', () => { broadcaster, chain, consensus, - liskBFTAPI, pool, stateMachine, keypair: new dataStructures.BufferMap(), @@ -201,7 +199,7 @@ describe('generator endpoint', () => { const bftProps = { height: 200, maxHeightPrevoted: 200, - maxHeightPreviouslyForged: 10, + maxHeightGenerated: 10, }; it('should reject with error when request schema is invalid', async () => { @@ -308,8 +306,7 @@ describe('generator endpoint', () => { expect(endpoint['_keypairs'].has(Buffer.from(config.address, 'hex'))).toBeFalse(); }); - it('should fail to enable to enable if verify generator fails', async () => { - (liskBFTAPI.verifyGeneratorInfo as jest.Mock).mockRejectedValue(new Error('invalid')); + it('should update the keypair and return enabled', async () => { await expect( endpoint.updateForgingStatus({ logger, @@ -322,10 +319,149 @@ describe('generator endpoint', () => { ...bftProps, }, }), - ).rejects.toThrow(); + ).resolves.toEqual({ + address: config.address, + enabled: true, + }); + expect(endpoint['_keypairs'].has(Buffer.from(config.address, 'hex'))).toBeTrue(); }); - it('should update the keypair and return enabled', async () => { + it('should accept if BFT properties specified are zero and there is no previous values', async () => { + const db = (new InMemoryKVStore() as unknown) as KVStore; + endpoint.init({ + blockchainDB: new InMemoryKVStore() as never, + generatorDB: db, + logger, + }); + await expect( + endpoint.updateForgingStatus({ + logger, + getStore: jest.fn(), + params: { + address: config.address, + enable: true, + password: defaultPassword, + overwrite: false, + height: 0, + maxHeightPrevoted: 0, + maxHeightGenerated: 0, + }, + }), + ).resolves.toEqual({ + address: config.address, + enabled: true, + }); + }); + + it('should reject if BFT properties specified are non-zero and there is no previous values', async () => { + const db = (new InMemoryKVStore() as unknown) as KVStore; + endpoint.init({ + blockchainDB: new InMemoryKVStore() as never, + generatorDB: db, + logger, + }); + await expect( + endpoint.updateForgingStatus({ + logger, + getStore: jest.fn(), + params: { + address: config.address, + enable: true, + password: defaultPassword, + overwrite: false, + height: 100, + maxHeightPrevoted: 40, + maxHeightGenerated: 3, + }, + }), + ).rejects.toThrow('Last generated information does not exist.'); + }); + + it('should reject if BFT properties specified are zero and there is non zero previous values', async () => { + const encodedInfo = codec.encode(previouslyGeneratedInfoSchema, { + height: 100, + maxHeightPrevoted: 40, + maxHeightGenerated: 3, + }); + const db = (new InMemoryKVStore() as unknown) as KVStore; + const generatorStore = new GeneratorStore(db as never); + const subStore = generatorStore.getGeneratorStore(GENERATOR_STORE_RESERVED_PREFIX); + await subStore.set(Buffer.from(config.address, 'hex'), encodedInfo); + const batch = db.batch(); + subStore.finalize(batch); + await batch.write(); + endpoint.init({ + blockchainDB: new InMemoryKVStore() as never, + generatorDB: db, + logger, + }); + await expect( + endpoint.updateForgingStatus({ + logger, + getStore: jest.fn(), + params: { + address: config.address, + enable: true, + password: defaultPassword, + overwrite: false, + height: 0, + maxHeightPrevoted: 0, + maxHeightGenerated: 0, + }, + }), + ).rejects.toThrow('Request does not match last generated information.'); + }); + + it('should reject if BFT properties specified specified does not match existing properties', async () => { + const encodedInfo = codec.encode(previouslyGeneratedInfoSchema, { + height: 50, + maxHeightPrevoted: 40, + maxHeightGenerated: 3, + }); + const db = (new InMemoryKVStore() as unknown) as KVStore; + const generatorStore = new GeneratorStore(db as never); + const subStore = generatorStore.getGeneratorStore(GENERATOR_STORE_RESERVED_PREFIX); + await subStore.set(Buffer.from(config.address, 'hex'), encodedInfo); + const batch = db.batch(); + subStore.finalize(batch); + await batch.write(); + endpoint.init({ + blockchainDB: new InMemoryKVStore() as never, + generatorDB: db, + logger, + }); + await expect( + endpoint.updateForgingStatus({ + logger, + getStore: jest.fn(), + params: { + address: config.address, + enable: true, + password: defaultPassword, + overwrite: false, + height: 100, + maxHeightPrevoted: 40, + maxHeightGenerated: 3, + }, + }), + ).rejects.toThrow('Request does not match last generated information.'); + }); + + it('should overwrite if BFT properties specified specified does not match existing properties and overwrite is true', async () => { + const encodedInfo = codec.encode(previouslyGeneratedInfoSchema, { + height: 50, + maxHeightPrevoted: 40, + maxHeightGenerated: 3, + }); + const db = (new InMemoryKVStore() as unknown) as KVStore; + const generatorStore = new GeneratorStore(db as never); + const subStore = generatorStore.getGeneratorStore(GENERATOR_STORE_RESERVED_PREFIX); + await subStore.set(Buffer.from(config.address, 'hex'), encodedInfo); + endpoint.init({ + blockchainDB: new InMemoryKVStore() as never, + generatorDB: db, + logger, + }); await expect( endpoint.updateForgingStatus({ logger, @@ -335,14 +471,24 @@ describe('generator endpoint', () => { enable: true, password: defaultPassword, overwrite: true, - ...bftProps, + height: 100, + maxHeightPrevoted: 40, + maxHeightGenerated: 3, }, }), ).resolves.toEqual({ address: config.address, enabled: true, }); - expect(endpoint['_keypairs'].has(Buffer.from(config.address, 'hex'))).toBeTrue(); + const updatedGeneratorStore = new GeneratorStore(db); + const updated = updatedGeneratorStore.getGeneratorStore(GENERATOR_STORE_RESERVED_PREFIX); + const val = await updated.get(Buffer.from(config.address, 'hex')); + const decodedInfo = codec.decode(previouslyGeneratedInfoSchema, val); + expect(decodedInfo).toEqual({ + height: 100, + maxHeightPrevoted: 40, + maxHeightGenerated: 3, + }); }); }); }); diff --git a/framework/test/unit/node/generator/generator.spec.ts b/framework/test/unit/node/generator/generator.spec.ts index c9f2aa872d6..f019bb09fa7 100644 --- a/framework/test/unit/node/generator/generator.spec.ts +++ b/framework/test/unit/node/generator/generator.spec.ts @@ -114,7 +114,13 @@ describe('generator', () => { getSlotTime: jest.fn(), getGenerator: jest.fn(), } as never; - liskBFTAPI = {} as never; + liskBFTAPI = { + getBFTHeights: jest.fn().mockResolvedValue({ + maxHeightPrevoted: 0, + maxHeightPrecommited: 0, + maxHeightCertified: 0, + }), + } as never; stateMachine = { verifyTransaction: jest.fn(), diff --git a/framework/test/unit/node/generator/strategies.spec.ts b/framework/test/unit/node/generator/strategies.spec.ts index d2f75adaa98..a057e2a2815 100644 --- a/framework/test/unit/node/generator/strategies.spec.ts +++ b/framework/test/unit/node/generator/strategies.spec.ts @@ -14,7 +14,7 @@ import { when } from 'jest-when'; import { BlockHeader } from '@liskhq/lisk-chain/dist-node/block_header'; -import { getAddressFromPublicKey } from '@liskhq/lisk-cryptography'; +import { getAddressFromPublicKey, hash } from '@liskhq/lisk-cryptography'; import { dataStructures } from '@liskhq/lisk-utils'; import { InMemoryKVStore } from '@liskhq/lisk-db'; import { HighFeeGenerationStrategy } from '../../../../src/node/generator/strategies'; @@ -107,10 +107,20 @@ describe('strategies', () => { beforeEach(() => { header = new BlockHeader({ version: 2, - assets: [], generatorAddress: Buffer.from('address'), height: 20, previousBlockID: Buffer.from('id'), + maxHeightGenerated: 0, + maxHeightPrevoted: 0, + assetsRoot: hash(Buffer.alloc(0)), + transactionRoot: hash(Buffer.alloc(0)), + validatorsHash: hash(Buffer.alloc(0)), + stateRoot: hash(Buffer.alloc(0)), + aggregateCommit: { + aggregationBits: Buffer.alloc(0), + certificateSignature: Buffer.alloc(0), + height: 0, + }, timestamp: 100000000, }); strategy = new HighFeeGenerationStrategy({ diff --git a/framework/test/unit/node/state_machine/state_machine.spec.ts b/framework/test/unit/node/state_machine/state_machine.spec.ts index 946963cd3b7..29459bd8039 100644 --- a/framework/test/unit/node/state_machine/state_machine.spec.ts +++ b/framework/test/unit/node/state_machine/state_machine.spec.ts @@ -11,20 +11,21 @@ * * Removal or modification of this copyright notice is prohibited. */ -import { Transaction } from '@liskhq/lisk-chain'; +import { Transaction, StateStore, BlockAssets } from '@liskhq/lisk-chain'; import { Logger } from '../../../../src/logger'; import { BlockContext } from '../../../../src/node/state_machine/block_context'; import { EventQueue } from '../../../../src/node/state_machine/event_queue'; import { GenesisBlockContext } from '../../../../src/node/state_machine/genesis_block_context'; import { StateMachine } from '../../../../src/node/state_machine/state_machine'; import { TransactionContext } from '../../../../src/node/state_machine/transaction_context'; -import { StateStore, BlockHeader, VerifyStatus } from '../../../../src/node/state_machine/types'; +import { BlockHeader, VerifyStatus } from '../../../../src/node/state_machine'; import { CustomModule0, CustomModule1, CustomModule2 } from './custom_modules'; describe('state_machine', () => { const genesisHeader = {} as BlockHeader; const header = {} as BlockHeader; const logger = {} as Logger; + const assets = new BlockAssets(); const stateStore = {} as StateStore; const eventQueue = new EventQueue(); const networkIdentifier = Buffer.from('network identifier', 'utf8'); @@ -51,6 +52,7 @@ describe('state_machine', () => { const ctx = new GenesisBlockContext({ eventQueue, header: genesisHeader, + assets, logger, stateStore, }); @@ -62,6 +64,7 @@ describe('state_machine', () => { getAPIContext: expect.any(Function), getStore: expect.any(Function), header: genesisHeader, + assets, }); }); }); @@ -110,6 +113,7 @@ describe('state_machine', () => { logger, stateStore, header, + assets, networkIdentifier, transaction, }); @@ -119,6 +123,7 @@ describe('state_machine', () => { logger, transaction, header, + assets, eventQueue, getAPIContext: expect.any(Function), getStore: expect.any(Function), @@ -135,6 +140,7 @@ describe('state_machine', () => { logger, stateStore, header, + assets, networkIdentifier, transactions: [transaction], }); @@ -143,6 +149,7 @@ describe('state_machine', () => { networkIdentifier, logger, header, + assets, eventQueue, getAPIContext: expect.any(Function), getStore: expect.any(Function), @@ -159,6 +166,7 @@ describe('state_machine', () => { logger, stateStore, header, + assets, networkIdentifier, transactions: [transaction], }); @@ -167,6 +175,7 @@ describe('state_machine', () => { networkIdentifier, logger, header, + assets, eventQueue, getAPIContext: expect.any(Function), getStore: expect.any(Function), @@ -183,6 +192,7 @@ describe('state_machine', () => { logger, stateStore, header, + assets, networkIdentifier, transactions: [transaction], }); @@ -191,6 +201,7 @@ describe('state_machine', () => { networkIdentifier, logger, header, + assets, eventQueue, getAPIContext: expect.any(Function), getStore: expect.any(Function), @@ -207,6 +218,7 @@ describe('state_machine', () => { logger, stateStore, header, + assets, networkIdentifier, transactions: [transaction], }); @@ -215,6 +227,7 @@ describe('state_machine', () => { networkIdentifier, logger, header, + assets, eventQueue, getAPIContext: expect.any(Function), getStore: expect.any(Function), @@ -225,6 +238,7 @@ describe('state_machine', () => { networkIdentifier, logger, header, + assets, eventQueue, getAPIContext: expect.any(Function), getStore: expect.any(Function),