diff --git a/commander/package-lock.json b/commander/package-lock.json index 7a9d09f4e60..79f0ae4d958 100644 --- a/commander/package-lock.json +++ b/commander/package-lock.json @@ -1,6 +1,6 @@ { "name": "lisk-commander", - "version": "3.0.1", + "version": "3.0.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/commander/package.json b/commander/package.json index c93ed6a1b77..03669934645 100644 --- a/commander/package.json +++ b/commander/package.json @@ -1,6 +1,6 @@ { "name": "lisk-commander", - "version": "3.0.1", + "version": "3.0.2", "description": "A command line interface for Lisk", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", @@ -101,10 +101,10 @@ "dependencies": { "@liskhq/lisk-api-client": "3.0.1", "@liskhq/lisk-constants": "1.3.0", - "@liskhq/lisk-cryptography": "2.4.1", + "@liskhq/lisk-cryptography": "2.4.2", "@liskhq/lisk-passphrase": "3.0.0", - "@liskhq/lisk-transactions": "3.0.1", - "@liskhq/lisk-validator": "0.3.0", + "@liskhq/lisk-transactions": "3.0.2", + "@liskhq/lisk-validator": "0.3.1", "@oclif/command": "1.5.6", "@oclif/config": "1.9.0", "@oclif/errors": "1.2.2", diff --git a/elements/lisk-client/package-lock.json b/elements/lisk-client/package-lock.json index 26451bed830..554fd1547c5 100644 --- a/elements/lisk-client/package-lock.json +++ b/elements/lisk-client/package-lock.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-client", - "version": "3.0.1", + "version": "3.0.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/elements/lisk-client/package.json b/elements/lisk-client/package.json index 5c9df51dbe6..9090c343326 100644 --- a/elements/lisk-client/package.json +++ b/elements/lisk-client/package.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-client", - "version": "3.0.1", + "version": "3.0.2", "description": "A default set of Elements for use by clients of the Lisk network", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", @@ -61,9 +61,9 @@ "dependencies": { "@liskhq/lisk-api-client": "3.0.1", "@liskhq/lisk-constants": "1.3.0", - "@liskhq/lisk-cryptography": "2.4.1", + "@liskhq/lisk-cryptography": "2.4.2", "@liskhq/lisk-passphrase": "3.0.0", - "@liskhq/lisk-transactions": "3.0.1", + "@liskhq/lisk-transactions": "3.0.2", "@types/node": "12.12.11" }, "devDependencies": { diff --git a/elements/lisk-cryptography/package-lock.json b/elements/lisk-cryptography/package-lock.json index d3e8220c41c..e12fa31fb2c 100644 --- a/elements/lisk-cryptography/package-lock.json +++ b/elements/lisk-cryptography/package-lock.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-cryptography", - "version": "2.4.1", + "version": "2.4.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/elements/lisk-cryptography/package.json b/elements/lisk-cryptography/package.json index 571477696d8..1fd41836554 100644 --- a/elements/lisk-cryptography/package.json +++ b/elements/lisk-cryptography/package.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-cryptography", - "version": "2.4.1", + "version": "2.4.2", "description": "General cryptographic functions for use with Lisk-related software", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", diff --git a/elements/lisk-cryptography/src/nacl/index.ts b/elements/lisk-cryptography/src/nacl/index.ts index 9e92d61f7f6..f1bab4b1d26 100644 --- a/elements/lisk-cryptography/src/nacl/index.ts +++ b/elements/lisk-cryptography/src/nacl/index.ts @@ -14,21 +14,16 @@ */ import { NaclInterface } from './nacl_types'; -// tslint:disable-next-line no-let -let lib: NaclInterface; +// tslint:disable-next-line no-let no-require-imports no-var-requires +let lib: NaclInterface = require('./slow'); try { - if (process.env.NACL_FAST === 'disable') { - throw new Error('Use tweetnacl'); + if (process.env.NACL_FAST !== 'disable') { + // tslint:disable-next-line no-var-requires no-require-imports + lib = require('./fast'); } - // Require used for conditional importing - // tslint:disable-next-line no-var-requires no-require-imports - lib = require('./fast'); -} catch (err) { - process.env.NACL_FAST = 'disable'; - // tslint:disable-next-line no-var-requires no-require-imports - lib = require('./slow'); -} + // tslint:disable-next-line no-empty +} catch (err) {} export const NACL_SIGN_PUBLICKEY_LENGTH = 32; diff --git a/elements/lisk-cryptography/test/nacl/index.ts b/elements/lisk-cryptography/test/nacl/index.ts index 42ab0b57ef4..61821a48f54 100644 --- a/elements/lisk-cryptography/test/nacl/index.ts +++ b/elements/lisk-cryptography/test/nacl/index.ts @@ -104,9 +104,9 @@ describe('nacl index.js', () => { return Promise.resolve(); }); - it('should set process.env.NACL_FAST to disable', () => { + it('should not set process.env.NACL_FAST to disable', () => { require('../../src/nacl'); - return expect(process.env.NACL_FAST).to.eql('disable'); + return expect(process.env.NACL_FAST).not.to.eql('disable'); }); it('should load nacl slow if process.env.NACL_FAST is set to enable', () => { diff --git a/elements/lisk-elements/package-lock.json b/elements/lisk-elements/package-lock.json index 45e82dc06c3..aa9e2ee3891 100644 --- a/elements/lisk-elements/package-lock.json +++ b/elements/lisk-elements/package-lock.json @@ -1,6 +1,6 @@ { "name": "lisk-elements", - "version": "3.0.1", + "version": "3.0.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/elements/lisk-elements/package.json b/elements/lisk-elements/package.json index ca47307f212..8359b8b4d97 100644 --- a/elements/lisk-elements/package.json +++ b/elements/lisk-elements/package.json @@ -1,6 +1,6 @@ { "name": "lisk-elements", - "version": "3.0.1", + "version": "3.0.2", "description": "Elements for building blockchain applications in the Lisk network", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", @@ -61,12 +61,12 @@ "dependencies": { "@liskhq/lisk-api-client": "3.0.1", "@liskhq/lisk-constants": "1.3.0", - "@liskhq/lisk-cryptography": "2.4.1", - "@liskhq/lisk-p2p": "0.4.1", + "@liskhq/lisk-cryptography": "2.4.2", + "@liskhq/lisk-p2p": "0.4.2", "@liskhq/lisk-passphrase": "3.0.0", - "@liskhq/lisk-transaction-pool": "0.2.0", - "@liskhq/lisk-transactions": "3.0.1", - "@liskhq/lisk-validator": "0.3.0", + "@liskhq/lisk-transaction-pool": "0.2.1", + "@liskhq/lisk-transactions": "3.0.2", + "@liskhq/lisk-validator": "0.3.1", "@types/node": "12.12.11" }, "devDependencies": { diff --git a/elements/lisk-p2p/package-lock.json b/elements/lisk-p2p/package-lock.json index 24494cb7181..05dadc23407 100644 --- a/elements/lisk-p2p/package-lock.json +++ b/elements/lisk-p2p/package-lock.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-p2p", - "version": "0.4.1", + "version": "0.4.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/elements/lisk-p2p/package.json b/elements/lisk-p2p/package.json index 27cca3a8048..24d31f5fbef 100644 --- a/elements/lisk-p2p/package.json +++ b/elements/lisk-p2p/package.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-p2p", - "version": "0.4.1", + "version": "0.4.2", "description": "Unstructured P2P library for use with Lisk-related software", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", @@ -55,7 +55,7 @@ "disableLocalIPs": "./scripts/disableTestLocalIPs.sh 2 19" }, "dependencies": { - "@liskhq/lisk-cryptography": "2.4.1", + "@liskhq/lisk-cryptography": "2.4.2", "lodash.shuffle": "4.2.0", "semver": "5.6.0", "socketcluster-client": "14.3.1", diff --git a/elements/lisk-transaction-pool/package-lock.json b/elements/lisk-transaction-pool/package-lock.json index 9b99dd5b0be..45b7a5de8f2 100644 --- a/elements/lisk-transaction-pool/package-lock.json +++ b/elements/lisk-transaction-pool/package-lock.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-transaction-pool", - "version": "0.2.0", + "version": "0.2.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/elements/lisk-transaction-pool/package.json b/elements/lisk-transaction-pool/package.json index 598d4f58b4a..93be70472a5 100644 --- a/elements/lisk-transaction-pool/package.json +++ b/elements/lisk-transaction-pool/package.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-transaction-pool", - "version": "0.2.0", + "version": "0.2.1", "description": "Transaction pool library for use with Lisk-related software", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", @@ -64,6 +64,6 @@ "typescript": "3.7.2" }, "dependencies": { - "@liskhq/lisk-cryptography": "2.4.1" + "@liskhq/lisk-cryptography": "2.4.2" } } diff --git a/elements/lisk-transactions/package-lock.json b/elements/lisk-transactions/package-lock.json index cc29e1c3885..d4d99fa942a 100644 --- a/elements/lisk-transactions/package-lock.json +++ b/elements/lisk-transactions/package-lock.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-transactions", - "version": "3.0.1", + "version": "3.0.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/elements/lisk-transactions/package.json b/elements/lisk-transactions/package.json index 72c60e44448..2260aee17b9 100644 --- a/elements/lisk-transactions/package.json +++ b/elements/lisk-transactions/package.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-transactions", - "version": "3.0.1", + "version": "3.0.2", "description": "Everything related to transactions according to the Lisk protocol", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", @@ -58,8 +58,8 @@ }, "dependencies": { "@liskhq/bignum": "1.3.1", - "@liskhq/lisk-cryptography": "2.4.1", - "@liskhq/lisk-validator": "0.3.0", + "@liskhq/lisk-cryptography": "2.4.2", + "@liskhq/lisk-validator": "0.3.1", "ajv": "6.8.1", "ajv-merge-patch": "4.1.0" }, diff --git a/elements/lisk-validator/package-lock.json b/elements/lisk-validator/package-lock.json index 3d4ab9f0935..20b0d007096 100644 --- a/elements/lisk-validator/package-lock.json +++ b/elements/lisk-validator/package-lock.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-validator", - "version": "0.3.0", + "version": "0.3.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/elements/lisk-validator/package.json b/elements/lisk-validator/package.json index c7fb8d1c53e..340e61bed01 100644 --- a/elements/lisk-validator/package.json +++ b/elements/lisk-validator/package.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-validator", - "version": "0.3.0", + "version": "0.3.1", "description": "Validation library according to the Lisk protocol", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", @@ -61,7 +61,7 @@ }, "dependencies": { "@liskhq/bignum": "1.3.1", - "@liskhq/lisk-cryptography": "2.4.1", + "@liskhq/lisk-cryptography": "2.4.2", "@types/node": "12.12.11", "@types/semver": "5.5.0", "@types/validator": "10.9.0", diff --git a/framework/package-lock.json b/framework/package-lock.json index 42345914303..86c88ed4442 100644 --- a/framework/package-lock.json +++ b/framework/package-lock.json @@ -1,6 +1,6 @@ { "name": "lisk-framework", - "version": "0.5.1", + "version": "0.5.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/framework/package.json b/framework/package.json index 314de660b88..786bca40dc9 100644 --- a/framework/package.json +++ b/framework/package.json @@ -1,6 +1,6 @@ { "name": "lisk-framework", - "version": "0.5.1", + "version": "0.5.2", "description": "Lisk blockchain application platform", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", @@ -47,11 +47,11 @@ }, "dependencies": { "@liskhq/bignum": "1.3.1", - "@liskhq/lisk-cryptography": "2.4.1", - "@liskhq/lisk-p2p": "0.4.1", - "@liskhq/lisk-transaction-pool": "0.2.0", - "@liskhq/lisk-transactions": "3.0.1", - "@liskhq/lisk-validator": "0.3.0", + "@liskhq/lisk-cryptography": "2.4.2", + "@liskhq/lisk-p2p": "0.4.2", + "@liskhq/lisk-transaction-pool": "0.2.1", + "@liskhq/lisk-transactions": "3.0.2", + "@liskhq/lisk-validator": "0.3.1", "ajv": "6.7.0", "ajv-keywords": "3.4.0", "async": "2.6.1", diff --git a/framework/src/modules/chain/bft/bft.js b/framework/src/modules/chain/bft/bft.js index 3f9ac1b860b..326b35bb838 100644 --- a/framework/src/modules/chain/bft/bft.js +++ b/framework/src/modules/chain/bft/bft.js @@ -22,6 +22,10 @@ const { } = require('./finality_manager'); const forkChoiceRule = require('./fork_choice_rule'); const { validateBlockHeader } = require('./utils'); +const { + BFT_MIGRATION_ROUND_OFFSET, + BFT_ROUND_THRESHOLD, +} = require('./constant'); const CHAIN_STATE_FINALIZED_HEIGHT = 'BFT.finalizedHeight'; const EVENT_BFT_BLOCK_FINALIZED = 'EVENT_BFT_BLOCK_FINALIZED'; @@ -72,7 +76,10 @@ class BFT extends EventEmitter { const loadFromHeight = Math.max( finalizedHeight, - lastBlockHeight - this.constants.activeDelegates * 2, + // Search is inclusive, therefore, it should start from one above (ex: 288 - 500, which results total 303) + lastBlockHeight - + this.constants.activeDelegates * BFT_ROUND_THRESHOLD + + 1, this.constants.startingHeight, ); @@ -111,21 +118,8 @@ class BFT extends EventEmitter { this.finalityManager.removeBlockHeaders({ aboveHeight: removeFromHeight - 1, }); - - // Make sure there are 2 rounds of block headers available - if ( - this.finalityManager.maxHeight - this.finalityManager.minHeight < - this.constants.activeDelegates * 2 - ) { - const tillHeight = this.finalityManager.minHeight - 1; - const fromHeight = - this.finalityManager.maxHeight - this.constants.activeDelegates * 2; - await this._loadBlocksFromStorage({ - fromHeight, - tillHeight, - minActiveHeightsOfDelegates, - }); - } + // Make sure there are BFT_ROUND_THRESHOLD rounds of block headers available + await this._fillCache(minActiveHeightsOfDelegates); } addNewBlock(block, stateStore) { @@ -197,7 +191,8 @@ class BFT extends EventEmitter { // Check BFT migration height // https://github.com/LiskHQ/lips/blob/master/proposals/lip-0014.md#backwards-compatibility const bftMigrationHeight = - this.constants.startingHeight - this.constants.activeDelegates * 2; + this.constants.startingHeight - + this.constants.activeDelegates * BFT_MIGRATION_ROUND_OFFSET; // Choose max between stored finalized height or migration height const finalizedHeight = Math.max(finalizedHeightStored, bftMigrationHeight); @@ -323,7 +318,71 @@ class BFT extends EventEmitter { } get maxHeightPrevoted() { - return this.finalityManager.prevotedConfirmedHeight; + return this.finalityManager.chainMaxHeightPrevoted; + } + + reset() { + this.finalityManager.headers.empty(); + this.finalityManager.recompute(); + } + + async _fillCache(minActiveHeightsOfDelegates) { + if ( + this.finalityManager.maxHeight - this.finalityManager.minHeight >= + this.constants.activeDelegates * BFT_ROUND_THRESHOLD + ) { + return; + } + + const tillHeight = this.finalityManager.minHeight - 1; + const fromHeight = + this.finalityManager.maxHeight - + // Search is inclusive, therefore, it should start from one above (ex: 288 - 500, which results total 303) + this.constants.activeDelegates * BFT_ROUND_THRESHOLD + + 1; + const blocksJSON = await this.blockEntity.get( + { height_gte: fromHeight, height_lte: tillHeight }, + { limit: null, sort: 'height:desc' }, + ); + for (const blockJSON of blocksJSON) { + if (blockJSON.height === 1) { + this.finalityManager.headers.add( + extractBFTBlockHeaderFromBlock({ + ...blockJSON, + delegateMinHeightActive: 1, + }), + ); + return; + } + + const activeHeights = + minActiveHeightsOfDelegates[blockJSON.generatorPublicKey]; + if (!activeHeights) { + throw new Error( + `Minimum active heights were not found for delegate "${blockJSON.generatorPublicKey}".`, + ); + } + + // If there is no minHeightActive until this point, + // we can set the value to 0 + const minimumPossibleActiveHeight = this.slots.calcRoundStartHeight( + this.slots.calcRound( + Math.max(blockJSON.height - this.constants.activeDelegates * 3, 1), + ), + ); + const [delegateMinHeightActive] = activeHeights.filter( + height => height >= minimumPossibleActiveHeight, + ); + + const blockHeader = { + ...blockJSON, + delegateMinHeightActive, + }; + this.finalityManager.headers.add( + extractBFTBlockHeaderFromBlock(blockHeader), + ); + } + this.finalityManager.recompute(); } } diff --git a/framework/src/modules/chain/bft/constant.js b/framework/src/modules/chain/bft/constant.js new file mode 100644 index 00000000000..13dbf30fa69 --- /dev/null +++ b/framework/src/modules/chain/bft/constant.js @@ -0,0 +1,20 @@ +/* + * Copyright © 2018 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. + */ + +'use strict'; + +module.exports = { + BFT_ROUND_THRESHOLD: 3, + BFT_MIGRATION_ROUND_OFFSET: 2, +}; diff --git a/framework/src/modules/chain/bft/errors.js b/framework/src/modules/chain/bft/errors.js index cb29b9df47b..784a7b2043d 100644 --- a/framework/src/modules/chain/bft/errors.js +++ b/framework/src/modules/chain/bft/errors.js @@ -41,11 +41,7 @@ class BFTForkChoiceRuleError extends BFTError { } } -class BFTInvalidAttributeError extends BFTError { - constructor() { - super('Invalid BFT attribute'); - } -} +class BFTInvalidAttributeError extends BFTError {} module.exports = { BFTError, diff --git a/framework/src/modules/chain/bft/finality_manager.js b/framework/src/modules/chain/bft/finality_manager.js index 210c9bd53bb..7e93aad872c 100644 --- a/framework/src/modules/chain/bft/finality_manager.js +++ b/framework/src/modules/chain/bft/finality_manager.js @@ -17,6 +17,7 @@ const assert = require('assert'); const debug = require('debug')('lisk:bft:consensus_manager'); const EventEmitter = require('events'); +const { BFT_ROUND_THRESHOLD } = require('./constant'); const { HeadersList } = require('./headers_list'); const { validateBlockHeader } = require('./utils'); const { @@ -45,7 +46,7 @@ class FinalityManager extends EventEmitter { this.preCommitThreshold = Math.ceil((this.activeDelegates * 2) / 3); // Limit for blocks to make perform verification or pre-vote/pre-commit (1 block less than 3 rounds) - this.processingThreshold = this.activeDelegates * 3 - 1; + this.processingThreshold = this.activeDelegates * BFT_ROUND_THRESHOLD - 1; // Maximum headers to store (5 rounds) this.maxHeaders = this.activeDelegates * 5; @@ -57,7 +58,7 @@ class FinalityManager extends EventEmitter { this.finalizedHeight = finalizedHeight; // Height up to which blocks have pre-voted - this.prevotedConfirmedHeight = 0; + this.chainMaxHeightPrevoted = 0; this.state = {}; this.preVotes = {}; @@ -72,15 +73,7 @@ class FinalityManager extends EventEmitter { validateBlockHeader(blockHeader); // Verify the integrity of the header with chain - try { - this.verifyBlockHeaders(blockHeader); - } catch (error) { - // TODO: Remove hardcoded value of maxHeightPreviouslyForged to avoid this - // https://github.com/LiskHQ/lisk-sdk/blob/fa1bb6907955c12297336f80f59951ba4754da7f/framework/src/modules/chain/blocks/process.js#L125-L126 - if (!(error instanceof BFTChainDisjointError)) { - throw error; - } - } + this.verifyBlockHeaders(blockHeader); // Add the header to the list this.headers.add(blockHeader); @@ -95,7 +88,7 @@ class FinalityManager extends EventEmitter { debug('after adding block header', { finalizedHeight: this.finalizedHeight, - prevotedConfirmedHeight: this.prevotedConfirmedHeight, + chainMaxHeightPrevoted: this.chainMaxHeightPrevoted, minHeight: this.minHeight, maxHeight: this.maxHeight, }); @@ -105,7 +98,7 @@ class FinalityManager extends EventEmitter { removeBlockHeaders({ aboveHeight }) { debug('removeBlockHeaders invoked'); - const removeAboveHeight = aboveHeight || this.maxHeight - 1; + const removeAboveHeight = aboveHeight; // Remove block header from the list this.headers.remove({ aboveHeight: removeAboveHeight }); @@ -135,7 +128,7 @@ class FinalityManager extends EventEmitter { maxPreCommitHeight: 0, }; - const validMinHeightToVoteAndCommit = this._getValidMinHeightToCommit( + const minValidHeightToPreCommit = this._getMinValidHeightToPreCommit( header, ); @@ -144,7 +137,7 @@ class FinalityManager extends EventEmitter { // delegate can't pre-commit a block before the above mentioned conditions const minPreCommitHeight = Math.max( header.delegateMinHeightActive, - validMinHeightToVoteAndCommit, + minValidHeightToPreCommit, delegateState.maxPreCommitHeight + 1, ); @@ -198,9 +191,9 @@ class FinalityManager extends EventEmitter { .reverse() .find(key => this.preVotes[key] >= this.preVoteThreshold); - this.prevotedConfirmedHeight = highestHeightPreVoted + this.chainMaxHeightPrevoted = highestHeightPreVoted ? parseInt(highestHeightPreVoted, 10) - : this.prevotedConfirmedHeight; + : this.chainMaxHeightPrevoted; const highestHeightPreCommitted = Object.keys(this.preCommits) .reverse() @@ -220,7 +213,15 @@ class FinalityManager extends EventEmitter { return true; } - _getValidMinHeightToCommit(header) { + /** + * Get the min height from which a delegate can make pre-commits + * + * The flow is as following: + * - We search backward from top block to bottom block in the chain + * - We can search down to current block height - processingThreshold(302) + * - + */ + _getMinValidHeightToPreCommit(header) { // We search backward from top block to bottom block in the chain // We should search down to the height we have in our headers list @@ -245,6 +246,10 @@ class FinalityManager extends EventEmitter { // maxHeightPreviouslyForged always refers to a height with a block forged by the same delegate. if (needleHeight === currentBlockHeader.maxHeightPreviouslyForged) { const previousBlockHeader = this.headers.get(needleHeight); + if (!previousBlockHeader) { + debug('Fail to get cached block header'); + return 0; + } // Was the previous block suggested by current block header // was actually forged by same delegate? If not then just return from here @@ -253,9 +258,8 @@ class FinalityManager extends EventEmitter { previousBlockHeader.delegatePublicKey !== header.delegatePublicKey || previousBlockHeader.maxHeightPreviouslyForged >= needleHeight ) { - return needleHeight; + return needleHeight + 1; } - // Move the needle to previous block and consider it current for next iteration needleHeight = previousBlockHeader.maxHeightPreviouslyForged; currentBlockHeader = previousBlockHeader; @@ -263,13 +267,12 @@ class FinalityManager extends EventEmitter { needleHeight -= 1; } } - return needleHeight; + return Math.max(needleHeight + 1, searchTillHeight); } recompute() { this.state = {}; - this.finalizedHeight = this._initialFinalizedHeight; - this.prevotedConfirmedHeight = 0; + this.chainMaxHeightPrevoted = 0; this.preVotes = {}; this.preCommits = {}; @@ -292,14 +295,16 @@ class FinalityManager extends EventEmitter { verifyBlockHeaders(blockHeader) { debug('verifyBlockHeaders invoked'); + debug(blockHeader); + // We need minimum processingThreshold to decide // if maxHeightPrevoted is correct if ( this.headers.length >= this.processingThreshold && - blockHeader.maxHeightPrevoted !== this.prevotedConfirmedHeight + blockHeader.maxHeightPrevoted !== this.chainMaxHeightPrevoted ) { throw new BFTInvalidAttributeError( - 'Wrong prevotedConfirmedHeight in blockHeader.', + `Wrong maxHeightPrevoted in blockHeader. maxHeightPrevoted: ${blockHeader.maxHeightPrevoted}, : ${this.chainMaxHeightPrevoted}`, ); } @@ -312,10 +317,31 @@ class FinalityManager extends EventEmitter { return true; } + // Order the two block headers such that earlierBlock must be forged first + let earlierBlock = delegateLastBlock; + let laterBlock = blockHeader; + const higherMaxHeightPreviouslyForgerd = + earlierBlock.maxHeightPreviouslyForged > + laterBlock.maxHeightPreviouslyForged; + const sameMaxHeightPreviouslyForgerd = + earlierBlock.maxHeightPreviouslyForged === + laterBlock.maxHeightPreviouslyForged; + const higherMaxHeightPrevoted = + earlierBlock.maxHeightPrevoted > laterBlock.maxHeightPrevoted; + const sameMaxHeightPrevoted = + earlierBlock.maxHeightPrevoted === laterBlock.maxHeightPrevoted; + const higherHeight = earlierBlock.height > laterBlock.height; + if ( + higherMaxHeightPreviouslyForgerd || + (sameMaxHeightPreviouslyForgerd && higherMaxHeightPrevoted) || + (sameMaxHeightPreviouslyForgerd && sameMaxHeightPrevoted && higherHeight) + ) { + [earlierBlock, laterBlock] = [laterBlock, earlierBlock]; + } + if ( - delegateLastBlock.maxHeightPreviouslyForged === - blockHeader.maxHeightPreviouslyForged && - delegateLastBlock.height >= blockHeader.height + earlierBlock.maxHeightPrevoted === laterBlock.maxHeightPrevoted && + earlierBlock.height >= laterBlock.height ) { // Violation of the fork choice rule as delegate moved to different chain // without strictly larger maxHeightPreviouslyForged or larger height as @@ -323,11 +349,11 @@ class FinalityManager extends EventEmitter { throw new BFTForkChoiceRuleError(); } - if (delegateLastBlock.height > blockHeader.maxHeightPreviouslyForged) { + if (earlierBlock.height > laterBlock.maxHeightPreviouslyForged) { throw new BFTChainDisjointError(); } - if (delegateLastBlock.maxHeightPrevoted > blockHeader.maxHeightPrevoted) { + if (earlierBlock.maxHeightPrevoted > laterBlock.maxHeightPrevoted) { throw new BFTLowerChainBranchError(); } diff --git a/framework/src/modules/chain/bft/headers_list.js b/framework/src/modules/chain/bft/headers_list.js index 553d7f2675a..b2fcb8c5559 100644 --- a/framework/src/modules/chain/bft/headers_list.js +++ b/framework/src/modules/chain/bft/headers_list.js @@ -112,6 +112,10 @@ class HeadersList { top(size) { assert(size, 'Please provide the size'); + if (this.length <= size) { + return [...this._items]; + } + return this.items.slice(this.length - size, this.length + 1); } @@ -122,6 +126,9 @@ class HeadersList { } get(height) { + if (this.length === 0) { + return undefined; + } return this._items[height - this.first.height]; } } diff --git a/framework/src/modules/chain/block_processor_v2.js b/framework/src/modules/chain/block_processor_v2.js index f89d59464c0..9e6202c9f71 100644 --- a/framework/src/modules/chain/block_processor_v2.js +++ b/framework/src/modules/chain/block_processor_v2.js @@ -27,8 +27,7 @@ const { const { baseBlockSchema } = require('./blocks'); const { BaseBlockProcessor } = require('./processor'); -const FORGER_INFO_KEY_MAX_HEIGHT_PREVIOUSLY_FORGED = - 'maxHeightPreviouslyForged'; +const FORGER_INFO_KEY_PREVIOUSLY_FORGED = 'previouslyForged'; const SIZE_INT32 = 4; const SIZE_INT64 = 8; @@ -167,13 +166,13 @@ class BlockProcessorV2 extends BaseBlockProcessor { this.init.pipe([ async ({ stateStore }) => { - // minActiveHeightsOfDelegates will be used to load 202 blocks from the storage - // That's why we need to get the delegates who were active in the last 2 rounds. - const numberOfRounds = 2; + // minActiveHeightsOfDelegates will be used to load 303 blocks from the storage + // That's why we need to get the delegates who were active in the last 3 rounds. + const numberOfRounds = 3; const minActiveHeightsOfDelegates = await this.dposModule.getMinActiveHeightsOfDelegates( numberOfRounds, ); - this.bftModule.init(stateStore, minActiveHeightsOfDelegates); + await this.bftModule.init(stateStore, minActiveHeightsOfDelegates); }, ]); @@ -269,32 +268,33 @@ class BlockProcessorV2 extends BaseBlockProcessor { async ({ block, tx }) => { // minActiveHeightsOfDelegates will be used to load 202 blocks from the storage // That's why we need to get the delegates who were active in the last 2 rounds. - const numberOfRounds = 2; + const numberOfRounds = 3; const minActiveHeightsOfDelegates = await this.dposModule.getMinActiveHeightsOfDelegates( numberOfRounds, { tx }, ); - this.bftModule.deleteBlocks([block], minActiveHeightsOfDelegates); + await this.bftModule.deleteBlocks([block], minActiveHeightsOfDelegates); }, ]); this.create.pipe([ // Create a block with with basic block and bft properties async data => { - const previouslyForged = await this._getPreviouslyForgedMap(); + const previouslyForgedMap = await this._getPreviouslyForgedMap(); const delegatePublicKey = data.keypair.publicKey.toString('hex'); - const maxHeightPreviouslyForged = - previouslyForged[delegatePublicKey] || 0; + const height = data.previousBlock.height + 1; + const previousBlockId = data.previousBlock.id; + const forgerInfo = previouslyForgedMap[delegatePublicKey] || {}; + const maxHeightPreviouslyForged = forgerInfo.height || 0; const block = this._create({ ...data, + height, + previousBlockId, maxHeightPreviouslyForged, maxHeightPrevoted: this.bftModule.maxHeightPrevoted, }); - await this._saveMaxHeightPreviouslyForged( - block.generatorPublicKey, - block.height, - ); + await this._saveMaxHeightPreviouslyForged(block, previouslyForgedMap); return block; }, ]); @@ -307,15 +307,14 @@ class BlockProcessorV2 extends BaseBlockProcessor { _create({ transactions, - previousBlock, + height, + previousBlockId, keypair, timestamp, maxHeightPreviouslyForged, maxHeightPrevoted, }) { - const nextHeight = previousBlock ? previousBlock.height + 1 : 1; - - const reward = this.blocksModule.blockReward.calculateReward(nextHeight); + const reward = this.blocksModule.blockReward.calculateReward(height); let totalFee = new BigNum(0); let totalAmount = new BigNum(0); let size = 0; @@ -353,10 +352,10 @@ class BlockProcessorV2 extends BaseBlockProcessor { timestamp, numberOfTransactions: blockTransactions.length, payloadLength: size, - previousBlockId: previousBlock.id, + previousBlockId, generatorPublicKey: keypair.publicKey.toString('hex'), transactions: blockTransactions, - height: nextHeight, + height, maxHeightPreviouslyForged, maxHeightPrevoted, }; @@ -377,7 +376,7 @@ class BlockProcessorV2 extends BaseBlockProcessor { async _getPreviouslyForgedMap() { const previouslyForgedStr = await this.storage.entities.ForgerInfo.getKey( - FORGER_INFO_KEY_MAX_HEIGHT_PREVIOUSLY_FORGED, + FORGER_INFO_KEY_PREVIOUSLY_FORGED, ); return previouslyForgedStr ? JSON.parse(previouslyForgedStr) : {}; } @@ -386,21 +385,31 @@ class BlockProcessorV2 extends BaseBlockProcessor { * Saving a height which delegate last forged. this needs to be saved before broadcasting * so it needs to be outside of the DB transaction */ - async _saveMaxHeightPreviouslyForged(delegatePublicKey, height) { - const previouslyForgedMap = await this._getPreviouslyForgedMap(); - const previouslyForgedHeightByDelegate = - previouslyForgedMap[delegatePublicKey] || 0; + async _saveMaxHeightPreviouslyForged(block, previouslyForgedMap) { + const { + generatorPublicKey, + height, + maxHeightPreviouslyForged, + maxHeightPrevoted, + } = block; + // In order to compare with the minimum height in case of the first block, here it should be 0 + const previouslyForged = previouslyForgedMap[generatorPublicKey] || {}; + const previouslyForgedHeightByDelegate = previouslyForged.height || 0; // previously forged height only saves maximum forged height if (height <= previouslyForgedHeightByDelegate) { return; } const updatedPreviouslyForged = { ...previouslyForgedMap, - [delegatePublicKey]: height, + [generatorPublicKey]: { + height, + maxHeightPrevoted, + maxHeightPreviouslyForged, + }, }; const previouslyForgedStr = JSON.stringify(updatedPreviouslyForged); await this.storage.entities.ForgerInfo.setKey( - FORGER_INFO_KEY_MAX_HEIGHT_PREVIOUSLY_FORGED, + FORGER_INFO_KEY_PREVIOUSLY_FORGED, previouslyForgedStr, ); } diff --git a/framework/src/modules/chain/chain.js b/framework/src/modules/chain/chain.js index 65b450c1fda..9a248d08111 100644 --- a/framework/src/modules/chain/chain.js +++ b/framework/src/modules/chain/chain.js @@ -171,10 +171,27 @@ module.exports = class Chain { this.options.genesisBlock, ); + this.channel.subscribe('app:state:updated', event => { + Object.assign(this.scope.applicationState, event.data); + }); + + this.logger.info('Modules ready and launched'); + // After binding, it should immediately load blockchain + await this.processor.init(this.options.genesisBlock); + + // Update Application State after processor is initialized + this.channel.invoke('app:updateApplicationState', { + height: this.blocks.lastBlock.height, + lastBlockId: this.blocks.lastBlock.id, + maxHeightPrevoted: this.blocks.lastBlock.maxHeightPrevoted || 0, + blockVersion: this.blocks.lastBlock.version, + }); + // Deactivate broadcast and syncing during snapshotting process - if (this.options.loading.rebuildUpToRound) { + if (!Number.isNaN(parseInt(this.options.loading.rebuildUpToRound, 10))) { this.options.broadcasts.active = false; this.options.syncing.active = false; + await this.rebuilder.rebuild( this.options.loading.rebuildUpToRound, this.options.loading.loadPerIteration, @@ -190,22 +207,6 @@ module.exports = class Chain { return; } - this.channel.subscribe('app:state:updated', event => { - Object.assign(this.scope.applicationState, event.data); - }); - - this.logger.info('Modules ready and launched'); - // After binding, it should immediately load blockchain - await this.processor.init(this.options.genesisBlock); - - // Update Application State after processor is initialized - this.channel.invoke('app:updateApplicationState', { - height: this.blocks.lastBlock.height, - lastBlockId: this.blocks.lastBlock.id, - maxHeightPrevoted: this.blocks.lastBlock.maxHeightPrevoted || 0, - blockVersion: this.blocks.lastBlock.version, - }); - this._subscribeToEvents(); this.channel.subscribe('network:bootstrap', async () => { @@ -469,6 +470,7 @@ module.exports = class Chain { genesisBlock: this.options.genesisBlock, blocksModule: this.blocks, processorModule: this.processor, + bftModule: this.bft, activeDelegates: this.options.constants.ACTIVE_DELEGATES, }); this.scope.modules.rebuilder = this.rebuilder; @@ -519,7 +521,7 @@ module.exports = class Chain { return this.scope.sequence.add(async () => { try { if (!this.forger.delegatesEnabled()) { - this.logger.debug('No delegates are enabled'); + this.logger.trace('No delegates are enabled'); return; } if (this.synchronizer.isActive) { diff --git a/framework/src/modules/chain/components/storage/entities/round_delegates.js b/framework/src/modules/chain/components/storage/entities/round_delegates.js index 819265862e0..26e8ed30f2b 100644 --- a/framework/src/modules/chain/components/storage/entities/round_delegates.js +++ b/framework/src/modules/chain/components/storage/entities/round_delegates.js @@ -28,6 +28,7 @@ const sqlFiles = { delete: 'round_delegates/delete.sql', get: 'round_delegates/get.sql', getActiveDelegatesForRound: 'round_delegates/get_round_delegates.sql', + resetRoundDelegates: 'round_delegates/reset_round_delegates.sql', }; class RoundDelegates extends BaseEntity { @@ -111,6 +112,10 @@ class RoundDelegates extends BaseEntity { return this._getResults(filters, options, tx, expectedResultCount); } + async resetRoundDelegates(tx) { + return this.adapter.executeFile(this.SQLs.resetRoundDelegates, {}, {}, tx); + } + _getResults(filters, options, tx, expectedResultCount) { this.validateFilters(filters); this.validateOptions(options); diff --git a/framework/src/modules/chain/components/storage/sql/round_delegates/reset_round_delegates.sql b/framework/src/modules/chain/components/storage/sql/round_delegates/reset_round_delegates.sql new file mode 100644 index 00000000000..cc75bf19d82 --- /dev/null +++ b/framework/src/modules/chain/components/storage/sql/round_delegates/reset_round_delegates.sql @@ -0,0 +1,15 @@ +/* + * Copyright © 2019 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. + */ + +DELETE FROM round_delegates; diff --git a/framework/src/modules/chain/rebuilder.js b/framework/src/modules/chain/rebuilder.js index e35eb6e51da..31a2a2d8e4f 100644 --- a/framework/src/modules/chain/rebuilder.js +++ b/framework/src/modules/chain/rebuilder.js @@ -29,6 +29,7 @@ class Rebuilder { // Modules processorModule, blocksModule, + bftModule, // Constants activeDelegates, }) { @@ -42,6 +43,7 @@ class Rebuilder { this.processorModule = processorModule; this.blocksModule = blocksModule; + this.bftModule = bftModule; this.constants = { activeDelegates, }; @@ -81,6 +83,11 @@ class Rebuilder { const limit = loadPerIteration; await this.storage.entities.Account.resetMemTables(); + await this.storage.entities.RoundDelegates.resetRoundDelegates(); + + // Need to reset the BFT to rebuild from start of the chain + this.bftModule.reset(); + let { lastBlock } = this.blocksModule; for ( let currentHeight = 0; diff --git a/framework/src/modules/chain/synchronizer/fast_chain_switching_mechanism.js b/framework/src/modules/chain/synchronizer/fast_chain_switching_mechanism.js index 7f45bfece12..34fc023fb69 100644 --- a/framework/src/modules/chain/synchronizer/fast_chain_switching_mechanism.js +++ b/framework/src/modules/chain/synchronizer/fast_chain_switching_mechanism.js @@ -22,7 +22,6 @@ const { } = require('./utils'); const { ApplyPenaltyAndAbortError, - ApplyPenaltyAndRestartError, AbortError, BlockProcessingError, RestartError, @@ -66,14 +65,6 @@ class FastChainSwitchingMechanism extends BaseSynchronizer { await this._validateBlocks(blocks, highestCommonBlock, peerId); await this._switchChain(highestCommonBlock, blocks, peerId); } catch (err) { - if (err instanceof ApplyPenaltyAndRestartError) { - return this._applyPenaltyAndRestartSync( - err.peerId, - receivedBlock, - err.reason, - ); - } - if (err instanceof ApplyPenaltyAndAbortError) { this.logger.info( { err, peerId, reason: err.reason }, @@ -110,7 +101,11 @@ class FastChainSwitchingMechanism extends BaseSynchronizer { return true; } - async isValidFor(receivedBlock) { + async isValidFor(receivedBlock, peerId) { + if (!peerId) { + // If peerId is not specified, fast chain switching cannot be done + return false; + } const { lastBlock } = this.blocks; // 3. Step: Check whether B justifies fast chain switching mechanism @@ -162,14 +157,14 @@ class FastChainSwitchingMechanism extends BaseSynchronizer { async _queryBlocks(receivedBlock, highestCommonBlock, peerId) { if (!highestCommonBlock) { - throw new ApplyPenaltyAndRestartError( + throw new ApplyPenaltyAndAbortError( peerId, "Peer didn't return a common block", ); } if (highestCommonBlock.height < this.bft.finalizedHeight) { - throw new ApplyPenaltyAndRestartError( + throw new ApplyPenaltyAndAbortError( peerId, `Common block height ${highestCommonBlock.height} is lower than the finalized height of the chain ${this.bft.finalizedHeight}`, ); @@ -203,7 +198,7 @@ class FastChainSwitchingMechanism extends BaseSynchronizer { ); if (!blocks || !blocks.length) { - throw new ApplyPenaltyAndRestartError( + throw new ApplyPenaltyAndAbortError( peerId, `Peer didn't return any requested block within IDs ${highestCommonBlock.id} and ${receivedBlock.id}`, ); @@ -279,7 +274,7 @@ class FastChainSwitchingMechanism extends BaseSynchronizer { ); this.logger.debug('Restoring blocks from temporary table'); await restoreBlocks(this.blocks, this.processor); - throw new ApplyPenaltyAndRestartError( + throw new ApplyPenaltyAndAbortError( peerId, 'Detected invalid block while processing list of requested blocks', ); diff --git a/framework/src/modules/chain/synchronizer/synchronizer.js b/framework/src/modules/chain/synchronizer/synchronizer.js index 9b7d0b22f73..6302229fe44 100644 --- a/framework/src/modules/chain/synchronizer/synchronizer.js +++ b/framework/src/modules/chain/synchronizer/synchronizer.js @@ -81,11 +81,6 @@ class Synchronizer { receivedBlock, 'A block must be provided to the Synchronizer in order to run', ); - assert( - peerId, - 'A peer ID from the peer sending the block must be provided to the Synchronizer in order to run', - ); - this.logger.info( { blockId: receivedBlock.id, height: receivedBlock.height }, 'Starting synchronizer', @@ -101,6 +96,7 @@ class Synchronizer { // Choose the right mechanism to sync const validMechanism = await this._determineSyncMechanism( receivedBlockInstance, + peerId, ); if (!validMechanism) { @@ -132,9 +128,9 @@ class Synchronizer { } // eslint-disable-next-line class-methods-use-this, no-unused-vars - async _determineSyncMechanism(receivedBlock) { + async _determineSyncMechanism(receivedBlock, peerId) { for (const mechanism of this.mechanisms) { - if (await mechanism.isValidFor(receivedBlock)) { + if (await mechanism.isValidFor(receivedBlock, peerId)) { return mechanism; } } diff --git a/framework/src/modules/network/defaults/config.js b/framework/src/modules/network/defaults/config.js index cca71ad3ba1..ff357b085b3 100644 --- a/framework/src/modules/network/defaults/config.js +++ b/framework/src/modules/network/defaults/config.js @@ -74,8 +74,6 @@ const defaultConfig = { }, }, maximum: 4, - env: { variable: 'LISK_PEERS', formatter: 'stringToIpPortSet' }, - arg: { name: '--peers,-x', formatter: 'stringToIpPortSet' }, }, // Warning! Beware of declaring only trustworthy peers in this array as these could attack a // node with a denial-of-service attack because the banning mechanism is deactivated. @@ -95,8 +93,6 @@ const defaultConfig = { }, }, }, - env: { variable: 'LISK_PEERS', formatter: 'stringToIpPortSet' }, - arg: { name: '--peers,-x', formatter: 'stringToIpPortSet' }, }, peerBanTime: { type: 'integer', diff --git a/framework/test/jest/unit/specs/modules/chain/bft/bft.spec.js b/framework/test/jest/unit/specs/modules/chain/bft/bft.spec.js index 3c0b50a5bc6..b3b8df01fc5 100644 --- a/framework/test/jest/unit/specs/modules/chain/bft/bft.spec.js +++ b/framework/test/jest/unit/specs/modules/chain/bft/bft.spec.js @@ -35,6 +35,15 @@ const generateBlocks = ({ startHeight, numberOfBlocks }) => blockFixture({ height: startHeight + index, version: 2 }), ); +const extractBFTInfo = bft => ({ + finalizedHeight: bft.finalizedHeight, + maxHeightPrevoted: bft.maxHeightPrevoted, + headers: [...bft.finalityManager.headers.items], + preVotes: { ...bft.finalityManager.preVotes }, + preCommits: { ...bft.finalityManager.preCommits }, + state: { ...bft.finalityManager.state }, +}); + describe('bft', () => { describe('extractBFTBlockHeaderFromBlock', () => { it('should extract particular headers for bft', async () => { @@ -187,7 +196,7 @@ describe('bft', () => { }); }); - it('should invoke loadBlocksFromStorage() for lastBlockHeight - TWO_ROUNDS if its highest', async () => { + it('should invoke loadBlocksFromStorage() for lastBlockHeight - THREE_ROUNDS if its highest', async () => { bft.constants.startingHeight = 0; const finalizedHeight = 200; const lastBlockHeight = 600; @@ -208,7 +217,7 @@ describe('bft', () => { expect(bft._loadBlocksFromStorage).toHaveBeenCalledTimes(1); expect(bft._loadBlocksFromStorage).toHaveBeenCalledWith({ - fromHeight: lastBlockHeight - activeDelegates * 2, + fromHeight: lastBlockHeight - activeDelegates * 3 + 1, tillHeight: lastBlockHeight, minActiveHeightsOfDelegates, }); @@ -365,8 +374,9 @@ describe('bft', () => { ]); }); - it('should load more blocks from storage if remaining in headers list is less than 2 rounds', async () => { + it('should load more blocks from storage if remaining in headers list is less than 3 rounds', async () => { // Arrange + jest.spyOn(bft.finalityManager, 'recompute'); // Generate 500 blocks const numberOfBlocks = 500; const numberOfBlocksToDelete = 50; @@ -406,19 +416,21 @@ describe('bft', () => { await bft.deleteBlocks(blocksToDelete, minActiveHeightsOfDelegates); // Assert + expect(bft.finalityManager.recompute).toHaveBeenCalledTimes(2); expect(bft.finalityManager.maxHeight).toEqual(450); expect(bft.finalityManager.minHeight).toEqual( 450 - activeDelegates * 2, ); expect(storageMock.entities.Block.get).toHaveBeenCalledTimes(1); expect(storageMock.entities.Block.get).toHaveBeenLastCalledWith( - { height_lte: 400, height_gte: 450 - activeDelegates * 2 }, + { height_lte: 400, height_gte: 450 - activeDelegates * 3 + 1 }, { limit: null, sort: 'height:desc' }, ); }); - it('should not load more blocks from storage if remaining in headers list is more than 2 rounds', async () => { + it('should not load more blocks from storage if remaining in headers list is more than 3 rounds', async () => { // Arrange + jest.spyOn(bft.finalityManager, 'recompute'); // Generate 500 blocks const numberOfBlocks = 500; const numberOfBlocksToDelete = 50; @@ -426,8 +438,8 @@ describe('bft', () => { startHeight: 1, numberOfBlocks, }); - // Last 300 blocks from height 201 to 500 - const blocksInBft = blocks.slice(200); + // Last 400 blocks from height 101 to 500 + const blocksInBft = blocks.slice(100); // Last 50 blocks from height 451 to 500 const blocksToDelete = blocks.slice(-numberOfBlocksToDelete); @@ -440,10 +452,10 @@ describe('bft', () => { return acc; }, {}); - // Load last 300 blocks to bft (201 to 500) - // eslint-disable-next-line no-restricted-syntax + // Load last 400 blocks to bft (101 to 500) for (const block of blocksInBft) { - // eslint-disable-next-line no-await-in-loop + // This value is mutated to pass the chainMaxHeightPrevoted validation + block.maxHeightPrevoted = bft.finalityManager.chainMaxHeightPrevoted; await bft.addNewBlock(block, stateStore); } @@ -451,39 +463,39 @@ describe('bft', () => { await bft.deleteBlocks(blocksToDelete, minActiveHeightsOfDelegates); // Assert + expect(bft.finalityManager.recompute).toHaveBeenCalledTimes(1); expect(bft.finalityManager.maxHeight).toEqual(450); - expect(bft.finalityManager.minHeight).toEqual(201); + expect(bft.finalityManager.minHeight).toEqual(101); expect(storageMock.entities.Block.get).toHaveBeenCalledTimes(0); }); - it('should not load more blocks from storage if remaining in headers list is exactly 2 rounds', async () => { + it('should not load more blocks from storage if remaining in headers list is exactly 3 rounds', async () => { // Arrange + jest.spyOn(bft.finalityManager, 'recompute'); // Generate 500 blocks const numberOfBlocks = 500; const blocks = generateBlocks({ startHeight: 1, numberOfBlocks, }); - // Last 300 blocks from height 201 to 500 - const blocksInBft = blocks.slice(200); + // Last 300 blocks from height 100 to 500 + const blocksInBft = blocks.slice(99); - // Delete blocks keeping exactly two rounds in the list from (201 to 298) - const blocksToDelete = blocks.slice( - -1 * (300 - activeDelegates * 2 - 1), - ); + // Delete blocks keeping exactly 3 rounds in the list from (201 to 298) + const blocksToDelete = blocks.slice(-97); // minActiveHeightsOfDelegates is provided to deleteBlocks function // in block_processor_v2 from DPoS module. const minActiveHeightsOfDelegates = blocks.reduce((acc, block) => { // the value is not important in this test. - acc[block.generatorPublicKey] = [1]; + acc[block.generatorPublicKey] = [0]; return acc; }, {}); // Load last 300 blocks to bft (201 to 500) - // eslint-disable-next-line no-restricted-syntax for (const block of blocksInBft) { - // eslint-disable-next-line no-await-in-loop + // This value is mutated to pass the chainMaxHeightPrevoted validation + block.maxHeightPrevoted = bft.finalityManager.chainMaxHeightPrevoted; await bft.addNewBlock(block, stateStore); } @@ -491,10 +503,9 @@ describe('bft', () => { await bft.deleteBlocks(blocksToDelete, minActiveHeightsOfDelegates); // Assert + expect(bft.finalityManager.recompute).toHaveBeenCalledTimes(1); expect(bft.finalityManager.maxHeight).toEqual(403); - expect(bft.finalityManager.minHeight).toEqual( - 403 - activeDelegates * 2, - ); + expect(bft.finalityManager.minHeight).toEqual(100); expect(storageMock.entities.Block.get).toHaveBeenCalledTimes(0); }); }); @@ -647,6 +658,48 @@ describe('bft', () => { }); }); + describe('#reset', () => { + it('should reset headers and related stats to initial state except finality', async () => { + // Arrange + storageMock.entities.Block.get.mockReturnValue([]); + storageMock.entities.ChainState.get.mockResolvedValue([ + { key: 'BFT.finalizedHeight', value: 1 }, + ]); + const stateStore = new StateStore(storageMock); + await stateStore.chainState.cache(); + const bft = new BFT(bftParams); + await bft.init(stateStore); + const initialInfo = extractBFTInfo(bft); + const numberOfBlocks = 500; + const blocks = generateBlocks({ + startHeight: 1, + numberOfBlocks, + }); + for (const block of blocks) { + await bft.addNewBlock( + { + ...block, + maxHeightPrevoted: bft.finalityManager.chainMaxHeightPrevoted, + }, + stateStore, + ); + } + const beforeResetInfo = extractBFTInfo(bft); + + // Act + bft.reset(); + const afterResetInfo = extractBFTInfo(bft); + + // Assert + expect(beforeResetInfo).not.toEqual(initialInfo); + // Finalized height should not change + expect(afterResetInfo).toEqual({ + ...initialInfo, + finalizedHeight: beforeResetInfo.finalizedHeight, + }); + }); + }); + // TODO: Remove tests for private methods describe('#_initFinalityManager', () => { let stateStore; @@ -696,9 +749,9 @@ describe('bft', () => { expect(finalityManager.finalizedHeight).toEqual(finalizedHeight); }); - it('should initialize finalityManager with stored startingHeight - TWO_ROUNDS if its highest', async () => { + it('should initialize finalityManager with stored startingHeight - THREE_ROUNDS if its highest', async () => { // Arrange - const finalizedHeight = 500; + const finalizedHeight = 400; const startingHeightHigher = 800; storageMock.entities.ChainState.get.mockResolvedValue([ { key: 'BFT.finalizedHeight', value: finalizedHeight }, diff --git a/framework/test/jest/unit/specs/modules/chain/bft/bft_finality_manager_protocol_specs.spec.js b/framework/test/jest/unit/specs/modules/chain/bft/bft_finality_manager_protocol_specs.spec.js index c91b558846d..36ee3748e13 100644 --- a/framework/test/jest/unit/specs/modules/chain/bft/bft_finality_manager_protocol_specs.spec.js +++ b/framework/test/jest/unit/specs/modules/chain/bft/bft_finality_manager_protocol_specs.spec.js @@ -53,7 +53,7 @@ describe('FinalityManager', () => { testCase.output.finalizedHeight, ); - expect(myBft.prevotedConfirmedHeight).toEqual( + expect(myBft.chainMaxHeightPrevoted).toEqual( testCase.output.preVotedConfirmedHeight, ); }); @@ -84,7 +84,7 @@ describe('FinalityManager', () => { expect(myBft.finalizedHeight).toEqual( lastTestCaseOutput.finalizedHeight, ); - expect(myBft.prevotedConfirmedHeight).toEqual( + expect(myBft.chainMaxHeightPrevoted).toEqual( lastTestCaseOutput.preVotedConfirmedHeight, ); @@ -95,7 +95,7 @@ describe('FinalityManager', () => { expect(myBft.finalizedHeight).toEqual( lastTestCaseOutput.finalizedHeight, ); - expect(myBft.prevotedConfirmedHeight).toEqual( + expect(myBft.chainMaxHeightPrevoted).toEqual( lastTestCaseOutput.preVotedConfirmedHeight, ); @@ -143,7 +143,7 @@ describe('FinalityManager', () => { // Assert - Values should match with out of that step expect(myBft.finalizedHeight).toEqual(testCaseOutput.finalizedHeight); - expect(myBft.prevotedConfirmedHeight).toEqual( + expect(myBft.chainMaxHeightPrevoted).toEqual( testCaseOutput.preVotedConfirmedHeight, ); }); diff --git a/framework/test/jest/unit/specs/modules/chain/bft/finality_manager.spec.js b/framework/test/jest/unit/specs/modules/chain/bft/finality_manager.spec.js index a759b980f26..60ef4acf4c4 100644 --- a/framework/test/jest/unit/specs/modules/chain/bft/finality_manager.spec.js +++ b/framework/test/jest/unit/specs/modules/chain/bft/finality_manager.spec.js @@ -105,7 +105,7 @@ describe('finality_manager', () => { expect(() => finalityManager.verifyBlockHeaders(header)).toThrow( BFTInvalidAttributeError, - 'Wrong prevotedConfirmedHeight in blockHeader.', + 'Wrong chainMaxHeightPrevoted in blockHeader.', ); }); @@ -117,7 +117,7 @@ describe('finality_manager', () => { }, ); const header = blockHeaderFixture({ maxHeightPrevoted: 10 }); - finalityManager.prevotedConfirmedHeight = 10; + finalityManager.chainMaxHeightPrevoted = 10; expect(() => finalityManager.verifyBlockHeaders(header)).not.toThrow(); }); @@ -130,16 +130,18 @@ describe('finality_manager', () => { }); it('should throw error if same delegate forged block on different height', async () => { - const maxHeightPreviouslyForged = 10; + const maxHeightPrevoted = 10; const delegateAccount = accountFixture(); const lastBlock = blockHeaderFixture({ delegatePublicKey: delegateAccount.publicKey, - maxHeightPreviouslyForged, + maxHeightPreviouslyForged: 5, + maxHeightPrevoted, height: 10, }); const currentBlock = blockHeaderFixture({ delegatePublicKey: delegateAccount.publicKey, - maxHeightPreviouslyForged, + maxHeightPrevoted, + maxHeightPreviouslyForged: 6, height: 9, }); @@ -272,6 +274,34 @@ describe('finality_manager', () => { header1, ); }); + + it('should throw error if blockheader has conflict (Violates disjointness condition)', async () => { + const header1 = blockHeaderFixture({ + height: 34624, + maxHeightPreviouslyForged: 34501, + }); + const header2 = blockHeaderFixture({ + height: 34666, + maxHeightPreviouslyForged: 34501, + delegatePublicKey: header1.delegatePublicKey, + }); + const headers = [header1]; + for ( + let height = header1.height + 1; + height < header2.height; + height += 1 + ) { + const header = blockHeaderFixture({ + height, + maxHeightPreviouslyForged: height - 129, + }); + headers.push(header); + } + headers.push(header2); + expect(() => { + headers.forEach(header => finalityManager.addBlockHeader(header)); + }).toThrow('Violation of disjointness condition.'); + }); }); describe('recompute', () => {}); diff --git a/framework/test/jest/unit/specs/modules/chain/block_processor_v2.spec.js b/framework/test/jest/unit/specs/modules/chain/block_processor_v2.spec.js index 6ebefcf09b6..930beca2992 100644 --- a/framework/test/jest/unit/specs/modules/chain/block_processor_v2.spec.js +++ b/framework/test/jest/unit/specs/modules/chain/block_processor_v2.spec.js @@ -40,17 +40,20 @@ describe('block processor v2', () => { beforeEach(async () => { blocksModuleStub = { + undo: jest.fn(), blockReward: { calculateReward: jest.fn().mockReturnValue(5), }, }; bftModuleStub = { init: jest.fn(), + deleteBlocks: jest.fn(), maxHeightPrevoted: 0, isBFTProtocolCompliant: jest.fn().mockReturnValue(true), }; dposModuleStub = { + undo: jest.fn(), getMinActiveHeightsOfDelegates: jest.fn(), }; storageStub = { @@ -78,14 +81,34 @@ describe('block processor v2', () => { }); describe('init', () => { - it('should get activeSince from dpos for 2 rounds', async () => { + it('should get activeSince from dpos for 3 rounds', async () => { // Arrange & Act const stateStore = new StateStore(storageStub); await blockProcessor.init.run({ stateStore }); // Assert expect( dposModuleStub.getMinActiveHeightsOfDelegates, - ).toHaveBeenCalledWith(2); + ).toHaveBeenCalledWith(3); + }); + }); + + describe('undo', () => { + it('should reject the promise when dpos getMinActiveHeightsOfDelegates fails', async () => { + const stateStore = new StateStore(storageStub); + dposModuleStub.getMinActiveHeightsOfDelegates.mockRejectedValue( + new Error('Invalid error'), + ); + await expect(blockProcessor.undo.run({ stateStore })).rejects.toThrow( + 'Invalid error', + ); + }); + + it('should reject the promise when bft deleteBlocks fails', async () => { + const stateStore = new StateStore(storageStub); + bftModuleStub.deleteBlocks.mockRejectedValue(new Error('Invalid error')); + await expect(blockProcessor.undo.run({ stateStore })).rejects.toThrow( + 'Invalid error', + ); }); }); @@ -107,8 +130,9 @@ describe('block processor v2', () => { }); // Assert expect(storageStub.entities.ForgerInfo.getKey).toHaveBeenCalledWith( - 'maxHeightPreviouslyForged', + 'previouslyForged', ); + // previousBlock.height + 1 expect(block.maxHeightPreviouslyForged).toBe(0); }); @@ -116,7 +140,7 @@ describe('block processor v2', () => { const previouslyForgedHeight = 100; // Arrange const maxHeightResult = JSON.stringify({ - [defaultKeyPair.publicKey.toString('hex')]: 100, + [defaultKeyPair.publicKey.toString('hex')]: { height: 100 }, }); storageStub.entities.ForgerInfo.getKey.mockResolvedValue(maxHeightResult); // Act @@ -130,12 +154,23 @@ describe('block processor v2', () => { }); // Assert expect(storageStub.entities.ForgerInfo.getKey).toHaveBeenCalledWith( - 'maxHeightPreviouslyForged', + 'previouslyForged', ); expect(block.maxHeightPreviouslyForged).toBe(previouslyForgedHeight); }); - it('should set maxPreviouslyForgedHeight to previously forged height', async () => { + it('should update maxPreviouslyForgedHeight to the next higher one but not change for other delegates', async () => { + // Arrange + const list = { + [defaultKeyPair.publicKey.toString('hex')]: { height: 5 }, + a: { height: 4 }, + b: { height: 6 }, + c: { height: 7 }, + x: { height: 8 }, + }; + storageStub.entities.ForgerInfo.getKey.mockResolvedValue( + JSON.stringify(list), + ); // Act block = await blockProcessor.create.run({ keypair: defaultKeyPair, @@ -146,14 +181,61 @@ describe('block processor v2', () => { }, }); const maxHeightResult = JSON.stringify({ - [defaultKeyPair.publicKey.toString('hex')]: 11, + ...list, + [defaultKeyPair.publicKey.toString('hex')]: { + height: 11, + maxHeightPrevoted: 0, + maxHeightPreviouslyForged: 5, + }, }); expect(storageStub.entities.ForgerInfo.setKey).toHaveBeenCalledWith( - 'maxHeightPreviouslyForged', + 'previouslyForged', maxHeightResult, ); }); + it('should set maxPreviouslyForgedHeight to forging height', async () => { + // Act + block = await blockProcessor.create.run({ + keypair: defaultKeyPair, + timestamp: 10, + transactions: [], + previousBlock: { + height: 10, + }, + }); + const maxHeightResult = JSON.stringify({ + [defaultKeyPair.publicKey.toString('hex')]: { + height: 11, + maxHeightPrevoted: 0, + maxHeightPreviouslyForged: 0, + }, + }); + expect(storageStub.entities.ForgerInfo.setKey).toHaveBeenCalledWith( + 'previouslyForged', + maxHeightResult, + ); + }); + + it('should not set maxPreviouslyForgedHeight to next height if lower', async () => { + // Arrange + storageStub.entities.ForgerInfo.getKey.mockResolvedValue( + JSON.stringify({ + [defaultKeyPair.publicKey.toString('hex')]: { height: 15 }, + }), + ); + // Act + block = await blockProcessor.create.run({ + keypair: defaultKeyPair, + timestamp: 10, + transactions: [], + previousBlock: { + height: 10, + }, + }); + expect(storageStub.entities.ForgerInfo.setKey).not.toHaveBeenCalled(); + }); + it('should return a block', async () => { // Act block = await blockProcessor.create.run({ diff --git a/framework/test/jest/unit/specs/modules/chain/synchronizer/fast_chain_switching_mechanism/fast_chain_switching_mechanism.spec.js b/framework/test/jest/unit/specs/modules/chain/synchronizer/fast_chain_switching_mechanism/fast_chain_switching_mechanism.spec.js index 2a57a14c9b5..165587668f3 100644 --- a/framework/test/jest/unit/specs/modules/chain/synchronizer/fast_chain_switching_mechanism/fast_chain_switching_mechanism.spec.js +++ b/framework/test/jest/unit/specs/modules/chain/synchronizer/fast_chain_switching_mechanism/fast_chain_switching_mechanism.spec.js @@ -156,24 +156,6 @@ describe('fast_chain_switching_mechanism', () => { const aPeerId = '127.0.0.1:5000'; let aBlock; - const checkApplyPenaltyAndRestartIsCalled = ( - receivedBlock, - peerId, - reason, - ) => { - expect(loggerMock.info).toHaveBeenCalledWith( - { peerId, reason }, - 'Applying penalty to peer and restarting synchronizer', - ); - expect(channelMock.invoke).toHaveBeenCalledWith('network:applyPenalty', { - peerId, - penalty: 100, - }); - expect(channelMock.publish).toHaveBeenCalledWith('chain:processor:sync', { - block: receivedBlock, - }); - }; - const checkApplyPenaltyAndAbortIsCalled = (peerId, err) => { expect(loggerMock.info).toHaveBeenCalledWith( { err, peerId, reason: err.reason }, @@ -297,10 +279,12 @@ describe('fast_chain_switching_mechanism', () => { // Assert expect(storageMock.entities.Block.get).toHaveBeenCalledTimes(12); // 10 + 2 from beforeEach hooks expect(channelMock.invoke).toHaveBeenCalledTimes(10); - checkApplyPenaltyAndRestartIsCalled( - aBlock, + checkApplyPenaltyAndAbortIsCalled( aPeerId, - "Peer didn't return a common block", + new Errors.ApplyPenaltyAndAbortError( + aPeerId, + "Peer didn't return a common block", + ), ); }); }); @@ -345,10 +329,12 @@ describe('fast_chain_switching_mechanism', () => { await fastChainSwitchingMechanism.run(aBlock, aPeerId); // Assert - checkApplyPenaltyAndRestartIsCalled( - aBlock, + checkApplyPenaltyAndAbortIsCalled( aPeerId, - 'Common block height 0 is lower than the finalized height of the chain 1', + new Errors.ApplyPenaltyAndAbortError( + aPeerId, + 'Common block height 0 is lower than the finalized height of the chain 1', + ), ); expect(fastChainSwitchingMechanism._queryBlocks).toHaveBeenCalledWith( aBlock, diff --git a/framework/test/jest/unit/specs/modules/chain/synchronizer/synchronizer.spec.js b/framework/test/jest/unit/specs/modules/chain/synchronizer/synchronizer.spec.js index 1b3eff738a7..69ca5447c00 100644 --- a/framework/test/jest/unit/specs/modules/chain/synchronizer/synchronizer.spec.js +++ b/framework/test/jest/unit/specs/modules/chain/synchronizer/synchronizer.spec.js @@ -462,13 +462,6 @@ describe('Synchronizer', () => { expect(synchronizer.active).toBeFalsy(); }); - it('should reject with error if required properties are missing (peerId)', async () => { - await expect(synchronizer.run({ height: 1 })).rejects.toThrow( - 'A peer ID from the peer sending the block must be provided to the Synchronizer in order to run', - ); - expect(synchronizer.active).toBeFalsy(); - }); - it('should validate the block before sync', async () => { jest.spyOn(processorModule, 'validateDetached'); diff --git a/framework/test/mocha/unit/modules/chain/chain.js b/framework/test/mocha/unit/modules/chain/chain.js index d62c0c58cd3..47f3eddc851 100644 --- a/framework/test/mocha/unit/modules/chain/chain.js +++ b/framework/test/mocha/unit/modules/chain/chain.js @@ -42,6 +42,7 @@ describe('Chain', () => { /* Arranging Stubs start */ stubs.logger = { + trace: sinonSandbox.stub(), error: sinonSandbox.stub(), debug: sinonSandbox.stub(), fatal: sinonSandbox.stub(), @@ -55,7 +56,10 @@ describe('Chain', () => { stubs.storage = { cleanup: sinonSandbox.stub(), entities: { - Block: { get: sinonSandbox.stub().resolves([]) }, + Block: { + get: sinonSandbox.stub().resolves([]), + count: sinonSandbox.stub().resolves(0), + }, ChainMeta: { getKey: sinonSandbox.stub() }, }, }; @@ -222,13 +226,13 @@ describe('Chain', () => { return expect(global.exceptions).to.be.equal(chainOptions.exceptions); }); - describe('when options.loading.rebuildUpToRound is truthy', () => { + describe('when options.loading.rebuildUpToRound is set to an integer value', () => { beforeEach(async () => { // Arrange chain = new Chain(stubs.channel, { ...chainOptions, loading: { - rebuildUpToRound: true, + rebuildUpToRound: 0, }, broadcasts: {}, syncing: {}, @@ -481,7 +485,7 @@ describe('Chain', () => { await chain._forgingTask(); // Assert - expect(stubs.logger.debug.getCall(1)).to.be.calledWith( + expect(stubs.logger.trace.getCall(0)).to.be.calledWith( 'No delegates are enabled', ); expect(chain.scope.sequence.add).to.be.called; diff --git a/framework/test/utils/legacy/application.js b/framework/test/utils/legacy/application.js index 91f16ec0876..88325d79412 100644 --- a/framework/test/utils/legacy/application.js +++ b/framework/test/utils/legacy/application.js @@ -212,6 +212,7 @@ const initStepsForTest = { genesisBlock: __testContext.config.genesisBlock, blocksModule: modules.blocks, processorModule: modules.processor, + bftModule: modules.bft, activeDelegates: __testContext.config.constants.ACTIVE_DELEGATES, }); diff --git a/sdk/package-lock.json b/sdk/package-lock.json index 5a2da210842..86c45e7d2de 100644 --- a/sdk/package-lock.json +++ b/sdk/package-lock.json @@ -1,6 +1,6 @@ { "name": "lisk-sdk", - "version": "3.0.1", + "version": "3.0.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/sdk/package.json b/sdk/package.json index d37a7685efa..d717a1834af 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "lisk-sdk", - "version": "3.0.1", + "version": "3.0.2", "description": "Official SDK for the Lisk blockchain application platform", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", @@ -23,9 +23,9 @@ "main": "src/index.js", "dependencies": { "@liskhq/bignum": "1.3.1", - "@liskhq/lisk-cryptography": "2.4.1", - "@liskhq/lisk-transactions": "3.0.1", - "@liskhq/lisk-validator": "0.3.0", - "lisk-framework": "0.5.1" + "@liskhq/lisk-cryptography": "2.4.2", + "@liskhq/lisk-transactions": "3.0.2", + "@liskhq/lisk-validator": "0.3.1", + "lisk-framework": "0.5.2" } }