diff --git a/elements/lisk-chain/package.json b/elements/lisk-chain/package.json index d5a22332f31..f97e64367ea 100644 --- a/elements/lisk-chain/package.json +++ b/elements/lisk-chain/package.json @@ -35,6 +35,7 @@ }, "dependencies": { "@liskhq/lisk-cryptography": "2.5.0-alpha.0", + "@liskhq/lisk-db": "0.1.0", "@liskhq/lisk-transactions": "4.0.0-alpha.0", "@types/node": "12.12.11", "debug": "4.1.1", diff --git a/elements/lisk-chain/src/chain.ts b/elements/lisk-chain/src/chain.ts index 384d329d018..57ab8fa661e 100644 --- a/elements/lisk-chain/src/chain.ts +++ b/elements/lisk-chain/src/chain.ts @@ -12,6 +12,7 @@ * Removal or modification of this copyright notice is prohibited. */ +import { KVStore } from '@liskhq/lisk-db'; import { BaseTransaction, Status as TransactionStatus, @@ -56,8 +57,6 @@ import { BlockRewardOptions, Contexter, MatcherTransaction, - Storage, - StorageTransaction, GenesisBlock, GenesisBlockJSON, } from './types'; @@ -77,7 +76,7 @@ import { interface ChainConstructor { // Components - readonly storage: Storage; + readonly db: KVStore; // Unique requirements readonly genesisBlock: GenesisBlockJSON; // Modules @@ -100,27 +99,6 @@ interface ChainConstructor { const TRANSACTION_TYPES_VOTE = [3, 11]; -const saveBlock = async ( - storage: Storage, - blockJSON: BlockJSON, - tx: StorageTransaction, -): Promise => { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (!tx) { - throw new Error('Block should only be saved in a database tx'); - } - // If there is already a running transaction use it - const promises = [storage.entities.Block.create(blockJSON, {}, tx)]; - - if (blockJSON.transactions.length) { - promises.push( - storage.entities.Transaction.create(blockJSON.transactions, {}, tx), - ); - } - - return tx.batch(promises); -}; - const applyConfirmedStep = async ( blockInstance: BlockInstance, stateStore: StateStore, @@ -197,7 +175,7 @@ export class Chain { private _lastBlock: BlockInstance; private readonly blocksVerify: BlocksVerify; - private readonly storage: Storage; + private readonly _db: KVStore; private readonly _networkIdentifier: string; private readonly blockRewardArgs: BlockRewardOptions; private readonly genesisBlock: GenesisBlock; @@ -210,7 +188,7 @@ export class Chain { public constructor({ // Components - storage, + db, // Unique requirements genesisBlock, // Modules @@ -230,9 +208,9 @@ export class Chain { }: ChainConstructor) { this.events = new EventEmitter(); - this.storage = storage; + this._db = db; this.dataAccess = new DataAccess({ - dbStorage: storage, + db, registeredTransactions, minBlockHeaderCache, maxBlockHeaderCache, @@ -303,9 +281,10 @@ export class Chain { public async init(): Promise { // Check mem tables - const genesisBlock = await this.dataAccess.getBlockHeaderByHeight(1); - - if (!genesisBlock) { + let genesisBlock: BlockHeader; + try { + genesisBlock = await this.dataAccess.getBlockHeaderByHeight(1); + } catch (error) { throw new Error('Failed to load genesis block'); } @@ -315,8 +294,10 @@ export class Chain { throw new Error('Genesis block does not match'); } - const storageLastBlock = await this.dataAccess.getLastBlock(); - if (!storageLastBlock) { + let storageLastBlock: BlockInstance; + try { + storageLastBlock = await this.dataAccess.getLastBlock(); + } catch (error) { throw new Error('Failed to load last block'); } @@ -347,7 +328,7 @@ export class Chain { lastBlockHeaders[0]?.height ?? 1, ); - return new StateStore(this.storage, { + return new StateStore(this.dataAccess, { networkIdentifier: this._networkIdentifier, lastBlockHeaders, lastBlockReward, @@ -378,8 +359,7 @@ export class Chain { } public async resetState(): Promise { - await this.storage.entities.Account.resetMemTables(); - await this.storage.entities.ConsensusState.delete(); + await this.dataAccess.resetMemTables(); this.dataAccess.resetBlockHeaderCache(); } @@ -391,7 +371,7 @@ export class Chain { verifyPreviousBlockId(blockInstance, this._lastBlock, this.genesisBlock); validateBlockSlot(blockInstance, this._lastBlock, this.slots); if (!skipExistingCheck) { - await verifyBlockNotExists(this.storage, blockInstance); + await verifyBlockNotExists(this.dataAccess, blockInstance); const transactionsResponses = await checkPersistedTransactions( this.dataAccess, )(blockInstance.transactions); @@ -432,26 +412,27 @@ export class Chain { removeFromTempTable: false, }, ): Promise { - return this.storage.entities.Block.begin('saveBlock', async tx => { - await stateStore.finalize(tx); - if (!saveOnlyState) { - const blockJSON = this.serialize(blockInstance); - await saveBlock(this.storage, blockJSON, tx); - } - if (removeFromTempTable) { - await this.removeBlockFromTempTable(blockInstance.id, tx); - } - this.dataAccess.addBlockHeader(blockInstance); - this._lastBlock = blockInstance; - - const accounts = stateStore.account - .getUpdated() - .map(anAccount => anAccount.toJSON()); + const accounts = stateStore.account + .getUpdated() + .map(anAccount => anAccount.toJSON()); + + if (saveOnlyState) { + const batch = this._db.batch(); + stateStore.finalize(batch); + await batch.write(); + } else { + await this.dataAccess.saveBlock( + blockInstance, + stateStore, + removeFromTempTable, + ); + } + this.dataAccess.addBlockHeader(blockInstance); + this._lastBlock = blockInstance; - this.events.emit(EVENT_NEW_BLOCK, { - block: this.serialize(blockInstance), - accounts, - }); + this.events.emit(EVENT_NEW_BLOCK, { + block: this.serialize(blockInstance), + accounts, }); } @@ -469,48 +450,34 @@ export class Chain { stateStore: StateStore, { saveTempBlock } = { saveTempBlock: false }, ): Promise { - await this.storage.entities.Block.begin('revertBlock', async tx => { - const secondLastBlock = await this._deleteLastBlock(block, tx); - - if (saveTempBlock) { - const blockJSON = this.serialize(block); - const blockTempEntry = { - id: blockJSON.id, - height: blockJSON.height, - fullBlock: blockJSON, - }; - await this.storage.entities.TempBlock.create(blockTempEntry, {}, tx); - } - await stateStore.finalize(tx); - await this.dataAccess.removeBlockHeader(block.id); - this._lastBlock = secondLastBlock; - - const accounts = stateStore.account - .getUpdated() - .map(anAccount => anAccount.toJSON()); - - this.events.emit(EVENT_DELETE_BLOCK, { - block: this.serialize(block), - accounts, - }); - }); - } + if (block.height === 1) { + throw new Error('Cannot delete genesis block'); + } + let secondLastBlock: BlockInstance; + try { + secondLastBlock = await this.dataAccess.getBlockByID( + block.previousBlockId, + ); + } catch (error) { + throw new Error('PreviousBlock is null'); + } - public async removeBlockFromTempTable( - blockId: string, - tx: StorageTransaction, - ): Promise { - return this.storage.entities.TempBlock.delete({ id: blockId }, {}, tx); + await this.dataAccess.deleteBlock(block, stateStore, saveTempBlock); + await this.dataAccess.removeBlockHeader(block.id); + this._lastBlock = secondLastBlock; + + const accounts = stateStore.account + .getUpdated() + .map(anAccount => anAccount.toJSON()); + + this.events.emit(EVENT_DELETE_BLOCK, { + block: this.serialize(block), + accounts, + }); } public async exists(block: BlockInstance): Promise { - try { - await verifyBlockNotExists(this.storage, block); - - return false; - } catch (err) { - return true; - } + return this.dataAccess.isBlockPersisted(block.id); } public async getHighestCommonBlock( @@ -640,22 +607,4 @@ export class Chain { this.dataAccess.addBlockHeader(blockHeader); } } - - private async _deleteLastBlock( - lastBlock: BlockInstance, - tx?: StorageTransaction, - ): Promise { - if (lastBlock.height === 1) { - throw new Error('Cannot delete genesis block'); - } - const block = await this.dataAccess.getBlockByID(lastBlock.previousBlockId); - - if (!block) { - throw new Error('PreviousBlock is null'); - } - - await this.storage.entities.Block.delete({ id: lastBlock.id }, {}, tx); - - return block; - } } diff --git a/elements/lisk-chain/src/constants.ts b/elements/lisk-chain/src/constants.ts index e2ae8fc19f9..a3f95d29fb7 100644 --- a/elements/lisk-chain/src/constants.ts +++ b/elements/lisk-chain/src/constants.ts @@ -12,7 +12,7 @@ * Removal or modification of this copyright notice is prohibited. */ -export const CHAIN_STATE_BURNT_FEE = 'chain:burntFee'; +export const CHAIN_STATE_BURNT_FEE = 'burntFee'; export const DEFAULT_MIN_BLOCK_HEADER_CACHE = 309; export const DEFAULT_MAX_BLOCK_HEADER_CACHE = 515; diff --git a/elements/lisk-chain/src/data_access/constants.ts b/elements/lisk-chain/src/data_access/constants.ts new file mode 100644 index 00000000000..95e2a54dde1 --- /dev/null +++ b/elements/lisk-chain/src/data_access/constants.ts @@ -0,0 +1,22 @@ +/* + * Copyright © 2020 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. + */ + +export const DB_KEY_BLOCKS_ID = 'blocks:id'; +export const DB_KEY_BLOCKS_HEIGHT = 'blocks:height'; +export const DB_KEY_TRANSACTIONS_BLOCK_ID = 'transactions:blockID'; +export const DB_KEY_TRANSACTIONS_ID = 'transactions:id'; +export const DB_KEY_TEMPBLOCKS_HEIGHT = 'tempBlocks:height'; +export const DB_KEY_ACCOUNTS_ADDRESS = 'accounts:address'; +export const DB_KEY_CHAIN_STATE = 'chain'; +export const DB_KEY_CONSENSUS_STATE = 'consensus'; diff --git a/elements/lisk-chain/src/data_access/data_access.ts b/elements/lisk-chain/src/data_access/data_access.ts index 280c842face..9238a355a43 100644 --- a/elements/lisk-chain/src/data_access/data_access.ts +++ b/elements/lisk-chain/src/data_access/data_access.ts @@ -13,22 +13,22 @@ */ import { BaseTransaction, TransactionJSON } from '@liskhq/lisk-transactions'; +import { KVStore } from '@liskhq/lisk-db'; import { Account } from '../account'; import { BlockHeader, BlockHeaderJSON, BlockInstance, BlockJSON, - Storage as DBStorage, - TempBlock, } from '../types'; import { BlockCache } from './cache'; import { Storage as StorageAccess } from './storage'; import { TransactionInterfaceAdapter } from './transaction_interface_adapter'; +import { StateStore } from '../state_store'; interface DAConstructor { - readonly dbStorage: DBStorage; + readonly db: KVStore; readonly registeredTransactions: { readonly [key: number]: typeof BaseTransaction; }; @@ -42,12 +42,12 @@ export class DataAccess { private readonly _transactionAdapter: TransactionInterfaceAdapter; public constructor({ - dbStorage, + db, registeredTransactions, minBlockHeaderCache, maxBlockHeaderCache, }: DAConstructor) { - this._storage = new StorageAccess(dbStorage); + this._storage = new StorageAccess(db); this._blocksCache = new BlockCache( minBlockHeaderCache, maxBlockHeaderCache, @@ -121,18 +121,15 @@ export class DataAccess { return blocks.map(block => this.deserializeBlockHeader(block)); } - public async getBlockHeaderByHeight( - height: number, - ): Promise { + public async getBlockHeaderByHeight(height: number): Promise { const cachedBlock = this._blocksCache.getByHeight(height); if (cachedBlock) { return cachedBlock; } - const block = await this._storage.getBlockByHeight(height); + const header = await this._storage.getBlockHeaderByHeight(height); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - return block ? this.deserializeBlockHeader(block) : undefined; + return this.deserializeBlockHeader(header); } public async getBlockHeadersByHeightBetween( @@ -210,11 +207,10 @@ export class DataAccess { return blocksCount; } - public async getBlockByID(id: string): Promise { + public async getBlockByID(id: string): Promise { const blockJSON = await this._storage.getBlockByID(id); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - return blockJSON ? this.deserialize(blockJSON) : undefined; + return this.deserialize(blockJSON); } public async getBlocksByIDs( @@ -265,11 +261,10 @@ export class DataAccess { return sortedBlocks; } - public async getLastBlock(): Promise { + public async getLastBlock(): Promise { const block = await this._storage.getLastBlock(); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - return block && this.deserialize(block); + return this.deserialize(block); } public async deleteBlocksWithHeightGreaterThan( @@ -284,7 +279,7 @@ export class DataAccess { return isPersisted; } - public async getTempBlocks(): Promise { + public async getTempBlocks(): Promise { const blocks = await this._storage.getTempBlocks(); return blocks; @@ -344,8 +339,8 @@ export class DataAccess { return accounts.map(account => new Account(account)); } - public async resetAccountMemTables(): Promise { - await this._storage.resetAccountMemTables(); + public async resetMemTables(): Promise { + await this._storage.resetMemTables(); } /** End: Accounts */ @@ -432,4 +427,30 @@ export class DataAccess { ): BaseTransaction { return this._transactionAdapter.fromJSON(transactionJSON); } + + /* + Save Block + */ + public async saveBlock( + block: BlockInstance, + stateStore: StateStore, + removeFromTemp = false, + ): Promise { + const blockJSON = this.serialize(block); + + return this._storage.saveBlock(blockJSON, stateStore, removeFromTemp); + } + + /* + Delete Block + */ + public async deleteBlock( + block: BlockInstance, + stateStore: StateStore, + saveToTemp = false, + ): Promise { + const blockJSON = this.serialize(block); + + return this._storage.deleteBlock(blockJSON, stateStore, saveToTemp); + } } diff --git a/elements/lisk-chain/src/data_access/storage.ts b/elements/lisk-chain/src/data_access/storage.ts index cca5808e8c0..6bbd3af1776 100644 --- a/elements/lisk-chain/src/data_access/storage.ts +++ b/elements/lisk-chain/src/data_access/storage.ts @@ -12,107 +12,153 @@ * Removal or modification of this copyright notice is prohibited. */ +import { + KVStore, + formatInt, + getFirstPrefix, + getLastPrefix, + NotFoundError, +} from '@liskhq/lisk-db'; import { TransactionJSON } from '@liskhq/lisk-transactions'; +import { getAddressFromPublicKey } from '@liskhq/lisk-cryptography'; + +import { AccountJSON, BlockJSON, BlockHeaderJSON } from '../types'; import { - AccountJSON, - BlockJSON, - Storage as DBStorage, - TempBlock, -} from '../types'; + DB_KEY_BLOCKS_ID, + DB_KEY_BLOCKS_HEIGHT, + DB_KEY_TRANSACTIONS_BLOCK_ID, + DB_KEY_TRANSACTIONS_ID, + DB_KEY_TEMPBLOCKS_HEIGHT, + DB_KEY_ACCOUNTS_ADDRESS, + DB_KEY_CHAIN_STATE, + DB_KEY_CONSENSUS_STATE, +} from './constants'; +import { StateStore } from '../state_store'; export class Storage { - private readonly _storage: DBStorage; + private readonly _db: KVStore; - public constructor(storage: DBStorage) { - this._storage = storage; + public constructor(db: KVStore) { + this._db = db; } /* Block headers */ - - public async getBlockHeaderByID(id: string): Promise { - const [block] = await this._storage.entities.Block.get({ id }); - + public async getBlockHeaderByID(id: string): Promise { + const block = await this._db.get( + `${DB_KEY_BLOCKS_ID}:${id}`, + ); return block; } public async getBlockHeadersByIDs( arrayOfBlockIds: ReadonlyArray, - ): Promise { - const blocks = await this._storage.entities.Block.get( - // eslint-disable-next-line camelcase - { id_in: arrayOfBlockIds }, - { limit: arrayOfBlockIds.length }, - ); - + ): Promise { + const blocks = []; + for (const id of arrayOfBlockIds) { + const block = await this._db.get( + `${DB_KEY_BLOCKS_ID}:${id}`, + ); + blocks.push(block); + } return blocks; } - public async getBlockHeaderByHeight(height: number): Promise { - const [block] = await this._storage.entities.Block.get({ height }); - - return block; + public async getBlockHeaderByHeight( + height: number, + ): Promise { + const stringHeight = formatInt(height); + const id = await this._db.get( + `${DB_KEY_BLOCKS_HEIGHT}:${stringHeight}`, + ); + return this.getBlockHeaderByID(id); } public async getBlockHeadersByHeightBetween( fromHeight: number, toHeight: number, - ): Promise { - const blocks = await this._storage.entities.Block.get( - // eslint-disable-next-line camelcase - { height_gte: fromHeight, height_lte: toHeight }, - { limit: null, sort: 'height:desc' }, - ); + ): Promise { + const stream = this._db.createReadStream({ + gte: `${DB_KEY_BLOCKS_HEIGHT}:${formatInt(fromHeight)}`, + lte: `${DB_KEY_BLOCKS_HEIGHT}:${formatInt(toHeight)}`, + reverse: true, + }); + const blockIDs = await new Promise((resolve, reject) => { + const ids: string[] = []; + stream + .on('data', ({ value }) => { + ids.push(value); + }) + .on('error', error => { + reject(error); + }) + .on('end', () => { + resolve(ids); + }); + }); - return blocks; + return this.getBlockHeadersByIDs(blockIDs); } public async getBlockHeadersWithHeights( heightList: ReadonlyArray, - ): Promise { - const blocks = await this._storage.entities.Block.get( - { - // eslint-disable-next-line camelcase - height_in: heightList, - }, - { - sort: 'height:asc', - limit: heightList.length, - }, - ); - + ): Promise { + const blocks = []; + for (const height of heightList) { + const block = await this.getBlockHeaderByHeight(height); + blocks.push(block); + } return blocks; } - public async getLastBlockHeader(): Promise { - const [lastBlockHeader] = await this._storage.entities.Block.get( - {}, - { limit: 1, sort: 'height:desc' }, - ); + public async getLastBlockHeader(): Promise { + const stream = this._db.createReadStream({ + gte: getFirstPrefix(DB_KEY_BLOCKS_HEIGHT), + lte: getLastPrefix(DB_KEY_BLOCKS_HEIGHT), + reverse: true, + limit: 1, + }); + const [blockID] = await new Promise((resolve, reject) => { + const ids: string[] = []; + stream + .on('data', ({ value }) => { + ids.push(value); + }) + .on('error', error => { + reject(error); + }) + .on('end', () => { + resolve(ids); + }); + }); - return lastBlockHeader; + return this.getBlockHeaderByID(blockID); } public async getLastCommonBlockHeader( arrayOfBlockIds: ReadonlyArray, - ): Promise { - const [block] = await this._storage.entities.Block.get( - { - // eslint-disable-next-line camelcase - id_in: arrayOfBlockIds, - }, - { sort: 'height:desc', limit: 1 }, - ); - - return block; + ): Promise { + const blocks = []; + for (const id of arrayOfBlockIds) { + try { + const block = await this.getBlockHeaderByID(id); + blocks.push(block); + } catch (error) { + if (!(error instanceof NotFoundError)) { + throw error; + } + } + } + blocks.sort((a, b) => b.height - a.height); + + return blocks[0]; } public async getBlocksCount(): Promise { - const count = await this._storage.entities.Block.count({}, {}); - - return count; + const lastBlock = await this.getLastBlockHeader(); + return lastBlock.height; } /* @@ -120,183 +166,383 @@ export class Storage { */ public async getBlockByID(id: string): Promise { - const [block] = await this._storage.entities.Block.get( - { id }, - { extended: true }, - ); + const blockHeader = await this.getBlockHeaderByID(id); + const transactions = await this._getTransactions(id); - return block; + return { + ...blockHeader, + transactions, + }; } public async getBlocksByIDs( arrayOfBlockIds: ReadonlyArray, ): Promise { - const blocks = await this._storage.entities.Block.get( - // eslint-disable-next-line camelcase - { id_in: arrayOfBlockIds }, - { extended: true }, - ); + const blocks = []; + for (const id of arrayOfBlockIds) { + const block = await this.getBlockByID(id); + blocks.push(block); + } return blocks; } public async getBlockByHeight(height: number): Promise { - const [block] = await this._storage.entities.Block.get( - { height }, - { extended: true }, - ); + const header = await this.getBlockHeaderByHeight(height); + const transactions = await this._getTransactions(header.id); - return block; + return { + ...header, + transactions, + }; } public async getBlocksByHeightBetween( fromHeight: number, toHeight: number, ): Promise { - const blocks = await this._storage.entities.Block.get( - // eslint-disable-next-line camelcase - { height_gte: fromHeight, height_lte: toHeight }, - { extended: true, limit: null, sort: 'height:desc' }, + const headers = await this.getBlockHeadersByHeightBetween( + fromHeight, + toHeight, ); + const blocks = []; + for (const header of headers) { + const transactions = await this._getTransactions(header.id); + blocks.push({ ...header, transactions }); + } return blocks; } public async getLastBlock(): Promise { - const [lastBlock] = await this._storage.entities.Block.get( - {}, - { sort: 'height:desc', limit: 1, extended: true }, - ); + const header = await this.getLastBlockHeader(); + const transactions = await this._getTransactions(header.id); - return lastBlock; + return { + ...header, + transactions, + }; } - public async getTempBlocks(): Promise { - const tempBlocks = await this._storage.entities.TempBlock.get( - {}, - { sort: 'height:asc', limit: null }, - ); + public async getTempBlocks(): Promise { + const stream = this._db.createReadStream({ + gte: getFirstPrefix(DB_KEY_TEMPBLOCKS_HEIGHT), + lte: getLastPrefix(DB_KEY_TEMPBLOCKS_HEIGHT), + reverse: true, + }); + const tempBlocks = await new Promise((resolve, reject) => { + const blocks: BlockJSON[] = []; + stream + .on('data', ({ value }) => { + blocks.push(value); + }) + .on('error', error => { + reject(error); + }) + .on('end', () => { + resolve(blocks); + }); + }); return tempBlocks; } public async isTempBlockEmpty(): Promise { - const isEmpty = await this._storage.entities.TempBlock.isEmpty(); + const stream = this._db.createReadStream({ + gte: getFirstPrefix(DB_KEY_TEMPBLOCKS_HEIGHT), + lte: getLastPrefix(DB_KEY_TEMPBLOCKS_HEIGHT), + limit: 1, + }); + const tempBlocks = await new Promise((resolve, reject) => { + const blocks: BlockJSON[] = []; + stream + .on('data', ({ value }) => { + blocks.push(value); + }) + .on('error', error => { + reject(error); + }) + .on('end', () => { + resolve(blocks); + }); + }); - return isEmpty; + return tempBlocks.length === 0; } public async clearTempBlocks(): Promise { - await this._storage.entities.TempBlock.truncate(); + await this._db.clear({ + gte: getFirstPrefix(DB_KEY_TEMPBLOCKS_HEIGHT), + lte: getLastPrefix(DB_KEY_TEMPBLOCKS_HEIGHT), + }); } public async deleteBlocksWithHeightGreaterThan( height: number, ): Promise { - await this._storage.entities.Block.delete({ - // eslint-disable-next-line camelcase - height_gt: height, - }); + const lastBlockHeader = await this.getLastBlockHeader(); + const batchSize = 5000; + const loops = Math.ceil((lastBlockHeader.height - height + 1) / batchSize); + const start = height + 1; + // tslint:disable-next-line no-let + for (let i = 0; i < loops; i += 1) { + // Get all the required info + const startHeight = i * batchSize + start + i; + const endHeight = startHeight + batchSize - 1; + const headers = await this.getBlockHeadersByHeightBetween( + startHeight, + endHeight, + ); + const blockIDs = headers.map(header => header.id); + const transactionIDs = []; + const batch = this._db.batch(); + for (const blockID of blockIDs) { + try { + const ids = await this._db.get( + `${DB_KEY_TRANSACTIONS_BLOCK_ID}:${blockID}`, + ); + transactionIDs.push(...ids); + } catch (error) { + if (!(error instanceof NotFoundError)) { + throw error; + } + } + batch.del(`${DB_KEY_BLOCKS_ID}:${blockID}`); + batch.del(`${DB_KEY_TRANSACTIONS_BLOCK_ID}:${blockID}`); + } + // tslint:disable-next-line no-let + for (let j = startHeight; j <= endHeight; j += 1) { + batch.del(`${DB_KEY_BLOCKS_HEIGHT}:${formatInt(j)}`); + } + for (const txID of transactionIDs) { + batch.del(`${DB_KEY_TRANSACTIONS_ID}:${txID}`); + } + await batch.write(); + } } - public async isBlockPersisted(blockId: string): Promise { - const isPersisted = await this._storage.entities.Block.isPersisted({ - blockId, - }); - - return isPersisted; + public async isBlockPersisted(blockID: string): Promise { + return this._db.exists(`${DB_KEY_BLOCKS_ID}:${blockID}`); } /* ChainState */ public async getChainState(key: string): Promise { - const value = await this._storage.entities.ChainState.getKey(key); - - return value; + try { + const value = await this._db.get(`${DB_KEY_CHAIN_STATE}:${key}`); + + return value; + } catch (error) { + if (error instanceof NotFoundError) { + return undefined; + } + throw error; + } } /* ConsensusState */ public async getConsensusState(key: string): Promise { - const value = await this._storage.entities.ConsensusState.getKey(key); - - return value; + try { + const value = await this._db.get( + `${DB_KEY_CONSENSUS_STATE}:${key}`, + ); + + return value; + } catch (error) { + if (error instanceof NotFoundError) { + return undefined; + } + throw error; + } } /* Accounts */ - public async getAccountsByPublicKey( - arrayOfPublicKeys: ReadonlyArray, - ): Promise { - const accounts = await this._storage.entities.Account.get( - // eslint-disable-next-line camelcase - { publicKey_in: arrayOfPublicKeys }, - { limit: arrayOfPublicKeys.length }, + public async getAccountByAddress(address: string): Promise { + const account = await this._db.get( + `${DB_KEY_ACCOUNTS_ADDRESS}:${address}`, ); - return accounts; + return account; } - public async getAccountByAddress(address: string): Promise { - const account = await this._storage.entities.Account.getOne( - { address }, - { limit: 1 }, - ); + public async getAccountsByPublicKey( + arrayOfPublicKeys: ReadonlyArray, + ): Promise { + const addresses = arrayOfPublicKeys.map(getAddressFromPublicKey); - return account; + return this.getAccountsByAddress(addresses); } public async getAccountsByAddress( arrayOfAddresses: ReadonlyArray, ): Promise { - const accounts = await this._storage.entities.Account.get( - // eslint-disable-next-line camelcase - { address_in: arrayOfAddresses }, - { limit: arrayOfAddresses.length }, - ); + const accounts = []; + for (const address of arrayOfAddresses) { + const account = await this.getAccountByAddress(address); + accounts.push(account); + } return accounts; } + // TODO: Remove this with issue #5259 public async getDelegates(): Promise { - const accounts = await this._storage.entities.Account.get( - { isDelegate: true }, - // Sort address:asc is always added in the storage - { limit: null, sort: ['totalVotesReceived:desc'] }, - ); + const stream = this._db.createReadStream({ + gte: getFirstPrefix(DB_KEY_ACCOUNTS_ADDRESS), + lte: getLastPrefix(DB_KEY_ACCOUNTS_ADDRESS), + }); + const accounts = await new Promise((resolve, reject) => { + const accountJSONs: AccountJSON[] = []; + stream + .on('data', ({ value }) => { + const { username } = value as AccountJSON; + if (username) { + accountJSONs.push(value); + } + }) + .on('error', error => { + reject(error); + }) + .on('end', () => { + resolve(accountJSONs); + }); + }); + accounts.sort((a, b) => { + const diff = BigInt(b.totalVotesReceived) - BigInt(a.totalVotesReceived); + if (diff > BigInt(0)) { + return 1; + } + if (diff < BigInt(0)) { + return -1; + } + return a.address.localeCompare(b.address); + }); return accounts; } - public async resetAccountMemTables(): Promise { - await this._storage.entities.Account.resetMemTables(); + public async resetMemTables(): Promise { + await this._db.clear({ + gte: getFirstPrefix(DB_KEY_ACCOUNTS_ADDRESS), + lte: getLastPrefix(DB_KEY_ACCOUNTS_ADDRESS), + }); + await this._db.clear({ + gte: getFirstPrefix(DB_KEY_CHAIN_STATE), + lte: getLastPrefix(DB_KEY_CHAIN_STATE), + }); + await this._db.clear({ + gte: getFirstPrefix(DB_KEY_CONSENSUS_STATE), + lte: getLastPrefix(DB_KEY_CONSENSUS_STATE), + }); } /* Transactions */ + public async getTransactionByID(id: string): Promise { + const transaction = this._db.get( + `${DB_KEY_TRANSACTIONS_ID}:${id}`, + ); + + return transaction; + } + public async getTransactionsByIDs( arrayOfTransactionIds: ReadonlyArray, ): Promise { - const transactions = await this._storage.entities.Transaction.get( - { - // eslint-disable-next-line camelcase - id_in: arrayOfTransactionIds, - }, - { limit: arrayOfTransactionIds.length }, - ); + const transactions = []; + for (const id of arrayOfTransactionIds) { + const transaction = await this.getTransactionByID(id); + transactions.push(transaction); + } return transactions; } public async isTransactionPersisted(transactionId: string): Promise { - const isPersisted = await this._storage.entities.Transaction.isPersisted({ - id: transactionId, - }); + return this._db.exists(`${DB_KEY_TRANSACTIONS_ID}:${transactionId}`); + } - return isPersisted; + /* + Save Block + */ + public async saveBlock( + blockJSON: BlockJSON, + stateStore: StateStore, + removeFromTemp = false, + ): Promise { + const batch = this._db.batch(); + const { transactions, ...header } = blockJSON; + batch.put(`${DB_KEY_BLOCKS_ID}:${header.id}`, header); + batch.put(`${DB_KEY_BLOCKS_HEIGHT}:${formatInt(header.height)}`, header.id); + if (transactions.length > 0) { + const ids = []; + for (const tx of transactions) { + ids.push(tx.id); + batch.put(`${DB_KEY_TRANSACTIONS_ID}:${tx.id as string}`, tx); + } + batch.put(`${DB_KEY_TRANSACTIONS_BLOCK_ID}:${header.id}`, ids); + } + if (removeFromTemp) { + batch.del(`${DB_KEY_TEMPBLOCKS_HEIGHT}:${formatInt(blockJSON.height)}`); + } + stateStore.finalize(batch); + await batch.write(); + } + + public async deleteBlock( + blockJSON: BlockJSON, + stateStore: StateStore, + saveToTemp = false, + ): Promise { + const batch = this._db.batch(); + const { transactions, ...header } = blockJSON; + batch.del(`${DB_KEY_BLOCKS_ID}:${header.id}`); + batch.del(`${DB_KEY_BLOCKS_HEIGHT}:${formatInt(header.height)}`); + if (transactions.length > 0) { + for (const tx of transactions) { + batch.del(`${DB_KEY_TRANSACTIONS_ID}:${tx.id as string}`); + } + batch.del(`${DB_KEY_TRANSACTIONS_BLOCK_ID}:${header.id}`); + } + if (saveToTemp) { + batch.put( + `${DB_KEY_TEMPBLOCKS_HEIGHT}:${formatInt(blockJSON.height)}`, + blockJSON, + ); + } + stateStore.finalize(batch); + await batch.write(); + } + + private async _getTransactions(blockID: string): Promise { + const txIDs = []; + try { + const ids = await this._db.get( + `${DB_KEY_TRANSACTIONS_BLOCK_ID}:${blockID}`, + ); + txIDs.push(...ids); + } catch (error) { + if (!(error instanceof NotFoundError)) { + throw error; + } + } + if (txIDs.length === 0) { + return []; + } + const transactions = []; + for (const txID of txIDs) { + const tx = await this._db.get( + `${DB_KEY_TRANSACTIONS_ID}:${txID}`, + ); + transactions.push(tx); + } + + return transactions; } } diff --git a/elements/lisk-chain/src/state_store/account_store.ts b/elements/lisk-chain/src/state_store/account_store.ts index a39ebfff6bb..c832117147e 100644 --- a/elements/lisk-chain/src/state_store/account_store.ts +++ b/elements/lisk-chain/src/state_store/account_store.ts @@ -11,52 +11,30 @@ * * Removal or modification of this copyright notice is prohibited. */ +import { NotFoundError, BatchChain } from '@liskhq/lisk-db'; import { Account } from '../account'; -import { - AccountJSON, - IndexableAccount, - StorageEntity, - StorageFilters, - StorageTransaction, -} from '../types'; -import { uniqBy } from '../utils'; - +import { DataAccess } from '../data_access'; +import { DB_KEY_ACCOUNTS_ADDRESS } from '../data_access/constants'; // eslint-disable-next-line @typescript-eslint/no-require-imports import cloneDeep = require('lodash.clonedeep'); -// eslint-disable-next-line @typescript-eslint/no-require-imports -import isEqual = require('lodash.isequal'); export class AccountStore { - private readonly _account: StorageEntity; private _data: Account[]; private _originalData: Account[]; - private _updatedKeys: { [key: number]: string[] } = {}; - private _originalUpdatedKeys: { [key: number]: string[] } = {}; + private _updatedKeys: Set; + private _originalUpdatedKeys: Set; + private readonly _dataAccess: DataAccess; private readonly _primaryKey = 'address'; private readonly _name = 'Account'; - public constructor(accountEntity: StorageEntity) { - this._account = accountEntity; + public constructor(dataAccess: DataAccess) { + this._dataAccess = dataAccess; this._data = []; - this._updatedKeys = {}; + this._updatedKeys = new Set(); this._primaryKey = 'address'; this._name = 'Account'; this._originalData = []; - this._originalUpdatedKeys = {}; - } - - public async cache(filter: StorageFilters): Promise> { - const result = await this._account.get(filter, { limit: null }); - const resultAccountObjects = result.map( - accountJSON => new Account(accountJSON), - ); - - this._data = uniqBy( - [...this._data, ...resultAccountObjects] as IndexableAccount[], - this._primaryKey, - ); - - return resultAccountObjects; + this._originalUpdatedKeys = new Set(); } public createSnapshot(): void { @@ -68,7 +46,7 @@ export class AccountStore { this._data = this._originalData; this._updatedKeys = this._originalUpdatedKeys; this._originalData = []; - this._originalUpdatedKeys = {}; + this._originalUpdatedKeys = new Set(); } public async get(primaryValue: string): Promise { @@ -82,16 +60,15 @@ export class AccountStore { } // Account was not cached previously so we try to fetch it from db - const [elementFromDB] = await this._account.get( - { [this._primaryKey]: primaryValue }, - { limit: null }, + const elementFromDB = await this._dataAccess.getAccountByAddress( + primaryValue, ); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (elementFromDB) { - this._data.push(new Account(elementFromDB)); + this._data.push(elementFromDB); - return new Account(elementFromDB); + return new Account(elementFromDB.toJSON()); } // Account does not exist we can not continue @@ -110,22 +87,21 @@ export class AccountStore { } // Account was not cached previously so we try to fetch it from db (example delegate account is voted) - const [elementFromDB] = await this._account.get( - { [this._primaryKey]: primaryValue }, - { limit: null }, - ); - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (elementFromDB) { - this._data.push(new Account(elementFromDB)); + try { + const elementFromDB = await this._dataAccess.getAccountByAddress( + primaryValue, + ); + this._data.push(elementFromDB); - return new Account(elementFromDB); + return new Account(elementFromDB.toJSON()); + } catch (error) { + if (!(error instanceof NotFoundError)) { + throw error; + } } const defaultElement: Account = Account.getDefaultAccount(primaryValue); - - const newElementIndex = this._data.push(defaultElement) - 1; - this._updatedKeys[newElementIndex] = Object.keys(defaultElement); + this._data.push(defaultElement); return new Account(defaultElement.toJSON()); } @@ -156,51 +132,18 @@ export class AccountStore { ); } - const updatedKeys = Object.entries(updatedElement).reduce( - (existingUpdatedKeys, [key, value]) => { - const account = this._data[elementIndex]; - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-explicit-any - if (!isEqual(value, (account as any)[key])) { - existingUpdatedKeys.push(key); - } - - return existingUpdatedKeys; - }, - [], - ); - this._data[elementIndex] = updatedElement; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - this._updatedKeys[elementIndex] = this._updatedKeys[elementIndex] - ? [...new Set([...this._updatedKeys[elementIndex], ...updatedKeys])] - : updatedKeys; + this._updatedKeys.add(primaryValue); } - public async finalize(tx: StorageTransaction): Promise { - const affectedAccounts = Object.entries(this._updatedKeys).map( - ([index, updatedKeys]) => ({ - updatedItem: this._data[parseInt(index, 10)].toJSON(), - updatedKeys, - }), - ); - - const updateToAccounts = affectedAccounts.map( - async ({ updatedItem, updatedKeys }) => { - const filter = { [this._primaryKey]: updatedItem[this._primaryKey] }; - const updatedData = updatedKeys.reduce>( - (data, key) => { - // eslint-disable-next-line - (data as any)[key] = (updatedItem as any)[key]; - - return data; - }, - {}, + public finalize(batch: BatchChain): void { + for (const account of this._data) { + if (this._updatedKeys.has(account.address)) { + batch.put( + `${DB_KEY_ACCOUNTS_ADDRESS}:${account.address}`, + account.toJSON(), ); - - return this._account.upsert(filter, updatedData, null, tx); - }, - ); - - await Promise.all(updateToAccounts); + } + } } } diff --git a/elements/lisk-chain/src/state_store/chain_state_store.ts b/elements/lisk-chain/src/state_store/chain_state_store.ts index fff80cb7913..191257e52cc 100644 --- a/elements/lisk-chain/src/state_store/chain_state_store.ts +++ b/elements/lisk-chain/src/state_store/chain_state_store.ts @@ -12,7 +12,10 @@ * Removal or modification of this copyright notice is prohibited. */ -import { BlockHeader, ChainStateEntity, StorageTransaction } from '../types'; +import { BatchChain } from '@liskhq/lisk-db'; +import { DataAccess } from '../data_access'; +import { BlockHeader } from '../types'; +import { DB_KEY_CHAIN_STATE } from '../data_access/constants'; interface AdditionalInformation { readonly lastBlockHeader: BlockHeader; @@ -30,16 +33,16 @@ export class ChainStateStore { private _originalData: KeyValuePair; private _updatedKeys: Set; private _originalUpdatedKeys: Set; - private readonly _chainState: ChainStateEntity; + private readonly _dataAccess: DataAccess; private readonly _lastBlockHeader: BlockHeader; private readonly _networkIdentifier: string; private readonly _lastBlockReward: bigint; public constructor( - chainStateEntity: ChainStateEntity, + dataAccess: DataAccess, additionalInformation: AdditionalInformation, ) { - this._chainState = chainStateEntity; + this._dataAccess = dataAccess; this._lastBlockHeader = additionalInformation.lastBlockHeader; this._networkIdentifier = additionalInformation.networkIdentifier; this._lastBlockReward = additionalInformation.lastBlockReward; @@ -49,13 +52,6 @@ export class ChainStateStore { this._originalUpdatedKeys = new Set(); } - public async cache(): Promise { - const results = await this._chainState.get(); - for (const { key, value } of results) { - this._data[key] = value; - } - } - public get networkIdentifier(): string { return this._networkIdentifier; } @@ -85,7 +81,7 @@ export class ChainStateStore { return value; } - const dbValue = await this._chainState.getKey(key); + const dbValue = await this._dataAccess.getChainState(key); // If it doesn't exist in the database, return undefined without caching if (dbValue === undefined) { return dbValue; @@ -108,15 +104,13 @@ export class ChainStateStore { this._updatedKeys.add(key); } - public async finalize(tx: StorageTransaction): Promise { + public finalize(batch: BatchChain): void { if (this._updatedKeys.size === 0) { return; } - await Promise.all( - Array.from(this._updatedKeys).map(async key => - this._chainState.setKey(key, this._data[key], tx), - ), - ); + for (const key of Array.from(this._updatedKeys)) { + batch.put(`${DB_KEY_CHAIN_STATE}:${key}`, this._data[key]); + } } } diff --git a/elements/lisk-chain/src/state_store/consensus_state_store.ts b/elements/lisk-chain/src/state_store/consensus_state_store.ts index 5fa535e9384..bf530ed8761 100644 --- a/elements/lisk-chain/src/state_store/consensus_state_store.ts +++ b/elements/lisk-chain/src/state_store/consensus_state_store.ts @@ -12,11 +12,10 @@ * Removal or modification of this copyright notice is prohibited. */ -import { - BlockHeader, - ConsensusStateEntity, - StorageTransaction, -} from '../types'; +import { BatchChain } from '@liskhq/lisk-db'; +import { BlockHeader } from '../types'; +import { DB_KEY_CONSENSUS_STATE } from '../data_access/constants'; +import { DataAccess } from '../data_access'; interface KeyValuePair { [key: string]: string; @@ -32,14 +31,14 @@ export class ConsensusStateStore { private _originalData: KeyValuePair; private _updatedKeys: Set; private _originalUpdatedKeys: Set; + private readonly _dataAccess: DataAccess; private readonly _lastBlockHeaders: ReadonlyArray; - private readonly _consensusState: ConsensusStateEntity; public constructor( - consensusStateEntity: ConsensusStateEntity, + dataAccess: DataAccess, additionalInformation: AdditionalInformation, ) { - this._consensusState = consensusStateEntity; + this._dataAccess = dataAccess; this._lastBlockHeaders = additionalInformation.lastBlockHeaders; this._data = {}; this._originalData = {}; @@ -51,13 +50,6 @@ export class ConsensusStateStore { return this._lastBlockHeaders; } - public async cache(): Promise { - const results = await this._consensusState.get(); - for (const { key, value } of results) { - this._data[key] = value; - } - } - public createSnapshot(): void { this._originalData = { ...this._data }; this._originalUpdatedKeys = new Set(this._updatedKeys); @@ -75,7 +67,7 @@ export class ConsensusStateStore { return value; } - const dbValue = await this._consensusState.getKey(key); + const dbValue = await this._dataAccess.getConsensusState(key); // If it doesn't exist in the database, return undefined without caching if (dbValue === undefined) { return dbValue; @@ -98,15 +90,13 @@ export class ConsensusStateStore { this._updatedKeys.add(key); } - public async finalize(tx: StorageTransaction): Promise { + public finalize(batch: BatchChain): void { if (this._updatedKeys.size === 0) { return; } - await Promise.all( - Array.from(this._updatedKeys).map(async key => - this._consensusState.setKey(key, this._data[key], tx), - ), - ); + for (const key of Array.from(this._updatedKeys)) { + batch.put(`${DB_KEY_CONSENSUS_STATE}:${key}`, this._data[key]); + } } } diff --git a/elements/lisk-chain/src/state_store/state_store.ts b/elements/lisk-chain/src/state_store/state_store.ts index 7e747d192cc..2d46894195e 100644 --- a/elements/lisk-chain/src/state_store/state_store.ts +++ b/elements/lisk-chain/src/state_store/state_store.ts @@ -12,11 +12,12 @@ * Removal or modification of this copyright notice is prohibited. */ -import { BlockHeader, Storage, StorageTransaction } from '../types'; - +import { BatchChain } from '@liskhq/lisk-db'; +import { BlockHeader } from '../types'; import { AccountStore } from './account_store'; import { ChainStateStore } from './chain_state_store'; import { ConsensusStateStore } from './consensus_state_store'; +import { DataAccess } from '../data_access'; interface AdditionalInformation { readonly lastBlockHeaders: ReadonlyArray; @@ -30,14 +31,14 @@ export class StateStore { public readonly consensus: ConsensusStateStore; public constructor( - storage: Storage, + dataAccess: DataAccess, additionalInformation: AdditionalInformation, ) { - this.account = new AccountStore(storage.entities.Account); - this.consensus = new ConsensusStateStore(storage.entities.ConsensusState, { + this.account = new AccountStore(dataAccess); + this.consensus = new ConsensusStateStore(dataAccess, { lastBlockHeaders: additionalInformation.lastBlockHeaders, }); - this.chain = new ChainStateStore(storage.entities.ChainState, { + this.chain = new ChainStateStore(dataAccess, { lastBlockHeader: additionalInformation.lastBlockHeaders[0], networkIdentifier: additionalInformation.networkIdentifier, lastBlockReward: additionalInformation.lastBlockReward, @@ -56,11 +57,9 @@ export class StateStore { this.chain.restoreSnapshot(); } - public async finalize(tx: StorageTransaction): Promise { - await Promise.all([ - this.account.finalize(tx), - this.chain.finalize(tx), - this.consensus.finalize(tx), - ]); + public finalize(batch: BatchChain): void { + this.account.finalize(batch); + this.chain.finalize(batch); + this.consensus.finalize(batch); } } diff --git a/elements/lisk-chain/src/transactions/transactions_handlers.ts b/elements/lisk-chain/src/transactions/transactions_handlers.ts index 3a2a2f2395b..b117c3506bb 100644 --- a/elements/lisk-chain/src/transactions/transactions_handlers.ts +++ b/elements/lisk-chain/src/transactions/transactions_handlers.ts @@ -36,11 +36,6 @@ export const applyGenesisTransactions = () => async ( transactions: ReadonlyArray, stateStore: StateStore, ): Promise => { - // Avoid merging both prepare statements into one for...of loop as this slows down the call dramatically - for (const transaction of transactions) { - await transaction.prepare(stateStore); - } - const transactionsResponses: TransactionResponse[] = []; for (const transaction of transactions) { const transactionResponse = await transaction.apply(stateStore); @@ -58,11 +53,6 @@ export const applyTransactions = () => async ( transactions: ReadonlyArray, stateStore: StateStore, ): Promise> => { - // Avoid merging both prepare statements into one for...of loop as this slows down the call dramatically - for (const transaction of transactions) { - await transaction.prepare(stateStore); - } - const transactionsResponses: TransactionResponse[] = []; for (const transaction of transactions) { stateStore.account.createSnapshot(); @@ -84,37 +74,23 @@ export const checkPersistedTransactions = (dataAccess: DataAccess) => async ( return []; } - const confirmedTransactions = await dataAccess.getTransactionsByIDs( - transactions.map(transaction => transaction.id), - ); - - const persistedTransactionIds = confirmedTransactions.map( - (transaction: BaseTransaction) => transaction.id, - ); - const persistedTransactions = transactions.filter(transaction => - persistedTransactionIds.includes(transaction.id), - ); - const nonPersistedTransactions = transactions.filter( - transaction => !persistedTransactionIds.includes(transaction.id), - ); - const transactionsResponses = [ - ...nonPersistedTransactions.map(transaction => ({ - id: transaction.id, - status: TransactionStatus.OK, - errors: [], - })), - ...persistedTransactions.map(transaction => ({ - id: transaction.id, - status: TransactionStatus.FAIL, - errors: [ - new TransactionError( - `Transaction is already confirmed: ${transaction.id}`, - transaction.id, - '.id', - ), - ], - })), - ]; + const transactionsResponses = []; + for (const tx of transactions) { + const exist = await dataAccess.isTransactionPersisted(tx.id); + transactionsResponses.push({ + id: tx.id, + status: !exist ? TransactionStatus.OK : TransactionStatus.FAIL, + errors: !exist + ? [] + : [ + new TransactionError( + `Transaction is already confirmed: ${tx.id}`, + tx.id, + '.id', + ), + ], + }); + } return transactionsResponses; }; @@ -147,11 +123,6 @@ export const undoTransactions = () => async ( transactions: ReadonlyArray, stateStore: StateStore, ): Promise> => { - // Avoid merging both prepare statements into one for...of loop as this slows down the call dramatically - for (const transaction of transactions) { - await transaction.prepare(stateStore); - } - const transactionsResponses = []; for (const transaction of transactions) { const transactionResponse = await transaction.undo(stateStore); diff --git a/elements/lisk-chain/src/types.ts b/elements/lisk-chain/src/types.ts index e1756bfa90e..9e011ea2c9f 100644 --- a/elements/lisk-chain/src/types.ts +++ b/elements/lisk-chain/src/types.ts @@ -17,17 +17,6 @@ import { TransactionResponse, } from '@liskhq/lisk-transactions'; -// eslint-disable-next-line import/no-cycle -import { Account } from './account'; - -export interface Indexable { - readonly [key: string]: unknown; -} - -export type IndexableAccount = Account & Indexable; - -export type IndexableTransactionJSON = TransactionJSON & Indexable; - export interface AccountVoteJSON { readonly delegateAddress: string; readonly amount: string; @@ -151,139 +140,6 @@ export interface ChainState { readonly value: string; } -export interface StorageTransaction { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - readonly batch: (input: any[]) => Promise; -} - -export interface StorageFilter { - readonly [key: string]: - | string - | number - | string[] - | ReadonlyArray - | number[] - | ReadonlyArray - | boolean - | null; -} - -export type StorageFilters = - | StorageFilter - | StorageFilter[] - | ReadonlyArray; - -export interface StorageOptions { - readonly limit?: number | null; - readonly extended?: boolean; - readonly offset?: number; - readonly sort?: string | string[]; -} - -export interface ChainStateEntity { - readonly get: ( - filters?: StorageFilters, - options?: StorageOptions, - tx?: StorageTransaction, - ) => Promise; - readonly getKey: ( - key: string, - tx?: StorageTransaction, - ) => Promise; - readonly setKey: ( - key: string, - value: string, - tx?: StorageTransaction, - ) => Promise; - readonly delete: () => Promise; -} - -export interface ConsensusStateEntity { - readonly get: ( - filters?: StorageFilters, - options?: StorageOptions, - tx?: StorageTransaction, - ) => Promise; - readonly getKey: ( - key: string, - tx?: StorageTransaction, - ) => Promise; - readonly setKey: ( - key: string, - value: string, - tx?: StorageTransaction, - ) => Promise; - readonly delete: () => Promise; -} - -export interface StorageEntity { - readonly get: ( - filters?: StorageFilters, - options?: StorageOptions, - tx?: StorageTransaction, - ) => Promise; - readonly getOne: ( - filters?: StorageFilters, - options?: StorageOptions, - tx?: StorageTransaction, - ) => Promise; - readonly isPersisted: ( - filters?: StorageFilters, - options?: StorageOptions, - tx?: StorageTransaction, - ) => Promise; - readonly count: ( - filters?: StorageFilters, - options?: StorageOptions, - tx?: StorageTransaction, - ) => Promise; - readonly upsert: ( - filters: StorageFilters, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data: any, - options: StorageOptions | null, - tx?: StorageTransaction, - ) => Promise; - readonly create: ( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data: any, - filters?: StorageFilters, - tx?: StorageTransaction, - ) => Promise; - readonly delete: ( - filters?: StorageFilters, - options?: StorageOptions | null, - tx?: StorageTransaction, - ) => Promise; -} - -export interface AccountStorageEntity extends StorageEntity { - readonly resetMemTables: () => Promise; -} - -export interface BlockStorageEntity extends StorageEntity { - readonly begin: ( - name: string, - fn: (tx: StorageTransaction) => Promise, - ) => Promise; -} - -export interface TempBlockStorageEntity extends StorageEntity { - readonly isEmpty: () => Promise; - readonly truncate: () => Promise; -} - -export interface Storage { - readonly entities: { - readonly Block: BlockStorageEntity; - readonly Account: AccountStorageEntity; - readonly Transaction: StorageEntity; - readonly ChainState: ChainStateEntity; - readonly ConsensusState: ConsensusStateEntity; - readonly TempBlock: TempBlockStorageEntity; - }; -} - export type WriteableTransactionResponse = { -readonly [P in keyof TransactionResponse]: TransactionResponse[P]; }; diff --git a/elements/lisk-chain/src/verify.ts b/elements/lisk-chain/src/verify.ts index fee715b2348..1dc6f7772b0 100644 --- a/elements/lisk-chain/src/verify.ts +++ b/elements/lisk-chain/src/verify.ts @@ -25,16 +25,13 @@ import { Context, GenesisBlock, MatcherTransaction, - Storage, } from './types'; export const verifyBlockNotExists = async ( - storage: Storage, + dataAccess: DataAccess, block: BlockInstance, ): Promise => { - const isPersisted = await storage.entities.Block.isPersisted({ - id: block.id, - }); + const isPersisted = await dataAccess.isBlockPersisted(block.id); if (isPersisted) { throw new Error(`Block ${block.id} already exists`); } diff --git a/elements/lisk-chain/test/integration/data_access/accounts.spec.ts b/elements/lisk-chain/test/integration/data_access/accounts.spec.ts new file mode 100644 index 00000000000..248536008eb --- /dev/null +++ b/elements/lisk-chain/test/integration/data_access/accounts.spec.ts @@ -0,0 +1,136 @@ +/* + * Copyright © 2020 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. + */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +import * as path from 'path'; +import * as fs from 'fs'; +import { KVStore, NotFoundError } from '@liskhq/lisk-db'; +import { Storage } from '../../../src/data_access/storage'; + +describe('dataAccess.transactions', () => { + let db: KVStore; + let storage: Storage; + let accounts: any; + + beforeAll(async () => { + const parentPath = path.join(__dirname, '../../tmp'); + if (!fs.existsSync(parentPath)) { + await fs.promises.mkdir(parentPath); + } + db = new KVStore(path.join(parentPath, '/test-accounts.db')); + storage = new Storage(db); + }); + + beforeEach(async () => { + accounts = [ + { + address: '7546125166665832140L', + publicKey: + '456efe283f25ea5bb21476b6dfb77cec4dbd33a4d1b5e60e4dc28e8e8b10fc4e', + balance: '99', + keys: { + mandatoryKeys: [], + optionalKeys: [], + numberOfSignatures: 0, + }, + }, + { + address: '10676488814586252632L', + publicKey: + 'd468707933e4f24888dc1f00c8f84b2642c0edf3d694e2bb5daa7a0d87d18708', + balance: '10000', + keys: { + mandatoryKeys: [ + '456efe283f25ea5bb21476b6dfb77cec4dbd33a4d1b5e60e4dc28e8e8b10fc4e', + ], + optionalKeys: [], + numberOfSignatures: 3, + }, + }, + ]; + const batch = db.batch(); + for (const account of accounts) { + batch.put(`accounts:address:${account.address}`, account); + } + await batch.write(); + }); + + afterEach(async () => { + await db.clear(); + }); + + describe('getAccountByAddress', () => { + it('should throw not found error if non existent address is specified', async () => { + expect.assertions(1); + try { + await storage.getAccountByAddress('8973039982577606154L'); + } catch (error) { + // eslint-disable-next-line jest/no-try-expect + expect(error).toBeInstanceOf(NotFoundError); + } + }); + + it('should return account by address', async () => { + const account = await storage.getAccountByAddress(accounts[1].address); + expect(account).toStrictEqual(accounts[1]); + }); + }); + + describe('getAccountsByPublicKey', () => { + it('should throw not found error if non existent public key is specified', async () => { + expect.assertions(1); + try { + await storage.getAccountsByPublicKey([ + 'e3ee6527848d873db7b8e7577384a3ee5f100b988b2f6c027a2851f5427e9426', + accounts[0].publicKey, + ]); + } catch (error) { + // eslint-disable-next-line jest/no-try-expect + expect(error).toBeInstanceOf(NotFoundError); + } + }); + + it('should return account by public keys', async () => { + const result = await storage.getAccountsByPublicKey([ + accounts[1].publicKey, + accounts[0].publicKey, + ]); + expect(result[0]).toStrictEqual(accounts[1]); + expect(result[1]).toStrictEqual(accounts[0]); + }); + }); + + describe('getAccountsByAddress', () => { + it('should throw not found error if non existent address is specified', async () => { + expect.assertions(1); + try { + await storage.getAccountsByAddress([ + '8973039982577606154L', + accounts[0].address, + ]); + } catch (error) { + // eslint-disable-next-line jest/no-try-expect + expect(error).toBeInstanceOf(NotFoundError); + } + }); + + it('should return account by address', async () => { + const result = await storage.getAccountsByAddress([ + accounts[1].address, + accounts[0].address, + ]); + expect(result[0]).toStrictEqual(accounts[1]); + expect(result[1]).toStrictEqual(accounts[0]); + }); + }); +}); diff --git a/elements/lisk-chain/test/integration/data_access/blocks.spec.ts b/elements/lisk-chain/test/integration/data_access/blocks.spec.ts new file mode 100644 index 00000000000..9967955729e --- /dev/null +++ b/elements/lisk-chain/test/integration/data_access/blocks.spec.ts @@ -0,0 +1,458 @@ +/* + * Copyright © 2020 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. + */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +import * as path from 'path'; +import * as fs from 'fs'; +import { KVStore, formatInt, NotFoundError } from '@liskhq/lisk-db'; +import { Storage } from '../../../src/data_access/storage'; +import { newBlock } from '../../utils/block'; + +describe('dataAccess.blocks', () => { + let db: KVStore; + let storage: Storage; + let blocks: any; + + beforeAll(async () => { + const parentPath = path.join(__dirname, '../../tmp'); + if (!fs.existsSync(parentPath)) { + await fs.promises.mkdir(parentPath); + } + db = new KVStore(path.join(parentPath, '/test-blocks.db')); + storage = new Storage(db); + }); + + beforeEach(async () => { + // Prepare sample data + const block300 = newBlock({ height: 300 }); + const block301 = newBlock({ height: 301 }); + const block302 = newBlock({ height: 302 }); + const block303 = newBlock({ height: 303 }); + blocks = []; + blocks.push({ + ...block300, + totalFee: block300.totalFee.toString(), + totalAmount: block300.totalAmount.toString(), + reward: block300.reward.toString(), + transactions: [ + { + id: 'transaction-id-1', + type: 20, + senderPublicKey: + '000efe283f25ea5bb21476b6dfb77cec4dbd33a4d1b5e60e4dc28e8e8b10fc4e', + nonce: '1000', + fee: '5000000', + asset: { newInfo: [1, 4, 5] }, + }, + ], + }); + blocks.push({ + ...block301, + reward: block301.reward.toString(), + totalFee: block301.totalFee.toString(), + totalAmount: block301.totalAmount.toString(), + transactions: [], + }); + blocks.push({ + ...block302, + reward: block302.reward.toString(), + totalFee: block302.totalFee.toString(), + totalAmount: block302.totalAmount.toString(), + transactions: [ + { + id: 'transaction-id-2', + type: 20, + senderPublicKey: + '001efe283f25ea5bb21476b6dfb77cec4dbd33a4d1b5e60e4dc28e8e8b10fc4e', + nonce: '1000', + fee: '2001110', + asset: { data: 'new data' }, + }, + { + id: 'transaction-id-3', + type: 15, + senderPublicKey: + '002283f25ea5bb21476b6dfb77cec4dbd33a4d1b5e60e4dc28e8e8b10fc4e', + nonce: '1000', + fee: '2000000', + asset: { valid: true }, + }, + ], + }); + blocks.push({ + ...block303, + reward: block303.reward.toString(), + totalFee: block303.totalFee.toString(), + totalAmount: block303.totalAmount.toString(), + transactions: [], + }); + const batch = db.batch(); + for (const block of blocks) { + const { transactions, ...blockHeader } = block; + batch.put(`blocks:id:${blockHeader.id}`, blockHeader); + batch.put(`blocks:height:${formatInt(block.height)}`, block.id); + if (block.transactions.length) { + batch.put( + `transactions:blockID:${block.id}`, + block.transactions.map((tx: any) => tx.id), + ); + for (const tx of block.transactions) { + batch.put(`transactions:id:${tx.id}`, tx); + } + } + batch.put(`tempBlocks:height:${formatInt(blocks[2].height)}`, blocks[2]); + batch.put(`tempBlocks:height:${formatInt(blocks[3].height)}`, blocks[3]); + batch.put( + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + `tempBlocks:height:${formatInt(blocks[3].height + 1)}`, + blocks[3], + ); + } + await batch.write(); + }); + + afterEach(async () => { + await db.clear(); + }); + + describe('getBlockHeaderByID', () => { + it('should throw not found error if non existent ID is specified', async () => { + expect.assertions(1); + try { + await storage.getBlockHeaderByID('randomId'); + } catch (error) { + // eslint-disable-next-line jest/no-try-expect + expect(error).toBeInstanceOf(NotFoundError); + } + }); + + it('should return block header by ID', async () => { + const header = await storage.getBlockHeaderByID(blocks[0].id); + expect((header as any).transactions).toBeUndefined(); + const { transactions, ...blockHeader } = blocks[0]; + expect(header).toStrictEqual(blockHeader); + }); + }); + + describe('getBlockHeadersByIDs', () => { + it('should throw not found error if non existent ID is specified', async () => { + expect.assertions(1); + try { + await storage.getBlockHeadersByIDs(['random-id', blocks[1].id]); + } catch (error) { + // eslint-disable-next-line jest/no-try-expect + expect(error).toBeInstanceOf(NotFoundError); + } + }); + + it('should return block headers by ID', async () => { + const headers = await storage.getBlockHeadersByIDs([ + blocks[1].id, + blocks[0].id, + ]); + const { transactions, ...blockHeader } = blocks[1]; + + expect((headers[0] as any).transactions).toBeUndefined(); + expect((headers[1] as any).transactions).toBeUndefined(); + expect(headers[0]).toEqual(blockHeader); + expect(headers).toHaveLength(2); + }); + }); + + describe('getBlockHeadersByHeightBetween', () => { + it('should return block headers with in the height order by height', async () => { + const headers = await storage.getBlockHeadersByHeightBetween( + blocks[0].height, + blocks[2].height, + ); + const { transactions, ...blockHeader } = blocks[2]; + + expect(headers).toHaveLength(3); + expect((headers[0] as any).transactions).toBeUndefined(); + expect((headers[1] as any).transactions).toBeUndefined(); + expect((headers[2] as any).transactions).toBeUndefined(); + expect(headers[0]).toEqual(blockHeader); + }); + }); + + describe('getBlockHeadersWithHeights', () => { + it('should throw not found error if one of heights does not exist', async () => { + expect.assertions(1); + try { + await storage.getBlockHeadersWithHeights([blocks[1].height, 500]); + } catch (error) { + // eslint-disable-next-line jest/no-try-expect + expect(error).toBeInstanceOf(NotFoundError); + } + }); + + it('should return block headers by height', async () => { + const headers = await storage.getBlockHeadersWithHeights([ + blocks[1].height, + blocks[3].height, + ]); + const { transactions: _transactions1, ...blockHeader1 } = blocks[1]; + const { transactions: _transactions3, ...blockHeader3 } = blocks[3]; + + expect((headers[0] as any).transactions).toBeUndefined(); + expect((headers[1] as any).transactions).toBeUndefined(); + expect(headers[0]).toEqual(blockHeader1); + expect(headers[1]).toEqual(blockHeader3); + expect(headers).toHaveLength(2); + }); + }); + + describe('getLastBlockHeader', () => { + it('should return block header with highest height', async () => { + const lastBlockHeader = await storage.getLastBlockHeader(); + const { transactions, ...blockHeader } = blocks[3]; + expect((lastBlockHeader as any).transactions).toBeUndefined(); + expect(lastBlockHeader).toEqual(blockHeader); + }); + }); + + describe('getLastCommonBlockHeader', () => { + it('should return highest block header which exist in the list and non-existent should not throw', async () => { + const header = await storage.getLastCommonBlockHeader([ + blocks[3].id, + 'random-id', + blocks[1].id, + ]); + const { transactions, ...blockHeader } = blocks[3]; + + expect((header as any).transactions).toBeUndefined(); + expect(header).toEqual(blockHeader); + }); + }); + + describe('getBlocksCount', () => { + it('should return highest height as blocks count', async () => { + const count = await storage.getBlocksCount(); + expect(count).toEqual(blocks[3].height); + }); + }); + + describe('getBlockByID', () => { + it('should throw not found error if non existent ID is specified', async () => { + expect.assertions(1); + try { + await storage.getBlockByID('randomId'); + } catch (error) { + // eslint-disable-next-line jest/no-try-expect + expect(error).toBeInstanceOf(NotFoundError); + } + }); + + it('should return full block by ID', async () => { + const block = await storage.getBlockByID(blocks[0].id); + expect(block).toStrictEqual(blocks[0]); + }); + }); + + describe('getBlockByHeight', () => { + it('should throw not found error if non existent height is specified', async () => { + expect.assertions(1); + try { + await storage.getBlockByHeight(500); + } catch (error) { + // eslint-disable-next-line jest/no-try-expect + expect(error).toBeInstanceOf(NotFoundError); + } + }); + + it('should return full block by height', async () => { + const block = await storage.getBlockByHeight(blocks[2].height); + expect(block).toStrictEqual(blocks[2]); + }); + }); + + describe('getLastBlock', () => { + it('should return highest height full block', async () => { + const block = await storage.getLastBlock(); + expect(block).toStrictEqual(blocks[3]); + }); + }); + + describe('isTempBlockEmpty', () => { + it('should return false if tempBlock exists', async () => { + const empty = await storage.isTempBlockEmpty(); + expect(empty).toBeFalse(); + }); + + it('should return true if tempBlock is empty', async () => { + await db.clear(); + + const empty = await storage.isTempBlockEmpty(); + expect(empty).toBeTrue(); + }); + }); + + describe('clearTempBlocks', () => { + it('should clean up all temp blocks, but not other data', async () => { + await storage.clearTempBlocks(); + + expect(await storage.isTempBlockEmpty()).toBeTrue(); + expect(await storage.getBlocksCount()).toEqual(blocks[3].height); + }); + }); + + describe('deleteBlocksWithHeightGreaterThan', () => { + it('should delete blocks with height > specified height', async () => { + await storage.deleteBlocksWithHeightGreaterThan(blocks[0].height); + + await expect(db.exists(`blocks:id:${blocks[0].id}`)).resolves.toBeTrue(); + await expect(db.exists(`blocks:id:${blocks[1].id}`)).resolves.toBeFalse(); + await expect( + db.exists(`transactions:blockID:${blocks[2].id}`), + ).resolves.toBeFalse(); + await expect( + db.exists(`transactions:id:${blocks[2].transactions[0].id}`), + ).resolves.toBeFalse(); + }); + }); + + describe('saveBlock', () => { + const blockInstance = newBlock({ height: 304 }); + const blockJSON = { + ...blockInstance, + reward: blockInstance.reward.toString(), + totalFee: blockInstance.totalFee.toString(), + totalAmount: blockInstance.totalAmount.toString(), + transactions: [ + { + id: 'transaction-id-10', + type: 20, + senderPublicKey: + '001efe283f25ea5bb21476b6dfb77cec4dbd33a4d1b5e60e4dc28e8e8b10fc4e', + nonce: '1000', + fee: '2001110', + asset: { data: 'new data' }, + }, + { + id: 'transaction-id-11', + type: 15, + senderPublicKey: + '002283f25ea5bb21476b6dfb77cec4dbd33a4d1b5e60e4dc28e8e8b10fc4e', + nonce: '1000', + fee: '2000000', + asset: { valid: true }, + }, + ], + }; + // eslint-disable-next-line @typescript-eslint/no-empty-function + const stateStore = { finalize: () => {} }; + + it('should create block with all index required', async () => { + await storage.saveBlock(blockJSON, stateStore as any); + + await expect(db.exists(`blocks:id:${blockJSON.id}`)).resolves.toBeTrue(); + await expect( + db.exists(`blocks:height:${formatInt(blockJSON.height)}`), + ).resolves.toBeTrue(); + await expect( + db.exists(`transactions:blockID:${blockJSON.id}`), + ).resolves.toBeTrue(); + await expect( + db.exists(`transactions:id:${blockJSON.transactions[0].id}`), + ).resolves.toBeTrue(); + await expect( + db.exists(`transactions:id:${blockJSON.transactions[1].id}`), + ).resolves.toBeTrue(); + await expect( + db.exists(`tempBlocks:height:${formatInt(blockJSON.height)}`), + ).resolves.toBeTrue(); + await expect(storage.getBlockByID(blockJSON.id)).resolves.toStrictEqual( + blockJSON, + ); + }); + + it('should create block with all index required and remove the same height block from temp', async () => { + await storage.saveBlock(blockJSON, stateStore as any, true); + + await expect(db.exists(`blocks:id:${blockJSON.id}`)).resolves.toBeTrue(); + await expect( + db.exists(`blocks:height:${formatInt(blockJSON.height)}`), + ).resolves.toBeTrue(); + await expect( + db.exists(`transactions:blockID:${blockJSON.id}`), + ).resolves.toBeTrue(); + await expect( + db.exists(`transactions:id:${blockJSON.transactions[0].id}`), + ).resolves.toBeTrue(); + await expect( + db.exists(`transactions:id:${blockJSON.transactions[1].id}`), + ).resolves.toBeTrue(); + await expect( + db.exists(`tempBlocks:height:${formatInt(blockJSON.height)}`), + ).resolves.toBeFalse(); + await expect(storage.getBlockByID(blockJSON.id)).resolves.toStrictEqual( + blockJSON, + ); + }); + }); + + describe('deleteBlock', () => { + // eslint-disable-next-line @typescript-eslint/no-empty-function + const stateStore = { finalize: () => {} }; + + it('should delete block and all related indexes', async () => { + // Deleting temp blocks to test the saving + await storage.clearTempBlocks(); + await storage.deleteBlock(blocks[2], stateStore as any); + + await expect(db.exists(`blocks:id:${blocks[2].id}`)).resolves.toBeFalse(); + await expect( + db.exists(`blocks:height:${formatInt(blocks[2].height)}`), + ).resolves.toBeFalse(); + await expect( + db.exists(`transactions:blockID:${blocks[2].id}`), + ).resolves.toBeFalse(); + await expect( + db.exists(`transactions:id:${blocks[2].transactions[0].id}`), + ).resolves.toBeFalse(); + await expect( + db.exists(`transactions:id:${blocks[2].transactions[1].id}`), + ).resolves.toBeFalse(); + await expect( + db.exists(`tempBlocks:height:${formatInt(blocks[2].height)}`), + ).resolves.toBeFalse(); + }); + + it('should delete block and all related indexes and save to temp', async () => { + // Deleting temp blocks to test the saving + await storage.clearTempBlocks(); + await storage.deleteBlock(blocks[2], stateStore as any, true); + + await expect(db.exists(`blocks:id:${blocks[2].id}`)).resolves.toBeFalse(); + await expect( + db.exists(`blocks:height:${formatInt(blocks[2].height)}`), + ).resolves.toBeFalse(); + await expect( + db.exists(`transactions:blockID:${blocks[2].id}`), + ).resolves.toBeFalse(); + await expect( + db.exists(`transactions:id:${blocks[2].transactions[0].id}`), + ).resolves.toBeFalse(); + await expect( + db.exists(`transactions:id:${blocks[2].transactions[1].id}`), + ).resolves.toBeFalse(); + await expect( + db.exists(`tempBlocks:height:${formatInt(blocks[2].height)}`), + ).resolves.toBeTrue(); + + const tempBlocks = await storage.getTempBlocks(); + expect(tempBlocks).toHaveLength(1); + expect(tempBlocks[0]).toStrictEqual(blocks[2]); + }); + }); +}); diff --git a/elements/lisk-chain/test/integration/data_access/transactions.spec.ts b/elements/lisk-chain/test/integration/data_access/transactions.spec.ts new file mode 100644 index 00000000000..b357904916c --- /dev/null +++ b/elements/lisk-chain/test/integration/data_access/transactions.spec.ts @@ -0,0 +1,117 @@ +/* + * Copyright © 2020 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. + */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +import * as path from 'path'; +import * as fs from 'fs'; +import { KVStore, NotFoundError } from '@liskhq/lisk-db'; +import { Storage } from '../../../src/data_access/storage'; + +describe('dataAccess.transactions', () => { + let db: KVStore; + let storage: Storage; + let transactions: any; + + beforeAll(async () => { + const parentPath = path.join(__dirname, '../../tmp'); + if (!fs.existsSync(parentPath)) { + await fs.promises.mkdir(parentPath); + } + db = new KVStore(path.join(parentPath, '/test-transactions.db')); + storage = new Storage(db); + }); + + beforeEach(async () => { + transactions = [ + { + id: 'transaction-id-2', + type: 20, + senderPublicKey: + '001efe283f25ea5bb21476b6dfb77cec4dbd33a4d1b5e60e4dc28e8e8b10fc4e', + nonce: '1000', + fee: '2001110', + asset: { data: 'new data' }, + }, + { + id: 'transaction-id-3', + type: 15, + senderPublicKey: + '002283f25ea5bb21476b6dfb77cec4dbd33a4d1b5e60e4dc28e8e8b10fc4e', + nonce: '1000', + fee: '2000000', + asset: { valid: true }, + }, + ]; + const batch = db.batch(); + for (const tx of transactions) { + batch.put(`transactions:id:${tx.id}`, tx); + } + await batch.write(); + }); + + afterEach(async () => { + await db.clear(); + }); + + describe('getTransactionByID', () => { + it('should throw not found error if non existent ID is specified', async () => { + expect.assertions(1); + try { + await storage.getTransactionByID('randomId'); + } catch (error) { + // eslint-disable-next-line jest/no-try-expect + expect(error).toBeInstanceOf(NotFoundError); + } + }); + + it('should return transaction by ID', async () => { + const transaction = await storage.getTransactionByID(transactions[0].id); + expect(transaction).toStrictEqual(transactions[0]); + }); + }); + + describe('getTransactionsByIDs', () => { + it('should throw not found error if one of ID specified does not exist', async () => { + expect.assertions(1); + try { + await storage.getTransactionsByIDs(['randomId', transactions[0].id]); + } catch (error) { + // eslint-disable-next-line jest/no-try-expect + expect(error).toBeInstanceOf(NotFoundError); + } + }); + + it('should return transaction by ID', async () => { + const result = await storage.getTransactionsByIDs([ + transactions[1].id, + transactions[0].id, + ]); + expect(result[1]).toStrictEqual(transactions[0]); + expect(result[0]).toStrictEqual(transactions[1]); + }); + }); + + describe('isTransactionPersisted', () => { + it('should return false if transaction does not exist', async () => { + await expect( + storage.isTransactionPersisted('random-id'), + ).resolves.toBeFalse(); + }); + + it('should return true if transaction exist', async () => { + await expect( + storage.isTransactionPersisted(transactions[1].id), + ).resolves.toBeTrue(); + }); + }); +}); diff --git a/elements/lisk-chain/test/unit/chain.spec.ts b/elements/lisk-chain/test/unit/chain.spec.ts index ffeb196520d..101c10d2e73 100644 --- a/elements/lisk-chain/test/unit/chain.spec.ts +++ b/elements/lisk-chain/test/unit/chain.spec.ts @@ -12,18 +12,22 @@ * Removal or modification of this copyright notice is prohibited. */ +import { Readable } from 'stream'; +import { when } from 'jest-when'; import { TransferTransaction } from '@liskhq/lisk-transactions'; import { getNetworkIdentifier } from '@liskhq/lisk-cryptography'; -import { Chain, StateStore } from '../../src'; +import { KVStore, NotFoundError, formatInt } from '@liskhq/lisk-db'; +import { Chain } from '../../src/chain'; +import { StateStore } from '../../src/state_store'; import * as genesisBlock from '../fixtures/genesis_block.json'; import { newBlock } from '../utils/block'; import { registeredTransactions } from '../utils/registered_transactions'; import * as randomUtils from '../utils/random'; -import { Slots } from '../../src/slots'; -import { BlockJSON } from '../../src/types'; +import { BlockInstance, BlockJSON } from '../../src/types'; import { Account } from '../../src/account'; jest.mock('events'); +jest.mock('@liskhq/lisk-db'); const networkIdentifier = getNetworkIdentifier( genesisBlock.payloadHash, @@ -31,7 +35,6 @@ const networkIdentifier = getNetworkIdentifier( ); describe('chain', () => { - const stubs = {} as any; const constants = { stateBlockSize: 309, maxPayloadLength: 15 * 1024, @@ -50,53 +53,18 @@ describe('chain', () => { epochTime: new Date(Date.UTC(2016, 4, 24, 17, 0, 0, 0)).toISOString(), }; let chainInstance: Chain; - let slots: Slots; + let db: any; beforeEach(() => { // Arrange - stubs.dependencies = { - storage: { - entities: { - Account: { - get: jest.fn(), - update: jest.fn(), - }, - Block: { - begin: jest.fn(), - create: jest.fn(), - count: jest.fn(), - getOne: jest.fn(), - delete: jest.fn(), - get: jest.fn(), - isPersisted: jest.fn(), - }, - Transaction: { - create: jest.fn(), - }, - TempBlock: { - create: jest.fn(), - delete: jest.fn(), - get: jest.fn(), - }, - }, - }, - }; - - slots = new Slots({ - epochTime: constants.epochTime, - interval: constants.blockTime, - }); - - stubs.tx = { - batch: jest.fn(), - }; + db = new KVStore('temp'); + (db.createReadStream as jest.Mock).mockReturnValue(Readable.from([])); chainInstance = new Chain({ - ...stubs.dependencies, + db, genesisBlock, networkIdentifier, registeredTransactions, - slots, ...constants, }); }); @@ -104,9 +72,7 @@ describe('chain', () => { describe('constructor', () => { it('should initialize private variables correctly', () => { // Assert stubbed values are assigned - Object.entries(stubs.dependencies).forEach(([stubName, stubValue]) => { - expect((chainInstance as any)[stubName]).toEqual(stubValue); - }); + // Assert constants Object.entries( (chainInstance as any).constants, @@ -114,9 +80,9 @@ describe('chain', () => { expect((constants as any)[constantName]).toEqual(constantValue), ); // Assert miscellaneous - expect(slots).toEqual((chainInstance as any).slots); expect(chainInstance.blockReward).toBeDefined(); expect((chainInstance as any).blocksVerify).toBeDefined(); + expect(chainInstance['_db']).toBe(db); }); }); @@ -137,34 +103,21 @@ describe('chain', () => { describe('init', () => { beforeEach(() => { - stubs.dependencies.storage.entities.Block.begin.mockImplementation( - (_: any, callback: any) => callback.call(chainInstance, stubs.tx), - ); - stubs.dependencies.storage.entities.Block.count.mockResolvedValue(5); - stubs.dependencies.storage.entities.Block.getOne.mockResolvedValue( - genesisBlock, - ); - stubs.dependencies.storage.entities.Block.get.mockResolvedValue([ - genesisBlock, - ]); - stubs.tx.batch.mockImplementation(async (promises: any) => - Promise.all(promises), - ); - const random101DelegateAccounts = new Array(101) - .fill('') - .map(() => randomUtils.account()); - stubs.dependencies.storage.entities.Account.get.mockResolvedValue( - random101DelegateAccounts, + (db.createReadStream as jest.Mock).mockReturnValue( + Readable.from([{ value: genesisBlock.id }]), ); }); describe('matchGenesisBlock', () => { it('should throw an error when failed to load genesis block', async () => { // Arrange - const error = new Error('Failed to load genesis block'); - stubs.dependencies.storage.entities.Block.get.mockResolvedValue([]); + (db.get as jest.Mock).mockRejectedValue( + new NotFoundError('Data not found') as never, + ); // Act & Assert - await expect(chainInstance.init()).rejects.toEqual(error); + await expect(chainInstance.init()).rejects.toThrow( + 'Failed to load genesis block', + ); }); it('should throw an error if the genesis block id is different', async () => { @@ -174,9 +127,11 @@ describe('chain', () => { ...genesisBlock, id: genesisBlock.id.replace('0', '1'), }; - stubs.dependencies.storage.entities.Block.get.mockResolvedValue([ - mutatedGenesisBlock, - ]); + when(db.get) + .calledWith(`blocks:height:${formatInt(1)}`) + .mockResolvedValue(mutatedGenesisBlock.id as never) + .calledWith(`blocks:id:${mutatedGenesisBlock.id}`) + .mockResolvedValue(mutatedGenesisBlock as never); // Act & Assert await expect(chainInstance.init()).rejects.toEqual(error); @@ -189,9 +144,11 @@ describe('chain', () => { ...genesisBlock, payloadHash: genesisBlock.payloadHash.replace('0', '1'), }; - stubs.dependencies.storage.entities.Block.get.mockResolvedValue([ - mutatedGenesisBlock, - ]); + when(db.get) + .calledWith(`blocks:height:${formatInt(1)}`) + .mockResolvedValue(mutatedGenesisBlock.id as never) + .calledWith(`blocks:id:${mutatedGenesisBlock.id}`) + .mockResolvedValue(mutatedGenesisBlock as never); // Act & Assert await expect(chainInstance.init()).rejects.toEqual(error); }); @@ -203,103 +160,94 @@ describe('chain', () => { ...genesisBlock, blockSignature: genesisBlock.blockSignature.replace('0', '1'), }; - stubs.dependencies.storage.entities.Block.get.mockResolvedValue([ - mutatedGenesisBlock, - ]); + when(db.get) + .calledWith(`blocks:height:${formatInt(1)}`) + .mockResolvedValue(mutatedGenesisBlock.id as never) + .calledWith(`blocks:id:${mutatedGenesisBlock.id}`) + .mockResolvedValue(mutatedGenesisBlock as never); // Act & Assert await expect(chainInstance.init()).rejects.toEqual(error); }); it('should not throw when genesis block matches', async () => { + when(db.get) + .mockRejectedValue(new NotFoundError('Data not found') as never) + .calledWith(`blocks:height:${formatInt(1)}`) + .mockResolvedValue(genesisBlock.id as never) + .calledWith(`blocks:id:${genesisBlock.id}`) + .mockResolvedValue(genesisBlock as never); // Act & Assert await expect(chainInstance.init()).resolves.toBeUndefined(); }); }); describe('loadLastBlock', () => { - it('should throw an error when Block.get throws error', async () => { + let lastBlock: BlockInstance; + beforeEach(() => { // Arrange - const error = 'get error'; - stubs.dependencies.storage.entities.Block.get.mockRejectedValue(error); - + lastBlock = newBlock({ height: 103 }); + (db.createReadStream as jest.Mock).mockReturnValue( + Readable.from([{ value: lastBlock.id }]), + ); + when(db.get) + .mockRejectedValue(new NotFoundError('Data not found') as never) + .calledWith(`blocks:height:${formatInt(1)}`) + .mockResolvedValue(genesisBlock.id as never) + .calledWith(`blocks:id:${genesisBlock.id}`) + .mockResolvedValue(genesisBlock as never) + .calledWith(`blocks:id:${lastBlock.id}`) + .mockResolvedValue(lastBlock as never); + jest + .spyOn(chainInstance.dataAccess, 'getBlockHeadersByHeightBetween') + .mockResolvedValue([]); + }); + it('should throw an error when Block.get throws error', async () => { // Act & Assert - await expect(chainInstance.init()).rejects.toEqual(error); + (db.createReadStream as jest.Mock).mockReturnValue( + Readable.from([{ value: 'randomID' }]), + ); + await expect(chainInstance.init()).rejects.toThrow( + 'Failed to load last block', + ); }); - it('should throw an error when Block.get returns empty array', async () => { - // Arrange - const errorMessage = 'Failed to load last block'; - stubs.dependencies.storage.entities.Block.get - .mockReturnValueOnce([genesisBlock]) - .mockReturnValueOnce([]); + it('should return the the stored last block', async () => { + // Act + await chainInstance.init(); - // Act && Assert - await expect(chainInstance.init()).rejects.toThrow(errorMessage); - }); - // TODO: The tests are minimal due to the changes we expect as part of https://github.com/LiskHQ/lisk-sdk/issues/4131 - describe('when Block.get returns rows', () => { - it('should return the first record from storage entity', async () => { - // Arrange - stubs.dependencies.storage.entities.Block.get.mockResolvedValue([ - genesisBlock, - newBlock(), - ]); - // Act - await chainInstance.init(); - - // Assert - expect(chainInstance.lastBlock.id).toEqual(genesisBlock.id); - }); + // Assert + expect(chainInstance.lastBlock.id).toEqual(lastBlock.id); + expect( + chainInstance.dataAccess.getBlockHeadersByHeightBetween, + ).toHaveBeenCalledWith(1, 103); }); }); - - it('should initialize the processor', async () => { - // Act - await chainInstance.init(); - // Assert - expect(chainInstance.lastBlock.id).toEqual(genesisBlock.id); - }); }); describe('newStateStore', () => { beforeEach(() => { // eslint-disable-next-line dot-notation chainInstance['_lastBlock'] = newBlock({ height: 532 }); - stubs.dependencies.storage.entities.Block.get.mockResolvedValue([ - newBlock(), - genesisBlock, - ]); + jest + .spyOn(chainInstance.dataAccess, 'getBlockHeadersByHeightBetween') + .mockResolvedValue([newBlock(), genesisBlock] as never); }); it('should populate the chain state with genesis block', async () => { - // eslint-disable-next-line dot-notation chainInstance['_lastBlock'] = newBlock({ height: 1 }); await chainInstance.newStateStore(); expect( - stubs.dependencies.storage.entities.Block.get, - ).toHaveBeenCalledWith( - { - // eslint-disable-next-line camelcase - height_gte: 1, - // eslint-disable-next-line camelcase - height_lte: 1, - }, - { limit: null, sort: 'height:desc' }, - ); + chainInstance.dataAccess.getBlockHeadersByHeightBetween, + ).toHaveBeenCalledWith(1, 1); }); it('should return with the chain state with lastBlock.height to lastBlock.height - 309', async () => { await chainInstance.newStateStore(); expect( - stubs.dependencies.storage.entities.Block.get, + chainInstance.dataAccess.getBlockHeadersByHeightBetween, ).toHaveBeenCalledWith( - { - // eslint-disable-next-line camelcase - height_gte: chainInstance.lastBlock.height - 309, - // eslint-disable-next-line camelcase - height_lte: chainInstance.lastBlock.height, - }, - { limit: null, sort: 'height:desc' }, + chainInstance.lastBlock.height - 309, + chainInstance.lastBlock.height, ); }); @@ -314,15 +262,10 @@ describe('chain', () => { it('should return with the chain state with lastBlock.height to lastBlock.height - 310', async () => { await chainInstance.newStateStore(1); expect( - stubs.dependencies.storage.entities.Block.get, + chainInstance.dataAccess.getBlockHeadersByHeightBetween, ).toHaveBeenCalledWith( - { - // eslint-disable-next-line camelcase - height_gte: chainInstance.lastBlock.height - 310, - // eslint-disable-next-line camelcase - height_lte: chainInstance.lastBlock.height - 1, - }, - { limit: null, sort: 'height:desc' }, + chainInstance.lastBlock.height - 310, + chainInstance.lastBlock.height - 1, ); }); }); @@ -405,6 +348,8 @@ describe('chain', () => { describe('save', () => { let stateStoreStub: StateStore; + let batchMock: any; + let savingBlock: BlockInstance; const fakeAccounts = [ Account.getDefaultAccount('1234L'), @@ -412,12 +357,13 @@ describe('chain', () => { ]; beforeEach(() => { - stubs.tx.batch.mockImplementation(async (promises: any) => - Promise.all(promises), - ); - stubs.dependencies.storage.entities.Block.begin.mockImplementation( - (_: any, callback: any) => callback.call(chainInstance, stubs.tx), - ); + savingBlock = newBlock({ height: 300 }); + batchMock = { + put: jest.fn(), + del: jest.fn(), + write: jest.fn(), + }; + (db.batch as jest.Mock).mockReturnValue(batchMock); stateStoreStub = { finalize: jest.fn(), account: { @@ -426,118 +372,40 @@ describe('chain', () => { } as any; }); - it('should throw error when block create fails', async () => { - // Arrange - const block = newBlock(); - const blockCreateError = 'block create error'; - stubs.tx.batch.mockRejectedValue(blockCreateError); - - // Act & Assert - await expect(chainInstance.save(block, stateStoreStub)).rejects.toEqual( - blockCreateError, - ); - }); - - it('should throw error when transaction create fails', async () => { - // Arrange - const transaction = new TransferTransaction(randomUtils.transaction()); - const block = newBlock({ transactions: [transaction] }); - const transactionCreateError = 'transaction create error'; - stubs.dependencies.storage.entities.Transaction.create.mockRejectedValue( - transactionCreateError, - ); - expect.assertions(1); - - // Act & Assert - await expect(chainInstance.save(block, stateStoreStub)).rejects.toEqual( - transactionCreateError, + it('should not save block when saveOnlyState is true', async () => { + await chainInstance.save(savingBlock, stateStoreStub, { + saveOnlyState: true, + removeFromTempTable: false, + }); + expect(batchMock.put).not.toHaveBeenCalledWith( + `blocks:id:${savingBlock.id}`, + expect.anything(), ); + expect(stateStoreStub.finalize).toHaveBeenCalledTimes(1); }); - it('should call Block.create with correct parameters', async () => { - // Arrange - const block = newBlock({ - reward: '0', - totalAmount: '0', - totalFee: '0', + it('should remove tempBlock by height when removeFromTempTable is true', async () => { + await chainInstance.save(savingBlock, stateStoreStub, { + saveOnlyState: false, + removeFromTempTable: true, }); - const blockJSON = chainInstance.serialize(block); - expect.assertions(1); - - // Act - await chainInstance.save(block, stateStoreStub); - - // Assert - expect( - stubs.dependencies.storage.entities.Block.create, - ).toHaveBeenCalledWith(blockJSON, {}, expect.any(Object)); - }); - - it('should not call Transaction.create with if block has no transactions', async () => { - // Arrange - const block = newBlock(); - - // Act - await chainInstance.save(block, stateStoreStub); - - // Assert - expect( - stubs.dependencies.storage.entities.Transaction.create, - ).not.toHaveBeenCalled(); - }); - - it('should call Transaction.create with correct parameters', async () => { - // Arrange - const transaction = new TransferTransaction(randomUtils.transaction()); - const block = newBlock({ transactions: [transaction] }); - (transaction as any).blockId = block.id; - const transactionJSON = transaction.toJSON(); - - // Act - await chainInstance.save(block, stateStoreStub); - - // Assert - expect( - stubs.dependencies.storage.entities.Transaction.create, - ).toHaveBeenCalledWith([transactionJSON], {}, stubs.tx); - }); - - it('should call state store finalize', async () => { - // Arrange - const block = newBlock(); - - // Act & Assert - await chainInstance.save(block, stateStoreStub); + expect(batchMock.del).toHaveBeenCalledWith( + `tempBlocks:height:${formatInt(savingBlock.height)}`, + ); expect(stateStoreStub.finalize).toHaveBeenCalledTimes(1); - expect(stateStoreStub.finalize).toHaveBeenCalledWith(stubs.tx); }); - it('should resolve when blocks module successfully performs save', async () => { - // Arrange - const block = newBlock(); - - // Act & Assert - expect.assertions(1); - - await expect( - chainInstance.save(block, stateStoreStub), - ).resolves.toBeUndefined(); - }); - - it('should throw error when storage create fails', async () => { - // Arrange - const block = newBlock(); - const blockCreateError = 'block create error'; - stubs.dependencies.storage.entities.Block.create.mockRejectedValue( - blockCreateError, + it('should save block when saveOnlyState is false', async () => { + await chainInstance.save(savingBlock, stateStoreStub); + expect(batchMock.put).toHaveBeenCalledWith( + `blocks:id:${savingBlock.id}`, + expect.anything(), ); - - expect.assertions(1); - - // Act & Assert - await expect(chainInstance.save(block, stateStoreStub)).rejects.toBe( - blockCreateError, + expect(batchMock.put).toHaveBeenCalledWith( + `blocks:height:${formatInt(savingBlock.height)}`, + expect.anything(), ); + expect(stateStoreStub.finalize).toHaveBeenCalledTimes(1); }); it('should emit block and accounts', async () => { @@ -560,29 +428,27 @@ describe('chain', () => { }); describe('remove', () => { - let stateStoreStub: StateStore; const fakeAccounts = [ Account.getDefaultAccount('1234L'), Account.getDefaultAccount('5678L'), ]; + let stateStoreStub: StateStore; + let batchMock: any; + beforeEach(() => { + batchMock = { + put: jest.fn(), + del: jest.fn(), + write: jest.fn(), + }; + (db.batch as jest.Mock).mockReturnValue(batchMock); stateStoreStub = { finalize: jest.fn(), account: { getUpdated: jest.fn().mockReturnValue(fakeAccounts), }, } as any; - stubs.tx.batch.mockImplementation(async (promises: any) => - Promise.all(promises), - ); - stubs.dependencies.storage.entities.Block.begin.mockImplementation( - (_: any, callback: any) => callback.call(chainInstance, stubs.tx), - ); - stubs.dependencies.storage.entities.Block.get.mockResolvedValue([ - genesisBlock, - ]); - stubs.dependencies.storage.entities.Block.delete.mockResolvedValue(); }); it('should throw an error when removing genesis block', async () => { @@ -597,7 +463,9 @@ describe('chain', () => { it('should throw an error when previous block does not exist in the database', async () => { // Arrange - stubs.dependencies.storage.entities.Block.get.mockResolvedValue([]); + (db.get as jest.Mock).mockRejectedValue( + new NotFoundError('Data not found') as never, + ); const block = newBlock(); // Act & Assert await expect(chainInstance.remove(block, stateStoreStub)).rejects.toThrow( @@ -607,13 +475,11 @@ describe('chain', () => { it('should throw an error when deleting block fails', async () => { // Arrange + jest + .spyOn(chainInstance.dataAccess, 'getBlockByID') + .mockResolvedValue(genesisBlock as never); const deleteBlockError = new Error('Delete block failed'); - stubs.dependencies.storage.entities.Block.get.mockResolvedValue([ - genesisBlock, - ]); - stubs.dependencies.storage.entities.Block.delete.mockRejectedValue( - deleteBlockError, - ); + batchMock.write.mockRejectedValue(deleteBlockError); const block = newBlock(); // Act & Assert await expect(chainInstance.remove(block, stateStoreStub)).rejects.toEqual( @@ -623,61 +489,37 @@ describe('chain', () => { it('should not create entry in temp block table when saveToTemp flag is false', async () => { // Arrange + jest + .spyOn(chainInstance.dataAccess, 'getBlockByID') + .mockResolvedValue(genesisBlock as never); const block = newBlock(); // Act await chainInstance.remove(block, stateStoreStub); // Assert - expect(chainInstance.lastBlock.id).toEqual(genesisBlock.id); - expect( - stubs.dependencies.storage.entities.TempBlock.create, - ).not.toHaveBeenCalled(); + expect(batchMock.put).not.toHaveBeenCalledWith( + `tempBlocks:height:${block.height}`, + block, + ); }); - describe('when saveToTemp parameter is set to true', () => { - beforeEach(() => { - stubs.dependencies.storage.entities.TempBlock.create.mockResolvedValue(); - }); - - it('should throw an error when temp block create function fails', async () => { - // Arrange - const tempBlockCreateError = new Error( - 'temp block entry creation failed', - ); - const block = newBlock(); - stubs.dependencies.storage.entities.TempBlock.create.mockRejectedValue( - tempBlockCreateError, - ); - // Act & Assert - await expect( - chainInstance.remove(block, stateStoreStub, { - saveTempBlock: true, - }), - ).rejects.toEqual(tempBlockCreateError); - }); - - it('should create entry in temp block with correct id, height and block property and tx', async () => { - // Arrange - const transaction = new TransferTransaction(randomUtils.transaction()); - const block = newBlock({ transactions: [transaction] }); - (transaction as any).blockId = block.id; - const blockJSON = chainInstance.serialize(block); - // Act - await chainInstance.remove(block, stateStoreStub, { - saveTempBlock: true, - }); - // Assert - expect( - stubs.dependencies.storage.entities.TempBlock.create, - ).toHaveBeenCalledWith( - { - id: blockJSON.id, - height: blockJSON.height, - fullBlock: blockJSON, - }, - {}, - stubs.tx, - ); + it('should create entry in temp block with full block when saveTempBlock is true', async () => { + // Arrange + jest + .spyOn(chainInstance.dataAccess, 'getBlockByID') + .mockResolvedValue(genesisBlock as never); + const transaction = new TransferTransaction(randomUtils.transaction()); + const block = newBlock({ transactions: [transaction] }); + (transaction as any).blockId = block.id; + const blockJSON = chainInstance.serialize(block); + // Act + await chainInstance.remove(block, stateStoreStub, { + saveTempBlock: true, }); + // Assert + expect(batchMock.put).not.toHaveBeenCalledWith( + `tempBlocks:height:${block.height}`, + blockJSON, + ); }); it('should emit block and accounts', async () => { @@ -699,121 +541,27 @@ describe('chain', () => { }); }); - describe('removeBlockFromTempTable()', () => { - it('should remove block from table for block ID', async () => { - // Arrange - const block = newBlock(); - - // Act - await chainInstance.removeBlockFromTempTable(block.id, stubs.tx); - - // Assert - expect( - stubs.dependencies.storage.entities.TempBlock.delete, - ).toHaveBeenCalledWith({ id: block.id }, {}, stubs.tx); - }); - }); - describe('exists()', () => { - beforeEach(() => { - stubs.dependencies.storage.entities.Block.isPersisted.mockResolvedValue( - true, - ); - }); - it('should return true if the block does not exist', async () => { // Arrange const block = newBlock(); - expect.assertions(2); + when(db.exists) + .calledWith(`blocks:id:${block.id}`) + .mockResolvedValue(true as never); // Act & Assert expect(await chainInstance.exists(block)).toEqual(true); - expect( - stubs.dependencies.storage.entities.Block.isPersisted, - ).toHaveBeenCalledWith({ - id: block.id, - }); + expect(db.exists).toHaveBeenCalledWith(`blocks:id:${block.id}`); }); it('should return false if the block does exist', async () => { // Arrange - stubs.dependencies.storage.entities.Block.isPersisted.mockResolvedValue( - false, - ); const block = newBlock(); - expect.assertions(2); + when(db.exists) + .calledWith(`blocks:id:${block.id}`) + .mockResolvedValue(false as never); // Act & Assert expect(await chainInstance.exists(block)).toEqual(false); - expect( - stubs.dependencies.storage.entities.Block.isPersisted, - ).toHaveBeenCalledWith({ - id: block.id, - }); - }); - }); - - describe('getBlocksWithLimitAndOffset', () => { - describe('when called without offset', () => { - const validBlocks = [ - { - height: 100, - id: 'block-id', - }, - ]; - - beforeEach(() => { - stubs.dependencies.storage.entities.Block.get.mockResolvedValue( - validBlocks, - ); - }); - - it('should use limit 1 as default', async () => { - await chainInstance.dataAccess.getBlocksWithLimitAndOffset(1); - - expect( - stubs.dependencies.storage.entities.Block.get, - ).toHaveBeenCalledWith( - // eslint-disable-next-line camelcase - { height_gte: 0, height_lte: 0 }, - { extended: true, limit: null, sort: 'height:desc' }, - ); - }); - }); - - describe('when blocks received in desending order', () => { - const validBlocks = [ - { - height: 101, - id: 'block-id1', - }, - { - height: 100, - id: 'block-id2', - }, - ]; - - beforeEach(() => { - stubs.dependencies.storage.entities.Block.get.mockResolvedValue( - validBlocks, - ); - }); - - it('should be sorted ascending by height', async () => { - const blocks = await chainInstance.dataAccess.getBlocksWithLimitAndOffset( - 2, - 100, - ); - - expect( - stubs.dependencies.storage.entities.Block.get, - ).toHaveBeenCalledWith( - // eslint-disable-next-line camelcase - { height_gte: 100, height_lte: 101 }, - { extended: true, limit: null, sort: 'height:desc' }, - ); - expect(blocks.map(b => b.height)).toEqual( - validBlocks.map(b => b.height).sort((a, b) => a - b), - ); - }); + expect(db.exists).toHaveBeenCalledWith(`blocks:id:${block.id}`); }); }); @@ -822,25 +570,36 @@ describe('chain', () => { // Arrange const ids = ['1', '2']; const block = newBlock(); - stubs.dependencies.storage.entities.Block.get.mockResolvedValue([block]); + jest + .spyOn(chainInstance.dataAccess, 'getBlockHeadersByIDs') + .mockResolvedValue([block] as never); // Act const result = await chainInstance.getHighestCommonBlock(ids); // Assert + expect( + chainInstance.dataAccess.getBlockHeadersByIDs, + ).toHaveBeenCalledWith(ids); expect(result).toEqual(block); }); it('should throw error if unable to get blocks from the storage', async () => { // Arrange const ids = ['1', '2']; - stubs.dependencies.storage.entities.Block.get.mockRejectedValue( - new Error('Failed to fetch the highest common block'), - ); - + jest + .spyOn(chainInstance.dataAccess, 'getBlockHeadersByIDs') + .mockRejectedValue(new NotFoundError('data not found') as never); // Act && Assert - await expect(chainInstance.getHighestCommonBlock(ids)).rejects.toThrow( - 'Failed to fetch the highest common block', - ); + expect.assertions(2); + try { + await chainInstance.getHighestCommonBlock(ids); + } catch (error) { + // eslint-disable-next-line jest/no-try-expect + expect(error).toBeInstanceOf(NotFoundError); + } + expect( + chainInstance.dataAccess.getBlockHeadersByIDs, + ).toHaveBeenCalledWith(ids); }); }); }); diff --git a/elements/lisk-chain/test/unit/data_access/data_access.spec.ts b/elements/lisk-chain/test/unit/data_access/data_access.spec.ts index f39b5b4238a..9f431fb4665 100644 --- a/elements/lisk-chain/test/unit/data_access/data_access.spec.ts +++ b/elements/lisk-chain/test/unit/data_access/data_access.spec.ts @@ -11,47 +11,35 @@ * * Removal or modification of this copyright notice is prohibited. */ +import { Readable } from 'stream'; +import { when } from 'jest-when'; +import { + KVStore, + formatInt, + NotFoundError, + getFirstPrefix, + getLastPrefix, +} from '@liskhq/lisk-db'; import { TransferTransaction } from '@liskhq/lisk-transactions'; import { DataAccess } from '../../../src/data_access'; import { BlockHeader as BlockHeaderInstance } from '../../fixtures/block'; import { BlockInstance, BlockJSON } from '../../../src/types'; -describe('data_access.storage', () => { +jest.mock('@liskhq/lisk-db'); + +describe('data_access', () => { let dataAccess: DataAccess; - let storageMock: any; + let db: any; let block: BlockInstance; beforeEach(() => { - storageMock = { - entities: { - Block: { - get: jest.fn().mockResolvedValue([{ height: 1 }]), - getOne: jest.fn().mockResolvedValue([{ height: 1 }]), - count: jest.fn(), - isPersisted: jest.fn(), - delete: jest.fn(), - }, - TempBlock: { - get: jest.fn(), - isEmpty: jest.fn(), - truncate: jest.fn(), - }, - Account: { - get: jest.fn().mockResolvedValue([{ balance: '0', address: '123L' }]), - getOne: jest.fn(), - resetMemTables: jest.fn(), - }, - Transaction: { - get: jest - .fn() - .mockResolvedValue([{ nonce: '2', type: 8, fee: '100' }]), - isPersisted: jest.fn(), - }, - }, - }; - + db = new KVStore('temp'); + (db.createReadStream as jest.Mock).mockReturnValue(Readable.from([])); + (formatInt as jest.Mock).mockImplementation(num => num); + (getFirstPrefix as jest.Mock).mockImplementation(str => str); + (getLastPrefix as jest.Mock).mockImplementation(str => str); dataAccess = new DataAccess({ - dbStorage: storageMock, + db, registeredTransactions: { 8: TransferTransaction }, minBlockHeaderCache: 3, maxBlockHeaderCache: 5, @@ -86,7 +74,7 @@ describe('data_access.storage', () => { }); describe('#getBlockHeadersByIDs', () => { - it('should not call storage if cache exists', async () => { + it('should not call db if cache exists', async () => { // Arrange dataAccess.addBlockHeader(block); @@ -94,7 +82,7 @@ describe('data_access.storage', () => { await dataAccess.getBlockHeadersByIDs([block.id]); // Assert - expect(storageMock.entities.Block.get).not.toHaveBeenCalled(); + expect(db.get).not.toHaveBeenCalled(); }); it('should return persisted blocks if cache does not exist', async () => { @@ -102,12 +90,12 @@ describe('data_access.storage', () => { await dataAccess.getBlockHeadersByIDs([block.id]); // Assert - expect(storageMock.entities.Block.get).toHaveBeenCalled(); + expect(db.get).toHaveBeenCalled(); }); }); describe('#getBlockHeaderByHeight', () => { - it('should not call storage if cache exists', async () => { + it('should not call db if cache exists', async () => { // Arrange dataAccess.addBlockHeader(block); @@ -115,20 +103,35 @@ describe('data_access.storage', () => { await dataAccess.getBlockHeaderByHeight(1); // Assert - expect(storageMock.entities.Block.get).not.toHaveBeenCalled(); + expect(db.get).not.toHaveBeenCalled(); }); it('should return persisted block header if cache does not exist', async () => { + // Arrange + (db.createReadStream as jest.Mock).mockReturnValue( + Readable.from([ + { + value: block.id, + }, + ]), + ); + when(db.get) + .calledWith(`blocks:height:${formatInt(block.height)}`) + .mockResolvedValue(block.id as never); // Act await dataAccess.getBlockHeaderByHeight(1); // Assert - expect(storageMock.entities.Block.get).toHaveBeenCalled(); + expect(db.get).toHaveBeenCalledTimes(2); + expect(db.get).toHaveBeenCalledWith( + `blocks:height:${formatInt(block.height)}`, + ); + expect(db.get).toHaveBeenCalledWith(`blocks:id:${block.id}`); }); }); describe('#getBlockHeadersByHeightBetween', () => { - it('should not call storage if cache exists', async () => { + it('should not call db if cache exists', async () => { // Arrange dataAccess.addBlockHeader({ ...block, height: 0 }); dataAccess.addBlockHeader(block); @@ -137,23 +140,31 @@ describe('data_access.storage', () => { await dataAccess.getBlockHeadersByHeightBetween(0, 1); // Assert - expect(storageMock.entities.Block.get).not.toHaveBeenCalled(); + expect(db.get).not.toHaveBeenCalled(); }); it('should return persisted blocks if cache does not exist', async () => { // Arrange (dataAccess as any)._blocksCache.items.shift(); + (db.createReadStream as jest.Mock).mockReturnValue( + Readable.from([ + { + value: block.id, + }, + ]), + ); // Act await dataAccess.getBlockHeadersByHeightBetween(0, 1); // Assert - expect(storageMock.entities.Block.get).toHaveBeenCalled(); + expect(db.createReadStream).toHaveBeenCalledTimes(1); + expect(db.get).toHaveBeenCalledTimes(1); }); }); describe('#getBlockHeadersWithHeights', () => { - it('should not call storage if cache exists', async () => { + it('should not call db if cache exists', async () => { // Arrange dataAccess.addBlockHeader(block); @@ -161,20 +172,28 @@ describe('data_access.storage', () => { await dataAccess.getBlockHeadersWithHeights([1]); // Assert - expect(storageMock.entities.Block.get).not.toHaveBeenCalled(); + expect(db.get).not.toHaveBeenCalled(); }); it('should return persisted blocks if cache does not exist', async () => { + // Arrange + when(db.get) + .calledWith(`blocks:height:${formatInt(block.height)}`) + .mockResolvedValue(block.id as never); // Act await dataAccess.getBlockHeadersWithHeights([1]); // Assert - expect(storageMock.entities.Block.get).toHaveBeenCalled(); + expect(db.get).toHaveBeenCalledTimes(2); + expect(db.get).toHaveBeenCalledWith( + `blocks:height:${formatInt(block.height)}`, + ); + expect(db.get).toHaveBeenCalledWith(`blocks:id:${block.id}`); }); }); describe('#getLastBlockHeader', () => { - it('should not call storage if cache exists', async () => { + it('should not call db if cache exists', async () => { // Arrange dataAccess.addBlockHeader(block); @@ -182,197 +201,373 @@ describe('data_access.storage', () => { await dataAccess.getLastBlockHeader(); // Assert - expect(storageMock.entities.Block.get).not.toHaveBeenCalled(); + expect(db.get).not.toHaveBeenCalled(); }); it('should return persisted blocks if cache does not exist', async () => { + // Arrange + (db.createReadStream as jest.Mock).mockReturnValue( + Readable.from([ + { + value: block.id, + }, + ]), + ); // Act await dataAccess.getLastBlockHeader(); // Assert - expect(storageMock.entities.Block.get).toHaveBeenCalled(); + expect(db.get).toHaveBeenCalledTimes(1); + expect(db.createReadStream).toHaveBeenCalledTimes(1); + expect(db.get).toHaveBeenCalledWith(`blocks:id:${block.id}`); }); }); describe('#getLastCommonBlockHeader', () => { - it('should not call storage if cache exists', async () => { + it('should not call db if cache exists', async () => { // Arrange dataAccess.addBlockHeader(block); // Act - await dataAccess.getLastBlockHeader(); + await dataAccess.getLastCommonBlockHeader([block.id]); // Assert - expect(storageMock.entities.Block.get).not.toHaveBeenCalled(); + expect(db.get).not.toHaveBeenCalled(); }); it('should return persisted blocks if cache does not exist', async () => { // Act - await dataAccess.getLastBlockHeader(); + await dataAccess.getLastCommonBlockHeader([block.id, 'random-id']); // Assert - expect(storageMock.entities.Block.get).toHaveBeenCalled(); + expect(db.get).toHaveBeenCalledTimes(2); }); }); describe('#getBlockCount', () => { - it('should call storage.getBlocksCount', async () => { + it('should get the height from stream', async () => { + // Arrange + (db.createReadStream as jest.Mock).mockReturnValue( + Readable.from([ + { + value: block.id, + }, + ]), + ); + when(db.get) + .calledWith(`blocks:id:${block.id}`) + .mockResolvedValue(block as never); // Act await dataAccess.getBlocksCount(); // Assert - expect(storageMock.entities.Block.count).toHaveBeenCalled(); + expect(db.createReadStream).toHaveBeenCalledTimes(1); + expect(db.get).toHaveBeenCalledTimes(1); + expect(db.get).toHaveBeenCalledWith(`blocks:id:${block.id}`); }); }); describe('#getBlocksByIDs', () => { - it('should return persisted blocks if cache does not exist', async () => { + it('should return persisted blocks by ids', async () => { + // Arrange + when(db.get) + .mockRejectedValue(new NotFoundError('Data not found') as never) + .calledWith('blocks:id:1') + .mockResolvedValue(block as never); // Act await dataAccess.getBlocksByIDs(['1']); // Assert - expect(storageMock.entities.Block.get).toHaveBeenCalled(); + expect(db.get).toHaveBeenCalledWith('blocks:id:1'); }); }); describe('#getBlocksByHeightBetween', () => { - it('should return persisted blocks if cache does not exist', async () => { + it('should return persisted blocks within the height range', async () => { + // Arrange + (db.createReadStream as jest.Mock).mockReturnValue( + Readable.from([ + { + value: block.id, + }, + ]), + ); + when(db.get) + .mockRejectedValue(new NotFoundError('Data not found') as never) + .calledWith(`blocks:id:${block.id}`) + .mockResolvedValue(block as never); // Act await dataAccess.getBlocksByHeightBetween(1, 2); // Assert - expect(storageMock.entities.Block.get).toHaveBeenCalled(); + expect(db.createReadStream).toHaveBeenCalledTimes(1); + expect(db.get).toHaveBeenCalledTimes(2); }); }); describe('#getLastBlock', () => { - it('should call storage.getLastBlock', async () => { + it('should get the highest height block', async () => { + // Arrange + (db.createReadStream as jest.Mock).mockReturnValue( + Readable.from([ + { + value: block.id, + }, + ]), + ); + when(db.get) + .mockRejectedValue(new NotFoundError('Data not found') as never) + .calledWith(`blocks:id:${block.id}`) + .mockResolvedValue(block as never); // Act await dataAccess.getLastBlock(); // Assert - expect(storageMock.entities.Block.get).toHaveBeenCalled(); + expect(db.createReadStream).toHaveBeenCalledTimes(1); + expect(db.get).toHaveBeenCalledTimes(2); }); }); describe('#deleteBlocksWithHeightGreaterThan', () => { - it('should call storage.Block.delete and return block', async () => { + it('should delete all block related keys using batch', async () => { + // Arrange + const batchMock = { del: jest.fn(), write: jest.fn() }; + (db.batch as jest.Mock).mockReturnValue(batchMock as never); + (db.createReadStream as jest.Mock).mockImplementation(() => + Readable.from([ + { + value: block.id, + }, + ]), + ); + when(db.get) + .mockRejectedValue(new NotFoundError('Data not found') as never) + .calledWith(`blocks:height:${formatInt(block.height)}`) + .mockResolvedValue(block.id as never) + .calledWith(`blocks:id:${block.id}`) + .mockResolvedValue(block as never); // Act - await dataAccess.deleteBlocksWithHeightGreaterThan(1); + await dataAccess.deleteBlocksWithHeightGreaterThan(0); // Assert - expect(storageMock.entities.Block.delete).toHaveBeenCalled(); + expect(batchMock.del).toHaveBeenCalledWith(`blocks:id:${block.id}`); + expect(batchMock.del).toHaveBeenCalledWith( + `blocks:height:${formatInt(block.height)}`, + ); + expect(batchMock.del).toHaveBeenCalledWith( + `transactions:blockID:${block.id}`, + ); + expect(batchMock.write).toHaveBeenCalledTimes(1); }); }); describe('#isBlockPersisted', () => { - it('should call storage.isBlockPersisted', async () => { + it('should call check if the id exists in the database', async () => { // Act await dataAccess.isBlockPersisted(block.id); // Assert - expect(storageMock.entities.Block.isPersisted).toHaveBeenCalled(); + expect(db.exists).toHaveBeenCalledWith(`blocks:id:${block.id}`); }); }); describe('#getTempBlocks', () => { - it('should call storage.getTempBlocks', async () => { + it('should call get temp blocks using stream', async () => { + // Arrange + (db.createReadStream as jest.Mock).mockImplementation(() => + Readable.from([ + { + value: block, + }, + ]), + ); // Act await dataAccess.getTempBlocks(); // Assert - expect(storageMock.entities.TempBlock.get).toHaveBeenCalled(); + expect(db.createReadStream).toHaveBeenCalledTimes(1); }); }); describe('#isTempBlockEmpty', () => { - it('should call storage.isTempBlockEmpty', async () => { + it('should return false when temp block exist', async () => { + // Arrange + (db.createReadStream as jest.Mock).mockImplementation(() => + Readable.from([ + { + value: block, + }, + ]), + ); + // Act + const result = await dataAccess.isTempBlockEmpty(); + + // Assert + expect(db.createReadStream).toHaveBeenCalledTimes(1); + expect(result).toBeFalse(); + }); + + it('should return true when temp block exist', async () => { + // Arrange + (db.createReadStream as jest.Mock).mockImplementation(() => + Readable.from([]), + ); // Act - await dataAccess.isTempBlockEmpty(); + const result = await dataAccess.isTempBlockEmpty(); // Assert - expect(storageMock.entities.TempBlock.isEmpty).toHaveBeenCalled(); + expect(db.createReadStream).toHaveBeenCalledTimes(1); + expect(result).toBeTrue(); }); }); describe('#clearTempBlocks', () => { - it('should call storage.clearTempBlocks', async () => { + it('should call db clear function', async () => { // Act await dataAccess.clearTempBlocks(); // Assert - expect(storageMock.entities.TempBlock.truncate).toHaveBeenCalled(); + expect(db.clear).toHaveBeenCalledTimes(1); + expect(db.clear).toHaveBeenCalledWith({ + gte: expect.stringContaining('tempBlocks:height'), + lte: expect.stringContaining('tempBlocks:height'), + }); }); }); describe('#getAccountsByPublicKey', () => { - it('should call storage.getAccountsByPublicKey', async () => { + it('should convert public key to address and get by address', async () => { + // Arrange + const account = { + publicKey: + '456efe283f25ea5bb21476b6dfb77cec4dbd33a4d1b5e60e4dc28e8e8b10fc4e', + address: '7546125166665832140L', + nonce: '0', + }; + when(db.get) + .calledWith(`accounts:address:${account.address}`) + .mockResolvedValue(account as never); // Act - const [result] = await dataAccess.getAccountsByPublicKey(['1L']); + const [result] = await dataAccess.getAccountsByPublicKey([ + account.publicKey, + ]); // Assert - expect(storageMock.entities.Account.get).toHaveBeenCalled(); + expect(db.get).toHaveBeenCalledWith( + `accounts:address:${account.address}`, + ); expect(typeof result.nonce).toBe('bigint'); }); }); describe('#getAccountByAddress', () => { - it('should call storage.getAccountsByAddress', async () => { + it('should get account by address and decode them', async () => { + // Arrange + const account = { + publicKey: + '456efe283f25ea5bb21476b6dfb77cec4dbd33a4d1b5e60e4dc28e8e8b10fc4e', + address: '7546125166665832140L', + nonce: '0', + balance: '100', + }; + when(db.get) + .calledWith(`accounts:address:${account.address}`) + .mockResolvedValue(account as never); // Act - storageMock.entities.Account.getOne.mockResolvedValue({ - address: '1L', - balance: '0', - }); - const account = await dataAccess.getAccountByAddress('1L'); + const result = await dataAccess.getAccountByAddress(account.address); // Assert - expect(storageMock.entities.Account.getOne).toHaveBeenCalled(); - expect(typeof account.balance).toEqual('bigint'); + expect(db.get).toHaveBeenCalledWith( + `accounts:address:${account.address}`, + ); + expect(typeof result.balance).toEqual('bigint'); }); }); describe('#getAccountsByAddress', () => { - it('should call storage.getAccountsByAddress', async () => { + it('should get accounts by each address and decode them', async () => { + // Arrange + const accounts = [ + { + publicKey: + '456efe283f25ea5bb21476b6dfb77cec4dbd33a4d1b5e60e4dc28e8e8b10fc4e', + address: '7546125166665832140L', + nonce: '0', + balance: '100', + }, + { + publicKey: + 'd468707933e4f24888dc1f00c8f84b2642c0edf3d694e2bb5daa7a0d87d18708', + address: '10676488814586252632L', + nonce: '0', + balance: '300', + }, + ]; + when(db.get) + .calledWith(`accounts:address:${accounts[0].address}`) + .mockResolvedValue(accounts[0] as never) + .calledWith(`accounts:address:${accounts[1].address}`) + .mockResolvedValue(accounts[1] as never); // Act - storageMock.entities.Account.get.mockResolvedValue([ - { address: '1L', balance: '0' }, - ]); - const accounts = await dataAccess.getAccountsByAddress(['1L']); + const result = await dataAccess.getAccountsByAddress( + accounts.map(acc => acc.address), + ); // Assert - expect(storageMock.entities.Account.get).toHaveBeenCalled(); - expect(typeof accounts[0].balance).toEqual('bigint'); + expect(db.get).toHaveBeenCalledTimes(2); + expect(typeof result[0].balance).toEqual('bigint'); }); }); describe('#getTransactionsByIDs', () => { - it('should call storage.getTransactionsByIDs', async () => { + it('should get transaction by id', async () => { + // Arrange + when(db.get) + .calledWith('transactions:id:1') + .mockResolvedValue({ + id: '1', + fee: '100', + nonce: '0', + type: 8, + } as never); // Act const [result] = await dataAccess.getTransactionsByIDs(['1']); // Assert - expect(storageMock.entities.Transaction.get).toHaveBeenCalled(); + expect(db.get).toHaveBeenCalledWith('transactions:id:1'); expect(typeof result.fee).toBe('bigint'); }); }); describe('#isTransactionPersisted', () => { - it('should call storage.isTransactionPersisted', async () => { + it('should call exists with the id', async () => { // Act await dataAccess.isTransactionPersisted('1'); // Assert - expect(storageMock.entities.Transaction.isPersisted).toHaveBeenCalled(); + expect(db.exists).toHaveBeenCalledWith('transactions:id:1'); }); }); - describe('#resetAccountMemTables', () => { - it('should call storage.resetAccountMemTables', async () => { + describe('#resetMemTables', () => { + it('should clear all calculated states', async () => { // Act - await dataAccess.resetAccountMemTables(); + await dataAccess.resetMemTables(); // Assert - expect(storageMock.entities.Account.resetMemTables).toHaveBeenCalled(); + expect(db.clear).toHaveBeenCalledTimes(3); + expect(db.clear).toHaveBeenCalledWith({ + gte: expect.stringContaining('accounts:address'), + lte: expect.stringContaining('accounts:address'), + }); + expect(db.clear).toHaveBeenCalledWith({ + gte: expect.stringContaining('chain'), + lte: expect.stringContaining('chain'), + }); + expect(db.clear).toHaveBeenCalledWith({ + gte: expect.stringContaining('consensus'), + lte: expect.stringContaining('consensus'), + }); }); }); @@ -446,11 +641,7 @@ describe('data_access.storage', () => { // Arrange jest.spyOn(dataAccess, 'getBlocksByHeightBetween'); - storageMock.entities.Block.get.mockResolvedValue([ - { height: 9 }, - { height: 8 }, - { height: 7 }, - ]); + db.get.mockResolvedValue([{ height: 9 }, { height: 8 }, { height: 7 }]); const blocks = []; for (let i = 0; i < 5; i += 1) { diff --git a/elements/lisk-chain/test/unit/data_access/storage.spec.ts b/elements/lisk-chain/test/unit/data_access/storage.spec.ts deleted file mode 100644 index 66990a37e7f..00000000000 --- a/elements/lisk-chain/test/unit/data_access/storage.spec.ts +++ /dev/null @@ -1,396 +0,0 @@ -/* - * 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. - */ -import { Storage as StorageAccess } from '../../../src/data_access'; - -describe('data access - storage', () => { - const defaultBlocks = [ - { - id: 2, - version: 2, - height: 2, - previousBlockId: 1, - timestamp: 1000, - }, - { - id: 3, - version: 2, - height: 3, - previousBlockId: 2, - timestamp: 2000, - }, - ]; - - const defaultAccounts = [ - { publicKey: '1L', address: '1276152240083265771L', balance: '100' }, - { publicKey: '2L', address: '5059876081639179984L', balance: '555' }, - ]; - - const defaultTransactions = [ - { - type: 8, - senderPublicKey: - 'efaf1d977897cb60d7db9d30e8fd668dee070ac0db1fb8d184c06152a8b75f8d', - }, - { - type: 8, - senderPublicKey: - 'dfff1d977897cb60d7db9d30e8fd668dee070ac0db1fb8d184c06152a8b75f8a', - }, - ]; - - let storageMock: any; - let storageAccess: StorageAccess; - - beforeEach(() => { - storageMock = { - entities: { - Block: { - get: jest.fn(), - count: jest.fn(), - isPersisted: jest.fn(), - delete: jest.fn(), - }, - TempBlock: { - get: jest.fn(), - isEmpty: jest.fn(), - truncate: jest.fn(), - }, - Account: { - get: jest.fn(), - resetMemTables: jest.fn(), - }, - Transaction: { - get: jest.fn(), - isPersisted: jest.fn(), - }, - }, - }; - - storageAccess = new StorageAccess({ - ...storageMock, - minCachedItems: 3, - maxCachedItems: 5, - }); - }); - - describe('#getBlockHeadersByIDs', () => { - beforeEach(() => { - // Arrange - storageMock.entities.Block.get.mockResolvedValue(defaultBlocks); - }); - - it('should call storage.Block.get and return blocks', async () => { - // Act - const blocksFromStorage = await storageAccess.getBlockHeadersByIDs([ - '1', - '2', - ]); - - // Assert - expect(blocksFromStorage).toEqual(defaultBlocks); - expect(storageMock.entities.Block.get).toHaveBeenCalled(); - }); - }); - - describe('#getBlockHeadersByHeightBetween', () => { - beforeEach(() => { - // Arrange - storageMock.entities.Block.get.mockResolvedValue(defaultBlocks); - }); - - it('should call storage.Block.get and return blocks', async () => { - // Act - const blocksFromStorage = await storageAccess.getBlockHeadersByHeightBetween( - 2, - 3, - ); - - // Assert - expect(blocksFromStorage).toEqual(defaultBlocks); - expect(storageMock.entities.Block.get).toHaveBeenCalled(); - }); - }); - - describe('#getBlockHeadersWithHeights', () => { - beforeEach(() => { - // Arrange - storageMock.entities.Block.get.mockResolvedValue(defaultBlocks); - }); - - it('should call storage.Block.get and return blocks', async () => { - // Act - const blocksFromStorage = await storageAccess.getBlockHeadersWithHeights([ - 2, - ]); - - // Assert - expect(blocksFromStorage).toEqual(defaultBlocks); - expect(storageMock.entities.Block.get).toHaveBeenCalled(); - }); - }); - - describe('#getLastBlockHeader', () => { - beforeEach(() => { - // Arrange - storageMock.entities.Block.get.mockResolvedValue([defaultBlocks[1]]); - }); - - it('should call storage.Block.get and return block', async () => { - // Act - const blockFromStorage = await storageAccess.getLastBlockHeader(); - - // Assert - expect(blockFromStorage).toEqual(defaultBlocks[1]); - expect(storageMock.entities.Block.get).toHaveBeenCalled(); - }); - }); - - describe('#getLastCommonBlockHeader', () => { - beforeEach(() => { - // Arrange - storageMock.entities.Block.get.mockResolvedValue([defaultBlocks[1]]); - }); - - it('should call storage.Block.get and return block', async () => { - // Act - const blockFromStorage = await storageAccess.getLastCommonBlockHeader([ - '2', - '3', - ]); - - // Assert - expect(blockFromStorage).toEqual(defaultBlocks[1]); - expect(storageMock.entities.Block.get).toHaveBeenCalled(); - }); - }); - - describe('#getBlocksCount', () => { - beforeEach(() => { - // Arrange - storageMock.entities.Block.count.mockResolvedValue(2); - }); - - it('should call storage.Block.get and return block', async () => { - // Act - const blockCountStorage = await storageAccess.getBlocksCount(); - - // Assert - expect(blockCountStorage).toEqual(2); - expect(storageMock.entities.Block.count).toHaveBeenCalled(); - }); - }); - - describe('#getBlocksByIDs', () => { - beforeEach(() => { - // Arrange - storageMock.entities.Block.get.mockResolvedValue(defaultBlocks); - }); - - it('should call storage.Block.get and return blocks', async () => { - // Act - const blocksFromStorage = await storageAccess.getBlocksByIDs(['2', '3']); - - // Assert - expect(blocksFromStorage).toEqual(defaultBlocks); - expect(storageMock.entities.Block.get).toHaveBeenCalled(); - }); - }); - - describe('#getBlocksByHeightBetween', () => { - beforeEach(() => { - // Arrange - storageMock.entities.Block.get.mockResolvedValue(defaultBlocks); - }); - - it('should call storage.Block.get and return blocks', async () => { - // Act - const blocksFromStorage = await storageAccess.getBlocksByHeightBetween( - 2, - 3, - ); - - // Assert - expect(blocksFromStorage).toEqual(defaultBlocks); - expect(storageMock.entities.Block.get).toHaveBeenCalled(); - }); - }); - - describe('#getLastBlock', () => { - beforeEach(() => { - // Arrange - storageMock.entities.Block.get.mockResolvedValue([defaultBlocks[1]]); - }); - - it('should call storage.Block.get and return block', async () => { - // Act - const blockFromStorage = await storageAccess.getLastBlock(); - - // Assert - expect(blockFromStorage).toEqual(defaultBlocks[1]); - expect(storageMock.entities.Block.get).toHaveBeenCalled(); - }); - }); - - describe('#getTempBlocks', () => { - beforeEach(() => { - // Arrange - storageMock.entities.TempBlock.get.mockResolvedValue(defaultBlocks); - }); - - it('should call storage.TempBlock.get and return temporary blocks', async () => { - // Act - const blocksFromStorage = await storageAccess.getTempBlocks(); - - // Assert - expect(blocksFromStorage).toEqual(defaultBlocks); - expect(storageMock.entities.TempBlock.get).toHaveBeenCalled(); - }); - }); - - describe('#isTempBlockEmpty', () => { - beforeEach(() => { - // Arrange - storageMock.entities.TempBlock.isEmpty.mockResolvedValue(true); - }); - - it('should call storage.TempBlock.isEmpty and return boolean', async () => { - // Act - const existsInStorage = await storageAccess.isTempBlockEmpty(); - - // Assert - expect(existsInStorage).toEqual(true); - expect(storageMock.entities.TempBlock.isEmpty).toHaveBeenCalled(); - }); - }); - - describe('#clearTempBlocks', () => { - it('should call storage.TempBlock.truncate', async () => { - // Act - await storageAccess.clearTempBlocks(); - - // Assert - expect(storageMock.entities.TempBlock.truncate).toHaveBeenCalled(); - }); - }); - - describe('#deleteBlocksWithHeightGreaterThan', () => { - it('should call storage.Block.delete and return block', async () => { - // Act - await storageAccess.deleteBlocksWithHeightGreaterThan(1); - - // Assert - expect(storageMock.entities.Block.delete).toHaveBeenCalled(); - }); - }); - - describe('#isBlockPersisted', () => { - beforeEach(() => { - // Arrange - storageMock.entities.Block.isPersisted.mockResolvedValue(true); - }); - - it('should call storage.Block.isPersisted and return boolean', async () => { - // Act - const existsInStorage = await storageAccess.isBlockPersisted('2'); - - // Assert - expect(existsInStorage).toEqual(true); - expect(storageMock.entities.Block.isPersisted).toHaveBeenCalled(); - }); - }); - - describe('#getAccountsByPublicKey', () => { - beforeEach(() => { - // Arrange - storageMock.entities.Account.get.mockResolvedValue(defaultAccounts); - }); - - it('should call storage.Account.get and return accounts', async () => { - // Act - const accountsInStorage = await storageAccess.getAccountsByPublicKey([ - defaultAccounts[0].publicKey, - ]); - - // Assert - expect(accountsInStorage).toEqual(defaultAccounts); - expect(storageMock.entities.Account.get).toHaveBeenCalled(); - }); - }); - - describe('#getAccountsByAddress', () => { - beforeEach(() => { - // Arrange - storageMock.entities.Account.get.mockResolvedValue(defaultAccounts); - }); - - it('should call storage.Account.get and return accounts', async () => { - // Act - const accountsInStorage = await storageAccess.getAccountsByAddress([ - '1L', - '2L', - ]); - - // Assert - expect(accountsInStorage).toEqual(defaultAccounts); - expect(storageMock.entities.Account.get).toHaveBeenCalled(); - }); - }); - - describe('#resetAccountMemTables', () => { - it('should call storage.Account.resetMemTables', async () => { - // Act - await storageAccess.resetAccountMemTables(); - - // Assert - expect(storageMock.entities.Account.resetMemTables).toHaveBeenCalled(); - }); - }); - - describe('#getTransactionsByIDs', () => { - beforeEach(() => { - // Arrange - storageMock.entities.Transaction.get.mockResolvedValue( - defaultTransactions, - ); - }); - - it('should call storage.Transaction.get and return transactions', async () => { - // Act - const transactionsFromStorage = await storageAccess.getTransactionsByIDs([ - '2', - '3', - ]); - - // Assert - expect(transactionsFromStorage).toEqual(defaultTransactions); - expect(storageMock.entities.Transaction.get).toHaveBeenCalled(); - }); - }); - - describe('#isTransactionPersisted', () => { - beforeEach(() => { - // Arrange - storageMock.entities.Transaction.isPersisted.mockResolvedValue(true); - }); - - it('should call storage.Transaction.isTransactionPersisted and return boolean', async () => { - // Act - const existsInStorage = await storageAccess.isTransactionPersisted('1L'); - - // Assert - expect(existsInStorage).toEqual(true); - expect(storageMock.entities.Transaction.isPersisted).toHaveBeenCalled(); - }); - }); -}); diff --git a/elements/lisk-chain/test/unit/process.spec.ts b/elements/lisk-chain/test/unit/process.spec.ts index 9a7b30de2c7..0a2461a82e9 100644 --- a/elements/lisk-chain/test/unit/process.spec.ts +++ b/elements/lisk-chain/test/unit/process.spec.ts @@ -13,6 +13,7 @@ */ import { when } from 'jest-when'; +import { KVStore, NotFoundError } from '@liskhq/lisk-db'; import { transfer, castVotes, @@ -24,7 +25,9 @@ import { getAddressFromPublicKey, } from '@liskhq/lisk-cryptography'; import { newBlock, getBytes, defaultNetworkIdentifier } from '../utils/block'; -import { Chain, StateStore } from '../../src'; +import { Chain } from '../../src/chain'; +import { StateStore } from '../../src/state_store'; +import { DataAccess } from '../../src/data_access'; import * as genesisBlock from '../fixtures/genesis_block.json'; import { genesisAccount } from '../fixtures/default_account'; import { registeredTransactions } from '../utils/registered_transactions'; @@ -32,6 +35,7 @@ import { BlockInstance } from '../../src/types'; import { CHAIN_STATE_BURNT_FEE } from '../../src/constants'; jest.mock('events'); +jest.mock('@liskhq/lisk-db'); describe('blocks/header', () => { const constants = { @@ -56,46 +60,14 @@ describe('blocks/header', () => { ); let chainInstance: Chain; - let storageStub: any; let block: BlockInstance; let blockBytes: Buffer; + let db: any; beforeEach(() => { - storageStub = { - entities: { - Account: { - get: jest.fn(), - upsert: jest.fn(), - getOne: jest.fn(), - }, - Block: { - begin: jest.fn(), - create: jest.fn(), - count: jest.fn(), - getOne: jest.fn(), - delete: jest.fn(), - get: jest.fn(), - isPersisted: jest.fn(), - }, - Transaction: { - get: jest.fn(), - create: jest.fn(), - }, - ChainState: { - get: jest.fn(), - getKey: jest.fn(), - setKey: jest.fn(), - }, - TempBlock: { - create: jest.fn(), - delete: jest.fn(), - get: jest.fn(), - }, - }, - }; - + db = new KVStore('temp'); chainInstance = new Chain({ - storage: storageStub, + db, genesisBlock, networkIdentifier, registeredTransactions, @@ -248,7 +220,13 @@ describe('blocks/header', () => { beforeEach(() => { // Arrange - stateStore = new StateStore(storageStub, { + const dataAccess = new DataAccess({ + db, + maxBlockHeaderCache: 505, + minBlockHeaderCache: 309, + registeredTransactions: {}, + }); + stateStore = new StateStore(dataAccess, { lastBlockHeaders: [], networkIdentifier: defaultNetworkIdentifier, lastBlockReward: BigInt(500000000), @@ -368,7 +346,13 @@ describe('blocks/header', () => { it('should not call apply for the transaction and throw error', async () => { // Arrange - stateStore = new StateStore(storageStub, { + const dataAccess = new DataAccess({ + db, + maxBlockHeaderCache: 505, + minBlockHeaderCache: 309, + registeredTransactions: {}, + }); + stateStore = new StateStore(dataAccess, { lastBlockHeaders: [], networkIdentifier: defaultNetworkIdentifier, lastBlockReward: BigInt(500000000), @@ -393,9 +377,13 @@ describe('blocks/header', () => { beforeEach(() => { // Arrage - storageStub.entities.Account.get.mockResolvedValue([ - { address: genesisAccount.address, balance: '100000000000000' }, - ]); + when(db.get) + .calledWith(`accounts:address:${genesisAccount.address}`) + .mockResolvedValue({ + address: genesisAccount.address, + balance: '100000000000000', + } as never); + invalidTx = chainInstance.deserializeTransaction( transfer({ fee: '10000000', @@ -411,7 +399,13 @@ describe('blocks/header', () => { it('should not call apply for the transaction and throw error', async () => { // Act - stateStore = new StateStore(storageStub, { + const dataAccess = new DataAccess({ + db, + maxBlockHeaderCache: 505, + minBlockHeaderCache: 309, + registeredTransactions: {}, + }); + stateStore = new StateStore(dataAccess, { lastBlockHeaders: [], networkIdentifier: defaultNetworkIdentifier, lastBlockReward: BigInt(500000000), @@ -432,10 +426,6 @@ describe('blocks/header', () => { describe('when skip existing check is false and block exists in database', () => { beforeEach(() => { // Arrage - storageStub.entities.Block.isPersisted.mockResolvedValue(true); - storageStub.entities.Account.get.mockResolvedValue([ - { address: genesisAccount.address, balance: '100000000000000' }, - ]); const validTx = chainInstance.deserializeTransaction( transfer({ fee: '10000000', @@ -447,11 +437,24 @@ describe('blocks/header', () => { }) as TransactionJSON, ); block = newBlock({ transactions: [validTx] }); + when(db.get) + .calledWith(`accounts:address:${genesisAccount.address}`) + .mockResolvedValue({ + address: genesisAccount.address, + balance: '100000000000000', + } as never); + (db.exists as jest.Mock).mockResolvedValue(true as never); }); it('should not call apply for the transaction and throw error', async () => { // Arrange - stateStore = new StateStore(storageStub, { + const dataAccess = new DataAccess({ + db, + maxBlockHeaderCache: 505, + minBlockHeaderCache: 309, + registeredTransactions: {}, + }); + stateStore = new StateStore(dataAccess, { lastBlockHeaders: [], networkIdentifier: defaultNetworkIdentifier, lastBlockReward: BigInt(500000000), @@ -468,10 +471,6 @@ describe('blocks/header', () => { describe('when skip existing check is false and block does not exist in database but transaction does', () => { beforeEach(() => { - // Arrage - storageStub.entities.Account.get.mockResolvedValue([ - { address: genesisAccount.address, balance: '100000000000000' }, - ]); const validTxJSON = transfer({ fee: '10000000', nonce: '0', @@ -483,13 +482,22 @@ describe('blocks/header', () => { const validTx = chainInstance.deserializeTransaction( validTxJSON as TransactionJSON, ); - storageStub.entities.Transaction.get.mockResolvedValue([validTxJSON]); block = newBlock({ transactions: [validTx] }); + when(db.exists) + .mockResolvedValue(false as never) + .calledWith(`transactions:id:${validTx.id}`) + .mockResolvedValue(true as never); }); it('should not call apply for the transaction and throw error', async () => { // Arrange - stateStore = new StateStore(storageStub, { + const dataAccess = new DataAccess({ + db, + maxBlockHeaderCache: 505, + minBlockHeaderCache: 309, + registeredTransactions: {}, + }); + stateStore = new StateStore(dataAccess, { lastBlockHeaders: [], networkIdentifier: defaultNetworkIdentifier, lastBlockReward: BigInt(500000000), @@ -517,26 +525,36 @@ describe('blocks/header', () => { beforeEach(async () => { block = newBlock({ reward: BigInt(500000000) }); - storageStub.entities.Account.get.mockResolvedValue([ - { address: genesisAccount.address, balance: '0' }, - { - address: getAddressFromPublicKey(block.generatorPublicKey), - balance: '0', - }, - ]); - storageStub.entities.ChainState.getKey.mockResolvedValue('100'); - stateStore = new StateStore(storageStub, { + const dataAccess = new DataAccess({ + db, + maxBlockHeaderCache: 505, + minBlockHeaderCache: 309, + registeredTransactions: {}, + }); + stateStore = new StateStore(dataAccess, { lastBlockHeaders: [], networkIdentifier: defaultNetworkIdentifier, lastBlockReward: BigInt(500000000), }); - await stateStore.account.cache({ - // eslint-disable-next-line camelcase - address_in: [ - genesisAccount.address, - getAddressFromPublicKey(block.generatorPublicKey), - ], - }); + when(db.get) + .calledWith(`accounts:address:${genesisAccount.address}`) + .mockResolvedValue({ + address: genesisAccount.address, + balance: '0', + } as never) + .calledWith( + `accounts:address:${getAddressFromPublicKey( + block.generatorPublicKey, + )}`, + ) + .mockResolvedValue({ + address: getAddressFromPublicKey(block.generatorPublicKey), + balance: '0', + } as never) + .calledWith(`chain:burntFee`) + .mockResolvedValue('100' as never); + jest.spyOn(stateStore.chain, 'set'); + // Arrage await chainInstance.apply(block, stateStore); }); @@ -549,7 +567,7 @@ describe('blocks/header', () => { }); it('should not have updated burnt fee', () => { - expect(storageStub.entities.ChainState.getKey).not.toHaveBeenCalled(); + expect(stateStore.chain.set).not.toHaveBeenCalled(); }); }); @@ -557,7 +575,7 @@ describe('blocks/header', () => { let validTx; let stateStore: StateStore; - beforeEach(async () => { + beforeEach(() => { // Arrage validTx = chainInstance.deserializeTransaction( transfer({ @@ -570,23 +588,35 @@ describe('blocks/header', () => { }) as TransactionJSON, ); block = newBlock({ transactions: [validTx] }); - storageStub.entities.Account.get.mockResolvedValue([ - { - address: getAddressFromPublicKey(block.generatorPublicKey), - balance: '0', - }, - { address: genesisAccount.address, balance: '0' }, - ]); // Act - stateStore = new StateStore(storageStub, { + const dataAccess = new DataAccess({ + db, + maxBlockHeaderCache: 505, + minBlockHeaderCache: 309, + registeredTransactions: {}, + }); + stateStore = new StateStore(dataAccess, { lastBlockHeaders: [], networkIdentifier: defaultNetworkIdentifier, lastBlockReward: BigInt(500000000), }); - await stateStore.account.cache({ - // eslint-disable-next-line camelcase - address_in: [genesisAccount.address], - }); + when(db.get) + .calledWith(`accounts:address:123L`) + .mockRejectedValue(new NotFoundError('data not found') as never) + .calledWith(`accounts:address:${genesisAccount.address}`) + .mockResolvedValue({ + address: genesisAccount.address, + balance: '0', + } as never) + .calledWith( + `accounts:address:${getAddressFromPublicKey( + block.generatorPublicKey, + )}`, + ) + .mockResolvedValue({ + address: getAddressFromPublicKey(block.generatorPublicKey), + balance: '0', + } as never); }); it('should throw error', async () => { @@ -675,33 +705,44 @@ describe('blocks/header', () => { reward: BigInt(500000000), transactions: [validTx, validTx2], }); - when(storageStub.entities.Account.get) - .mockResolvedValue([ - { - address: getAddressFromPublicKey(block.generatorPublicKey), - balance: '0', - producedBlocks: 0, - nonce: '0', - }, - { - address: genesisAccount.address, - balance: '1000000000000', - nonce: '0', - }, - delegate1, - delegate2, - ] as never) - .calledWith({ address: '124L' }) - .mockResolvedValue([] as never); - storageStub.entities.ChainState.getKey.mockResolvedValue( - defaultBurntFee, - ); // Act - stateStore = new StateStore(storageStub, { + const dataAccess = new DataAccess({ + db, + maxBlockHeaderCache: 505, + minBlockHeaderCache: 309, + registeredTransactions: {}, + }); + stateStore = new StateStore(dataAccess, { lastBlockHeaders: [], networkIdentifier: defaultNetworkIdentifier, lastBlockReward: BigInt(500000000), }); + when(db.get) + .mockRejectedValue(new NotFoundError('Data not found') as never) + .calledWith(`accounts:address:${genesisAccount.address}`) + .mockResolvedValue({ + address: genesisAccount.address, + balance: '1000000000000', + } as never) + .calledWith( + `accounts:address:${getAddressFromPublicKey( + block.generatorPublicKey, + )}`, + ) + .mockResolvedValue({ + address: getAddressFromPublicKey(block.generatorPublicKey), + balance: '0', + producedBlocks: 0, + nonce: '0', + } as never) + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + .calledWith(`accounts:address:${delegate1.address}`) + .mockResolvedValue(delegate1 as never) + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + .calledWith(`accounts:address:${delegate2.address}`) + .mockResolvedValue(delegate2 as never) + .calledWith(`chain:burntFee`) + .mockResolvedValue(defaultBurntFee as never); await chainInstance.apply(block, stateStore); }); @@ -710,10 +751,6 @@ describe('blocks/header', () => { expect(validTx2ApplySpy).toHaveBeenCalledTimes(1); }); - it('should not call account update', () => { - expect(storageStub.entities.Account.upsert).not.toHaveBeenCalled(); - }); - it('should add produced block for generator', async () => { const generator = await stateStore.account.get( getAddressFromPublicKey(block.generatorPublicKey), @@ -751,12 +788,20 @@ describe('blocks/header', () => { beforeEach(async () => { // Arrage - storageStub.entities.Account.get.mockResolvedValue([]); + (db.get as jest.Mock).mockRejectedValue( + new NotFoundError('no data found'), + ); // Act genesisInstance = chainInstance.deserialize(genesisBlock as any); genesisInstance.transactions.forEach(tx => tx.validate()); // Act - stateStore = new StateStore(storageStub, { + const dataAccess = new DataAccess({ + db, + maxBlockHeaderCache: 505, + minBlockHeaderCache: 309, + registeredTransactions: {}, + }); + stateStore = new StateStore(dataAccess, { lastBlockHeaders: [], networkIdentifier: defaultNetworkIdentifier, lastBlockReward: BigInt(500000000), @@ -775,10 +820,6 @@ describe('blocks/header', () => { ); }); - it('should not call account update', () => { - expect(storageStub.entities.Account.upsert).not.toHaveBeenCalled(); - }); - it('should not update burnt fee on chain state', async () => { const genesisAccountFromStore = await stateStore.chain.get( CHAIN_STATE_BURNT_FEE, @@ -795,27 +836,37 @@ describe('blocks/header', () => { let stateStore: StateStore; beforeEach(async () => { - stateStore = new StateStore(storageStub, { + const dataAccess = new DataAccess({ + db, + maxBlockHeaderCache: 505, + minBlockHeaderCache: 309, + registeredTransactions: {}, + }); + stateStore = new StateStore(dataAccess, { lastBlockHeaders: [], networkIdentifier: defaultNetworkIdentifier, lastBlockReward: BigInt(500000000), }); // Arrage block = newBlock({ reward }); - storageStub.entities.Account.get.mockResolvedValue([ - { + when(db.get) + .calledWith(`accounts:address:${genesisAccount.address}`) + .mockResolvedValue({ + address: genesisAccount.address, + balance: '0', + } as never) + .calledWith( + `accounts:address:${getAddressFromPublicKey( + block.generatorPublicKey, + )}`, + ) + .mockResolvedValue({ address: getAddressFromPublicKey(block.generatorPublicKey), balance: reward.toString(), - }, - { address: genesisAccount.address, balance: '0' }, - ]); + } as never); await chainInstance.undo(block, stateStore); }); - it('should not call account update', () => { - expect(storageStub.entities.Account.upsert).not.toHaveBeenCalled(); - }); - it('should update generator balance to debit rewards and fees - minFee', async () => { const generator = await stateStore.account.get( getAddressFromPublicKey(block.generatorPublicKey), @@ -824,7 +875,7 @@ describe('blocks/header', () => { }); it('should not deduct burntFee from chain state', () => { - expect(storageStub.entities.ChainState.getKey).not.toHaveBeenCalled(); + expect(db.get).not.toHaveBeenCalledWith('chain:burntFee'); }); }); @@ -902,13 +953,22 @@ describe('blocks/header', () => { reward: BigInt(defaultReward), transactions: [validTx, validTx2], }); - storageStub.entities.Account.get.mockResolvedValue([ - { - address: getAddressFromPublicKey(block.generatorPublicKey), - balance: defaultGeneratorBalance.toString(), - producedBlocks: 1, - }, - { + + // Act + const dataAccess = new DataAccess({ + db, + maxBlockHeaderCache: 505, + minBlockHeaderCache: 309, + registeredTransactions: {}, + }); + stateStore = new StateStore(dataAccess, { + lastBlockHeaders: [], + networkIdentifier: defaultNetworkIdentifier, + lastBlockReward: BigInt(500000000), + }); + when(db.get) + .calledWith(`accounts:address:${genesisAccount.address}`) + .mockResolvedValue({ address: genesisAccount.address, balance: '9889999900', votes: [ @@ -921,21 +981,27 @@ describe('blocks/header', () => { amount: '10000000000', }, ], - }, - delegate1, - delegate2, - recipient, - ]); - storageStub.entities.ChainState.getKey.mockResolvedValue( - defaultBurntFee.toString(), - ); - - // Act - stateStore = new StateStore(storageStub, { - lastBlockHeaders: [], - networkIdentifier: defaultNetworkIdentifier, - lastBlockReward: BigInt(500000000), - }); + } as never) + .calledWith( + `accounts:address:${getAddressFromPublicKey( + block.generatorPublicKey, + )}`, + ) + .mockResolvedValue({ + address: getAddressFromPublicKey(block.generatorPublicKey), + balance: defaultGeneratorBalance.toString(), + producedBlocks: 1, + } as never) + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + .calledWith(`accounts:address:${delegate1.address}`) + .mockResolvedValue(delegate1 as never) + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + .calledWith(`accounts:address:${delegate2.address}`) + .mockResolvedValue(delegate2 as never) + .calledWith(`accounts:address:${recipient.address}`) + .mockResolvedValue(recipient as never) + .calledWith(`chain:burntFee`) + .mockResolvedValue(defaultBurntFee.toString() as never); await chainInstance.undo(block, stateStore); }); @@ -944,10 +1010,6 @@ describe('blocks/header', () => { expect(validTx2UndoSpy).toHaveBeenCalledTimes(1); }); - it('should not call account update', () => { - expect(storageStub.entities.Account.upsert).not.toHaveBeenCalled(); - }); - it('should reduce produced block for generator', async () => { const generator = await stateStore.account.get( getAddressFromPublicKey(block.generatorPublicKey), diff --git a/elements/lisk-chain/test/unit/state_store_account.spec.ts b/elements/lisk-chain/test/unit/state_store_account.spec.ts index 7cdb14de92c..516f3eda7a8 100644 --- a/elements/lisk-chain/test/unit/state_store_account.spec.ts +++ b/elements/lisk-chain/test/unit/state_store_account.spec.ts @@ -11,11 +11,14 @@ * * Removal or modification of this copyright notice is prohibited. */ +import { KVStore, BatchChain, NotFoundError } from '@liskhq/lisk-db'; import { when } from 'jest-when'; import { StateStore } from '../../src'; -import { StorageTransaction } from '../../src/types'; +import { DataAccess } from '../../src/data_access'; import { Account, accountDefaultValues } from '../../src/account'; +jest.mock('@liskhq/lisk-db'); + describe('state store / account', () => { const defaultAccounts = [ { @@ -28,6 +31,11 @@ describe('state store / account', () => { address: '5059876081639179984L', balance: '555', }, + { + ...accountDefaultValues, + address: '1059876081639179984L', + balance: '444', + }, ]; const stateStoreAccounts = [ @@ -44,110 +52,64 @@ describe('state store / account', () => { ]; let stateStore: StateStore; - let storageStub: any; + let db: any; beforeEach(() => { - storageStub = { - entities: { - Account: { - get: jest.fn(), - upsert: jest.fn(), - }, - }, - }; - stateStore = new StateStore(storageStub, { + db = new KVStore('temp'); + const dataAccess = new DataAccess({ + db, + maxBlockHeaderCache: 505, + minBlockHeaderCache: 309, + registeredTransactions: {}, + }); + stateStore = new StateStore(dataAccess, { lastBlockHeaders: [], networkIdentifier: 'network-identifier', lastBlockReward: BigInt(500000000), }); - }); - - describe('cache', () => { - beforeEach(() => { - // Arrange - storageStub.entities.Account.get.mockResolvedValue(defaultAccounts); - }); - - it('should call storage get and store in cache', async () => { - // Act - const filter = [ - { address: defaultAccounts[0].address }, - { address: defaultAccounts[1].address }, - ]; - const results = await stateStore.account.cache(filter); - // Assert - expect(results).toHaveLength(2); - expect(results.map(account => account.address)).toStrictEqual([ - defaultAccounts[0].address, - defaultAccounts[1].address, - ]); - }); - - it('should cache to the state store', async () => { - // Act - const filter = [ - { address: defaultAccounts[0].address }, - { address: defaultAccounts[1].address }, - ]; - await stateStore.account.cache(filter); - // Assert - expect((stateStore.account as any)._data).toStrictEqual( - stateStoreAccounts, - ); - }); + // Setting this as default behavior throws UnhandledPromiseRejection, so it is specifiying the non-existing account + const dbGetMock = when(db.get) + .calledWith('accounts:address:123L') + .mockRejectedValue(new NotFoundError('Data not found') as never); + for (const account of defaultAccounts) { + dbGetMock + .calledWith(`accounts:address:${account.address}`) + .mockResolvedValue(account as never); + } + stateStore.account['_data'] = [...stateStoreAccounts]; }); describe('get', () => { - beforeEach(async () => { - // Arrange - storageStub.entities.Account.get.mockResolvedValue(defaultAccounts); - - const filter = [ - { address: defaultAccounts[0].address }, - { address: defaultAccounts[1].address }, - ]; - await stateStore.account.cache(filter); - }); - it('should get the account', async () => { // Act const account = await stateStore.account.get(defaultAccounts[0].address); // Assert expect(account).toStrictEqual(stateStoreAccounts[0]); + expect(db.get).not.toHaveBeenCalled(); }); it('should try to get account from db if not found in memory', async () => { // Act - await stateStore.account.get('321L'); + await stateStore.account.get(defaultAccounts[2].address); // Assert - expect(storageStub.entities.Account.get.mock.calls[1]).toEqual([ - { address: '321L' }, - { limit: null }, - ]); + expect(db.get).toHaveBeenCalledWith( + `accounts:address:${defaultAccounts[2].address}`, + ); }); it('should throw an error if not exist', async () => { - when(storageStub.entities.Account.get) - .calledWith({ address: '123L' }) - .mockResolvedValue([] as never); // Act && Assert - await expect(stateStore.account.get('123L')).rejects.toThrow( - 'does not exist', - ); + expect.assertions(1); + try { + await stateStore.account.get('123L'); + } catch (error) { + // eslint-disable-next-line jest/no-try-expect + expect(error).toBeInstanceOf(NotFoundError); + } }); }); describe('getOrDefault', () => { - beforeEach(async () => { - // Arrange - storageStub.entities.Account.get.mockResolvedValue(defaultAccounts); - const filter = [ - { address: defaultAccounts[0].address }, - { address: defaultAccounts[1].address }, - ]; - await stateStore.account.cache(filter); - }); - it('should get the account', async () => { // Act const account = await stateStore.account.getOrDefault( @@ -159,17 +121,15 @@ describe('state store / account', () => { it('should try to get account from db if not found in memory', async () => { // Act - await stateStore.account.getOrDefault('321L'); + await stateStore.account.get(defaultAccounts[2].address); // Assert - expect(storageStub.entities.Account.get.mock.calls[1]).toEqual([ - { address: '321L' }, - { limit: null }, - ]); + expect(db.get).toHaveBeenCalledWith( + `accounts:address:${defaultAccounts[2].address}`, + ); }); it('should get the default account', async () => { // Arrange - storageStub.entities.Account.get.mockResolvedValueOnce([]); // Act const account = await stateStore.account.getOrDefault('123L'); // Assert @@ -184,16 +144,10 @@ describe('state store / account', () => { let missedBlocks: number; let producedBlocks: number; - beforeEach(async () => { + beforeEach(() => { // Arrange missedBlocks = 1; producedBlocks = 1; - storageStub.entities.Account.get.mockResolvedValue(defaultAccounts); - const filter = [ - { address: defaultAccounts[0].address }, - { address: defaultAccounts[1].address }, - ]; - await stateStore.account.cache(filter); }); it('should set the updated values for the account', async () => { @@ -214,7 +168,6 @@ describe('state store / account', () => { }); it('should update the updateKeys property', async () => { - const updatedKeys = ['producedBlocks', 'missedBlocks']; const existingAccount = await stateStore.account.get( defaultAccounts[0].address, ); @@ -226,36 +179,24 @@ describe('state store / account', () => { stateStore.account.set(defaultAccounts[0].address, updatedAccount); - expect((stateStore.account as any)._updatedKeys[0]).toStrictEqual( - updatedKeys, - ); + expect( + stateStore.account['_updatedKeys'].has(defaultAccounts[0].address), + ).toBeTrue(); }); }); describe('finalize', () => { - const txStub = {} as StorageTransaction; let existingAccount; - let updatedAccount; + let updatedAccount: Account; let missedBlocks: number; let producedBlocks: number; - let accountUpsertObj: object; + let batchStub: BatchChain; beforeEach(async () => { missedBlocks = 1; producedBlocks = 1; - accountUpsertObj = { - missedBlocks, - producedBlocks, - }; - - storageStub.entities.Account.get.mockResolvedValue(defaultAccounts); - - const filter = [ - { address: defaultAccounts[0].address }, - { address: defaultAccounts[1].address }, - ]; - await stateStore.account.cache(filter); + batchStub = { put: jest.fn() } as any; existingAccount = await stateStore.account.get( defaultAccounts[0].address, @@ -269,14 +210,12 @@ describe('state store / account', () => { stateStore.account.set(updatedAccount.address, updatedAccount); }); - it('should save the account state in the database', async () => { - await stateStore.account.finalize(txStub); + it('should save the account state in the database', () => { + stateStore.account.finalize(batchStub); - expect(storageStub.entities.Account.upsert).toHaveBeenCalledWith( - { address: defaultAccounts[0].address }, - accountUpsertObj, - null, - txStub, + expect(batchStub.put).toHaveBeenCalledWith( + `accounts:address:${updatedAccount.address}`, + updatedAccount.toJSON(), ); }); }); diff --git a/elements/lisk-chain/test/unit/state_store_chain_state.spec.ts b/elements/lisk-chain/test/unit/state_store_chain_state.spec.ts index 7d0f31f1fbc..ddbcd2aeca3 100644 --- a/elements/lisk-chain/test/unit/state_store_chain_state.spec.ts +++ b/elements/lisk-chain/test/unit/state_store_chain_state.spec.ts @@ -11,12 +11,17 @@ * * Removal or modification of this copyright notice is prohibited. */ +import { KVStore, BatchChain } from '@liskhq/lisk-db'; +import { when } from 'jest-when'; import { StateStore } from '../../src'; -import { StorageTransaction, BlockHeader } from '../../src/types'; +import { DataAccess } from '../../src/data_access'; +import { BlockHeader } from '../../src/types'; + +jest.mock('@liskhq/lisk-db'); describe('state store / chain_state', () => { let stateStore: StateStore; - let storageStub: any; + let db: any; const lastBlockHeaders = ([ { height: 30 }, @@ -24,16 +29,14 @@ describe('state store / chain_state', () => { ] as unknown) as ReadonlyArray; beforeEach(() => { - storageStub = { - entities: { - ChainState: { - get: jest.fn(), - getKey: jest.fn(), - setKey: jest.fn(), - }, - }, - }; - stateStore = new StateStore(storageStub, { + db = new KVStore('temp'); + const dataAccess = new DataAccess({ + db, + maxBlockHeaderCache: 505, + minBlockHeaderCache: 309, + registeredTransactions: {}, + }); + stateStore = new StateStore(dataAccess, { lastBlockHeaders, networkIdentifier: 'network-identifier-chain-1', lastBlockReward: BigInt(500000000), @@ -60,46 +63,24 @@ describe('state store / chain_state', () => { }); }); - describe('cache', () => { - it('should call storage get and store in cache', async () => { - // Arrange - storageStub.entities.ChainState.get.mockResolvedValue([ - { key: 'key1', value: 'value1' }, - { key: 'key2', value: 'value2' }, - ]); - // Act - await stateStore.chain.cache(); - // Assert - expect(await stateStore.chain.get('key1')).toBe('value1'); - expect(await stateStore.chain.get('key2')).toBe('value2'); - }); - }); - describe('get', () => { it('should get value from cache', async () => { // Arrange - storageStub.entities.ChainState.get.mockResolvedValue([ - { key: 'key1', value: 'value1' }, - { key: 'key2', value: 'value2' }, - ]); - await stateStore.chain.cache(); + stateStore.chain.set('key1', 'value1'); + when(db.get) + .calledWith('chain:key1') + .mockResolvedValue('value5' as never); // Act & Assert expect(await stateStore.chain.get('key1')).toEqual('value1'); }); it('should try to get value from database if not in cache', async () => { // Arrange - storageStub.entities.ChainState.get.mockResolvedValue([ - { key: 'key1', value: 'value1' }, - { key: 'key2', value: 'value2' }, - ]); - await stateStore.chain.cache(); - // Act - await stateStore.chain.get('key3'); - // Assert - expect(storageStub.entities.ChainState.getKey.mock.calls[0]).toEqual([ - 'key3', - ]); + when(db.get) + .calledWith('chain:key1') + .mockResolvedValue('value5' as never); + // Act & Assert + expect(await stateStore.chain.get('key1')).toEqual('value5'); }); }); @@ -123,45 +104,28 @@ describe('state store / chain_state', () => { }); describe('finalize', () => { - const txStub = {} as StorageTransaction; + let batchStub: BatchChain; - it('should not call storage if nothing is set', async () => { - // Act - await stateStore.chain.finalize(txStub); - // Assert - expect(storageStub.entities.ChainState.setKey).not.toHaveBeenCalled(); + beforeEach(() => { + batchStub = { put: jest.fn() } as any; }); - it('should call storage for all the updated keys', async () => { + it('should not call storage if nothing is set', () => { // Act - stateStore.chain.set('key3', 'value3'); - stateStore.chain.set('key3', 'value4'); - stateStore.chain.set('key4', 'value5'); - await stateStore.chain.finalize(txStub); + stateStore.chain.finalize(batchStub); // Assert - expect(storageStub.entities.ChainState.setKey).toHaveBeenCalledWith( - 'key3', - 'value4', - txStub, - ); - expect(storageStub.entities.ChainState.setKey).toHaveBeenCalledWith( - 'key4', - 'value5', - txStub, - ); + expect(batchStub.put).not.toHaveBeenCalled(); }); - it('should handle promise rejection', async () => { - // Prepare - storageStub.entities.ChainState.setKey.mockImplementation(async () => - Promise.reject(new Error('Fake storage layer error')), - ); + it('should call storage for all the updated keys', () => { // Act stateStore.chain.set('key3', 'value3'); + stateStore.chain.set('key3', 'value4'); + stateStore.chain.set('key4', 'value5'); + stateStore.chain.finalize(batchStub); // Assert - return expect(stateStore.chain.finalize(txStub)).rejects.toThrow( - 'Fake storage layer error', - ); + expect(batchStub.put).toHaveBeenCalledWith('chain:key3', 'value4'); + expect(batchStub.put).toHaveBeenCalledWith('chain:key4', 'value5'); }); }); }); diff --git a/elements/lisk-chain/test/unit/state_store_consensus_state.spec.ts b/elements/lisk-chain/test/unit/state_store_consensus_state.spec.ts index 72d200e5fd3..026507cb376 100644 --- a/elements/lisk-chain/test/unit/state_store_consensus_state.spec.ts +++ b/elements/lisk-chain/test/unit/state_store_consensus_state.spec.ts @@ -11,12 +11,17 @@ * * Removal or modification of this copyright notice is prohibited. */ +import { KVStore, BatchChain } from '@liskhq/lisk-db'; +import { when } from 'jest-when'; import { StateStore } from '../../src'; -import { StorageTransaction, BlockHeader } from '../../src/types'; +import { BlockHeader } from '../../src/types'; +import { DataAccess } from '../../src/data_access'; + +jest.mock('@liskhq/lisk-db'); describe('state store / chain_state', () => { let stateStore: StateStore; - let storageStub: any; + let db: any; const lastBlockHeaders = ([ { height: 30 }, @@ -24,16 +29,14 @@ describe('state store / chain_state', () => { ] as unknown) as ReadonlyArray; beforeEach(() => { - storageStub = { - entities: { - ConsensusState: { - get: jest.fn(), - getKey: jest.fn(), - setKey: jest.fn(), - }, - }, - }; - stateStore = new StateStore(storageStub, { + db = new KVStore('temp'); + const dataAccess = new DataAccess({ + db, + maxBlockHeaderCache: 505, + minBlockHeaderCache: 309, + registeredTransactions: {}, + }); + stateStore = new StateStore(dataAccess, { lastBlockHeaders, networkIdentifier: 'network-identifier-chain-1', lastBlockReward: BigInt(500000000), @@ -46,46 +49,24 @@ describe('state store / chain_state', () => { }); }); - describe('cache', () => { - it('should call storage get and store in cache', async () => { - // Arrange - storageStub.entities.ConsensusState.get.mockResolvedValue([ - { key: 'key1', value: 'value1' }, - { key: 'key2', value: 'value2' }, - ]); - // Act - await stateStore.consensus.cache(); - // Assert - expect(await stateStore.consensus.get('key1')).toBe('value1'); - expect(await stateStore.consensus.get('key2')).toBe('value2'); - }); - }); - describe('get', () => { it('should get value from cache', async () => { // Arrange - storageStub.entities.ConsensusState.get.mockResolvedValue([ - { key: 'key1', value: 'value1' }, - { key: 'key2', value: 'value2' }, - ]); - await stateStore.consensus.cache(); + stateStore.consensus.set('key1', 'value1'); + when(db.get) + .calledWith('consensus:key1') + .mockResolvedValue('value5' as never); // Act & Assert expect(await stateStore.consensus.get('key1')).toEqual('value1'); }); it('should try to get value from database if not in cache', async () => { // Arrange - storageStub.entities.ConsensusState.get.mockResolvedValue([ - { key: 'key1', value: 'value1' }, - { key: 'key2', value: 'value2' }, - ]); - await stateStore.consensus.cache(); - // Act - await stateStore.consensus.get('key3'); - // Assert - expect(storageStub.entities.ConsensusState.getKey.mock.calls[0]).toEqual([ - 'key3', - ]); + when(db.get) + .calledWith('consensus:key1') + .mockResolvedValue('value5' as never); + // Act & Assert + expect(await stateStore.consensus.get('key1')).toEqual('value5'); }); }); @@ -109,45 +90,28 @@ describe('state store / chain_state', () => { }); describe('finalize', () => { - const txStub = {} as StorageTransaction; + let batchStub: BatchChain; - it('should not call storage if nothing is set', async () => { - // Act - await stateStore.consensus.finalize(txStub); - // Assert - expect(storageStub.entities.ConsensusState.setKey).not.toHaveBeenCalled(); + beforeEach(() => { + batchStub = { put: jest.fn() } as any; }); - it('should call storage for all the updated keys', async () => { + it('should not call storage if nothing is set', () => { // Act - stateStore.consensus.set('key3', 'value3'); - stateStore.consensus.set('key3', 'value4'); - stateStore.consensus.set('key4', 'value5'); - await stateStore.consensus.finalize(txStub); + stateStore.consensus.finalize(batchStub); // Assert - expect(storageStub.entities.ConsensusState.setKey).toHaveBeenCalledWith( - 'key3', - 'value4', - txStub, - ); - expect(storageStub.entities.ConsensusState.setKey).toHaveBeenCalledWith( - 'key4', - 'value5', - txStub, - ); + expect(batchStub.put).not.toHaveBeenCalled(); }); - it('should handle promise rejection', async () => { - // Prepare - storageStub.entities.ConsensusState.setKey.mockImplementation(async () => - Promise.reject(new Error('Fake storage layer error')), - ); + it('should call storage for all the updated keys', () => { // Act stateStore.consensus.set('key3', 'value3'); + stateStore.consensus.set('key3', 'value4'); + stateStore.consensus.set('key4', 'value5'); + stateStore.consensus.finalize(batchStub); // Assert - return expect(stateStore.consensus.finalize(txStub)).rejects.toThrow( - 'Fake storage layer error', - ); + expect(batchStub.put).toHaveBeenCalledWith('consensus:key3', 'value4'); + expect(batchStub.put).toHaveBeenCalledWith('consensus:key4', 'value5'); }); }); }); diff --git a/elements/lisk-chain/test/unit/transactions.spec.ts b/elements/lisk-chain/test/unit/transactions.spec.ts index c98ae0dad5c..ab0464603b0 100644 --- a/elements/lisk-chain/test/unit/transactions.spec.ts +++ b/elements/lisk-chain/test/unit/transactions.spec.ts @@ -11,6 +11,8 @@ * * Removal or modification of this copyright notice is prohibited. */ +import { when } from 'jest-when'; +import { Readable } from 'stream'; import { transfer, castVotes, @@ -19,13 +21,15 @@ import { registerDelegate, TransactionResponse, } from '@liskhq/lisk-transactions'; +import { KVStore, NotFoundError } from '@liskhq/lisk-db'; import { getNetworkIdentifier } from '@liskhq/lisk-cryptography'; -import { Chain, GenesisBlockJSON } from '../../src'; +import { Chain } from '../../src'; import * as genesisBlock from '../fixtures/genesis_block.json'; import { genesisAccount } from '../fixtures/default_account'; import { registeredTransactions } from '../utils/registered_transactions'; jest.mock('events'); +jest.mock('@liskhq/lisk-db'); describe('blocks/transactions', () => { const constants = { @@ -49,45 +53,15 @@ describe('blocks/transactions', () => { ); let chainInstance: Chain; - let storageStub: any; + let db: any; beforeEach(() => { - storageStub = { - entities: { - Account: { - get: jest.fn(), - getOne: jest.fn(), - update: jest.fn(), - }, - Block: { - begin: jest.fn(), - create: jest.fn(), - count: jest.fn(), - getOne: jest.fn(), - delete: jest.fn(), - get: jest.fn(), - isPersisted: jest.fn(), - }, - Transaction: { - get: jest.fn(), - create: jest.fn(), - }, - TempBlock: { - create: jest.fn(), - delete: jest.fn(), - get: jest.fn(), - }, - }, - }; - - storageStub.entities.Block.get.mockResolvedValue([ - { height: 40 }, - { height: 39 }, - ]); + db = new KVStore('temp'); + (db.createReadStream as jest.Mock).mockReturnValue(Readable.from([])); chainInstance = new Chain({ - storage: storageStub, - genesisBlock: genesisBlock as GenesisBlockJSON, + db, + genesisBlock, networkIdentifier, registeredTransactions, ...constants, @@ -102,16 +76,20 @@ describe('blocks/transactions', () => { describe('when transactions include not allowed transaction based on the context', () => { it('should return transaction which are allowed', async () => { // Arrange - storageStub.entities.Account.get.mockResolvedValue([ - { address: genesisAccount.address, balance: '10000000000' }, - ]); + when(db.get) + .mockRejectedValue(new NotFoundError('data not found') as never) + .calledWith(`accounts:address:${genesisAccount.address}`) + .mockResolvedValue({ + address: genesisAccount.address, + balance: '1000000000000', + } as never); const validTx = chainInstance.deserializeTransaction( transfer({ fee: '10000000', nonce: '0', passphrase: genesisAccount.passphrase, recipientId: '123L', - amount: '100', + amount: '10000000000', networkIdentifier, }) as TransactionJSON, ); @@ -149,11 +127,13 @@ describe('blocks/transactions', () => { describe('when transactions include not applicable transaction', () => { it('should return transaction which are applicable', async () => { // Arrange - storageStub.entities.Account.get - .mockResolvedValueOnce([ - { address: genesisAccount.address, balance: '210000000' }, - ]) - .mockResolvedValue([]); + when(db.get) + .mockRejectedValue(new NotFoundError('data not found') as never) + .calledWith(`accounts:address:${genesisAccount.address}`) + .mockResolvedValue({ + address: genesisAccount.address, + balance: '210000000', + } as never); const validTx = chainInstance.deserializeTransaction( transfer({ fee: '10000000', @@ -194,26 +174,30 @@ describe('blocks/transactions', () => { beforeEach(async () => { // Arrange - storageStub.entities.Account.get.mockResolvedValue([ - { address: genesisAccount.address, balance: '100000000' }, - ]); + when(db.get) + .mockRejectedValue(new NotFoundError('data not found') as never) + .calledWith(`accounts:address:${genesisAccount.address}`) + .mockResolvedValue({ + address: genesisAccount.address, + balance: '10000000000000', + } as never); validTx = chainInstance.deserializeTransaction( transfer({ fee: '10000000', nonce: '0', passphrase: genesisAccount.passphrase, recipientId: '123L', - amount: '100', + amount: '100000000', networkIdentifier, }) as TransactionJSON, ); validTx2 = chainInstance.deserializeTransaction( transfer({ fee: '10000000', - nonce: '0', + nonce: '1', passphrase: genesisAccount.passphrase, recipientId: '124L', - amount: '100', + amount: '100000000', networkIdentifier, }) as TransactionJSON, ); @@ -245,9 +229,12 @@ describe('blocks/transactions', () => { describe('when transactions include not allowed transaction based on the context', () => { it('should return transaction response corresponds to the setup', async () => { // Arrange - storageStub.entities.Account.get.mockResolvedValue([ - { address: genesisAccount.address, balance: '10000000000' }, - ]); + when(db.get) + .calledWith(`accounts:address:${genesisAccount.address}`) + .mockResolvedValue({ + address: genesisAccount.address, + balance: '10000000000', + } as never); const validTx = chainInstance.deserializeTransaction( transfer({ passphrase: genesisAccount.passphrase, @@ -301,9 +288,12 @@ describe('blocks/transactions', () => { describe('when transactions include invalid transaction', () => { it('should return transaction response corresponds to the setup', async () => { // Arrange - storageStub.entities.Account.get.mockResolvedValue([ - { address: genesisAccount.address, balance: '10000000000' }, - ]); + when(db.get) + .calledWith(`accounts:address:${genesisAccount.address}`) + .mockResolvedValue({ + address: genesisAccount.address, + balance: '10000000000', + } as never); const validTx = chainInstance.deserializeTransaction( transfer({ fee: '10000000', @@ -352,9 +342,12 @@ describe('blocks/transactions', () => { beforeEach(async () => { // Arrange - storageStub.entities.Account.get.mockResolvedValue([ - { address: genesisAccount.address, balance: '100000000' }, - ]); + when(db.get) + .calledWith(`accounts:address:${genesisAccount.address}`) + .mockResolvedValue({ + address: genesisAccount.address, + balance: '100000000', + } as never); const validTx = chainInstance.deserializeTransaction( transfer({ fee: '10000000', @@ -403,17 +396,20 @@ describe('blocks/transactions', () => { describe('when transactions include not allowed transaction based on the context', () => { it('should return transaction response corresponds to the setup', async () => { // Arrange - storageStub.entities.Account.get.mockResolvedValue([ - { address: genesisAccount.address, balance: '10000000000' }, - ]); - storageStub.entities.Transaction.get.mockResolvedValue([]); + when(db.get) + .mockRejectedValue(new NotFoundError('data not found') as never) + .calledWith(`accounts:address:${genesisAccount.address}`) + .mockResolvedValue({ + address: genesisAccount.address, + balance: '100000000000', + } as never); const validTx = chainInstance.deserializeTransaction( transfer({ fee: '10000000', nonce: '0', passphrase: genesisAccount.passphrase, recipientId: '123L', - amount: '100', + amount: '100000000', networkIdentifier, }) as TransactionJSON, ); @@ -460,16 +456,20 @@ describe('blocks/transactions', () => { describe('when transactions include existing transaction in database', () => { it('should return status FAIL for the existing transaction', async () => { // Arrange - storageStub.entities.Account.get.mockResolvedValue([ - { address: genesisAccount.address, balance: '100000000' }, - ]); + when(db.get) + .mockRejectedValue(new NotFoundError('data not found') as never) + .calledWith(`accounts:address:${genesisAccount.address}`) + .mockResolvedValue({ + address: genesisAccount.address, + balance: '10000000000', + } as never); const validTx = chainInstance.deserializeTransaction( transfer({ fee: '10000000', nonce: '0', passphrase: genesisAccount.passphrase, recipientId: '123L', - amount: '100', + amount: '100000000', networkIdentifier, }) as TransactionJSON, ); @@ -479,13 +479,14 @@ describe('blocks/transactions', () => { nonce: '0', passphrase: genesisAccount.passphrase, recipientId: '124L', - amount: '100', + amount: '100000000', networkIdentifier, }) as TransactionJSON, ); - storageStub.entities.Transaction.get.mockResolvedValue([ - validTx2.toJSON(), - ]); + when(db.exists) + .mockResolvedValue(false as never) + .calledWith(`transactions:id:${validTx2.id}`) + .mockResolvedValue(true as never); // Act const transactionsResponses = await chainInstance.applyTransactions([ validTx, @@ -527,15 +528,18 @@ describe('blocks/transactions', () => { '2c638a3b2fccbde21b6773a595e2abf697fbda1a5b8495f040f79a118e0b291c', username: 'genesis_201', }; - storageStub.entities.Account.get.mockResolvedValue([ - { + when(db.get) + .mockRejectedValue(new NotFoundError('data not found') as never) + .calledWith(`accounts:address:${genesisAccount.address}`) + .mockResolvedValue({ address: genesisAccount.address, balance: '10000000000', - }, - delegate1, - delegate2, - ]); - storageStub.entities.Transaction.get.mockResolvedValue([]); + } as never) + .calledWith(`accounts:address:${delegate1.address}`) + .mockResolvedValue(delegate1 as never) + .calledWith(`accounts:address:${delegate2.address}`) + .mockResolvedValue(delegate2 as never); + (db.exists as jest.Mock).mockResolvedValue(false as never); // Act const validTx = chainInstance.deserializeTransaction( castVotes({ @@ -561,7 +565,7 @@ describe('blocks/transactions', () => { nonce: '1', passphrase: genesisAccount.passphrase, recipientId: '124L', - amount: '100', + amount: '100000000', networkIdentifier, }) as TransactionJSON, ); diff --git a/elements/lisk-chain/test/unit/transactions_handlers.spec.ts b/elements/lisk-chain/test/unit/transactions_handlers.spec.ts index 08e9d7db932..4f36344edaf 100644 --- a/elements/lisk-chain/test/unit/transactions_handlers.spec.ts +++ b/elements/lisk-chain/test/unit/transactions_handlers.spec.ts @@ -12,6 +12,7 @@ * Removal or modification of this copyright notice is prohibited. */ +import { when } from 'jest-when'; import { Status as TransactionStatus, TransactionResponse, @@ -38,10 +39,6 @@ describe('transactions', () => { trs1.matcher = (): boolean => true; trs2.matcher = (): boolean => true; - // Add prepare steps to transactions - trs1.prepare = jest.fn(); - trs2.prepare = jest.fn(); - // Add apply steps to transactions trs1.apply = jest.fn(); trs2.apply = jest.fn(); @@ -65,7 +62,7 @@ describe('transactions', () => { }; dataAccessMock = { - getTransactionsByIDs: jest.fn().mockReturnValue([]), + isTransactionPersisted: jest.fn().mockResolvedValue(false), }; }); @@ -215,23 +212,28 @@ describe('transactions', () => { }); it('should invoke entities.Transaction to check persistence of transactions', async () => { - dataAccessMock.getTransactionsByIDs.mockResolvedValue([trs1, trs2]); - await transactionHandlers.checkPersistedTransactions(dataAccessMock)([ trs1, trs2, ]); - expect(dataAccessMock.getTransactionsByIDs).toHaveBeenCalledTimes(1); - expect(dataAccessMock.getTransactionsByIDs).toHaveBeenCalledWith([ + expect(dataAccessMock.isTransactionPersisted).toHaveBeenCalledTimes(2); + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + expect(dataAccessMock.isTransactionPersisted).toHaveBeenCalledWith( trs1.id, + ); + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + expect(dataAccessMock.isTransactionPersisted).toHaveBeenCalledWith( trs2.id, - ]); + ); }); it('should return TransactionStatus.OK for non-persisted transactions', async () => { // Treat trs1 as persisted transaction - dataAccessMock.getTransactionsByIDs.mockResolvedValue([trs1]); + when(dataAccessMock.isTransactionPersisted) + .mockResolvedValue(false as never) + .calledWith(trs1.id) + .mockResolvedValue(true as never); const result = await transactionHandlers.checkPersistedTransactions( dataAccessMock, @@ -239,13 +241,16 @@ describe('transactions', () => { const transactionResponse = result.find(({ id }) => id === trs2.id); - expect((transactionResponse as any).status).toEqual(TransactionStatus.OK); - expect((transactionResponse as any).errors).toEqual([]); + expect(transactionResponse?.status).toEqual(TransactionStatus.OK); + expect(transactionResponse?.errors).toEqual([]); }); it('should return TransactionStatus.FAIL for persisted transactions', async () => { // Treat trs1 as persisted transaction - dataAccessMock.getTransactionsByIDs.mockResolvedValue([trs1]); + when(dataAccessMock.isTransactionPersisted) + .mockResolvedValue(false as never) + .calledWith(trs1.id) + .mockResolvedValue(true as never); const result = await transactionHandlers.checkPersistedTransactions( dataAccessMock, @@ -253,11 +258,9 @@ describe('transactions', () => { const transactionResponse = result.find(({ id }) => id === trs1.id); - expect((transactionResponse as any).status).toEqual( - TransactionStatus.FAIL, - ); - expect((transactionResponse as any).errors).toHaveLength(1); - expect((transactionResponse as any).errors[0].message).toEqual( + expect(transactionResponse?.status).toEqual(TransactionStatus.FAIL); + expect(transactionResponse?.errors).toHaveLength(1); + expect(transactionResponse?.errors[0].message).toEqual( // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `Transaction is already confirmed: ${trs1.id}`, ); @@ -279,16 +282,6 @@ describe('transactions', () => { trs2.apply.mockReturnValue(trs2Response); }); - it('should prepare all transactions', async () => { - await transactionHandlers.applyGenesisTransactions()( - [trs1, trs2], - stateStoreMock, - ); - - expect(trs1.prepare).toHaveBeenCalledTimes(1); - expect(trs2.prepare).toHaveBeenCalledTimes(1); - }); - it('should apply all transactions', async () => { await transactionHandlers.applyGenesisTransactions()( [trs1, trs2], @@ -344,14 +337,14 @@ describe('transactions', () => { trs2.apply.mockReturnValue(trs2Response); }); - it('should prepare all transactions', async () => { + it('should apply all transactions', async () => { await transactionHandlers.applyTransactions()( [trs1, trs2], stateStoreMock, ); - expect(trs1.prepare).toHaveBeenCalledTimes(1); - expect(trs2.prepare).toHaveBeenCalledTimes(1); + expect(trs1.apply).toHaveBeenCalledTimes(1); + expect(trs2.apply).toHaveBeenCalledTimes(1); }); }); @@ -375,16 +368,6 @@ describe('transactions', () => { trs2.undo.mockReturnValue(trs2Response); }); - it('should prepare all transactions', async () => { - await transactionHandlers.undoTransactions()( - [trs1, trs2], - stateStoreMock, - ); - - expect(trs1.prepare).toHaveBeenCalledTimes(1); - expect(trs2.prepare).toHaveBeenCalledTimes(1); - }); - it('should undo for every transaction', async () => { await transactionHandlers.undoTransactions()( [trs1, trs2], diff --git a/elements/lisk-db/src/utils.ts b/elements/lisk-db/src/utils.ts index 98a53336723..6b76abefc3a 100644 --- a/elements/lisk-db/src/utils.ts +++ b/elements/lisk-db/src/utils.ts @@ -15,11 +15,17 @@ export const formatInt = (num: number | bigint): string => { let buf: Buffer; if (typeof num === 'bigint') { + if (num < BigInt(0)) { + throw new Error('Negative number cannot be formatted'); + } buf = Buffer.alloc(8); - buf.writeBigInt64BE(num); + buf.writeBigUInt64BE(num); } else { + if (num < 0) { + throw new Error('Negative number cannot be formatted'); + } buf = Buffer.alloc(4); - buf.writeInt32BE(num); + buf.writeUInt32BE(num); } return buf.toString('binary'); }; diff --git a/elements/lisk-transactions/src/10_delegate_transaction.ts b/elements/lisk-transactions/src/10_delegate_transaction.ts index c4d02a31b44..4d7ea11b298 100644 --- a/elements/lisk-transactions/src/10_delegate_transaction.ts +++ b/elements/lisk-transactions/src/10_delegate_transaction.ts @@ -14,11 +14,7 @@ */ import { validator } from '@liskhq/lisk-validator'; -import { - BaseTransaction, - StateStore, - StateStorePrepare, -} from './base_transaction'; +import { BaseTransaction, StateStore } from './base_transaction'; import { DELEGATE_NAME_FEE } from './constants'; import { convertToAssetError, TransactionError } from './errors'; import { Account, TransactionJSON } from './transaction_types'; @@ -53,17 +49,6 @@ export class DelegateTransaction extends BaseTransaction { this.asset = (tx.asset ?? { delegate: {} }) as DelegateAsset; } - public async prepare(store: StateStorePrepare): Promise { - await store.account.cache([ - { - address: this.senderId, - }, - { - username: this.asset.username, - }, - ]); - } - protected assetToBytes(): Buffer { const { username } = this.asset; diff --git a/elements/lisk-transactions/src/12_multisignature_transaction.ts b/elements/lisk-transactions/src/12_multisignature_transaction.ts index d4c67583cfb..07373d0c5fa 100644 --- a/elements/lisk-transactions/src/12_multisignature_transaction.ts +++ b/elements/lisk-transactions/src/12_multisignature_transaction.ts @@ -23,11 +23,7 @@ import { } from '@liskhq/lisk-cryptography'; import { validator } from '@liskhq/lisk-validator'; -import { - BaseTransaction, - StateStore, - StateStorePrepare, -} from './base_transaction'; +import { BaseTransaction, StateStore } from './base_transaction'; import { convertToAssetError, TransactionError } from './errors'; import { createResponse, TransactionResponse } from './response'; import { TransactionJSON } from './transaction_types'; @@ -103,20 +99,6 @@ export class MultisignatureTransaction extends BaseTransaction { this.asset = (tx.asset ?? {}) as MultiSignatureAsset; } - public async prepare(store: StateStorePrepare): Promise { - const membersAddresses = [ - ...this.asset.mandatoryKeys, - ...this.asset.optionalKeys, - ].map(publicKey => ({ address: getAddressFromPublicKey(publicKey) })); - - await store.account.cache([ - { - address: this.senderId, - }, - ...membersAddresses, - ]); - } - // Verifies multisig signatures as per LIP-0017 // eslint-disable-next-line @typescript-eslint/require-await public async verifySignatures( diff --git a/elements/lisk-transactions/src/13_vote_transaction.ts b/elements/lisk-transactions/src/13_vote_transaction.ts index f9d4dbe2870..9b27864eb14 100644 --- a/elements/lisk-transactions/src/13_vote_transaction.ts +++ b/elements/lisk-transactions/src/13_vote_transaction.ts @@ -15,11 +15,7 @@ import { intToBuffer } from '@liskhq/lisk-cryptography'; import { isNumberString, validator } from '@liskhq/lisk-validator'; -import { - BaseTransaction, - StateStore, - StateStorePrepare, -} from './base_transaction'; +import { BaseTransaction, StateStore } from './base_transaction'; import { MAX_INT64 } from './constants'; import { convertToAssetError, TransactionError } from './errors'; import { TransactionJSON } from './transaction_types'; @@ -112,20 +108,6 @@ export class VoteTransaction extends BaseTransaction { }; } - public async prepare(store: StateStorePrepare): Promise { - const addressArray = this.asset.votes.map(vote => ({ - address: vote.delegateAddress, - })); - const filterArray = [ - { - address: this.senderId, - }, - ...addressArray, - ]; - - await store.account.cache(filterArray); - } - protected assetToBytes(): Buffer { const bufferArray = []; for (const vote of this.asset.votes) { diff --git a/elements/lisk-transactions/src/14_unlock_transaction.ts b/elements/lisk-transactions/src/14_unlock_transaction.ts index 2affe37bd5c..009d9521e49 100644 --- a/elements/lisk-transactions/src/14_unlock_transaction.ts +++ b/elements/lisk-transactions/src/14_unlock_transaction.ts @@ -15,11 +15,7 @@ import { intToBuffer } from '@liskhq/lisk-cryptography'; import { isNumberString, validator } from '@liskhq/lisk-validator'; -import { - BaseTransaction, - StateStore, - StateStorePrepare, -} from './base_transaction'; +import { BaseTransaction, StateStore } from './base_transaction'; import { convertToAssetError, TransactionError } from './errors'; import { Account, TransactionJSON } from './transaction_types'; import { getPunishmentPeriod, sortUnlocking } from './utils'; @@ -135,20 +131,6 @@ export class UnlockTransaction extends BaseTransaction { }; } - public async prepare(store: StateStorePrepare): Promise { - const addressArray = this.asset.unlockingObjects.map(unlock => ({ - address: unlock.delegateAddress, - })); - const filterArray = [ - { - address: this.senderId, - }, - ...addressArray, - ]; - - await store.account.cache(filterArray); - } - protected assetToBytes(): Buffer { const bufferArray = []; for (const unlock of this.asset.unlockingObjects) { diff --git a/elements/lisk-transactions/src/15_proof_of_misbehavior_transaction.ts b/elements/lisk-transactions/src/15_proof_of_misbehavior_transaction.ts index 1fd4f831cb4..ff66816cbf2 100644 --- a/elements/lisk-transactions/src/15_proof_of_misbehavior_transaction.ts +++ b/elements/lisk-transactions/src/15_proof_of_misbehavior_transaction.ts @@ -15,11 +15,7 @@ import { getAddressFromPublicKey } from '@liskhq/lisk-cryptography'; import { isNumberString, validator } from '@liskhq/lisk-validator'; -import { - BaseTransaction, - StateStore, - StateStorePrepare, -} from './base_transaction'; +import { BaseTransaction, StateStore } from './base_transaction'; import { MAX_POM_HEIGHTS, MAX_PUNISHABLE_BLOCK_HEIGHT_DIFFERENCE, @@ -157,23 +153,6 @@ export class ProofOfMisbehaviorTransaction extends BaseTransaction { }; } - public async prepare(store: StateStorePrepare): Promise { - const delegateAddress = getAddressFromPublicKey( - this.asset.header1.generatorPublicKey, - ); - - const filterArray = [ - { - address: this.senderId, - }, - { - address: delegateAddress, - }, - ]; - - await store.account.cache(filterArray); - } - protected assetToBytes(): Buffer { return Buffer.concat([ getBlockBytesWithSignature(this.asset.header1), diff --git a/elements/lisk-transactions/src/8_transfer_transaction.ts b/elements/lisk-transactions/src/8_transfer_transaction.ts index 2c535a1b7f3..438509e9c8f 100644 --- a/elements/lisk-transactions/src/8_transfer_transaction.ts +++ b/elements/lisk-transactions/src/8_transfer_transaction.ts @@ -19,11 +19,7 @@ import { validator, } from '@liskhq/lisk-validator'; -import { - BaseTransaction, - StateStore, - StateStorePrepare, -} from './base_transaction'; +import { BaseTransaction, StateStore } from './base_transaction'; import { BYTESIZES, MAX_TRANSACTION_AMOUNT } from './constants'; import { convertToAssetError, TransactionError } from './errors'; import { TransactionJSON } from './transaction_types'; @@ -96,17 +92,6 @@ export class TransferTransaction extends BaseTransaction { }; } - public async prepare(store: StateStorePrepare): Promise { - await store.account.cache([ - { - address: this.senderId, - }, - { - address: this.asset.recipientId, - }, - ]); - } - protected assetToBytes(): Buffer { const transactionAmount = intToBuffer( this.asset.amount.toString(), diff --git a/elements/lisk-transactions/src/base_transaction.ts b/elements/lisk-transactions/src/base_transaction.ts index c9c17964c8a..a2bbdaae203 100644 --- a/elements/lisk-transactions/src/base_transaction.ts +++ b/elements/lisk-transactions/src/base_transaction.ts @@ -53,18 +53,7 @@ export interface TransactionResponse { // Disabling method-signature-style otherwise type is not compatible with lisk-chain /* eslint-disable @typescript-eslint/method-signature-style */ -export interface StateStorePrepare { - readonly account: { - cache( - filterArray: ReadonlyArray<{ readonly [key: string]: string }>, - ): Promise>; - }; -} - export interface AccountState { - cache( - filterArray: ReadonlyArray<{ readonly [key: string]: string }>, - ): Promise>; get(key: string): Promise; getOrDefault(key: string): Promise; find(func: (item: Account) => boolean): Account | undefined; @@ -305,14 +294,6 @@ export abstract class BaseTransaction { return createResponse(this.id, errors); } - public async prepare(store: StateStorePrepare): Promise { - await store.account.cache([ - { - address: this.senderId, - }, - ]); - } - public async verifySignatures( store: StateStore, ): Promise { diff --git a/elements/lisk-transactions/src/index.ts b/elements/lisk-transactions/src/index.ts index 9cc9f86522a..7b556c735dd 100644 --- a/elements/lisk-transactions/src/index.ts +++ b/elements/lisk-transactions/src/index.ts @@ -19,11 +19,7 @@ import { VoteTransaction } from './13_vote_transaction'; import { UnlockTransaction } from './14_unlock_transaction'; import { ProofOfMisbehaviorTransaction } from './15_proof_of_misbehavior_transaction'; import { TransferTransaction } from './8_transfer_transaction'; -import { - BaseTransaction, - StateStore, - StateStorePrepare, -} from './base_transaction'; +import { BaseTransaction, StateStore } from './base_transaction'; import { castVotes } from './cast_votes'; import * as constants from './constants'; import { @@ -72,7 +68,6 @@ export { Account, BaseTransaction, StateStore, - StateStorePrepare, TransferTransaction, transfer, DelegateTransaction, diff --git a/elements/lisk-transactions/src/schema.ts b/elements/lisk-transactions/src/schema.ts index 6ce51ebb11f..2ff0450ec33 100644 --- a/elements/lisk-transactions/src/schema.ts +++ b/elements/lisk-transactions/src/schema.ts @@ -19,7 +19,6 @@ export const transactionInterface = { 'validate', 'apply', 'undo', - 'prepare', 'verifySignatures', ], properties: { @@ -38,9 +37,6 @@ export const transactionInterface = { undo: { typeof: 'function', }, - prepare: { - typeof: 'function', - }, verifySignatures: { typeof: 'function', }, diff --git a/elements/lisk-transactions/test/10_delegate_transaction.spec.ts b/elements/lisk-transactions/test/10_delegate_transaction.spec.ts index 432cdbf7436..b2516566662 100644 --- a/elements/lisk-transactions/test/10_delegate_transaction.spec.ts +++ b/elements/lisk-transactions/test/10_delegate_transaction.spec.ts @@ -62,7 +62,6 @@ describe('Delegate registration transaction class', () => { jest.spyOn(store.account, 'get'); jest.spyOn(store.account, 'find'); jest.spyOn(store.account, 'set'); - jest.spyOn(store.account, 'cache'); }); describe('#constructor', () => { @@ -121,16 +120,6 @@ describe('Delegate registration transaction class', () => { }); }); - describe('#prepare', () => { - it('should call state store', async () => { - await validTestTransaction.prepare(store); - expect(store.account.cache).toHaveBeenCalledWith([ - { address: validTestTransaction.senderId }, - { username: validTestTransaction.asset.username }, - ]); - }); - }); - describe('#validateAsset', () => { it('should no errors', () => { const errors = (validTestTransaction as any).validateAsset(); diff --git a/elements/lisk-transactions/test/12_multisignature_transaction.spec.ts b/elements/lisk-transactions/test/12_multisignature_transaction.spec.ts index 1454da2d13c..4be7bf1cbb5 100644 --- a/elements/lisk-transactions/test/12_multisignature_transaction.spec.ts +++ b/elements/lisk-transactions/test/12_multisignature_transaction.spec.ts @@ -12,7 +12,6 @@ * Removal or modification of this copyright notice is prohibited. * */ -import { getAddressFromPublicKey } from '@liskhq/lisk-cryptography'; import { MultisignatureTransaction } from '../src/12_multisignature_transaction'; import { Account } from '../src/transaction_types'; import { defaultAccount, StateStoreMock } from './utils/state_store_mock'; @@ -49,7 +48,6 @@ describe('Multisignature transaction class', () => { 'e48feb88db5b5cf5ad71d93cdcd1d879b6d5ed187a36b0002cc34e0ef9883255'; let validTestTransaction: MultisignatureTransaction; let multisignatureSender: Partial; - let storeAccountCacheStub: jest.SpyInstance; let storeAccountGetStub: jest.SpyInstance; let storeAccountSetStub: jest.SpyInstance; let store: StateStoreMock; @@ -77,7 +75,6 @@ describe('Multisignature transaction class', () => { .mockResolvedValue(targetMultisigAccount); storeAccountSetStub = jest.spyOn(store.account, 'set'); - storeAccountCacheStub = jest.spyOn(store.account, 'cache'); }); describe('#constructor', () => { @@ -116,25 +113,6 @@ describe('Multisignature transaction class', () => { }); }); - describe('#prepare', () => { - it('should call state store with correct params', async () => { - await validTestTransaction.prepare(store); - // Derive addresses from public keys - const mandatoryKeysAddressess = validTestTransaction.asset.mandatoryKeys.map( - aKey => ({ address: getAddressFromPublicKey(aKey) }), - ); - const optionalKeysAddressess = validTestTransaction.asset.optionalKeys.map( - aKey => ({ address: getAddressFromPublicKey(aKey) }), - ); - - expect(storeAccountCacheStub).toHaveBeenCalledWith([ - { address: validTestTransaction.senderId }, - ...mandatoryKeysAddressess, - ...optionalKeysAddressess, - ]); - }); - }); - describe('#validateSchema', () => { it('should return no errors', () => { const errors = (validTestTransaction as any).validateAsset(); diff --git a/elements/lisk-transactions/test/8_transfer_transaction.spec.ts b/elements/lisk-transactions/test/8_transfer_transaction.spec.ts index c0e0da56b2b..690d9fe1f49 100644 --- a/elements/lisk-transactions/test/8_transfer_transaction.spec.ts +++ b/elements/lisk-transactions/test/8_transfer_transaction.spec.ts @@ -50,7 +50,6 @@ describe('Transfer transaction class', () => { store = new StateStoreMock([sender, recipient]); - jest.spyOn(store.account, 'cache'); jest.spyOn(store.account, 'get'); jest.spyOn(store.account, 'getOrDefault'); jest.spyOn(store.account, 'set'); @@ -88,16 +87,6 @@ describe('Transfer transaction class', () => { }); }); - describe('#prepare', () => { - it('should call state store', async () => { - await validTransferTestTransaction.prepare(store); - expect(store.account.cache).toHaveBeenCalledWith([ - { address: validTransferTestTransaction.senderId }, - { address: validTransferTestTransaction.asset.recipientId }, - ]); - }); - }); - describe('#validateAsset', () => { it('should return no errors with a valid transfer transaction', () => { const errors = (validTransferTestTransaction as any).validateAsset(); diff --git a/elements/lisk-transactions/test/utils/state_store_mock.ts b/elements/lisk-transactions/test/utils/state_store_mock.ts index 6e07c846d40..ece50d8b7d3 100644 --- a/elements/lisk-transactions/test/utils/state_store_mock.ts +++ b/elements/lisk-transactions/test/utils/state_store_mock.ts @@ -71,12 +71,6 @@ export class StateStoreMock { this.transactionData = []; this.account = { - cache: async ( - _filterArray: ReadonlyArray<{ readonly [key: string]: string }>, - // eslint-disable-next-line @typescript-eslint/require-await - ): Promise> => { - return []; - }, // eslint-disable-next-line @typescript-eslint/require-await get: async (address: string): Promise => { const account = this.accountData.find(acc => acc.address === address); diff --git a/framework/src/application/application.ts b/framework/src/application/application.ts index 296e003b0a6..80eebb3aaf7 100644 --- a/framework/src/application/application.ts +++ b/framework/src/application/application.ts @@ -169,6 +169,7 @@ export class Application { // eslint-disable-next-line @typescript-eslint/no-explicit-any private readonly storage: any; private _forgerDB!: KVStore; + private _blockchainDB!: KVStore; public constructor( genesisBlock: GenesisBlockInstance, @@ -365,6 +366,7 @@ export class Application { // Initialize database instances this._forgerDB = this._getDBInstance(this.config, 'forger.db'); + this._blockchainDB = this._getDBInstance(this.config, 'blockchain.db'); // Initialize all objects this._applicationState = this._initApplicationState(); @@ -713,10 +715,8 @@ export class Application { registeredTransactions: this.getTransactions(), }, logger: this.logger, - // TODO: Remove the storage with PR 5257 - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - storage: this.storage, forgerDB: this._forgerDB, + blockchainDB: this._blockchainDB, applicationState: this._applicationState, }); diff --git a/framework/src/application/node/node.ts b/framework/src/application/node/node.ts index 9b4149ce947..a0f604efc31 100644 --- a/framework/src/application/node/node.ts +++ b/framework/src/application/node/node.ts @@ -96,10 +96,8 @@ interface NodeConstructor { readonly channel: InMemoryChannel; readonly options: Options; readonly logger: Logger; - // TODO: Remove storage with PR 5257 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - readonly storage: any; readonly forgerDB: KVStore; + readonly blockchainDB: KVStore; readonly applicationState: ApplicationState; } @@ -115,10 +113,8 @@ export class Node { private readonly _channel: InMemoryChannel; private readonly _options: Options; private readonly _logger: Logger; - // TODO: Replace storage with _blockchainDB in PR 5257 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private readonly _storage: any; private readonly _forgerDB: KVStore; + private readonly _blockchainDB: KVStore; private readonly _applicationState: ApplicationState; private readonly _components: { readonly logger: Logger }; private _sequence!: Sequence; @@ -138,7 +134,7 @@ export class Node { channel, options, logger, - storage, + blockchainDB, forgerDB, applicationState, }: NodeConstructor) { @@ -147,10 +143,7 @@ export class Node { this._logger = logger; this._applicationState = applicationState; this._components = { logger: this._logger }; - // TODO: Replace storage with _blockchainDB in PR 5257 - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - this._storage = storage; - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + this._blockchainDB = blockchainDB; this._forgerDB = forgerDB; } @@ -456,9 +449,7 @@ export class Node { private _initModules(): void { this._chain = new Chain({ - // TODO: Replace the storage with _blockchainDB in PR 5257 - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - storage: this._storage, + db: this._blockchainDB, genesisBlock: this._options.genesisBlock as GenesisBlockJSON, registeredTransactions: this._options.registeredTransactions, networkIdentifier: this._networkIdentifier, diff --git a/framework/src/application/node/synchronizer/block_synchronization_mechanism.ts b/framework/src/application/node/synchronizer/block_synchronization_mechanism.ts index 22aeafba207..7dd21ca937f 100644 --- a/framework/src/application/node/synchronizer/block_synchronization_mechanism.ts +++ b/framework/src/application/node/synchronizer/block_synchronization_mechanism.ts @@ -218,7 +218,7 @@ export class BlockSynchronizationMechanism extends BaseSynchronizer { } const tipBeforeApplyingInstance = await this.processorModule.deserialize( - tipBeforeApplying.fullBlock, + tipBeforeApplying, ); // Check if the new tip has priority over the last tip we had before applying const forkStatus = await this.processorModule.forkStatus( diff --git a/framework/src/application/node/synchronizer/utils.ts b/framework/src/application/node/synchronizer/utils.ts index 54595c6d290..085029f92f6 100644 --- a/framework/src/application/node/synchronizer/utils.ts +++ b/framework/src/application/node/synchronizer/utils.ts @@ -29,9 +29,7 @@ export const restoreBlocks = async ( } for (const tempBlockEntry of tempBlocks) { - const tempBlockInstance = await processorModule.deserialize( - tempBlockEntry.fullBlock, - ); + const tempBlockInstance = await processorModule.deserialize(tempBlockEntry); await processorModule.processValidated(tempBlockInstance, { removeFromTempTable: true, }); @@ -85,14 +83,12 @@ export const restoreBlocksUponStartup = async ( chainModule: Chain, processorModule: Processor, ): Promise => { - // Get all blocks and find lowest height (next one to be applied) + // Get all blocks and find lowest height (next one to be applied), as it should return in height desc const tempBlocks = await chainModule.dataAccess.getTempBlocks(); - const blockLowestHeight = tempBlocks[0]; - const blockHighestHeight = tempBlocks[tempBlocks.length - 1]; + const blockLowestHeight = tempBlocks[tempBlocks.length - 1]; + const blockHighestHeight = tempBlocks[0]; - const nextTempBlock = await processorModule.deserialize( - blockHighestHeight.fullBlock, - ); + const nextTempBlock = await processorModule.deserialize(blockHighestHeight); const forkStatus = await processorModule.forkStatus(nextTempBlock); const blockHasPriority = forkStatus === ForkStatus.DIFFERENT_CHAIN || diff --git a/framework/test/jest/integration/specs/application/node/__snapshots__/genesis_block.spec.js.snap b/framework/test/jest/integration/specs/application/node/__snapshots__/genesis_block.spec.ts.snap similarity index 100% rename from framework/test/jest/integration/specs/application/node/__snapshots__/genesis_block.spec.js.snap rename to framework/test/jest/integration/specs/application/node/__snapshots__/genesis_block.spec.ts.snap diff --git a/framework/test/jest/integration/specs/application/node/forger/transaction_pool.spec.ts b/framework/test/jest/integration/specs/application/node/forger/transaction_pool.spec.ts index 750feed933b..1aa7f7428aa 100644 --- a/framework/test/jest/integration/specs/application/node/forger/transaction_pool.spec.ts +++ b/framework/test/jest/integration/specs/application/node/forger/transaction_pool.spec.ts @@ -12,8 +12,10 @@ * Removal or modification of this copyright notice is prohibited. */ +import { KVStore } from '@liskhq/lisk-db'; import { transfer, utils } from '@liskhq/lisk-transactions'; -import { nodeUtils, storageUtils, configUtils } from '../../../../../../utils'; +import { nodeUtils } from '../../../../../../utils'; +import { createDB, removeDB } from '../../../../../../utils/kv_store'; import { accounts } from '../../../../../../fixtures'; const { convertLSKToBeddows } = utils; @@ -21,21 +23,20 @@ const { genesis } = accounts; describe('Transaction pool', () => { const dbName = 'transaction_pool'; - let storage: any; let node: any; + let blockchainDB: KVStore; + let forgerDB: KVStore; beforeAll(async () => { - storage = new storageUtils.StorageSandbox( - configUtils.storageConfig({ database: dbName }), - dbName, - ); - await storage.bootstrap(); - node = await nodeUtils.createAndLoadNode(storage); + ({ blockchainDB, forgerDB } = createDB(dbName)); + node = await nodeUtils.createAndLoadNode(blockchainDB, forgerDB); }); afterAll(async () => { await node.cleanup(); - await storage.cleanup(); + await blockchainDB.close(); + await forgerDB.close(); + removeDB(dbName); }); describe('given a valid transaction while forging is disabled', () => { diff --git a/framework/test/jest/integration/specs/application/node/genesis_block.spec.js b/framework/test/jest/integration/specs/application/node/genesis_block.spec.ts similarity index 82% rename from framework/test/jest/integration/specs/application/node/genesis_block.spec.js rename to framework/test/jest/integration/specs/application/node/genesis_block.spec.ts index ecfe5c96f6a..9d82b0a6d13 100644 --- a/framework/test/jest/integration/specs/application/node/genesis_block.spec.js +++ b/framework/test/jest/integration/specs/application/node/genesis_block.spec.ts @@ -12,46 +12,46 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; - -const { getAddressFromPublicKey } = require('@liskhq/lisk-cryptography'); -const { - nodeUtils, - storageUtils, - configUtils, -} = require('../../../../../utils'); -const genesisBlock = require('../../../../../fixtures/config/devnet/genesis_block.json'); +import { KVStore } from '@liskhq/lisk-db'; +import { getAddressFromPublicKey } from '@liskhq/lisk-cryptography'; +import { nodeUtils } from '../../../../../utils'; +import { createDB, removeDB } from '../../../../../utils/kv_store'; +import * as genesisBlock from '../../../../../fixtures/config/devnet/genesis_block.json'; +import { Node } from '../../../../../../src/application/node'; describe('genesis block', () => { const dbName = 'genesis_block'; const TRANSACTION_TYPE_DELEGATE_REGISTRATION = 10; - let storage; - let node; + let node: Node; + let blockchainDB: KVStore; + let forgerDB: KVStore; beforeAll(async () => { - storage = new storageUtils.StorageSandbox( - configUtils.storageConfig({ database: dbName }), - dbName, - ); - await storage.bootstrap(); - node = await nodeUtils.createAndLoadNode(storage, console); + ({ blockchainDB, forgerDB } = createDB(dbName)); + node = await nodeUtils.createAndLoadNode(blockchainDB, forgerDB); }); afterAll(async () => { await node.cleanup(); - await storage.cleanup(); + await blockchainDB.close(); + await forgerDB.close(); + removeDB(dbName); }); describe('given the application has not been initialized', () => { describe('when chain module is bootstrapped', () => { it('should save genesis block to the database', async () => { - const block = await storageUtils.getBlock(storage, genesisBlock.id); + const block = await node['_chain'].dataAccess.getBlockByID( + genesisBlock.id, + ); expect(block.id).toEqual(genesisBlock.id); expect(block.height).toEqual(1); }); it('should have genesis transactions in database', async () => { - const block = await storageUtils.getBlock(storage, genesisBlock.id); + const block = await node['_chain'].dataAccess.getBlockByID( + genesisBlock.id, + ); const ids = genesisBlock.transactions.map(t => t.id); const allExist = ids.every(id => block.transactions.map(tx => tx.id).includes(id), @@ -72,7 +72,7 @@ describe('genesis block', () => { // Get delegate accounts in genesis block from the database const accountsFromDb = await Promise.all( delegateAccountsAddressesInGenesisBlock.map(async address => - storageUtils.getAccount(storage, address), + node['_chain'].dataAccess.getAccountByAddress(address), ), ); const allAccountsAreDelegate = delegateAccountsAddressesInGenesisBlock.every( @@ -97,7 +97,7 @@ describe('genesis block', () => { // Get delegate accounts in genesis block from the database const accountsFromDb = await Promise.all( delegateAccountsAddressesInGenesisBlock.map(async address => - storageUtils.getAccount(storage, address), + node['_chain'].dataAccess.getAccountByAddress(address), ), ); const allAccountsHaveCorrectVoteWeight = delegateAccountsAddressesInGenesisBlock.every( @@ -106,7 +106,7 @@ describe('genesis block', () => { account => address === account.address && account.totalVotesReceived === - totalVotesReceivedOfDevnetDelegates, + BigInt(totalVotesReceivedOfDevnetDelegates), ), ); @@ -123,7 +123,9 @@ describe('genesis block', () => { describe('given the application has been initialized previously', () => { describe('when chain module is bootstrapped', () => { it('should have genesis transactions in database', async () => { - const block = await storageUtils.getBlock(storage, genesisBlock.id); + const block = await node['_chain'].dataAccess.getBlockByID( + genesisBlock.id, + ); const ids = genesisBlock.transactions.map(t => t.id); const allExist = ids.every(id => block.transactions.map(tx => tx.id).includes(id), @@ -144,7 +146,7 @@ describe('genesis block', () => { // Get delegate accounts in genesis block from the database const accountsFromDb = await Promise.all( delegateAccountsAddressesInGenesisBlock.map(async address => - storageUtils.getAccount(storage, address), + node['_chain'].dataAccess.getAccountByAddress(address), ), ); const allAccountsAreDelegate = delegateAccountsAddressesInGenesisBlock.every( @@ -169,7 +171,7 @@ describe('genesis block', () => { // Get delegate accounts in genesis block from the database const accountsFromDb = await Promise.all( delegateAccountsAddressesInGenesisBlock.map(async address => - storageUtils.getAccount(storage, address), + node['_chain'].dataAccess.getAccountByAddress(address), ), ); const allAccountsHaveCorrectVoteWeight = delegateAccountsAddressesInGenesisBlock.every( @@ -178,7 +180,7 @@ describe('genesis block', () => { account => address === account.address && account.totalVotesReceived === - totalVotesReceivedOfDevnetDelegates, + BigInt(totalVotesReceivedOfDevnetDelegates), ), ); diff --git a/framework/test/jest/integration/specs/application/node/matcher.spec.js b/framework/test/jest/integration/specs/application/node/matcher.spec.ts similarity index 60% rename from framework/test/jest/integration/specs/application/node/matcher.spec.js rename to framework/test/jest/integration/specs/application/node/matcher.spec.ts index 01e813f4a6a..c0243016e90 100644 --- a/framework/test/jest/integration/specs/application/node/matcher.spec.js +++ b/framework/test/jest/integration/specs/application/node/matcher.spec.ts @@ -12,75 +12,67 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; - -const { - BaseTransaction, - TransferTransaction, -} = require('@liskhq/lisk-transactions'); -const { KVStore } = require('@liskhq/lisk-db'); -const { - nodeUtils, - storageUtils, - configUtils, -} = require('../../../../../utils'); -const { - accounts: { genesis }, -} = require('../../../../../fixtures'); +import { BaseTransaction, TransactionError } from '@liskhq/lisk-transactions'; +import { KVStore } from '@liskhq/lisk-db'; +import { BlockInstance } from '@liskhq/lisk-chain'; +import { nodeUtils } from '../../../../../utils'; +import { accounts } from '../../../../../fixtures'; +import { createDB, removeDB } from '../../../../../utils/kv_store'; +import { Node } from '../../../../../../src/application/node'; + +const { genesis } = accounts; /** * Implementation of the Custom Transaction enclosed in a class */ class CustomTransationClass extends BaseTransaction { - constructor(input) { + public asset: any; + + public constructor(input: any) { super(input); this.asset = input.asset; } - static get TYPE() { + // eslint-disable-next-line @typescript-eslint/class-literal-property-style + public static get TYPE(): number { return 7; } - static get FEE() { - return TransferTransaction.FEE; - } - - assetToJSON() { + public assetToJSON() { return this.asset; } // eslint-disable-next-line class-methods-use-this - assetToBytes() { + public assetToBytes() { return Buffer.alloc(0); } - // eslint-disable-next-line class-methods-use-this - applyAsset() { + // eslint-disable-next-line + public async applyAsset(): Promise { return []; } - // eslint-disable-next-line class-methods-use-this - undoAsset() { + // eslint-disable-next-line + public async undoAsset(): Promise { return []; } // eslint-disable-next-line class-methods-use-this - matcher() { + public matcher() { return false; } // eslint-disable-next-line class-methods-use-this - validateAsset() { + public validateAsset() { return []; } +} - async prepare(store) { - await store.account.cache([ - { - address: this.senderId, - }, - ]); - } +interface CreateRawTransactionInput { + passphrase: string; + nonce: string; + networkIdentifier: string; + senderPublicKey: string; } const createRawCustomTransaction = ({ @@ -88,7 +80,7 @@ const createRawCustomTransaction = ({ nonce, networkIdentifier, senderPublicKey, -}) => { +}: CreateRawTransactionInput) => { const aCustomTransation = new CustomTransationClass({ type: 7, nonce, @@ -105,25 +97,22 @@ const createRawCustomTransaction = ({ describe('Matcher', () => { const dbName = 'transaction_matcher'; - let storage; - let node; - let forgerDB; + let node: Node; + let blockchainDB: KVStore; + let forgerDB: KVStore; beforeAll(async () => { - storage = new storageUtils.StorageSandbox( - configUtils.storageConfig({ database: dbName }), - dbName, - ); - await storage.bootstrap(); - forgerDB = new KVStore(`/tmp/${dbName}.db`); - node = await nodeUtils.createAndLoadNode(storage, forgerDB); - await node._forger.loadDelegates(); + ({ blockchainDB, forgerDB } = createDB(dbName)); + node = await nodeUtils.createAndLoadNode(blockchainDB, forgerDB); + await node['_forger'].loadDelegates(); }); afterAll(async () => { await forgerDB.clear(); await node.cleanup(); - await storage.cleanup(); + await blockchainDB.close(); + await forgerDB.close(); + removeDB(dbName); }); describe('given a disallowed transaction', () => { @@ -132,12 +121,12 @@ describe('Matcher', () => { const account = nodeUtils.createAccount(); const tx = createRawCustomTransaction({ passphrase: account.passphrase, - networkIdentifier: node._networkIdentifier, + networkIdentifier: node['_networkIdentifier'], senderPublicKey: account.publicKey, nonce: '0', }); await expect( - node._transport.handleEventPostTransaction({ transaction: tx }), + node['_transport'].handleEventPostTransaction({ transaction: tx }), ).resolves.toEqual( expect.objectContaining({ message: expect.stringContaining('Transaction was rejected'), @@ -149,11 +138,11 @@ describe('Matcher', () => { describe('given a block containing disallowed transaction', () => { describe('when the block is processed', () => { - let newBlock; + let newBlock: BlockInstance; beforeAll(async () => { - const genesisAccount = await node._chain.dataAccess.getAccountByAddress( - genesis.address, - ); + const genesisAccount = await node[ + '_chain' + ].dataAccess.getAccountByAddress(genesis.address); const aCustomTransation = new CustomTransationClass({ senderPublicKey: genesis.publicKey, nonce: genesisAccount.nonce.toString(), @@ -163,12 +152,14 @@ describe('Matcher', () => { }, fee: (10000000).toString(), }); - aCustomTransation.sign(node._networkIdentifier, genesis.passphrase); + aCustomTransation.sign(node['_networkIdentifier'], genesis.passphrase); newBlock = await nodeUtils.createBlock(node, [aCustomTransation]); }); it('should be rejected', async () => { - await expect(node._processor.process(newBlock)).rejects.toMatchObject([ + await expect( + node['_processor'].process(newBlock), + ).rejects.toMatchObject([ expect.objectContaining({ message: expect.stringContaining('is currently not allowed'), }), diff --git a/framework/test/jest/integration/specs/application/node/processor/delete_block.spec.js b/framework/test/jest/integration/specs/application/node/processor/delete_block.spec.ts similarity index 55% rename from framework/test/jest/integration/specs/application/node/processor/delete_block.spec.js rename to framework/test/jest/integration/specs/application/node/processor/delete_block.spec.ts index 8dcf5377eda..b3d5f8e647c 100644 --- a/framework/test/jest/integration/specs/application/node/processor/delete_block.spec.js +++ b/framework/test/jest/integration/specs/application/node/processor/delete_block.spec.ts @@ -12,49 +12,41 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; +import { KVStore } from '@liskhq/lisk-db'; +import { BlockInstance } from '@liskhq/lisk-chain'; +import { transfer, TransactionJSON, utils } from '@liskhq/lisk-transactions'; +import { createDB, removeDB } from '../../../../../../utils/kv_store'; +import { nodeUtils } from '../../../../../../utils'; +import { accounts } from '../../../../../../fixtures'; +import { Node } from '../../../../../../../src/application/node'; -const { - transfer, - utils: { convertLSKToBeddows }, -} = require('@liskhq/lisk-transactions'); -const { KVStore } = require('@liskhq/lisk-db'); -const { - nodeUtils, - storageUtils, - configUtils, -} = require('../../../../../../utils'); -const { - accounts: { genesis }, -} = require('../../../../../../fixtures'); +const { convertLSKToBeddows } = utils; +const { genesis } = accounts; describe('Delete block', () => { const dbName = 'delete_block'; - let storage; - let node; - let forgerDB; + let node: Node; + let blockchainDB: KVStore; + let forgerDB: KVStore; beforeAll(async () => { - storage = new storageUtils.StorageSandbox( - configUtils.storageConfig({ database: dbName }), - dbName, - ); - await storage.bootstrap(); - forgerDB = new KVStore(`/tmp/${dbName}.db`); - node = await nodeUtils.createAndLoadNode(storage, forgerDB); - await node._forger.loadDelegates(); + ({ blockchainDB, forgerDB } = createDB(dbName)); + node = await nodeUtils.createAndLoadNode(blockchainDB, forgerDB); + await node['_forger'].loadDelegates(); }); afterAll(async () => { await forgerDB.clear(); await node.cleanup(); - await storage.cleanup(); + await blockchainDB.close(); + await forgerDB.close(); + removeDB(dbName); }); describe('given there is only a genesis block', () => { describe('when deleteLastBlock is called', () => { it('should fail to delete genesis block', async () => { - await expect(node._processor.deleteLastBlock()).rejects.toEqual( + await expect(node['_processor'].deleteLastBlock()).rejects.toEqual( expect.objectContaining({ message: expect.stringContaining( 'Can not delete block below or same as finalized height', @@ -68,60 +60,60 @@ describe('Delete block', () => { describe('given there a valid block with transfer transaction is forged', () => { const account = nodeUtils.createAccount(); - let newBlock; - let transaction; + let newBlock: BlockInstance; + let transaction: TransactionJSON; let genesisAccount; beforeAll(async () => { - genesisAccount = await node._chain.dataAccess.getAccountByAddress( + genesisAccount = await node['_chain'].dataAccess.getAccountByAddress( genesis.address, ); transaction = transfer({ nonce: genesisAccount.nonce.toString(), - networkIdentifier: node._networkIdentifier, + networkIdentifier: node['_networkIdentifier'], fee: convertLSKToBeddows('0.002'), recipientId: account.address, amount: convertLSKToBeddows('1000'), passphrase: genesis.passphrase, - }); + }) as TransactionJSON; newBlock = await nodeUtils.createBlock(node, [ - node._chain.deserializeTransaction(transaction), + node['_chain'].deserializeTransaction(transaction), ]); - await node._processor.process(newBlock); + await node['_processor'].process(newBlock); }); describe('when deleteLastBlock is called', () => { beforeAll(async () => { - await node._processor.deleteLastBlock(); + await node['_processor'].deleteLastBlock(); }); it('should delete the block from the database', async () => { - const processedBlock = await node._chain.dataAccess.getBlockByID( - newBlock.id, - ); - expect(processedBlock).toBeUndefined(); + await expect( + node['_chain'].dataAccess.isBlockPersisted(newBlock.id), + ).resolves.toBeFalse(); }); it('should delete the transactions from the database', async () => { - const processedTxs = await node._chain.dataAccess.getTransactionsByIDs([ - transaction.id, - ]); - expect(processedTxs).toHaveLength(0); + await expect( + node['_chain'].dataAccess.isTransactionPersisted( + transaction.id as string, + ), + ).resolves.toBeFalse(); }); it('should match the sender account to the original state', async () => { - const genesisAfter = await node._chain.dataAccess.getAccountByAddress( - genesis.address, - ); + const genesisAfter = await node[ + '_chain' + ].dataAccess.getAccountByAddress(genesis.address); expect(genesisAfter.balance.toString()).toEqual( genesisAfter.balance.toString(), ); }); it('should match the recipient account to the original state', async () => { - const accountAfter = await node._chain.dataAccess.getAccountByAddress( - account.address, - ); + const accountAfter = await node[ + '_chain' + ].dataAccess.getAccountByAddress(account.address); expect(accountAfter.balance.toString()).toEqual('0'); }); }); diff --git a/framework/test/jest/integration/specs/application/node/processor/process_block.spec.js b/framework/test/jest/integration/specs/application/node/processor/process_block.spec.ts similarity index 59% rename from framework/test/jest/integration/specs/application/node/processor/process_block.spec.js rename to framework/test/jest/integration/specs/application/node/processor/process_block.spec.ts index 49f8bc2c2f6..505bd3f0fdf 100644 --- a/framework/test/jest/integration/specs/application/node/processor/process_block.spec.js +++ b/framework/test/jest/integration/specs/application/node/processor/process_block.spec.ts @@ -12,72 +12,68 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; - -const { +import { transfer, registerDelegate, - utils: { convertLSKToBeddows }, -} = require('@liskhq/lisk-transactions'); -const { KVStore } = require('@liskhq/lisk-db'); -const { - nodeUtils, - storageUtils, - configUtils, -} = require('../../../../../../utils'); -const { - accounts: { genesis }, -} = require('../../../../../../fixtures'); + utils, + TransactionJSON, +} from '@liskhq/lisk-transactions'; +import { BlockInstance, Account } from '@liskhq/lisk-chain'; +import { KVStore } from '@liskhq/lisk-db'; +import { nodeUtils } from '../../../../../../utils'; +import { createDB, removeDB } from '../../../../../../utils/kv_store'; +import { accounts } from '../../../../../../fixtures'; +import { Node } from '../../../../../../../src/application/node'; + +const { convertLSKToBeddows } = utils; +const { genesis } = accounts; describe('Process block', () => { const dbName = 'process_block'; const account = nodeUtils.createAccount(); - let storage; - let node; - let forgerDB; + let node: Node; + let blockchainDB: KVStore; + let forgerDB: KVStore; beforeAll(async () => { - storage = new storageUtils.StorageSandbox( - configUtils.storageConfig({ database: dbName }), - dbName, - ); - await storage.bootstrap(); - forgerDB = new KVStore(`/tmp/${dbName}.db`); - node = await nodeUtils.createAndLoadNode(storage, forgerDB); - await node._forger.loadDelegates(); + ({ blockchainDB, forgerDB } = createDB(dbName)); + node = await nodeUtils.createAndLoadNode(blockchainDB, forgerDB); + await node['_forger'].loadDelegates(); }); afterAll(async () => { await forgerDB.clear(); await node.cleanup(); - await storage.cleanup(); + await blockchainDB.close(); + await forgerDB.close(); + removeDB(dbName); }); describe('given an account has a balance', () => { describe('when processing a block with valid transactions', () => { - let newBlock; - let transaction; + let newBlock: BlockInstance; + let transaction: TransactionJSON; beforeAll(async () => { - const genesisAccount = await node._chain.dataAccess.getAccountByAddress( - genesis.address, - ); + const genesisAccount = await node[ + '_chain' + ].dataAccess.getAccountByAddress(genesis.address); transaction = transfer({ nonce: genesisAccount.nonce.toString(), - networkIdentifier: node._networkIdentifier, + networkIdentifier: node['_networkIdentifier'], fee: convertLSKToBeddows('0.002'), recipientId: account.address, amount: convertLSKToBeddows('1000'), passphrase: genesis.passphrase, - }); + }) as TransactionJSON; newBlock = await nodeUtils.createBlock(node, [ - node._chain.deserializeTransaction(transaction), + node['_chain'].deserializeTransaction(transaction), ]); - await node._processor.process(newBlock); + await node['_processor'].process(newBlock); }); it('should save account state changes from the transaction', async () => { - const recipient = await node._chain.dataAccess.getAccountByAddress( + const recipient = await node['_chain'].dataAccess.getAccountByAddress( account.address, ); expect(recipient.balance.toString()).toEqual( @@ -86,16 +82,16 @@ describe('Process block', () => { }); it('should save the block to the database', async () => { - const processedBlock = await node._chain.dataAccess.getBlockByID( + const processedBlock = await node['_chain'].dataAccess.getBlockByID( newBlock.id, ); expect(processedBlock.id).toEqual(newBlock.id); }); it('should save the transactions to the database', async () => { - const [ - processedTx, - ] = await node._chain.dataAccess.getTransactionsByIDs([transaction.id]); + const [processedTx] = await node[ + '_chain' + ].dataAccess.getTransactionsByIDs([transaction.id as string]); expect(processedTx.id).toEqual(transaction.id); }); }); @@ -103,15 +99,15 @@ describe('Process block', () => { describe('given a valid block with empty transaction', () => { describe('when processing the block', () => { - let newBlock; + let newBlock: BlockInstance; beforeAll(async () => { newBlock = await nodeUtils.createBlock(node); - await node._processor.process(newBlock); + await node['_processor'].process(newBlock); }); it('should add the block to the chain', async () => { - const processedBlock = await node._chain.dataAccess.getBlockByID( + const processedBlock = await node['_chain'].dataAccess.getBlockByID( newBlock.id, ); expect(processedBlock.id).toEqual(newBlock.id); @@ -121,32 +117,32 @@ describe('Process block', () => { describe('given a block with exsiting transactions', () => { describe('when processing the block', () => { - let newBlock; - let transaction; + let newBlock: BlockInstance; + let transaction: TransactionJSON; beforeAll(async () => { - const genesisAccount = await node._chain.dataAccess.getAccountByAddress( - genesis.address, - ); + const genesisAccount = await node[ + '_chain' + ].dataAccess.getAccountByAddress(genesis.address); transaction = transfer({ nonce: genesisAccount.nonce.toString(), - networkIdentifier: node._networkIdentifier, + networkIdentifier: node['_networkIdentifier'], fee: convertLSKToBeddows('0.002'), recipientId: account.address, amount: convertLSKToBeddows('1000'), passphrase: genesis.passphrase, - }); + }) as TransactionJSON; newBlock = await nodeUtils.createBlock(node, [ - node._chain.deserializeTransaction(transaction), + node['_chain'].deserializeTransaction(transaction), ]); - await node._processor.process(newBlock); + await node['_processor'].process(newBlock); }); it('should fail to process the block', async () => { const invalidBlock = await nodeUtils.createBlock(node, [ - node._chain.deserializeTransaction(transaction), + node['_chain'].deserializeTransaction(transaction), ]); - await expect(node._processor.process(invalidBlock)).rejects.toEqual([ + await expect(node['_processor'].process(invalidBlock)).rejects.toEqual([ expect.objectContaining({ message: expect.stringContaining( 'Transaction is already confirmed', @@ -159,7 +155,7 @@ describe('Process block', () => { describe('given a block forged by invalid delegate', () => { describe('when processing the block', () => { - let newBlock; + let newBlock: BlockInstance; beforeAll(async () => { newBlock = await nodeUtils.createBlock(node, [], { @@ -172,7 +168,7 @@ describe('Process block', () => { }); it('should discard the block', async () => { - await expect(node._processor.process(newBlock)).rejects.toEqual( + await expect(node['_processor'].process(newBlock)).rejects.toEqual( expect.objectContaining({ message: expect.stringContaining('Failed to verify slot'), }), @@ -183,16 +179,16 @@ describe('Process block', () => { describe('given a block which is already processed', () => { describe('when processing the block', () => { - let newBlock; + let newBlock: BlockInstance; beforeAll(async () => { newBlock = await nodeUtils.createBlock(node); - await node._processor.process(newBlock); + await node['_processor'].process(newBlock); }); it('should discard the block', async () => { await expect( - node._processor.process(newBlock), + node['_processor'].process(newBlock), ).resolves.toBeUndefined(); }); }); @@ -200,7 +196,7 @@ describe('Process block', () => { describe('given a block which is not continuous to the current chain', () => { describe('when processing the block', () => { - let newBlock; + let newBlock: BlockInstance; beforeAll(async () => { newBlock = await nodeUtils.createBlock(node, [], { @@ -210,79 +206,78 @@ describe('Process block', () => { it('should discard the block', async () => { await expect( - node._processor.process(newBlock), + node['_processor'].process(newBlock), ).resolves.toBeUndefined(); - const processedBlock = await node._chain.dataAccess.getBlockByID( - newBlock.id, - ); - expect(processedBlock).toBeUndefined(); + await expect( + node['_chain'].dataAccess.isBlockPersisted(newBlock.id), + ).resolves.toBeFalse(); }); }); }); describe('given an account is already a delegate', () => { - let newBlock; - let transaction; + let newBlock: BlockInstance; + let transaction: TransactionJSON; beforeAll(async () => { - const targetAccount = await node._chain.dataAccess.getAccountByAddress( + const targetAccount = await node['_chain'].dataAccess.getAccountByAddress( account.address, ); transaction = registerDelegate({ nonce: targetAccount.nonce.toString(), - networkIdentifier: node._networkIdentifier, + networkIdentifier: node['_networkIdentifier'], fee: convertLSKToBeddows('30'), username: 'number1', passphrase: account.passphrase, - }); + }) as TransactionJSON; newBlock = await nodeUtils.createBlock(node, [ - node._chain.deserializeTransaction(transaction), + node['_chain'].deserializeTransaction(transaction), ]); - await node._processor.process(newBlock); + await node['_processor'].process(newBlock); }); describe('when processing a block with a transaction which has delegate registration from the same account', () => { - let invalidBlock; - let invalidTx; - let originalAccount; + let invalidBlock: BlockInstance; + let invalidTx: TransactionJSON; + let originalAccount: Account; beforeAll(async () => { - originalAccount = await node._chain.dataAccess.getAccountByAddress( + originalAccount = await node['_chain'].dataAccess.getAccountByAddress( account.address, ); invalidTx = registerDelegate({ nonce: originalAccount.nonce.toString(), - networkIdentifier: node._networkIdentifier, + networkIdentifier: node['_networkIdentifier'], fee: convertLSKToBeddows('50'), username: 'number2', passphrase: account.passphrase, - }); + }) as TransactionJSON; invalidBlock = await nodeUtils.createBlock(node, [ - node._chain.deserializeTransaction(invalidTx), + node['_chain'].deserializeTransaction(invalidTx), ]); try { - await node._processor.process(invalidBlock); + await node['_processor'].process(invalidBlock); } catch (err) { // expected error } }); - it('should have the same account state as before', async () => { + it('should have the same account state as before', () => { expect(originalAccount.username).toEqual('number1'); }); it('should not save the block to the database', async () => { - const processedBlock = await node._chain.dataAccess.getBlockByID( - invalidBlock.id, - ); - expect(processedBlock).toBeUndefined(); + await expect( + node['_chain'].dataAccess.isBlockPersisted(invalidBlock.id), + ).resolves.toBeFalse(); }); it('should not save the transaction to the database', async () => { - const processedTxs = await node._chain.dataAccess.getTransactionsByIDs([ - invalidTx.id, - ]); - expect(processedTxs).toHaveLength(0); + await expect( + node['_chain'].dataAccess.isTransactionPersisted( + invalidTx.id as string, + ), + ).resolves.toBeFalse(); }); }); }); diff --git a/framework/test/jest/integration/specs/application/node/processor/transaction_order.spec.js b/framework/test/jest/integration/specs/application/node/processor/transaction_order.spec.ts similarity index 64% rename from framework/test/jest/integration/specs/application/node/processor/transaction_order.spec.js rename to framework/test/jest/integration/specs/application/node/processor/transaction_order.spec.ts index 4d76bb57282..d7f6c4186e7 100644 --- a/framework/test/jest/integration/specs/application/node/processor/transaction_order.spec.js +++ b/framework/test/jest/integration/specs/application/node/processor/transaction_order.spec.ts @@ -12,82 +12,78 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; - -const { +import { transfer, registerDelegate, castVotes, registerMultisignature, - utils: { convertLSKToBeddows }, -} = require('@liskhq/lisk-transactions'); -const { KVStore } = require('@liskhq/lisk-db'); -const { - nodeUtils, - storageUtils, - configUtils, -} = require('../../../../../../utils'); -const { - accounts: { genesis }, -} = require('../../../../../../fixtures'); + utils, + TransactionJSON, +} from '@liskhq/lisk-transactions'; +import { KVStore } from '@liskhq/lisk-db'; +import { BlockInstance } from '@liskhq/lisk-chain'; +import { nodeUtils } from '../../../../../../utils'; +import { createDB, removeDB } from '../../../../../../utils/kv_store'; +import { accounts } from '../../../../../../fixtures'; +import { Node } from '../../../../../../../src/application/node'; + +const { convertLSKToBeddows } = utils; +const { genesis } = accounts; describe('Transaction order', () => { const dbName = 'transaction_order'; - let storage; - let node; - let forgerDB; + let node: Node; + let blockchainDB: KVStore; + let forgerDB: KVStore; beforeAll(async () => { - storage = new storageUtils.StorageSandbox( - configUtils.storageConfig({ database: dbName }), - dbName, - ); - forgerDB = new KVStore(`/tmp/${dbName}.db`); - await storage.bootstrap(); - node = await nodeUtils.createAndLoadNode(storage, forgerDB); - await node._forger.loadDelegates(); + ({ blockchainDB, forgerDB } = createDB(dbName)); + node = await nodeUtils.createAndLoadNode(blockchainDB, forgerDB); + await node['_forger'].loadDelegates(); }); afterAll(async () => { await forgerDB.clear(); await node.cleanup(); - await storage.cleanup(); + await blockchainDB.close(); + await forgerDB.close(); + removeDB(dbName); }); describe('given transactions in specific order', () => { describe('when account does not have sufficient balance at the beginning, but receives before spending', () => { - let newBlock; + let newBlock: BlockInstance; beforeAll(async () => { - const genesisAccount = await node._chain.dataAccess.getAccountByAddress( - genesis.address, - ); + const genesisAccount = await node[ + '_chain' + ].dataAccess.getAccountByAddress(genesis.address); const accountWithoutBalance = nodeUtils.createAccount(); const fundingTx = transfer({ nonce: genesisAccount.nonce.toString(), - networkIdentifier: node._networkIdentifier, + networkIdentifier: node['_networkIdentifier'], fee: convertLSKToBeddows('0.002'), recipientId: accountWithoutBalance.address, amount: convertLSKToBeddows('100'), passphrase: genesis.passphrase, - }); + }) as TransactionJSON; const returningTx = transfer({ nonce: '0', - networkIdentifier: node._networkIdentifier, + networkIdentifier: node['_networkIdentifier'], fee: convertLSKToBeddows('0.002'), recipientId: genesis.address, amount: convertLSKToBeddows('99'), passphrase: accountWithoutBalance.passphrase, - }); + }) as TransactionJSON; newBlock = await nodeUtils.createBlock(node, [ - node._chain.deserializeTransaction(fundingTx), - node._chain.deserializeTransaction(returningTx), + node['_chain'].deserializeTransaction(fundingTx), + node['_chain'].deserializeTransaction(returningTx), ]); - await node._processor.process(newBlock); + await node['_processor'].process(newBlock); }); it('should accept the block', async () => { - const createdBlock = await node._chain.dataAccess.getBlockByID( + const createdBlock = await node['_chain'].dataAccess.getBlockByID( newBlock.id, ); expect(createdBlock).not.toBeUndefined(); @@ -95,31 +91,31 @@ describe('Transaction order', () => { }); describe('when account register as delegate and make self vote', () => { - let newBlock; + let newBlock: BlockInstance; beforeAll(async () => { - const genesisAccount = await node._chain.dataAccess.getAccountByAddress( - genesis.address, - ); + const genesisAccount = await node[ + '_chain' + ].dataAccess.getAccountByAddress(genesis.address); const newAccount = nodeUtils.createAccount(); const fundingTx = transfer({ nonce: genesisAccount.nonce.toString(), - networkIdentifier: node._networkIdentifier, + networkIdentifier: node['_networkIdentifier'], fee: convertLSKToBeddows('0.002'), recipientId: newAccount.address, amount: convertLSKToBeddows('100'), passphrase: genesis.passphrase, - }); + }) as TransactionJSON; const registerDelegateTx = registerDelegate({ nonce: '0', - networkIdentifier: node._networkIdentifier, + networkIdentifier: node['_networkIdentifier'], fee: convertLSKToBeddows('11'), username: 'newdelegate', passphrase: newAccount.passphrase, - }); + }) as TransactionJSON; const selfVoteTx = castVotes({ nonce: '1', - networkIdentifier: node._networkIdentifier, + networkIdentifier: node['_networkIdentifier'], fee: convertLSKToBeddows('1'), votes: [ { @@ -128,17 +124,17 @@ describe('Transaction order', () => { }, ], passphrase: newAccount.passphrase, - }); + }) as TransactionJSON; newBlock = await nodeUtils.createBlock(node, [ - node._chain.deserializeTransaction(fundingTx), - node._chain.deserializeTransaction(registerDelegateTx), - node._chain.deserializeTransaction(selfVoteTx), + node['_chain'].deserializeTransaction(fundingTx), + node['_chain'].deserializeTransaction(registerDelegateTx), + node['_chain'].deserializeTransaction(selfVoteTx), ]); - await node._processor.process(newBlock); + await node['_processor'].process(newBlock); }); it('should accept the block', async () => { - const createdBlock = await node._chain.dataAccess.getBlockByID( + const createdBlock = await node['_chain'].dataAccess.getBlockByID( newBlock.id, ); expect(createdBlock).not.toBeUndefined(); @@ -146,25 +142,25 @@ describe('Transaction order', () => { }); describe('when account register as multisignature and send from the accounts', () => { - let newBlock; + let newBlock: BlockInstance; beforeAll(async () => { - const genesisAccount = await node._chain.dataAccess.getAccountByAddress( - genesis.address, - ); + const genesisAccount = await node[ + '_chain' + ].dataAccess.getAccountByAddress(genesis.address); const newAccount = nodeUtils.createAccount(); const multiSignatureMembers = nodeUtils.createAccounts(2); const fundingTx = transfer({ nonce: genesisAccount.nonce.toString(), - networkIdentifier: node._networkIdentifier, + networkIdentifier: node['_networkIdentifier'], fee: convertLSKToBeddows('0.002'), recipientId: newAccount.address, amount: convertLSKToBeddows('100'), passphrase: genesis.passphrase, - }); + }) as TransactionJSON; const registerMultisigTx = registerMultisignature({ nonce: '0', - networkIdentifier: node._networkIdentifier, + networkIdentifier: node['_networkIdentifier'], fee: convertLSKToBeddows('11'), mandatoryKeys: [newAccount.publicKey], optionalKeys: multiSignatureMembers.map(acc => acc.publicKey), @@ -174,20 +170,20 @@ describe('Transaction order', () => { newAccount.passphrase, ...multiSignatureMembers.map(acc => acc.passphrase), ], - }); + }) as TransactionJSON; const transferTx = transfer({ nonce: '1', senderPublicKey: newAccount.publicKey, - networkIdentifier: node._networkIdentifier, + networkIdentifier: node['_networkIdentifier'], fee: convertLSKToBeddows('0.003'), recipientId: newAccount.address, amount: convertLSKToBeddows('80'), - }); - const deserializedTransferTx = node._chain.deserializeTransaction( + }) as TransactionJSON; + const deserializedTransferTx = node['_chain'].deserializeTransaction( transferTx, ); deserializedTransferTx.sign( - node._networkIdentifier, + node['_networkIdentifier'], undefined, [newAccount.passphrase, multiSignatureMembers[0].passphrase], { @@ -196,15 +192,15 @@ describe('Transaction order', () => { }, ); newBlock = await nodeUtils.createBlock(node, [ - node._chain.deserializeTransaction(fundingTx), - node._chain.deserializeTransaction(registerMultisigTx), + node['_chain'].deserializeTransaction(fundingTx), + node['_chain'].deserializeTransaction(registerMultisigTx), deserializedTransferTx, ]); - await node._processor.process(newBlock); + await node['_processor'].process(newBlock); }); it('should accept the block', async () => { - const createdBlock = await node._chain.dataAccess.getBlockByID( + const createdBlock = await node['_chain'].dataAccess.getBlockByID( newBlock.id, ); expect(createdBlock).not.toBeUndefined(); @@ -212,25 +208,25 @@ describe('Transaction order', () => { }); describe('when account register as multisignature and send transfer with old signature', () => { - let newBlock; + let newBlock: BlockInstance; beforeAll(async () => { - const genesisAccount = await node._chain.dataAccess.getAccountByAddress( - genesis.address, - ); + const genesisAccount = await node[ + '_chain' + ].dataAccess.getAccountByAddress(genesis.address); const newAccount = nodeUtils.createAccount(); const multiSignatureMembers = nodeUtils.createAccounts(2); const fundingTx = transfer({ nonce: genesisAccount.nonce.toString(), - networkIdentifier: node._networkIdentifier, + networkIdentifier: node['_networkIdentifier'], fee: convertLSKToBeddows('0.002'), recipientId: newAccount.address, amount: convertLSKToBeddows('100'), passphrase: genesis.passphrase, - }); + }) as TransactionJSON; const registerMultisigTx = registerMultisignature({ nonce: '0', - networkIdentifier: node._networkIdentifier, + networkIdentifier: node['_networkIdentifier'], fee: convertLSKToBeddows('11'), mandatoryKeys: [newAccount.publicKey], optionalKeys: multiSignatureMembers.map(acc => acc.publicKey), @@ -240,26 +236,26 @@ describe('Transaction order', () => { newAccount.passphrase, ...multiSignatureMembers.map(acc => acc.passphrase), ], - }); + }) as TransactionJSON; const transferTx = transfer({ nonce: '1', - networkIdentifier: node._networkIdentifier, + networkIdentifier: node['_networkIdentifier'], fee: convertLSKToBeddows('0.003'), recipientId: newAccount.address, amount: convertLSKToBeddows('80'), passphrase: newAccount.passphrase, - }); + }) as TransactionJSON; newBlock = await nodeUtils.createBlock(node, [ - node._chain.deserializeTransaction(fundingTx), - node._chain.deserializeTransaction(registerMultisigTx), - node._chain.deserializeTransaction(transferTx), + node['_chain'].deserializeTransaction(fundingTx), + node['_chain'].deserializeTransaction(registerMultisigTx), + node['_chain'].deserializeTransaction(transferTx), ]); }); it('should not accept the block', async () => { expect.assertions(2); try { - await node._processor.process(newBlock); + await node['_processor'].process(newBlock); } catch (errors) { // eslint-disable-next-line jest/no-try-expect expect(errors).toHaveLength(1); @@ -272,48 +268,48 @@ describe('Transaction order', () => { }); describe('when account does not have sufficient balance in the middle of process', () => { - let newBlock; + let newBlock: BlockInstance; beforeAll(async () => { - const genesisAccount = await node._chain.dataAccess.getAccountByAddress( - genesis.address, - ); + const genesisAccount = await node[ + '_chain' + ].dataAccess.getAccountByAddress(genesis.address); const accountWithoutBalance = nodeUtils.createAccount(); const fundingTx = transfer({ nonce: genesisAccount.nonce.toString(), - networkIdentifier: node._networkIdentifier, + networkIdentifier: node['_networkIdentifier'], fee: convertLSKToBeddows('0.002'), recipientId: accountWithoutBalance.address, amount: convertLSKToBeddows('100'), passphrase: genesis.passphrase, - }); + }) as TransactionJSON; const spendingTx = transfer({ nonce: '0', - networkIdentifier: node._networkIdentifier, + networkIdentifier: node['_networkIdentifier'], fee: convertLSKToBeddows('0.002'), recipientId: genesis.address, amount: convertLSKToBeddows('140'), passphrase: accountWithoutBalance.passphrase, - }); + }) as TransactionJSON; const refundingTx = transfer({ nonce: (genesisAccount.nonce + BigInt(1)).toString(), - networkIdentifier: node._networkIdentifier, + networkIdentifier: node['_networkIdentifier'], fee: convertLSKToBeddows('0.002'), recipientId: accountWithoutBalance.address, amount: convertLSKToBeddows('50'), passphrase: genesis.passphrase, - }); + }) as TransactionJSON; newBlock = await nodeUtils.createBlock(node, [ - node._chain.deserializeTransaction(fundingTx), - node._chain.deserializeTransaction(spendingTx), - node._chain.deserializeTransaction(refundingTx), + node['_chain'].deserializeTransaction(fundingTx), + node['_chain'].deserializeTransaction(spendingTx), + node['_chain'].deserializeTransaction(refundingTx), ]); }); it('should not accept the block', async () => { expect.assertions(2); try { - await node._processor.process(newBlock); + await node['_processor'].process(newBlock); } catch (errors) { // eslint-disable-next-line jest/no-try-expect expect(errors).toHaveLength(1); diff --git a/framework/test/jest/integration/specs/application/node/rebuilder/rebuilding.spec.js b/framework/test/jest/integration/specs/application/node/rebuilder/rebuilding.spec.js index 80f1ebc6320..60fda99dd71 100644 --- a/framework/test/jest/integration/specs/application/node/rebuilder/rebuilding.spec.js +++ b/framework/test/jest/integration/specs/application/node/rebuilder/rebuilding.spec.js @@ -19,39 +19,33 @@ const { utils: { convertLSKToBeddows }, } = require('@liskhq/lisk-transactions'); const { KVStore } = require('@liskhq/lisk-db'); -const { - nodeUtils, - storageUtils, - configUtils, -} = require('../../../../../../utils'); +const { nodeUtils, configUtils } = require('../../../../../../utils'); const { accounts: { genesis }, } = require('../../../../../../fixtures'); +const { createDB, removeDB } = require('../../../../../../utils/kv_store'); describe('Rebuilding blocks', () => { // This test takes long jest.setTimeout(100000); const dbName = 'rebuild_block'; - let storage; let node; + let blockchainDB; let forgerDB; beforeAll(async () => { - storage = new storageUtils.StorageSandbox( - configUtils.storageConfig({ database: dbName }), - dbName, - ); - await storage.bootstrap(); - forgerDB = new KVStore(`/tmp/${dbName}.db`); - node = await nodeUtils.createAndLoadNode(storage, forgerDB); + ({ blockchainDB, forgerDB } = createDB(dbName)); + node = await nodeUtils.createAndLoadNode(blockchainDB, forgerDB); await node._forger.loadDelegates(); }); afterAll(async () => { await forgerDB.clear(); await node.cleanup(); - await storage.cleanup(); + await blockchainDB.close(); + await forgerDB.close(); + removeDB(dbName); }); describe('given a valid blockchain for 3 rounds', () => { diff --git a/framework/test/jest/unit/specs/application/node/node.spec.ts b/framework/test/jest/unit/specs/application/node/node.spec.ts index de4bcd2d789..229b662c2fe 100644 --- a/framework/test/jest/unit/specs/application/node/node.spec.ts +++ b/framework/test/jest/unit/specs/application/node/node.spec.ts @@ -13,6 +13,7 @@ */ import { when } from 'jest-when'; +import { KVStore } from '@liskhq/lisk-db'; import { BFT } from '@liskhq/lisk-bft'; import { Node } from '../../../../../../src/application/node/node'; import { Synchronizer } from '../../../../../../src/application/node/synchronizer/synchronizer'; @@ -29,6 +30,8 @@ const setProperty = (object: object, property: string, value: any) => { return originalProperty; }; +jest.mock('@liskhq/lisk-db'); + describe('Node', () => { let node: Node; let subscribedEvents: any; @@ -44,6 +47,9 @@ describe('Node', () => { jest.spyOn(Processor.prototype, 'init').mockResolvedValue(undefined); jest.spyOn(Synchronizer.prototype, 'init').mockResolvedValue(undefined); + const blockchainDB = new KVStore('blockchain.db'); + const forgerDB = new KVStore('forger.db'); + /* Arranging Stubs start */ stubs.logger = { trace: jest.fn(), @@ -57,16 +63,6 @@ describe('Node', () => { stubs.cache = { cleanup: jest.fn(), }; - stubs.storage = { - cleanup: jest.fn(), - entities: { - Block: { - get: jest.fn().mockResolvedValue([]), - count: jest.fn().mockResolvedValue(0), - }, - ChainMeta: { getKey: jest.fn() }, - }, - }; stubs.forgerDB = { get: jest.fn(), put: jest.fn(), @@ -101,15 +97,11 @@ describe('Node', () => { .calledWith('app:getComponentConfig', 'cache') .mockResolvedValue(cacheConfig as never); - when(stubs.storage.entities.Block.get) - .calledWith({}, { sort: 'height:desc', limit: 1, extended: true }) - .mockResolvedValue(lastBlock as never); - // Act const params = { channel: stubs.channel, - storage: stubs.storage, - forgerDB: stubs.forgerDB, + blockchainDB, + forgerDB, logger: stubs.logger, options: nodeOptions, applicationState: stubs.applicationState, @@ -129,7 +121,6 @@ describe('Node', () => { }); it('should initialize class properties', () => { expect(node['_logger']).toEqual(stubs.logger); - expect(node['_storage']).toEqual(stubs.storage); expect(node['_channel']).toEqual(stubs.channel); expect(node['_components']).not.toBeUndefined(); expect(node['_sequence']).toBeUndefined(); @@ -162,7 +153,6 @@ describe('Node', () => { rebuildUpToRound: 0, }, logger: stubs.logger, - storage: stubs.storage, } as any); // Act @@ -328,7 +318,6 @@ describe('Node', () => { genesisBlock: null, }, logger: stubs.logger, - storage: stubs.storage, } as any); // Act diff --git a/framework/test/jest/unit/specs/application/node/synchronizer/block_synchronization_mechanism/block_synchronization_mechanism.spec.ts b/framework/test/jest/unit/specs/application/node/synchronizer/block_synchronization_mechanism/block_synchronization_mechanism.spec.ts index 53283dbc75d..f8a307a6e87 100644 --- a/framework/test/jest/unit/specs/application/node/synchronizer/block_synchronization_mechanism/block_synchronization_mechanism.spec.ts +++ b/framework/test/jest/unit/specs/application/node/synchronizer/block_synchronization_mechanism/block_synchronization_mechanism.spec.ts @@ -13,12 +13,12 @@ */ import { cloneDeep } from 'lodash'; +import { KVStore } from '@liskhq/lisk-db'; import { getNetworkIdentifier } from '@liskhq/lisk-cryptography'; import { when } from 'jest-when'; import { BlockInstance, BlockJSON, Chain } from '@liskhq/lisk-chain'; import { BFT } from '@liskhq/lisk-bft'; import { Dpos } from '@liskhq/lisk-dpos'; -import { KVStore } from '@liskhq/lisk-db'; import { BlockProcessorV2 } from '../../../../../../../../src/application/node/block_processor_v2'; import { BlockSynchronizationMechanism } from '../../../../../../../../src/application/node/synchronizer'; @@ -37,6 +37,8 @@ const { InMemoryChannel: ChannelMock } = jest.genMockFromModule( '../../../../../../../../src/controller/channels/in_memory_channel', ); +jest.mock('@liskhq/lisk-db'); + describe('block_synchronization_mechanism', () => { let bftModule: any; let blockProcessorV2; @@ -54,11 +56,6 @@ describe('block_synchronization_mechanism', () => { let blockIdsList: string[]; let blockList: BlockInstance[]; let dataAccessMock; - const forgerDBMock = new KVStore('/tmp/bsm.db'); - - afterAll(async () => { - await forgerDBMock.clear(); - }); beforeEach(() => { loggerMock = { @@ -67,7 +64,6 @@ describe('block_synchronization_mechanism', () => { error: jest.fn(), trace: jest.fn(), }; - const storageMock: any = {}; channelMock = new ChannelMock(); const networkIdentifier = getNetworkIdentifier( @@ -75,9 +71,12 @@ describe('block_synchronization_mechanism', () => { genesisBlockDevnet.communityIdentifier, ); + const blockchainDB = new KVStore('blockchain.db'); + const forgerDB = new KVStore('forger.db'); + chainModule = new Chain({ networkIdentifier, - storage: storageMock, + db: blockchainDB, genesisBlock: genesisBlockDevnet as any, registeredTransactions, maxPayloadLength: constants.maxPayloadLength, @@ -126,7 +125,7 @@ describe('block_synchronization_mechanism', () => { blockProcessorV2 = new BlockProcessorV2({ networkIdentifier: '', - forgerDB: forgerDBMock, + forgerDB, chainModule, bftModule, dposModule, @@ -910,17 +909,17 @@ describe('block_synchronization_mechanism', () => { ]; const tempTableBlocks = [ - { - fullBlock: newBlock({ height: highestCommonBlock.height + 1 }), - }, + previousTip, ...new Array(previousTip.height - highestCommonBlock.height - 1) .fill(0) - .map((_, index) => ({ - fullBlock: newBlock({ - height: index + 2 + highestCommonBlock.height, + .map((_, index) => + newBlock({ + height: previousTip.height - index - 1, }), - })), - { fullBlock: previousTip }, + ), + { + ...newBlock({ height: highestCommonBlock.height + 1 }), + }, ]; for (const expectedPeer of peersList.expectedSelection) { @@ -937,13 +936,7 @@ describe('block_synchronization_mechanism', () => { } chainModule.dataAccess.getTempBlocks - .mockResolvedValueOnce([ - { - fullBlock: previousTip, - height: previousTip.height, - version: previousTip.version, - }, - ]) + .mockResolvedValueOnce([previousTip]) .mockResolvedValueOnce(tempTableBlocks); when(processorModule.deleteLastBlock) @@ -1001,7 +994,7 @@ describe('block_synchronization_mechanism', () => { for (const tempTableBlock of tempTableBlocks) { expect(processorModule.processValidated).toHaveBeenCalledWith( - await processorModule.deserialize(tempTableBlock.fullBlock), + await processorModule.deserialize(tempTableBlock), { removeFromTempTable: true, }, @@ -1054,13 +1047,7 @@ describe('block_synchronization_mechanism', () => { } as never); } - chainModule.dataAccess.getTempBlocks.mockResolvedValue([ - { - fullBlock: previousTip, - height: previousTip.height, - version: previousTip.version, - }, - ]); + chainModule.dataAccess.getTempBlocks.mockResolvedValue([previousTip]); const processingError = new Error('Error processing blocks'); processorModule.processValidated.mockRejectedValueOnce( diff --git a/framework/test/jest/unit/specs/application/node/synchronizer/fast_chain_switching_mechanism/fast_chain_switching_mechanism.spec.ts b/framework/test/jest/unit/specs/application/node/synchronizer/fast_chain_switching_mechanism/fast_chain_switching_mechanism.spec.ts index ab9078acc39..309b4161270 100644 --- a/framework/test/jest/unit/specs/application/node/synchronizer/fast_chain_switching_mechanism/fast_chain_switching_mechanism.spec.ts +++ b/framework/test/jest/unit/specs/application/node/synchronizer/fast_chain_switching_mechanism/fast_chain_switching_mechanism.spec.ts @@ -13,10 +13,10 @@ */ import { when } from 'jest-when'; +import { KVStore } from '@liskhq/lisk-db'; import { BlockInstance, Chain } from '@liskhq/lisk-chain'; import { BFT } from '@liskhq/lisk-bft'; import { Dpos } from '@liskhq/lisk-dpos'; -import { KVStore } from '@liskhq/lisk-db'; import { BlockProcessorV2 } from '../../../../../../../../src/application/node/block_processor_v2'; import { @@ -34,6 +34,8 @@ const { InMemoryChannel: ChannelMock } = jest.genMockFromModule( '../../../../../../../../src/controller/channels/in_memory_channel', ); +jest.mock('@liskhq/lisk-db'); + describe('fast_chain_switching_mechanism', () => { let bftModule: any; let blockProcessorV2; @@ -45,11 +47,6 @@ describe('fast_chain_switching_mechanism', () => { let channelMock: any; let loggerMock: any; let dataAccessMock; - const forgerDBMock = new KVStore('/tmp/fsc.db'); - - afterAll(async () => { - await forgerDBMock.clear(); - }); beforeEach(() => { loggerMock = { @@ -58,13 +55,15 @@ describe('fast_chain_switching_mechanism', () => { error: jest.fn(), trace: jest.fn(), }; - const storageMock: any = {}; channelMock = new ChannelMock(); + const blockchainDB = new KVStore('blockchain.db'); + const forgerDB = new KVStore('forger.db'); + chainModule = new Chain({ networkIdentifier: '', - storage: storageMock, + db: blockchainDB, genesisBlock: genesisBlockDevnet as any, registeredTransactions, maxPayloadLength: constants.maxPayloadLength, @@ -109,7 +108,7 @@ describe('fast_chain_switching_mechanism', () => { blockProcessorV2 = new BlockProcessorV2({ networkIdentifier: '', - forgerDB: forgerDBMock, + forgerDB, chainModule, bftModule, dposModule, @@ -774,13 +773,7 @@ describe('fast_chain_switching_mechanism', () => { }) .mockResolvedValueOnce(genesisBlockDevnet as never); - const blocksInTempTable = [ - { - fullBlock: chainModule.lastBlock, - height: chainModule.lastBlock.height, - id: chainModule.lastBlock.id, - }, - ]; + const blocksInTempTable = [chainModule.lastBlock]; chainModule.dataAccess.getTempBlocks.mockResolvedValue( blocksInTempTable, @@ -823,7 +816,7 @@ describe('fast_chain_switching_mechanism', () => { ); // Restore blocks from temp table: expect(processorModule.processValidated).toHaveBeenCalledWith( - await processorModule.deserialize(blocksInTempTable[0].fullBlock), + await processorModule.deserialize(blocksInTempTable[0]), { removeFromTempTable: true, }, diff --git a/framework/test/jest/unit/specs/application/node/synchronizer/synchronizer.spec.ts b/framework/test/jest/unit/specs/application/node/synchronizer/synchronizer.spec.ts index dfc4ff52b38..30a80a33b66 100644 --- a/framework/test/jest/unit/specs/application/node/synchronizer/synchronizer.spec.ts +++ b/framework/test/jest/unit/specs/application/node/synchronizer/synchronizer.spec.ts @@ -29,6 +29,8 @@ import { registeredTransactions } from '../../../../../../utils/registered_trans import * as genesisBlockDevnet from '../../../../../../fixtures/config/devnet/genesis_block.json'; +jest.mock('@liskhq/lisk-db'); + const { InMemoryChannel: ChannelMock } = jest.genMockFromModule( '../../../../../../../src/controller/channels/in_memory_channel', ); @@ -49,11 +51,6 @@ describe('Synchronizer', () => { let loggerMock: any; let syncParameters; let dataAccessMock; - const forgerDBMock = new KVStore('/tmp/synchronizer.db'); - - afterAll(async () => { - await forgerDBMock.clear(); - }); beforeEach(() => { jest.spyOn(synchronizerUtils, 'restoreBlocksUponStartup'); @@ -63,7 +60,6 @@ describe('Synchronizer', () => { error: jest.fn(), trace: jest.fn(), }; - const storageMock: any = {}; transactionPoolModuleStub = { add: jest.fn(), @@ -77,9 +73,12 @@ describe('Synchronizer', () => { genesisBlockDevnet.communityIdentifier, ); + const blockchainDB = new KVStore('blockchain.db'); + const forgerDB = new KVStore('forger.db'); + chainModule = new Chain({ networkIdentifier, - storage: storageMock, + db: blockchainDB, genesisBlock: genesisBlockDevnet as any, registeredTransactions, maxPayloadLength: constants.maxPayloadLength, @@ -118,7 +117,7 @@ describe('Synchronizer', () => { blockProcessorV2 = new BlockProcessorV2({ networkIdentifier: '', - forgerDB: forgerDBMock, + forgerDB, chainModule, bftModule, dposModule: dposModuleMock, @@ -185,9 +184,7 @@ describe('Synchronizer', () => { const blocksTempTableEntries = new Array(10) .fill(0) .map((_, index) => ({ - height: index, - id: `${index}`, - fullBlock: newBlock({ + ...newBlock({ height: index, id: index.toString(), version: 2, @@ -208,7 +205,7 @@ describe('Synchronizer', () => { when(chainModule.dataAccess.getTempBlocks) .calledWith() - .mockResolvedValue(blocksTempTableEntries as never); + .mockResolvedValue(blocksTempTableEntries.reverse() as never); when(chainModule.dataAccess.getLastBlock) .calledWith() @@ -243,7 +240,7 @@ describe('Synchronizer', () => { // Assert whether temp blocks are being restored to main table expect.assertions(blocksTempTableEntries.length + 4); for (let i = 0; i < blocksTempTableEntries.length; i += 1) { - const tempBlock = blocksTempTableEntries[i].fullBlock; + const tempBlock = blocksTempTableEntries[i]; expect(processorModule.processValidated).toHaveBeenNthCalledWith( i + 1, await processorModule.deserialize(tempBlock as any), @@ -266,12 +263,8 @@ describe('Synchronizer', () => { { height: genesisBlockDevnet.height + 2, id: '3', - fullBlock: { - height: genesisBlockDevnet.height + 2, - id: '3', - version: 2, - previousBlockId: initialLastBlock.id, - }, + version: 2, + previousBlockId: initialLastBlock.id, }, ]; chainModule.dataAccess.getTempBlocks.mockResolvedValue( @@ -304,7 +297,7 @@ describe('Synchronizer', () => { // Assert whether temp blocks are being restored to main table expect.assertions(blocksTempTableEntries.length + 3); for (let i = 0; i < blocksTempTableEntries.length; i += 1) { - const tempBlock = blocksTempTableEntries[i].fullBlock; + const tempBlock = blocksTempTableEntries[i]; expect(processorModule.processValidated).toHaveBeenNthCalledWith( i + 1, await processorModule.deserialize(tempBlock as any), @@ -323,12 +316,7 @@ describe('Synchronizer', () => { previousBlockId: genesisBlockDevnet.id, version: 2, }; - const blocksTempTableEntries = [ - { - ...initialLastBlock, - fullBlock: initialLastBlock, - }, - ]; + const blocksTempTableEntries = [initialLastBlock]; chainModule.dataAccess.getTempBlocks.mockResolvedValue( blocksTempTableEntries, ); @@ -364,9 +352,7 @@ describe('Synchronizer', () => { const blocksTempTableEntries = new Array(10) .fill(0) .map((_, index) => ({ - height: index, - id: `${index}`, - fullBlock: newBlock({ + ...newBlock({ height: index, id: index.toString(), version: 2, @@ -380,7 +366,7 @@ describe('Synchronizer', () => { version: 1, }; chainModule.dataAccess.getTempBlocks.mockResolvedValue( - blocksTempTableEntries, + blocksTempTableEntries.reverse(), ); // To load storage tip block into lastBlock in memory variable when(chainModule.dataAccess.getLastBlock) diff --git a/framework/test/utils/kv_store.ts b/framework/test/utils/kv_store.ts new file mode 100644 index 00000000000..38d4a2aa341 --- /dev/null +++ b/framework/test/utils/kv_store.ts @@ -0,0 +1,34 @@ +/* + * Copyright © 2020 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 * as fs from 'fs-extra'; +import { KVStore } from '@liskhq/lisk-db'; + +export const defaultPath = '/tmp/lisk-framework/test'; + +const getPath = (name: string): string => `${defaultPath}/${name}`; + +// eslint-disable-next-line +export const createDB = (name: string) => { + const path = getPath(name); + fs.ensureDirSync(path); + const forgerDBPath = getPath(name); + return { + path, + blockchainDB: new KVStore(`${path}/blockchain.db`), + forgerDBPath, + forgerDB: new KVStore(`${path}/forger.db`), + }; +}; + +export const removeDB = (name: string): void => fs.removeSync(getPath(name)); diff --git a/framework/test/utils/node/node.js b/framework/test/utils/node/node.js index 7de63febac9..1da21052816 100644 --- a/framework/test/utils/node/node.js +++ b/framework/test/utils/node/node.js @@ -27,7 +27,13 @@ const config = require('../../fixtures/config/devnet/config.json'); const { components, modules, ...rootConfigs } = config; const { network, ...nodeConfigs } = rootConfigs; -const createNode = ({ storage, forgerDB, logger, channel, options = {} }) => { +const createNode = ({ + blockchainDB, + forgerDB, + logger, + channel, + options = {}, +}) => { const nodeOptions = { ...nodeConfig(), ...nodeConfigs, @@ -40,7 +46,7 @@ const createNode = ({ storage, forgerDB, logger, channel, options = {} }) => { channel: channel || createMockChannel(), options: nodeOptions, logger, - storage, + blockchainDB, forgerDB, applicationState: null, }); @@ -58,14 +64,14 @@ const fakeLogger = { /* eslint-enable @typescript-eslint/no-empty-function */ const createAndLoadNode = async ( - storage, + blockchainDB, forgerDB, logger = fakeLogger, channel = undefined, options = {}, ) => { const chainModule = createNode({ - storage, + blockchainDB, forgerDB, logger, channel,