From a420fcd667b74995e0cfd45dcca9cff28949a606 Mon Sep 17 00:00:00 2001 From: shuse2 Date: Fri, 6 Oct 2023 22:25:42 +0200 Subject: [PATCH] Add keys info to generator:status and create singleCommits when enabling generator (#9069) * :seedling: Add keys information to status * :bug: Fix single commits when enabling generator * :bug: remove unused metrics --- framework/src/engine/generator/endpoint.ts | 9 + framework/src/engine/generator/generator.ts | 105 +----- framework/src/engine/generator/schemas.ts | 2 + .../engine/generator/single_commit_handler.ts | 166 ++++++++++ .../unit/engine/generator/endpoint.spec.ts | 27 ++ .../unit/engine/generator/generator.spec.ts | 232 ++------------ .../generator/single_commit_handler.spec.ts | 301 ++++++++++++++++++ 7 files changed, 543 insertions(+), 299 deletions(-) create mode 100644 framework/src/engine/generator/single_commit_handler.ts create mode 100644 framework/test/unit/engine/generator/single_commit_handler.spec.ts diff --git a/framework/src/engine/generator/endpoint.ts b/framework/src/engine/generator/endpoint.ts index 77ffe2de699..a31225ee6df 100644 --- a/framework/src/engine/generator/endpoint.ts +++ b/framework/src/engine/generator/endpoint.ts @@ -53,6 +53,7 @@ import { RequestContext } from '../rpc/rpc_server'; import { ABI } from '../../abi'; import { JSONObject } from '../../types'; import { NotFoundError } from './errors'; +import { SingleCommitHandler } from './single_commit_handler'; interface EndpointArgs { keypair: dataStructures.BufferMap; @@ -64,6 +65,7 @@ interface EndpointArgs { interface EndpointInit { generatorDB: Database; + singleCommitHandler: SingleCommitHandler; genesisHeight: number; } @@ -77,6 +79,7 @@ export class Endpoint { private _generatorDB!: Database; private _genesisHeight!: number; + private _singleCommitHandler!: SingleCommitHandler; public constructor(args: EndpointArgs) { this._keypairs = args.keypair; @@ -89,6 +92,7 @@ export class Endpoint { public init(args: EndpointInit) { this._generatorDB = args.generatorDB; this._genesisHeight = args.genesisHeight; + this._singleCommitHandler = args.singleCommitHandler; } public async getStatus(_ctx: RequestContext): Promise { @@ -96,8 +100,11 @@ export class Endpoint { const list = await getGeneratedInfo(generatorStore); const status = []; for (const info of list) { + const keys = this._keypairs.get(info.address); status.push({ ...info, + generatorKey: keys?.publicKey.toString('hex') ?? '', + blsKey: keys?.blsPublicKey.toString('hex') ?? '', address: cryptoAddress.getLisk32AddressFromAddress(info.address), enabled: this._keypairs.has(info.address), }); @@ -196,6 +203,8 @@ export class Endpoint { ); } + await this._singleCommitHandler.initSingleCommits(address); + ctx.logger.info(`Block generation enabled on address: ${req.address}`); return { diff --git a/framework/src/engine/generator/generator.ts b/framework/src/engine/generator/generator.ts index c731fd77b6e..931971a16af 100644 --- a/framework/src/engine/generator/generator.ts +++ b/framework/src/engine/generator/generator.ts @@ -74,6 +74,7 @@ import { BFTModule } from '../bft'; import { isEmptyConsensusUpdate } from '../consensus'; import { getPathFromDataPath } from '../../utils/path'; import { defaultMetrics } from '../metrics/metrics'; +import { SingleCommitHandler } from './single_commit_handler'; interface GeneratorArgs { config: EngineConfig; @@ -111,7 +112,6 @@ export class Generator { private readonly _forgingStrategy: HighFeeGenerationStrategy; private readonly _blockTime: number; private readonly _metrics = { - signedCommits: defaultMetrics.counter('generator_signedCommits'), blockGeneration: defaultMetrics.counter('generator_blockGeneration'), }; @@ -119,6 +119,7 @@ export class Generator { private _generatorDB!: Database; private _blockchainDB!: Database; private _genesisHeight!: number; + private _singleCommitHandler!: SingleCommitHandler; public constructor(args: GeneratorArgs) { this._abi = args.abi; @@ -174,12 +175,22 @@ export class Generator { this._blockchainDB = args.blockchainDB; this._genesisHeight = args.genesisHeight; + this._singleCommitHandler = new SingleCommitHandler( + this._logger, + this._chain, + this._consensus, + this._bft, + this._keypairs, + this._blockchainDB, + ); + this._broadcaster.init({ logger: this._logger, }); this._endpoint.init({ generatorDB: this._generatorDB, genesisHeight: this._genesisHeight, + singleCommitHandler: this._singleCommitHandler, }); this._networkEndpoint.init({ logger: this._logger, @@ -209,14 +220,7 @@ export class Generator { this.events.emit(GENERATOR_EVENT_NEW_TRANSACTION, e); }); - const stateStore = new StateStore(this._blockchainDB); - - // On node start, it re generates certificate from maxRemovalHeight to maxHeightPrecommitted. - // in the _handleFinalizedHeightChanged, it loops between maxRemovalHeight + 1 and maxHeightPrecommitted. - // @see https://github.com/LiskHQ/lips/blob/main/proposals/lip-0061.md#initial-single-commit-creation - const maxRemovalHeight = await this._consensus.getMaxRemovalHeight(); - const { maxHeightPrecommitted } = await this._bft.method.getBFTHeights(stateStore); - await Promise.all(this._handleFinalizedHeightChanged(maxRemovalHeight, maxHeightPrecommitted)); + await this._singleCommitHandler.initAllSingleCommits(); } public get endpoint(): Endpoint { @@ -258,11 +262,8 @@ export class Generator { this._consensus.events.on( CONSENSUS_EVENT_FINALIZED_HEIGHT_CHANGED, ({ from, to }: { from: number; to: number }) => { - this._consensus - .getMaxRemovalHeight() - .then(async maxRemovalHeight => - Promise.all(this._handleFinalizedHeightChanged(Math.max(maxRemovalHeight, from), to)), - ) + this._singleCommitHandler + .handleFinalizedHeightChanged(from, to) .catch((err: Error) => this._logger.error({ err }, 'Fail to certify single commit')); }, ); @@ -648,82 +649,6 @@ export class Generator { return generatedBlock; } - private _handleFinalizedHeightChanged(from: number, to: number): Promise[] { - if (from >= to) { - return []; - } - const promises = []; - const stateStore = new StateStore(this._blockchainDB); - for (const [address, pairs] of this._keypairs.entries()) { - for (let height = from + 1; height < to; height += 1) { - promises.push( - this._certifySingleCommitForChangedHeight( - stateStore, - height, - address, - pairs.blsPublicKey, - pairs.blsSecretKey, - ), - ); - } - promises.push( - this._certifySingleCommit(stateStore, to, address, pairs.blsPublicKey, pairs.blsSecretKey), - ); - } - return promises; - } - - private async _certifySingleCommitForChangedHeight( - stateStore: StateStore, - height: number, - generatorAddress: Buffer, - blsPK: Buffer, - blsSK: Buffer, - ): Promise { - const paramExist = await this._bft.method.existBFTParameters(stateStore, height + 1); - if (!paramExist) { - return; - } - await this._certifySingleCommit(stateStore, height, generatorAddress, blsPK, blsSK); - } - - private async _certifySingleCommit( - stateStore: StateStore, - height: number, - generatorAddress: Buffer, - blsPK: Buffer, - blsSK: Buffer, - ): Promise { - const params = await this._bft.method.getBFTParametersActiveValidators(stateStore, height); - const registeredValidator = params.validators.find(v => v.address.equals(generatorAddress)); - if (!registeredValidator) { - return; - } - if (!registeredValidator.blsKey.equals(blsPK)) { - this._logger.warn( - { address: addressUtil.getLisk32AddressFromAddress(generatorAddress) }, - 'Validator does not have registered BLS key', - ); - return; - } - - const blockHeader = await this._chain.dataAccess.getBlockHeaderByHeight(height); - const validatorInfo = { - address: generatorAddress, - blsPublicKey: blsPK, - blsSecretKey: blsSK, - }; - this._consensus.certifySingleCommit(blockHeader, validatorInfo); - this._logger.debug( - { - height, - generator: addressUtil.getLisk32AddressFromAddress(generatorAddress), - }, - 'Certified single commit', - ); - this._metrics.signedCommits.inc(1); - } - private async _executeTransactions( contextID: Buffer, header: BlockHeader, diff --git a/framework/src/engine/generator/schemas.ts b/framework/src/engine/generator/schemas.ts index 661686af644..cd638db1865 100644 --- a/framework/src/engine/generator/schemas.ts +++ b/framework/src/engine/generator/schemas.ts @@ -85,6 +85,8 @@ export interface GetStatusResponse { height: number; maxHeightPrevoted: number; maxHeightGenerated: number; + blsKey: string; + generatorKey: string; enabled: boolean; }[]; } diff --git a/framework/src/engine/generator/single_commit_handler.ts b/framework/src/engine/generator/single_commit_handler.ts new file mode 100644 index 00000000000..64288d1f4cc --- /dev/null +++ b/framework/src/engine/generator/single_commit_handler.ts @@ -0,0 +1,166 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { Chain, StateStore } from '@liskhq/lisk-chain'; +import { dataStructures } from '@liskhq/lisk-utils'; +import { address as addressUtil } from '@liskhq/lisk-cryptography'; +import { Database } from '@liskhq/lisk-db'; +import { BFTModule } from '../bft'; +import { Consensus, Keypair } from './types'; +import { Logger } from '../../logger'; +import { defaultMetrics } from '../metrics/metrics'; + +export class SingleCommitHandler { + private readonly _logger: Logger; + private readonly _bft: BFTModule; + private readonly _chain: Chain; + private readonly _consensus: Consensus; + private readonly _keypairs: dataStructures.BufferMap; + private readonly _blockchainDB: Database; + + private readonly _metrics = { + signedCommits: defaultMetrics.counter('generator_signedCommits'), + }; + + public constructor( + logger: Logger, + chain: Chain, + consensus: Consensus, + bft: BFTModule, + keypairs: dataStructures.BufferMap, + blockchainDB: Database, + ) { + this._logger = logger; + this._chain = chain; + this._consensus = consensus; + this._bft = bft; + this._keypairs = keypairs; + this._blockchainDB = blockchainDB; + } + + // On node start, it re generates certificate from maxRemovalHeight to maxHeightPrecommitted. + // in the _handleFinalizedHeightChanged, it loops between maxRemovalHeight + 1 and maxHeightPrecommitted. + // @see https://github.com/LiskHQ/lips/blob/main/proposals/lip-0061.md#initial-single-commit-creation + public async initAllSingleCommits() { + for (const [address] of this._keypairs.entries()) { + await this.initSingleCommits(address); + } + } + + public async initSingleCommits(address: Buffer) { + const maxRemovalHeight = await this._consensus.getMaxRemovalHeight(); + const stateStore = new StateStore(this._blockchainDB); + const { maxHeightPrecommitted } = await this._bft.method.getBFTHeights(stateStore); + await Promise.all( + this._handleFinalizedHeightChanged(address, maxRemovalHeight, maxHeightPrecommitted), + ); + } + + public async handleFinalizedHeightChanged(from: number, to: number): Promise { + const maxRemovalHeight = await this._consensus.getMaxRemovalHeight(); + const cappedFrom = Math.max(maxRemovalHeight, from); + if (cappedFrom >= to) { + return; + } + for (const [address] of this._keypairs.entries()) { + await Promise.all(this._handleFinalizedHeightChanged(address, cappedFrom, to)); + } + } + + private _handleFinalizedHeightChanged( + address: Buffer, + from: number, + to: number, + ): Promise[] { + if (from >= to) { + return []; + } + const promises = []; + const stateStore = new StateStore(this._blockchainDB); + const pairs = this._keypairs.get(address); + if (!pairs) { + this._logger.warn( + { address: addressUtil.getLisk32AddressFromAddress(address) }, + 'Validator does not have registered BLS key on this node', + ); + return []; + } + for (let height = from + 1; height < to; height += 1) { + promises.push( + this._certifySingleCommitForChangedHeight( + stateStore, + height, + address, + pairs.blsPublicKey, + pairs.blsSecretKey, + ), + ); + } + promises.push( + this._certifySingleCommit(stateStore, to, address, pairs.blsPublicKey, pairs.blsSecretKey), + ); + return promises; + } + + private async _certifySingleCommitForChangedHeight( + stateStore: StateStore, + height: number, + generatorAddress: Buffer, + blsPK: Buffer, + blsSK: Buffer, + ): Promise { + const paramExist = await this._bft.method.existBFTParameters(stateStore, height + 1); + if (!paramExist) { + return; + } + await this._certifySingleCommit(stateStore, height, generatorAddress, blsPK, blsSK); + } + + private async _certifySingleCommit( + stateStore: StateStore, + height: number, + generatorAddress: Buffer, + blsPK: Buffer, + blsSK: Buffer, + ): Promise { + const params = await this._bft.method.getBFTParametersActiveValidators(stateStore, height); + const registeredValidator = params.validators.find(v => v.address.equals(generatorAddress)); + if (!registeredValidator) { + return; + } + if (!registeredValidator.blsKey.equals(blsPK)) { + this._logger.warn( + { address: addressUtil.getLisk32AddressFromAddress(generatorAddress) }, + 'Validator does not have registered BLS key', + ); + return; + } + + const blockHeader = await this._chain.dataAccess.getBlockHeaderByHeight(height); + const validatorInfo = { + address: generatorAddress, + blsPublicKey: blsPK, + blsSecretKey: blsSK, + }; + this._consensus.certifySingleCommit(blockHeader, validatorInfo); + this._logger.debug( + { + height, + generator: addressUtil.getLisk32AddressFromAddress(generatorAddress), + }, + 'Certified single commit', + ); + this._metrics.signedCommits.inc(1); + } +} diff --git a/framework/test/unit/engine/generator/endpoint.spec.ts b/framework/test/unit/engine/generator/endpoint.spec.ts index 774c4db0673..36df0b60c1b 100644 --- a/framework/test/unit/engine/generator/endpoint.spec.ts +++ b/framework/test/unit/engine/generator/endpoint.spec.ts @@ -34,6 +34,7 @@ import { previouslyGeneratedInfoSchema, } from '../../../../src/engine/generator/schemas'; import { fakeLogger } from '../../../utils/mocks'; +import { SingleCommitHandler } from '../../../../src/engine/generator/single_commit_handler'; describe('generator endpoint', () => { const logger: Logger = fakeLogger; @@ -104,6 +105,9 @@ describe('generator endpoint', () => { endpoint.init({ generatorDB: db, genesisHeight: 0, + singleCommitHandler: { + initSingleCommits: jest.fn(), + } as unknown as SingleCommitHandler, }); }); @@ -233,6 +237,27 @@ describe('generator endpoint', () => { expect(endpoint['_keypairs'].has(defaultEncryptedKeys.address)).toBeTrue(); }); + it('should create single commits for the address', async () => { + await expect( + endpoint.updateStatus({ + logger, + params: { + address: address.getLisk32AddressFromAddress(defaultEncryptedKeys.address), + enable: true, + password: defaultPassword, + ...bftProps, + }, + chainID, + }), + ).resolves.toEqual({ + address: address.getLisk32AddressFromAddress(defaultEncryptedKeys.address), + enabled: true, + }); + expect(endpoint['_singleCommitHandler'].initSingleCommits).toHaveBeenCalledWith( + defaultEncryptedKeys.address, + ); + }); + it('should accept if BFT properties specified are zero and there is no previous values', async () => { await db.del(Buffer.concat([GENERATOR_STORE_INFO_PREFIX, defaultEncryptedKeys.address])); await expect( @@ -403,6 +428,8 @@ describe('generator endpoint', () => { }); expect(resp.status).toHaveLength(2); expect(resp.status[0].address).not.toBeInstanceOf(Buffer); + expect(resp.status[0].blsKey).toBeString(); + expect(resp.status[0].generatorKey).toBeString(); }); }); diff --git a/framework/test/unit/engine/generator/generator.spec.ts b/framework/test/unit/engine/generator/generator.spec.ts index 081612ae2aa..1bf2a70d45d 100644 --- a/framework/test/unit/engine/generator/generator.spec.ts +++ b/framework/test/unit/engine/generator/generator.spec.ts @@ -14,11 +14,9 @@ import * as fs from 'fs'; import { EventEmitter } from 'events'; import { BlockAssets, Chain, Transaction } from '@liskhq/lisk-chain'; -import { bls, utils, address as cryptoAddress, legacy } from '@liskhq/lisk-cryptography'; +import { utils, address as cryptoAddress } from '@liskhq/lisk-cryptography'; import { InMemoryDatabase, Database } from '@liskhq/lisk-db'; import { codec } from '@liskhq/lisk-codec'; -import { when } from 'jest-when'; -import { Mnemonic } from '@liskhq/lisk-passphrase'; import { Generator } from '../../../../src/engine/generator'; import { Consensus } from '../../../../src/engine/generator/types'; import { Network } from '../../../../src/engine/network'; @@ -34,12 +32,12 @@ import { plainGeneratorKeysSchema, } from '../../../../src/engine/generator/schemas'; import { BFTModule } from '../../../../src/engine/bft'; -import { createFakeBlockHeader } from '../../../../src/testing'; import { ABI } from '../../../../src/abi'; import { defaultConfig } from '../../../../src/testing/fixtures'; import { testing } from '../../../../src'; import { GeneratorStore } from '../../../../src/engine/generator/generator_store'; import { CONSENSUS_EVENT_FINALIZED_HEIGHT_CHANGED } from '../../../../src/engine/consensus/constants'; +import { SingleCommitHandler } from '../../../../src/engine/generator/single_commit_handler'; describe('generator', () => { const logger = fakeLogger; @@ -190,6 +188,7 @@ describe('generator', () => { }), ); } + jest.spyOn(SingleCommitHandler.prototype, 'initAllSingleCommits'); }); it('should load all 101 validators', async () => { @@ -203,7 +202,6 @@ describe('generator', () => { }); it('should handle finalized height change between maxRemovalHeight and max height precommitted', async () => { - jest.spyOn(generator, '_handleFinalizedHeightChanged' as any).mockReturnValue([] as never); jest .spyOn(generator['_bft'].method, 'getBFTHeights') .mockResolvedValue({ maxHeightPrecommitted: 515, maxHeightCertified: 313 } as never); @@ -215,7 +213,7 @@ describe('generator', () => { logger, genesisHeight: 0, }); - expect(generator['_handleFinalizedHeightChanged']).toHaveBeenCalledWith(200, 515); + expect(generator['_singleCommitHandler'].initAllSingleCommits).toHaveBeenCalled(); }); }); @@ -641,216 +639,32 @@ describe('generator', () => { }); describe('events CONSENSUS_EVENT_FINALIZED_HEIGHT_CHANGED', () => { - const passphrase = Mnemonic.generateMnemonic(256); - const keys = legacy.getPrivateAndPublicKeyFromPassphrase(passphrase); - const address = cryptoAddress.getAddressFromPublicKey(keys.publicKey); - const blsSecretKey = bls.generatePrivateKey(Buffer.from(passphrase, 'utf-8')); - const keypair = { - ...keys, - blsSecretKey, - blsPublicKey: bls.getPublicKeyFromPrivateKey(blsSecretKey), - }; - const blsKey = bls.getPublicKeyFromPrivateKey(keypair.blsSecretKey); - const blockHeader = createFakeBlockHeader(); - - describe('when generator is a standby validator', () => { - beforeEach(async () => { - generator['_keypairs'].set(address, keypair); - when(generator['_bft'].method.existBFTParameters as jest.Mock) - .calledWith(expect.anything(), 1) - .mockResolvedValue(true as never) - .calledWith(expect.anything(), 12) - .mockResolvedValue(true as never) - .calledWith(expect.anything(), 21) - .mockResolvedValue(true as never) - .calledWith(expect.anything(), 51) - .mockResolvedValue(false as never) - .calledWith(expect.anything(), 55) - .mockResolvedValue(true as never); - when(generator['_bft'].method.getBFTParametersActiveValidators as jest.Mock) - .calledWith(expect.anything(), 11) - .mockResolvedValue({ - validators: [{ address: utils.getRandomBytes(20), blsKey: utils.getRandomBytes(48) }], - }) - .calledWith(expect.anything(), 20) - .mockResolvedValue({ - validators: [{ address: utils.getRandomBytes(20), blsKey: utils.getRandomBytes(48) }], - }) - .calledWith(expect.anything(), 50) - .mockResolvedValue({ - validators: [{ address: utils.getRandomBytes(20), blsKey: utils.getRandomBytes(48) }], - }) - .calledWith(expect.anything(), 54) - .mockResolvedValue({ validators: [] }); - - jest - .spyOn(generator['_chain'].dataAccess, 'getBlockHeaderByHeight') - .mockResolvedValue(blockHeader as never); - await generator.init({ - blockchainDB, - generatorDB, - logger, - genesisHeight: 0, - }); - await generator.start(); - jest.spyOn(generator['_consensus'], 'certifySingleCommit'); - }); - - afterAll(async () => { - await generator.stop(); - }); - it('should not call certifySingleCommit when standby validator creates block', async () => { - // Act - await Promise.all(generator['_handleFinalizedHeightChanged'](10, 50)); - - // Assert - expect(generator['_consensus'].certifySingleCommit).toHaveBeenCalledTimes(0); + beforeEach(async () => { + jest.spyOn(SingleCommitHandler.prototype, 'handleFinalizedHeightChanged'); + await generator.init({ + blockchainDB, + generatorDB, + logger, + genesisHeight: 0, }); + await generator.start(); }); - describe('when generator is an active validator', () => { - beforeEach(async () => { - generator['_keypairs'].set(address, keypair); - when(generator['_bft'].method.existBFTParameters as jest.Mock) - .calledWith(expect.anything(), 1) - .mockResolvedValue(true as never) - .calledWith(expect.anything(), 12) - .mockResolvedValue(true as never) - .calledWith(expect.anything(), 21) - .mockResolvedValue(true as never) - .calledWith(expect.anything(), 51) - .mockResolvedValue(false as never) - .calledWith(expect.anything(), 55) - .mockResolvedValue(true as never); - when(generator['_bft'].method.getBFTParametersActiveValidators as jest.Mock) - .calledWith(expect.anything(), 11) - .mockResolvedValue({ validators: [{ address, blsKey }] }) - .calledWith(expect.anything(), 20) - .mockResolvedValue({ validators: [{ address, blsKey: Buffer.alloc(48) }] }) - .calledWith(expect.anything(), 50) - .mockResolvedValue({ validators: [{ address, blsKey }] }) - .calledWith(expect.anything(), 54) - .mockResolvedValue({ validators: [] }); - - jest - .spyOn(generator['_chain'].dataAccess, 'getBlockHeaderByHeight') - .mockResolvedValue(blockHeader as never); - await generator.init({ - blockchainDB, - generatorDB, - logger, - genesisHeight: 0, - }); - await generator.start(); - jest.spyOn(generator['_consensus'], 'certifySingleCommit'); - }); - - afterAll(async () => { - await generator.stop(); - }); - - it('should call certifySingleCommit for range when params for height + 1 exist', async () => { - // Act - await Promise.all(generator['_handleFinalizedHeightChanged'](10, 50)); - - // Assert - expect(generator['_consensus'].certifySingleCommit).toHaveBeenCalledTimes(2); - expect(generator['_consensus'].certifySingleCommit).toHaveBeenCalledWith(blockHeader, { - address, - blsPublicKey: blsKey, - blsSecretKey: keypair.blsSecretKey, - }); - }); - - it('should not call certifySingleCommit for range when params for height + 1 does not exist', async () => { - // Act - await Promise.all(generator['_handleFinalizedHeightChanged'](51, 54)); - - // Assert - expect(generator['_consensus'].certifySingleCommit).not.toHaveBeenCalled(); - }); - - it('should not call certifySingleCommit for finalized height + 1 when BFT params exist', async () => { - // Act - await Promise.all(generator['_handleFinalizedHeightChanged'](53, 54)); - - // Assert - expect(generator['_consensus'].certifySingleCommit).not.toHaveBeenCalled(); - }); - - it('should not call certifySingleCommit for the validator who has not registered bls key', async () => { - // Act - await Promise.all(generator['_handleFinalizedHeightChanged'](20, 21)); - - // Assert - expect(generator['_consensus'].certifySingleCommit).not.toHaveBeenCalled(); - }); - - it('should call certifySingleCommit for finalized height + 1 when BFT params does not exist', async () => { - // For height 50, it should ceritifySingleCommit event though BFTParameter does not exist - await Promise.all(generator['_handleFinalizedHeightChanged'](15, 50)); - - // Assert - expect(generator['_consensus'].certifySingleCommit).toHaveBeenCalledTimes(1); - expect(generator['_consensus'].certifySingleCommit).toHaveBeenCalledWith(blockHeader, { - address, - blsPublicKey: blsKey, - blsSecretKey: keypair.blsSecretKey, - }); - }); - - it('should not call certifySingleCommit when validator is not active at the height', async () => { - // height 20 returns existBFTParameters true, but no active validators. - // Therefore, it should not certify single commit - // Act - await Promise.all(generator['_handleFinalizedHeightChanged'](15, 54)); - - // Assert - expect(generator['_consensus'].certifySingleCommit).not.toHaveBeenCalled(); - }); + afterEach(async () => { + await generator.stop(); }); - describe('when previous finalized height change and maxRemovalHeight is non zero', () => { - beforeEach(async () => { - await generator.start(); - jest.spyOn(generator, '_handleFinalizedHeightChanged' as never); - jest.spyOn(generator, '_certifySingleCommitForChangedHeight' as never); - jest.spyOn(generator, '_certifySingleCommit' as never); - }); - - afterEach(async () => { - await generator.stop(); + it('should call singleCommitHandler.handleFinalizedHeightChanged', async () => { + generator['_consensus'].events.emit(CONSENSUS_EVENT_FINALIZED_HEIGHT_CHANGED, { + from: 30001, + to: 30003, }); + await Promise.resolve(); - it('should not call certifySingleCommit when getMaxRemovalHeight is higher than next finalized height', async () => { - jest.spyOn(consensus, 'getMaxRemovalHeight').mockResolvedValue(30000); - generator['_consensus'].events.emit(CONSENSUS_EVENT_FINALIZED_HEIGHT_CHANGED, { - from: 0, - to: 25520, - }); - await Promise.resolve(); - - expect(generator['_handleFinalizedHeightChanged']).toHaveBeenCalledWith(30000, 25520); - expect(generator['_certifySingleCommitForChangedHeight']).not.toHaveBeenCalled(); - expect(generator['_certifySingleCommit']).not.toHaveBeenCalled(); - }); - - it('should call certifySingleCommit when getMaxRemovalHeight is lower than next finalized height', async () => { - jest.spyOn(consensus, 'getMaxRemovalHeight').mockResolvedValue(30000); - generator['_consensus'].events.emit(CONSENSUS_EVENT_FINALIZED_HEIGHT_CHANGED, { - from: 30001, - to: 30003, - }); - await Promise.resolve(); - - expect(generator['_handleFinalizedHeightChanged']).toHaveBeenCalledWith(30001, 30003); - expect(generator['_certifySingleCommitForChangedHeight']).toHaveBeenCalledTimes( - 1 * generator['_keypairs'].size, - ); - expect(generator['_certifySingleCommit']).toHaveBeenCalledTimes( - 1 * generator['_keypairs'].size, - ); - }); + expect(generator['_singleCommitHandler'].handleFinalizedHeightChanged).toHaveBeenCalledWith( + 30001, + 30003, + ); }); }); }); diff --git a/framework/test/unit/engine/generator/single_commit_handler.spec.ts b/framework/test/unit/engine/generator/single_commit_handler.spec.ts new file mode 100644 index 00000000000..bd532d9484f --- /dev/null +++ b/framework/test/unit/engine/generator/single_commit_handler.spec.ts @@ -0,0 +1,301 @@ +/* + * 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 { EventEmitter } from 'events'; +import { dataStructures } from '@liskhq/lisk-utils'; +import { Chain } from '@liskhq/lisk-chain'; +import { bls, utils, address as cryptoAddress, legacy } from '@liskhq/lisk-cryptography'; +import { InMemoryDatabase, Database } from '@liskhq/lisk-db'; +import { when } from 'jest-when'; +import { Mnemonic } from '@liskhq/lisk-passphrase'; +import { Consensus, Keypair } from '../../../../src/engine/generator/types'; +import { fakeLogger } from '../../../utils/mocks'; +import { BFTModule } from '../../../../src/engine/bft'; +import { createFakeBlockHeader } from '../../../../src/testing'; +import { SingleCommitHandler } from '../../../../src/engine/generator/single_commit_handler'; +import { testing } from '../../../../src'; + +describe('SingleCommitHandler', () => { + const logger = fakeLogger; + + let chain: Chain; + let consensus: Consensus; + let keypairs: dataStructures.BufferMap; + let blockchainDB: Database; + let bft: BFTModule; + let consensusEvent: EventEmitter; + let singleCommitHandler: SingleCommitHandler; + + beforeEach(() => { + blockchainDB = new InMemoryDatabase() as never; + keypairs = new dataStructures.BufferMap(); + for (const key of testing.fixtures.keysList.keys) { + keypairs.set(cryptoAddress.getAddressFromLisk32Address(key.address), { + publicKey: Buffer.from(key.plain.generatorKey, 'hex'), + privateKey: Buffer.from(key.plain.generatorPrivateKey, 'hex'), + blsPublicKey: Buffer.from(key.plain.blsKey, 'hex'), + blsSecretKey: Buffer.from(key.plain.blsPrivateKey, 'hex'), + }); + } + chain = { + chainID: utils.getRandomBytes(32), + lastBlock: { + header: { + id: Buffer.from('6846255774763267134'), + height: 9187702, + timestamp: 93716450, + }, + transactions: [], + }, + finalizedHeight: 100, + dataAccess: { + getBlockHeaderByHeight: jest.fn(), + }, + constants: { + chainID: Buffer.from('chainID'), + }, + } as never; + consensusEvent = new EventEmitter(); + consensus = { + execute: jest.fn(), + syncing: jest.fn().mockReturnValue(false), + getAggregateCommit: jest.fn(), + certifySingleCommit: jest.fn(), + getConsensusParams: jest.fn().mockResolvedValue({ + currentValidators: [], + implyMaxPrevote: true, + maxHeightCertified: 0, + }), + getMaxRemovalHeight: jest.fn().mockResolvedValue(0), + events: consensusEvent, + } as never; + bft = { + beforeTransactionsExecute: jest.fn(), + method: { + getBFTHeights: jest.fn().mockResolvedValue({ + maxHeightPrevoted: 0, + maxHeightPrecommitted: 0, + maxHeightCertified: 0, + }), + setBFTParameters: jest.fn(), + getBFTParameters: jest.fn().mockResolvedValue({ validators: [] }), + getBFTParametersActiveValidators: jest.fn().mockResolvedValue({ validators: [] }), + existBFTParameters: jest.fn().mockResolvedValue(false), + getGeneratorAtTimestamp: jest.fn(), + impliesMaximalPrevotes: jest.fn().mockResolvedValue(false), + getSlotNumber: jest.fn(), + getSlotTime: jest.fn(), + }, + } as never; + singleCommitHandler = new SingleCommitHandler( + logger, + chain, + consensus, + bft, + keypairs, + blockchainDB, + ); + }); + + describe('events CONSENSUS_EVENT_FINALIZED_HEIGHT_CHANGED', () => { + const passphrase = Mnemonic.generateMnemonic(256); + const keys = legacy.getPrivateAndPublicKeyFromPassphrase(passphrase); + const address = cryptoAddress.getAddressFromPublicKey(keys.publicKey); + const blsSecretKey = bls.generatePrivateKey(Buffer.from(passphrase, 'utf-8')); + const keypair = { + ...keys, + blsSecretKey, + blsPublicKey: bls.getPublicKeyFromPrivateKey(blsSecretKey), + }; + const blsKey = bls.getPublicKeyFromPrivateKey(keypair.blsSecretKey); + const blockHeader = createFakeBlockHeader(); + + describe('when generator is a standby validator', () => { + beforeEach(() => { + keypairs.set(address, keypair); + when(singleCommitHandler['_bft'].method.existBFTParameters as jest.Mock) + .calledWith(expect.anything(), 1) + .mockResolvedValue(true as never) + .calledWith(expect.anything(), 12) + .mockResolvedValue(true as never) + .calledWith(expect.anything(), 21) + .mockResolvedValue(true as never) + .calledWith(expect.anything(), 51) + .mockResolvedValue(false as never) + .calledWith(expect.anything(), 55) + .mockResolvedValue(true as never); + when(singleCommitHandler['_bft'].method.getBFTParametersActiveValidators as jest.Mock) + .calledWith(expect.anything(), 11) + .mockResolvedValue({ + validators: [{ address: utils.getRandomBytes(20), blsKey: utils.getRandomBytes(48) }], + }) + .calledWith(expect.anything(), 20) + .mockResolvedValue({ + validators: [{ address: utils.getRandomBytes(20), blsKey: utils.getRandomBytes(48) }], + }) + .calledWith(expect.anything(), 50) + .mockResolvedValue({ + validators: [{ address: utils.getRandomBytes(20), blsKey: utils.getRandomBytes(48) }], + }) + .calledWith(expect.anything(), 54) + .mockResolvedValue({ validators: [] }); + + jest + .spyOn(singleCommitHandler['_chain'].dataAccess, 'getBlockHeaderByHeight') + .mockResolvedValue(blockHeader as never); + jest.spyOn(singleCommitHandler['_consensus'], 'certifySingleCommit'); + }); + + it('should not call certifySingleCommit when standby validator creates block', async () => { + // Act + await singleCommitHandler.handleFinalizedHeightChanged(10, 50); + + // Assert + expect(singleCommitHandler['_consensus'].certifySingleCommit).toHaveBeenCalledTimes(0); + }); + }); + + describe('when generator is an active validator', () => { + beforeEach(() => { + keypairs.set(address, keypair); + when(singleCommitHandler['_bft'].method.existBFTParameters as jest.Mock) + .calledWith(expect.anything(), 1) + .mockResolvedValue(true as never) + .calledWith(expect.anything(), 12) + .mockResolvedValue(true as never) + .calledWith(expect.anything(), 21) + .mockResolvedValue(true as never) + .calledWith(expect.anything(), 51) + .mockResolvedValue(false as never) + .calledWith(expect.anything(), 55) + .mockResolvedValue(true as never); + when(singleCommitHandler['_bft'].method.getBFTParametersActiveValidators as jest.Mock) + .calledWith(expect.anything(), 11) + .mockResolvedValue({ validators: [{ address, blsKey }] }) + .calledWith(expect.anything(), 20) + .mockResolvedValue({ validators: [{ address, blsKey: Buffer.alloc(48) }] }) + .calledWith(expect.anything(), 50) + .mockResolvedValue({ validators: [{ address, blsKey }] }) + .calledWith(expect.anything(), 54) + .mockResolvedValue({ validators: [] }); + + jest + .spyOn(singleCommitHandler['_chain'].dataAccess, 'getBlockHeaderByHeight') + .mockResolvedValue(blockHeader as never); + jest.spyOn(singleCommitHandler['_consensus'], 'certifySingleCommit'); + }); + + it('should call certifySingleCommit for range when params for height + 1 exist', async () => { + // Act + await singleCommitHandler.handleFinalizedHeightChanged(10, 50); + + // Assert + expect(singleCommitHandler['_consensus'].certifySingleCommit).toHaveBeenCalledTimes(2); + expect(singleCommitHandler['_consensus'].certifySingleCommit).toHaveBeenCalledWith( + blockHeader, + { + address, + blsPublicKey: blsKey, + blsSecretKey: keypair.blsSecretKey, + }, + ); + }); + + it('should not call certifySingleCommit for range when params for height + 1 does not exist', async () => { + // Act + await singleCommitHandler.handleFinalizedHeightChanged(51, 54); + + // Assert + expect(singleCommitHandler['_consensus'].certifySingleCommit).not.toHaveBeenCalled(); + }); + + it('should not call certifySingleCommit for finalized height + 1 when BFT params exist', async () => { + // Act + await singleCommitHandler.handleFinalizedHeightChanged(53, 54); + + // Assert + expect(singleCommitHandler['_consensus'].certifySingleCommit).not.toHaveBeenCalled(); + }); + + it('should not call certifySingleCommit for the validator who has not registered bls key', async () => { + // Act + await singleCommitHandler.handleFinalizedHeightChanged(20, 21); + + // Assert + expect(singleCommitHandler['_consensus'].certifySingleCommit).not.toHaveBeenCalled(); + }); + + it('should call certifySingleCommit for finalized height + 1 when BFT params does not exist', async () => { + // For height 50, it should ceritifySingleCommit event though BFTParameter does not exist + await singleCommitHandler.handleFinalizedHeightChanged(15, 50); + + // Assert + expect(singleCommitHandler['_consensus'].certifySingleCommit).toHaveBeenCalledTimes(1); + expect(singleCommitHandler['_consensus'].certifySingleCommit).toHaveBeenCalledWith( + blockHeader, + { + address, + blsPublicKey: blsKey, + blsSecretKey: keypair.blsSecretKey, + }, + ); + }); + + it('should not call certifySingleCommit when validator is not active at the height', async () => { + // height 20 returns existBFTParameters true, but no active validators. + // Therefore, it should not certify single commit + // Act + await singleCommitHandler.handleFinalizedHeightChanged(15, 54); + + // Assert + expect(singleCommitHandler['_consensus'].certifySingleCommit).not.toHaveBeenCalled(); + }); + }); + + describe('when previous finalized height change and maxRemovalHeight is non zero', () => { + beforeEach(() => { + jest.spyOn(singleCommitHandler, '_handleFinalizedHeightChanged' as never); + jest.spyOn(singleCommitHandler, '_certifySingleCommitForChangedHeight' as never); + jest.spyOn(singleCommitHandler, '_certifySingleCommit' as never); + }); + + it('should not call certifySingleCommit when getMaxRemovalHeight is higher than next finalized height', async () => { + jest.spyOn(consensus, 'getMaxRemovalHeight').mockResolvedValue(30000); + await singleCommitHandler.handleFinalizedHeightChanged(0, 25520); + + expect(singleCommitHandler['_handleFinalizedHeightChanged']).not.toHaveBeenCalledWith( + address, + 30000, + 25520, + ); + }); + + it('should call certifySingleCommit when getMaxRemovalHeight is lower than next finalized height', async () => { + jest.spyOn(consensus, 'getMaxRemovalHeight').mockResolvedValue(30000); + await singleCommitHandler.handleFinalizedHeightChanged(30001, 30003); + + expect(singleCommitHandler['_handleFinalizedHeightChanged']).toHaveBeenCalledWith( + expect.any(Buffer), + 30001, + 30003, + ); + expect(singleCommitHandler['_certifySingleCommitForChangedHeight']).toHaveBeenCalledTimes( + 1 * keypairs.size, + ); + expect(singleCommitHandler['_certifySingleCommit']).toHaveBeenCalledTimes( + 1 * keypairs.size, + ); + }); + }); + }); +});