From af5e5423389be47470f82d99351f57cb8a871cd6 Mon Sep 17 00:00:00 2001 From: Bertie Date: Thu, 20 Aug 2020 10:27:21 -0400 Subject: [PATCH] fix(core-database): decouple database and state packages (#3936) --- .../transaction-forging/__support__/index.ts | 9 +- .../core-api/handlers/rounds.test.ts | 5 +- .../unit/core-blockchain/blockchain.test.ts | 25 +- .../processor/block-processor.test.ts | 31 +- .../handlers/accept-block-handler.test.ts | 27 +- .../handlers/unchained-handler.test.ts | 24 +- .../core-blockchain/service-provider.test.ts | 1 + .../state-machine/actions/initialize.test.ts | 17 +- .../core-database/database-service.test.ts | 751 +--------------- .../core-database/service-provider.test.ts | 12 +- .../unit/core-forger/forger-service.test.ts | 4 +- __tests__/unit/core-forger/setup.ts | 11 +- .../utils/get-blocktime-lookup.test.ts | 2 +- __tests__/unit/core-p2p/peer-verifier.test.ts | 288 +++--- .../controllers/internal.test.ts | 11 +- .../socket-server/controllers/peer.test.ts | 9 +- .../core-state/__fixtures__/block1760000.ts | 118 +++ .../core-state/database-interactions.test.ts | 817 ++++++++++++++++++ .../unit/core-state/service-provider.test.ts | 10 +- packages/core-blockchain/src/blockchain.ts | 12 +- .../src/processor/block-processor.ts | 2 +- .../handlers/accept-block-handler.ts | 8 +- .../src/state-machine/actions/initialize.ts | 8 +- packages/core-database/src/actions/index.ts | 1 - .../core-database/src/database-service.ts | 546 +----------- .../core-database/src/service-provider.ts | 11 +- packages/core-kernel/src/ioc/identifiers.ts | 1 + .../src/utils/get-blocktime-lookup.ts | 2 +- packages/core-p2p/src/peer-verifier.ts | 12 +- .../src/socket-server/controllers/internal.ts | 18 +- .../src/socket-server/controllers/peer.ts | 12 +- .../src/actions/get-active-delegates.ts | 8 +- packages/core-state/src/actions/index.ts | 1 + .../core-state/src/database-interactions.ts | 549 ++++++++++++ packages/core-state/src/index.ts | 1 + packages/core-state/src/service-provider.ts | 10 +- packages/core/bin/config/devnet/app.json | 5 +- packages/core/bin/config/mainnet/app.json | 5 +- packages/core/bin/config/testnet/app.json | 5 +- 39 files changed, 1898 insertions(+), 1491 deletions(-) create mode 100644 __tests__/unit/core-state/__fixtures__/block1760000.ts create mode 100644 __tests__/unit/core-state/database-interactions.test.ts delete mode 100644 packages/core-database/src/actions/index.ts rename packages/{core-database => core-state}/src/actions/get-active-delegates.ts (66%) create mode 100644 packages/core-state/src/database-interactions.ts diff --git a/__tests__/functional/transaction-forging/__support__/index.ts b/__tests__/functional/transaction-forging/__support__/index.ts index 2375ea0394..6db11db06f 100644 --- a/__tests__/functional/transaction-forging/__support__/index.ts +++ b/__tests__/functional/transaction-forging/__support__/index.ts @@ -1,15 +1,16 @@ import "jest-extended"; -import delay from "delay"; import { Container, Contracts } from "@arkecosystem/core-kernel"; import { Identities, Managers, Utils } from "@arkecosystem/crypto"; import secrets from "@packages/core-test-framework/src/internal/passphrases.json"; +import delay from "delay"; jest.setTimeout(1200000); import { DatabaseService } from "@arkecosystem/core-database"; -import { Sandbox } from "@packages/core-test-framework/src"; +import { DatabaseInteraction } from "@arkecosystem/core-state"; import { StateBuilder } from "@arkecosystem/core-state/src/state-builder"; +import { Sandbox } from "@packages/core-test-framework/src"; const sandbox: Sandbox = new Sandbox(); @@ -87,7 +88,9 @@ export const setUp = async (): Promise => { }), ); - await (databaseService as any).initializeActiveDelegates(1); + const databaseInteraction = app.get(Container.Identifiers.DatabaseInteraction); + + await (databaseInteraction as any).initializeActiveDelegates(1); }); return sandbox.app; diff --git a/__tests__/integration/core-api/handlers/rounds.test.ts b/__tests__/integration/core-api/handlers/rounds.test.ts index e253bdccc5..a765777792 100644 --- a/__tests__/integration/core-api/handlers/rounds.test.ts +++ b/__tests__/integration/core-api/handlers/rounds.test.ts @@ -2,6 +2,7 @@ import "@packages/core-test-framework/src/matchers"; import { DatabaseService } from "@arkecosystem/core-database"; import { Container, Contracts } from "@arkecosystem/core-kernel"; +import { DatabaseInteraction } from "@arkecosystem/core-state"; import { ApiHelpers, Factories } from "@packages/core-test-framework/src"; import { calculateRanks, setUp, tearDown } from "../__support__/setup"; @@ -14,7 +15,9 @@ beforeAll(async () => { const databaseService = app.get(Container.Identifiers.DatabaseService); await databaseService.saveRound(Factories.factory("Round").make()); - await (databaseService as any).initializeActiveDelegates(1); + const databaseInteraction = app.get(Container.Identifiers.DatabaseInteraction); + + await (databaseInteraction as any).initializeActiveDelegates(1); await calculateRanks(); }); diff --git a/__tests__/unit/core-blockchain/blockchain.test.ts b/__tests__/unit/core-blockchain/blockchain.test.ts index df811f07f6..59d6fa7ea1 100644 --- a/__tests__/unit/core-blockchain/blockchain.test.ts +++ b/__tests__/unit/core-blockchain/blockchain.test.ts @@ -3,8 +3,8 @@ import "jest-extended"; import { ProcessBlockAction } from "@packages/core-blockchain/src/actions"; import { Blockchain } from "@packages/core-blockchain/src/blockchain"; import { BlockProcessorResult } from "@packages/core-blockchain/src/processor/block-processor"; -import { GetActiveDelegatesAction } from "@packages/core-database/src/actions"; import { Container, Enums, Services, Utils as AppUtils } from "@packages/core-kernel"; +import { GetActiveDelegatesAction } from "@packages/core-state/src/actions"; import { Sandbox } from "@packages/core-test-framework"; import { Crypto, Interfaces, Managers, Networks, Utils } from "@packages/crypto"; import delay from "delay"; @@ -22,6 +22,7 @@ describe("Blockchain", () => { const peerNetworkMonitor: any = {}; const peerStorage: any = {}; const blockProcessor: any = {}; + const databaseInteractions: any = {}; beforeAll(() => { sandbox = new Sandbox(); @@ -29,6 +30,7 @@ describe("Blockchain", () => { sandbox.app.bind(Container.Identifiers.LogService).toConstantValue(logService); sandbox.app.bind(Container.Identifiers.StateStore).toConstantValue(stateStore); sandbox.app.bind(Container.Identifiers.DatabaseService).toConstantValue(databaseService); + sandbox.app.bind(Container.Identifiers.DatabaseInteraction).toConstantValue(databaseInteractions); sandbox.app.bind(Container.Identifiers.DatabaseBlockRepository).toConstantValue(blockRepository); sandbox.app.bind(Container.Identifiers.TransactionPoolService).toConstantValue(transactionPoolService); sandbox.app.bind(Container.Identifiers.StateMachine).toConstantValue(stateMachine); @@ -72,12 +74,15 @@ describe("Blockchain", () => { stateStore.pushPingBlock = jest.fn(); stateStore.pingBlock = jest.fn(); - databaseService.getTopBlocks = jest.fn(); - databaseService.getLastBlock = jest.fn(); - databaseService.loadBlocksFromCurrentRound = jest.fn(); - databaseService.revertBlock = jest.fn(); databaseService.deleteRound = jest.fn(); - databaseService.getActiveDelegates = jest.fn().mockReturnValue([]); + databaseService.revertBlock = jest.fn(); + + databaseInteractions.getTopBlocks = jest.fn(); + databaseInteractions.getLastBlock = jest.fn(); + databaseInteractions.loadBlocksFromCurrentRound = jest.fn(); + databaseInteractions.revertBlock = jest.fn(); + databaseInteractions.deleteRound = jest.fn(); + databaseInteractions.getActiveDelegates = jest.fn().mockReturnValue([]); blockRepository.deleteBlocks = jest.fn(); blockRepository.deleteTopBlocks = jest.fn(); @@ -597,7 +602,7 @@ describe("Blockchain", () => { await blockchain.removeBlocks(2); - expect(databaseService.revertBlock).toHaveBeenCalledTimes(2); + expect(databaseInteractions.revertBlock).toHaveBeenCalledTimes(2); expect(stateStore.setLastBlock).toHaveBeenCalledTimes(2); expect(blockRepository.deleteBlocks).toHaveBeenCalledTimes(1); }); @@ -622,7 +627,7 @@ describe("Blockchain", () => { stateStore.getLastBlock = jest.fn(); - expect(databaseService.revertBlock).toHaveBeenCalledTimes(1); + expect(databaseInteractions.revertBlock).toHaveBeenCalledTimes(1); expect(stateStore.setLastBlock).toHaveBeenCalledTimes(1); expect(blockRepository.deleteBlocks).toHaveBeenCalledTimes(1); }); @@ -637,7 +642,7 @@ describe("Blockchain", () => { await blockchain.removeTopBlocks(numberOfBlocks); expect(blockRepository.deleteTopBlocks).toHaveBeenLastCalledWith(numberOfBlocks); - expect(databaseService.loadBlocksFromCurrentRound).toHaveBeenCalled(); + expect(databaseInteractions.loadBlocksFromCurrentRound).toHaveBeenCalled(); }, ); }); @@ -701,7 +706,7 @@ describe("Blockchain", () => { expect(spyClearQueue).toBeCalledTimes(1); expect(spyResetLastDownloadedBlock).toBeCalledTimes(1); - expect(databaseService.revertBlock).toBeCalledTimes(1); + expect(databaseInteractions.revertBlock).toBeCalledTimes(1); }); it("should broadcast a block if (Crypto.Slots.getSlotNumber() * blocktime <= block.data.timestamp)", async () => { diff --git a/__tests__/unit/core-blockchain/processor/block-processor.test.ts b/__tests__/unit/core-blockchain/processor/block-processor.test.ts index f85f6ee83e..7d174c92bc 100644 --- a/__tests__/unit/core-blockchain/processor/block-processor.test.ts +++ b/__tests__/unit/core-blockchain/processor/block-processor.test.ts @@ -9,8 +9,8 @@ import { UnchainedHandler, VerificationFailedHandler, } from "@packages/core-blockchain/src/processor/handlers"; -import { GetActiveDelegatesAction } from "@packages/core-database/src/actions"; import { Container, Services } from "@packages/core-kernel"; +import { GetActiveDelegatesAction } from "@packages/core-state/src/actions"; import { Sandbox } from "@packages/core-test-framework"; import { Interfaces, Managers, Utils } from "@packages/crypto"; @@ -37,11 +37,17 @@ describe("BlockProcessor", () => { const transactionHandlerRegistry = { getActivatedHandlerForData: jest.fn(), }; - const databaseService = { - getActiveDelegates: jest.fn(), + const databaseService = {}; + const databaseInteractions = { walletRepository: { getNonce: jest.fn(), }, + getTopBlocks: jest.fn(), + getLastBlock: jest.fn(), + loadBlocksFromCurrentRound: jest.fn(), + revertBlock: jest.fn(), + deleteRound: jest.fn(), + getActiveDelegates: jest.fn().mockReturnValue([]), }; beforeAll(() => { @@ -50,6 +56,7 @@ describe("BlockProcessor", () => { sandbox.app.bind(Container.Identifiers.DatabaseTransactionRepository).toConstantValue(transactionRepository); sandbox.app.bind(Container.Identifiers.WalletRepository).toConstantValue(walletRepository); sandbox.app.bind(Container.Identifiers.DatabaseService).toConstantValue(databaseService); + sandbox.app.bind(Container.Identifiers.DatabaseInteraction).toConstantValue(databaseInteractions); sandbox.app.bind(Container.Identifiers.TransactionHandlerRegistry).toConstantValue(transactionHandlerRegistry); sandbox.app.bind(Container.Identifiers.StateStore).toConstantValue({}); sandbox.app.bind(Container.Identifiers.TransactionPoolService).toConstantValue({}); @@ -197,7 +204,7 @@ describe("BlockProcessor", () => { ], }; - databaseService.walletRepository.getNonce = jest.fn().mockReturnValueOnce(Utils.BigNumber.ONE); + databaseInteractions.walletRepository.getNonce = jest.fn().mockReturnValueOnce(Utils.BigNumber.ONE); const blockProcessor = sandbox.app.resolve(BlockProcessor); @@ -220,8 +227,8 @@ describe("BlockProcessor", () => { ], }; - databaseService.walletRepository.getNonce = jest.fn().mockReturnValueOnce(Utils.BigNumber.ONE); - databaseService.getActiveDelegates = jest.fn().mockReturnValueOnce([]); + databaseInteractions.walletRepository.getNonce = jest.fn().mockReturnValueOnce(Utils.BigNumber.ONE); + databaseInteractions.getActiveDelegates = jest.fn().mockReturnValueOnce([]); blockchain.getLastBlock = jest.fn().mockReturnValueOnce(baseBlock); const generatorWallet = { getAttribute: jest.fn().mockReturnValue("generatorusername"), @@ -247,7 +254,7 @@ describe("BlockProcessor", () => { }; walletRepository.findByPublicKey = jest.fn().mockReturnValueOnce(generatorWallet); UnchainedHandler.prototype.initialize = jest.fn().mockReturnValueOnce(new UnchainedHandler()); - databaseService.getActiveDelegates = jest.fn().mockReturnValueOnce([]); + databaseInteractions.getActiveDelegates = jest.fn().mockReturnValueOnce([]); const blockProcessor = sandbox.app.resolve(BlockProcessor); @@ -303,7 +310,7 @@ describe("BlockProcessor", () => { activeDelegatesWithoutGenerator.length = 51; activeDelegatesWithoutGenerator.fill(notBlockGenerator, 0); - databaseService.getActiveDelegates = jest.fn().mockReturnValueOnce(activeDelegatesWithoutGenerator); + databaseInteractions.getActiveDelegates = jest.fn().mockReturnValueOnce(activeDelegatesWithoutGenerator); const blockProcessor = sandbox.app.resolve(BlockProcessor); @@ -330,7 +337,7 @@ describe("BlockProcessor", () => { publicKey: "02ff171adaef486b7db9fc160b28433d20cf43163d56fd28fee72145f0d5219a4b", }; - databaseService.getActiveDelegates = jest.fn().mockReturnValueOnce([notBlockGenerator]); + databaseInteractions.getActiveDelegates = jest.fn().mockReturnValueOnce([notBlockGenerator]); const blockProcessor = sandbox.app.resolve(BlockProcessor); @@ -351,10 +358,10 @@ describe("BlockProcessor", () => { ...chainedBlock, transactions: [{ data: transactionData, id: transactionData.id } as Interfaces.ITransaction], }; - databaseService.getActiveDelegates = jest.fn().mockReturnValueOnce([]); + databaseInteractions.getActiveDelegates = jest.fn().mockReturnValueOnce([]); blockchain.getLastBlock = jest.fn().mockReturnValueOnce(baseBlock); transactionRepository.getForgedTransactionsIds = jest.fn().mockReturnValueOnce([transactionData.id]); - databaseService.walletRepository.getNonce = jest.fn().mockReturnValueOnce(Utils.BigNumber.ONE); + databaseInteractions.walletRepository.getNonce = jest.fn().mockReturnValueOnce(Utils.BigNumber.ONE); const generatorWallet = { getAttribute: jest.fn().mockReturnValue("generatorusername"), }; @@ -371,7 +378,7 @@ describe("BlockProcessor", () => { const block = { ...chainedBlock, }; - databaseService.getActiveDelegates = jest.fn().mockReturnValueOnce([]); + databaseInteractions.getActiveDelegates = jest.fn().mockReturnValueOnce([]); blockchain.getLastBlock = jest.fn().mockReturnValueOnce(baseBlock); transactionRepository.getForgedTransactionsIds = jest.fn().mockReturnValueOnce([]); const generatorWallet = { diff --git a/__tests__/unit/core-blockchain/processor/handlers/accept-block-handler.test.ts b/__tests__/unit/core-blockchain/processor/handlers/accept-block-handler.test.ts index d2d0c1d369..e9e6a6bb0c 100644 --- a/__tests__/unit/core-blockchain/processor/handlers/accept-block-handler.test.ts +++ b/__tests__/unit/core-blockchain/processor/handlers/accept-block-handler.test.ts @@ -1,8 +1,9 @@ import { Container } from "@arkecosystem/core-kernel"; -import { AcceptBlockHandler } from "../../../../../packages/core-blockchain/src/processor/handlers/accept-block-handler"; -import { BlockProcessorResult } from "../../../../../packages/core-blockchain/src/processor"; import { Interfaces } from "@arkecosystem/crypto"; +import { BlockProcessorResult } from "../../../../../packages/core-blockchain/src/processor"; +import { AcceptBlockHandler } from "../../../../../packages/core-blockchain/src/processor/handlers/accept-block-handler"; + describe("AcceptBlockHandler", () => { const container = new Container.Container(); @@ -14,9 +15,19 @@ describe("AcceptBlockHandler", () => { setLastBlock: jest.fn(), lastDownloadedBlock: undefined, }; - const database = { applyBlock: jest.fn() }; const transactionPool = { acceptForgedTransaction: jest.fn() }; - + const databaseInteractions = { + walletRepository: { + getNonce: jest.fn(), + }, + applyBlock: jest.fn(), + getTopBlocks: jest.fn(), + getLastBlock: jest.fn(), + loadBlocksFromCurrentRound: jest.fn(), + revertBlock: jest.fn(), + deleteRound: jest.fn(), + getActiveDelegates: jest.fn().mockReturnValue([]), + }; const application = { get: jest.fn() }; beforeAll(() => { @@ -25,7 +36,7 @@ describe("AcceptBlockHandler", () => { container.bind(Container.Identifiers.LogService).toConstantValue(logger); container.bind(Container.Identifiers.BlockchainService).toConstantValue(blockchain); container.bind(Container.Identifiers.StateStore).toConstantValue(state); - container.bind(Container.Identifiers.DatabaseService).toConstantValue(database); + container.bind(Container.Identifiers.DatabaseInteraction).toConstantValue(databaseInteractions); container.bind(Container.Identifiers.TransactionPoolService).toConstantValue(transactionPool); }); @@ -47,8 +58,8 @@ describe("AcceptBlockHandler", () => { expect(result).toBe(BlockProcessorResult.Accepted); - expect(database.applyBlock).toBeCalledTimes(1); - expect(database.applyBlock).toHaveBeenCalledWith(block); + expect(databaseInteractions.applyBlock).toBeCalledTimes(1); + expect(databaseInteractions.applyBlock).toHaveBeenCalledWith(block); expect(blockchain.resetWakeUp).toBeCalledTimes(1); @@ -85,7 +96,7 @@ describe("AcceptBlockHandler", () => { it("should return Reject and resetLastDownloadedBlock when something throws", async () => { const acceptBlockHandler = container.resolve(AcceptBlockHandler); - database.applyBlock = jest.fn().mockRejectedValueOnce(new Error("oops")); + databaseInteractions.applyBlock = jest.fn().mockRejectedValueOnce(new Error("oops")); const result = await acceptBlockHandler.execute(block as Interfaces.IBlock); expect(result).toBe(BlockProcessorResult.Rejected); diff --git a/__tests__/unit/core-blockchain/processor/handlers/unchained-handler.test.ts b/__tests__/unit/core-blockchain/processor/handlers/unchained-handler.test.ts index 8e183f6bbd..bc22f2f9b7 100644 --- a/__tests__/unit/core-blockchain/processor/handlers/unchained-handler.test.ts +++ b/__tests__/unit/core-blockchain/processor/handlers/unchained-handler.test.ts @@ -1,9 +1,9 @@ import { Container, Services } from "@arkecosystem/core-kernel"; -import { UnchainedHandler } from "@packages/core-blockchain/src/processor/handlers/unchained-handler"; import { BlockProcessorResult } from "@packages/core-blockchain/src/processor"; -import { Interfaces } from "@packages/crypto"; -import { GetActiveDelegatesAction } from "@packages/core-database/src/actions"; +import { UnchainedHandler } from "@packages/core-blockchain/src/processor/handlers/unchained-handler"; +import { GetActiveDelegatesAction } from "@packages/core-state/src/actions"; import { Sandbox } from "@packages/core-test-framework"; +import { Interfaces } from "@packages/crypto"; let sandbox: Sandbox; @@ -15,7 +15,18 @@ const blockchain = { queue: { length: jest.fn() }, }; const stateStore = { numberOfBlocksToRollback: undefined }; -const database = { getActiveDelegates: jest.fn() }; +const database = {}; +const databaseInteractions = { + walletRepository: { + getNonce: jest.fn(), + }, + getTopBlocks: jest.fn(), + getLastBlock: jest.fn(), + loadBlocksFromCurrentRound: jest.fn(), + revertBlock: jest.fn(), + deleteRound: jest.fn(), + getActiveDelegates: jest.fn().mockReturnValue([]), +}; beforeEach(() => { sandbox = new Sandbox(); @@ -24,6 +35,7 @@ beforeEach(() => { sandbox.app.bind(Container.Identifiers.BlockchainService).toConstantValue(blockchain); sandbox.app.bind(Container.Identifiers.LogService).toConstantValue(logger); sandbox.app.bind(Container.Identifiers.DatabaseService).toConstantValue(database); + sandbox.app.bind(Container.Identifiers.DatabaseInteraction).toConstantValue(databaseInteractions); sandbox.app.bind(Container.Identifiers.TriggerService).to(Services.Triggers.Triggers).inSingletonScope(); sandbox.app @@ -52,7 +64,7 @@ describe("UnchainedHandler", () => { }, }; blockchain.getLastBlock = jest.fn().mockReturnValueOnce(lastBlock); - database.getActiveDelegates = jest + databaseInteractions.getActiveDelegates = jest .fn() .mockResolvedValueOnce( [ @@ -81,7 +93,7 @@ describe("UnchainedHandler", () => { }, }; blockchain.getLastBlock = jest.fn().mockReturnValueOnce(lastBlock); - database.getActiveDelegates = jest + databaseInteractions.getActiveDelegates = jest .fn() .mockResolvedValueOnce( [ diff --git a/__tests__/unit/core-blockchain/service-provider.test.ts b/__tests__/unit/core-blockchain/service-provider.test.ts index 01b01889b6..d163a9feeb 100644 --- a/__tests__/unit/core-blockchain/service-provider.test.ts +++ b/__tests__/unit/core-blockchain/service-provider.test.ts @@ -11,6 +11,7 @@ describe("ServiceProvider", () => { app.bind(Container.Identifiers.StateStore).toConstantValue({ reset: jest.fn() }); app.bind(Container.Identifiers.DatabaseService).toConstantValue({}); + app.bind(Container.Identifiers.DatabaseInteraction).toConstantValue({}); app.bind(Container.Identifiers.DatabaseBlockRepository).toConstantValue({}); app.bind(Container.Identifiers.TransactionPoolService).toConstantValue({}); app.bind(Container.Identifiers.LogService).toConstantValue({}); diff --git a/__tests__/unit/core-blockchain/state-machine/actions/initialize.test.ts b/__tests__/unit/core-blockchain/state-machine/actions/initialize.test.ts index da7cd0f847..330729dc7e 100644 --- a/__tests__/unit/core-blockchain/state-machine/actions/initialize.test.ts +++ b/__tests__/unit/core-blockchain/state-machine/actions/initialize.test.ts @@ -18,8 +18,20 @@ describe("Initialize", () => { restoredDatabaseIntegrity: undefined, verifyBlockchain: jest.fn(), deleteRound: jest.fn(), + }; + const databaseInteractions = { + walletRepository: { + getNonce: jest.fn(), + }, buildWallets: jest.fn(), restoreCurrentRound: jest.fn(), + applyBlock: jest.fn(), + getTopBlocks: jest.fn(), + getLastBlock: jest.fn(), + loadBlocksFromCurrentRound: jest.fn(), + revertBlock: jest.fn(), + deleteRound: jest.fn(), + getActiveDelegates: jest.fn().mockReturnValue([]), }; const peerNetworkMonitor = { boot: jest.fn() }; @@ -30,6 +42,7 @@ describe("Initialize", () => { container.bind(Container.Identifiers.Application).toConstantValue(application); container.bind(Container.Identifiers.LogService).toConstantValue(logger); container.bind(Container.Identifiers.DatabaseService).toConstantValue(databaseService); + container.bind(Container.Identifiers.DatabaseInteraction).toConstantValue(databaseInteractions); container.bind(Container.Identifiers.TransactionPoolService).toConstantValue(transactionPool); container.bind(Container.Identifiers.StateStore).toConstantValue(stateStore); container.bind(Container.Identifiers.BlockchainService).toConstantValue(blockchain); @@ -57,7 +70,7 @@ describe("Initialize", () => { expect(stateStore.setLastBlock).toHaveBeenCalledTimes(1); expect(databaseService.deleteRound).toHaveBeenCalledTimes(1); - expect(databaseService.restoreCurrentRound).toHaveBeenCalledTimes(1); + expect(databaseInteractions.restoreCurrentRound).toHaveBeenCalledTimes(1); expect(transactionPool.readdTransactions).toHaveBeenCalledTimes(1); expect(peerNetworkMonitor.boot).toHaveBeenCalledTimes(1); expect(blockchain.dispatch).toHaveBeenCalledTimes(1); @@ -182,7 +195,7 @@ describe("Initialize", () => { expect(stateStore.setLastBlock).toHaveBeenCalledTimes(1); expect(databaseService.deleteRound).toHaveBeenCalledTimes(1); - expect(databaseService.restoreCurrentRound).toHaveBeenCalledTimes(0); + expect(databaseInteractions.restoreCurrentRound).toHaveBeenCalledTimes(0); expect(transactionPool.readdTransactions).toHaveBeenCalledTimes(0); expect(peerNetworkMonitor.boot).toHaveBeenCalledTimes(1); expect(blockchain.dispatch).toHaveBeenCalledTimes(1); diff --git a/__tests__/unit/core-database/database-service.test.ts b/__tests__/unit/core-database/database-service.test.ts index 6c4c107911..28ab2dfbf7 100644 --- a/__tests__/unit/core-database/database-service.test.ts +++ b/__tests__/unit/core-database/database-service.test.ts @@ -1,5 +1,5 @@ -import { Container, Enums } from "@arkecosystem/core-kernel"; -import { Blocks, Identities, Utils } from "@arkecosystem/crypto"; +import { Container } from "@arkecosystem/core-kernel"; +import { Blocks } from "@arkecosystem/crypto"; import { DatabaseService } from "../../../packages/core-database/src/database-service"; import block1760000 from "./__fixtures__/block1760000"; @@ -47,51 +47,6 @@ const roundRepository = { delete: jest.fn(), }; -const stateStore = { - setGenesisBlock: jest.fn(), - getGenesisBlock: jest.fn(), - setLastBlock: jest.fn(), - getLastBlock: jest.fn(), - getLastBlocksByHeight: jest.fn(), - getCommonBlocks: jest.fn(), - getLastBlockIds: jest.fn(), -}; - -const stateBlockStore = { - resize: jest.fn(), -}; - -const stateTransactionStore = { - resize: jest.fn(), -}; - -const handlerRegistry = { - getActivatedHandlerForData: jest.fn(), -}; - -const walletRepository = { - createWallet: jest.fn(), - findByPublicKey: jest.fn(), - findByUsername: jest.fn(), -}; - -const blockState = { - applyBlock: jest.fn(), - revertBlock: jest.fn(), -}; - -const dposState = { - buildDelegateRanking: jest.fn(), - setDelegatesRound: jest.fn(), - getRoundDelegates: jest.fn(), -}; - -const getDposPreviousRoundState = jest.fn(); - -const triggers = { - call: jest.fn(), -}; - const events = { call: jest.fn(), dispatch: jest.fn(), @@ -110,15 +65,6 @@ container.bind(Container.Identifiers.DatabaseConnection).toConstantValue(connect container.bind(Container.Identifiers.DatabaseBlockRepository).toConstantValue(blockRepository); container.bind(Container.Identifiers.DatabaseTransactionRepository).toConstantValue(transactionRepository); container.bind(Container.Identifiers.DatabaseRoundRepository).toConstantValue(roundRepository); -container.bind(Container.Identifiers.StateStore).toConstantValue(stateStore); -container.bind(Container.Identifiers.StateBlockStore).toConstantValue(stateBlockStore); -container.bind(Container.Identifiers.StateTransactionStore).toConstantValue(stateTransactionStore); -container.bind(Container.Identifiers.TransactionHandlerRegistry).toConstantValue(handlerRegistry); -container.bind(Container.Identifiers.WalletRepository).toConstantValue(walletRepository); -container.bind(Container.Identifiers.BlockState).toConstantValue(blockState); -container.bind(Container.Identifiers.DposState).toConstantValue(dposState); -container.bind(Container.Identifiers.DposPreviousRoundStateProvider).toConstantValue(getDposPreviousRoundState); -container.bind(Container.Identifiers.TriggerService).toConstantValue(triggers); container.bind(Container.Identifiers.EventDispatcherService).toConstantValue(events); container.bind(Container.Identifiers.LogService).toConstantValue(logger); @@ -152,34 +98,6 @@ beforeEach(() => { roundRepository.save.mockReset(); roundRepository.delete.mockReset(); - stateStore.setGenesisBlock.mockReset(); - stateStore.getGenesisBlock.mockReset(); - stateStore.setLastBlock.mockReset(); - stateStore.getLastBlock.mockReset(); - stateStore.getLastBlocksByHeight.mockReset(); - stateStore.getCommonBlocks.mockReset(); - stateStore.getLastBlockIds.mockReset(); - - stateBlockStore.resize.mockReset(); - stateTransactionStore.resize.mockReset(); - - handlerRegistry.getActivatedHandlerForData.mockReset(); - - walletRepository.createWallet.mockReset(); - walletRepository.findByPublicKey.mockReset(); - walletRepository.findByUsername.mockReset(); - - blockState.applyBlock.mockReset(); - blockState.revertBlock.mockReset(); - - dposState.buildDelegateRanking.mockReset(); - dposState.setDelegatesRound.mockReset(); - dposState.getRoundDelegates.mockReset(); - - getDposPreviousRoundState.mockReset(); - - triggers.call.mockReset(); - logger.error.mockReset(); logger.warning.mockReset(); logger.info.mockReset(); @@ -190,97 +108,34 @@ beforeEach(() => { }); describe("DatabaseService.initialize", () => { - it("should dispatch starting event", async () => { - const databaseService = container.resolve(DatabaseService); - await databaseService.initialize(); - expect(events.dispatch).toBeCalledWith(Enums.StateEvent.Starting); - }); - it("should reset database when CORE_RESET_DATABASE variable is set", async () => { try { const databaseService = container.resolve(DatabaseService); process.env.CORE_RESET_DATABASE = "1"; - const genesisBlock = {}; - stateStore.getGenesisBlock.mockReturnValueOnce(genesisBlock); await databaseService.initialize(); expect(connection.query).toBeCalledWith("TRUNCATE TABLE blocks, rounds, transactions RESTART IDENTITY;"); - expect(stateStore.getGenesisBlock).toBeCalled(); - expect(blockRepository.saveBlocks).toBeCalledWith([genesisBlock]); } finally { delete process.env.CORE_RESET_DATABASE; } }); it("should terminate app if exception was raised", async () => { - const databaseService = container.resolve(DatabaseService); - stateStore.setGenesisBlock.mockImplementationOnce(() => { - throw new Error("Fail"); - }); - await databaseService.initialize(); - expect(app.terminate).toBeCalled(); - }); - - it("should terminate if unable to deserialize last 5 blocks", async () => { - const databaseService = container.resolve(DatabaseService); - - const block101data = { id: "block101", height: 101 }; - const block102data = { id: "block102", height: 102 }; - const block103data = { id: "block103", height: 103 }; - const block104data = { id: "block104", height: 104 }; - const block105data = { id: "block105", height: 105 }; - const block106data = { id: "block106", height: 105 }; - - blockRepository.findLatest.mockResolvedValueOnce(block106data); - - blockRepository.findLatest.mockResolvedValueOnce(block106data); // this.getLastBlock - transactionRepository.findByBlockIds.mockResolvedValueOnce([]); // this.getLastBlock - - blockRepository.findLatest.mockResolvedValueOnce(block106data); // blockRepository.deleteBlocks - blockRepository.findLatest.mockResolvedValueOnce(block105data); // this.getLastBlock - transactionRepository.findByBlockIds.mockResolvedValueOnce([]); // this.getLastBlock - - blockRepository.findLatest.mockResolvedValueOnce(block105data); // blockRepository.deleteBlocks - blockRepository.findLatest.mockResolvedValueOnce(block104data); // this.getLastBlock - transactionRepository.findByBlockIds.mockResolvedValueOnce([]); // this.getLastBlock - - blockRepository.findLatest.mockResolvedValueOnce(block104data); // blockRepository.deleteBlocks - blockRepository.findLatest.mockResolvedValueOnce(block103data); // this.getLastBlock - transactionRepository.findByBlockIds.mockResolvedValueOnce([]); // this.getLastBlock - - blockRepository.findLatest.mockResolvedValueOnce(block103data); // blockRepository.deleteBlocks - blockRepository.findLatest.mockResolvedValueOnce(block102data); // this.getLastBlock - transactionRepository.findByBlockIds.mockResolvedValueOnce([]); // this.getLastBlock - - blockRepository.findLatest.mockResolvedValueOnce(block102data); // blockRepository.deleteBlocks - blockRepository.findLatest.mockResolvedValueOnce(block101data); // this.getLastBlock - transactionRepository.findByBlockIds.mockResolvedValueOnce([]); // this.getLastBlock - - await databaseService.initialize(); - - expect(stateStore.setGenesisBlock).toBeCalled(); - expect(blockRepository.findLatest).toBeCalledTimes(12); - - expect(transactionRepository.findByBlockIds).toBeCalledWith([block106data.id]); - - expect(blockRepository.deleteBlocks).toBeCalledWith([block106data]); - expect(transactionRepository.findByBlockIds).toBeCalledWith([block105data.id]); - - expect(blockRepository.deleteBlocks).toBeCalledWith([block105data]); - expect(transactionRepository.findByBlockIds).toBeCalledWith([block104data.id]); - - expect(blockRepository.deleteBlocks).toBeCalledWith([block104data]); - expect(transactionRepository.findByBlockIds).toBeCalledWith([block103data.id]); - - expect(blockRepository.deleteBlocks).toBeCalledWith([block103data]); - expect(transactionRepository.findByBlockIds).toBeCalledWith([block102data.id]); + try { + const databaseService = container.resolve(DatabaseService); - expect(blockRepository.deleteBlocks).toBeCalledWith([block102data]); - expect(transactionRepository.findByBlockIds).toBeCalledWith([block101data.id]); + process.env.CORE_RESET_DATABASE = "1"; - expect(app.terminate).toBeCalled(); + jest.spyOn(databaseService, "reset").mockImplementationOnce(() => { + throw new Error("Fail"); + }); + await databaseService.initialize(); + expect(app.terminate).toBeCalled(); + } finally { + delete process.env.CORE_RESET_DATABASE; + } }); }); @@ -299,326 +154,13 @@ describe("DatabaseService.disconnect", () => { }); }); -describe("DatabaseService.restoreCurrentRound", () => { - it("should restore round to its initial state", async () => { - const databaseService = container.resolve(DatabaseService); - - const lastBlock = Blocks.BlockFactory.fromData(block1760000, getTimeStampForBlock); - stateStore.getLastBlock.mockReturnValueOnce(lastBlock); - - const lastBlocksByHeight = [lastBlock.data]; - stateStore.getLastBlocksByHeight.mockReturnValueOnce(lastBlocksByHeight); - blockRepository.findByHeightRangeWithTransactions.mockReturnValueOnce(lastBlocksByHeight); - - const prevRoundState = { getAllDelegates: jest.fn(), getRoundDelegates: jest.fn(), revert: jest.fn() }; - getDposPreviousRoundState.mockReturnValueOnce(prevRoundState); - - const delegateWallet = { setAttribute: jest.fn(), getAttribute: jest.fn() }; - walletRepository.findByUsername.mockReturnValueOnce(delegateWallet); - - const dposStateRoundDelegates = [delegateWallet]; - dposState.getRoundDelegates.mockReturnValueOnce(dposStateRoundDelegates); - dposState.getRoundDelegates.mockReturnValueOnce(dposStateRoundDelegates); - - const forgingDelegates = [delegateWallet]; - triggers.call.mockResolvedValue(forgingDelegates); - - await databaseService.restoreCurrentRound(1760000); - - expect(getDposPreviousRoundState).not.toBeCalled(); // restoring current round should not need previous round state - // important: getActiveDelegates should be called with only roundInfo (restoreCurrentRound does *not* provide delegates to it) - expect(triggers.call).toHaveBeenLastCalledWith("getActiveDelegates", { - roundInfo: expect.anything(), - delegates: undefined, - }); - // @ts-ignore - expect(databaseService.forgingDelegates).toEqual(forgingDelegates); - }); -}); - describe("DatabaseService.reset", () => { it("should reset database", async () => { const databaseService = container.resolve(DatabaseService); - const genesisBlock = {}; - stateStore.getGenesisBlock.mockReturnValueOnce(genesisBlock); - await databaseService.reset(); expect(connection.query).toBeCalledWith("TRUNCATE TABLE blocks, rounds, transactions RESTART IDENTITY;"); - expect(blockRepository.saveBlocks).toBeCalledWith([genesisBlock]); - }); -}); - -describe("DatabaseService.applyBlock", () => { - it("should apply block, round, detect missing blocks, and fire events", async () => { - const databaseService = container.resolve(DatabaseService); - - const lastBlock = { data: { height: 53, timestamp: 0 } }; - stateStore.getLastBlock.mockReturnValueOnce(lastBlock); - - const delegateWallet = { publicKey: "delegate public key", getAttribute: jest.fn() }; - const delegateUsername = "test_delegate"; - delegateWallet.getAttribute.mockReturnValueOnce(delegateUsername); - - const handler = { emitEvents: jest.fn() }; - handlerRegistry.getActivatedHandlerForData.mockResolvedValueOnce(handler); - - // still previous last block! - stateStore.getLastBlock.mockReturnValueOnce(lastBlock); - - // @ts-ignore - databaseService.blocksInCurrentRound = []; - // @ts-ignore - databaseService.forgingDelegates = [delegateWallet] as any; - - const transaction = {}; - const block = { data: { height: 54, timestamp: 35 }, transactions: [transaction] }; - await databaseService.applyBlock(block as any); - - expect(stateStore.getLastBlock).toBeCalledTimes(1); - expect(blockState.applyBlock).toBeCalledWith(block); - // @ts-ignore - expect(databaseService.blocksInCurrentRound).toEqual([block]); - expect(events.dispatch).toBeCalledWith("forger.missing", { delegate: delegateWallet }); - expect(handler.emitEvents).toBeCalledWith(transaction, events); - expect(events.dispatch).toBeCalledWith("block.applied", block.data); - }); - - it("should apply block, not apply round, and not detect missed blocks when last block height is 1", async () => { - const databaseService = container.resolve(DatabaseService); - - const lastBlock = { data: { height: 1 } }; - stateStore.getLastBlock.mockReturnValueOnce(lastBlock); - - const handler = { emitEvents: jest.fn() }; - handlerRegistry.getActivatedHandlerForData.mockResolvedValueOnce(handler); - - // still previous last block! - stateStore.getLastBlock.mockReturnValueOnce(lastBlock); - - const transaction = {}; - const block = { data: { height: 2, timestamp: 35 }, transactions: [transaction] }; - await databaseService.applyBlock(block as any); - - expect(stateStore.getLastBlock).toBeCalledTimes(1); - expect(handler.emitEvents).toBeCalledWith(transaction, events); - expect(events.dispatch).toBeCalledWith("block.applied", block.data); - }); -}); - -describe("DatabaseService.applyRound", () => { - it("should build delegates, save round, dispatch events when round changes on next height", async () => { - const databaseService = container.resolve(DatabaseService); - - const forgingDelegate = { getAttribute: jest.fn() }; - const forgingDelegateRound = 1; - forgingDelegate.getAttribute.mockReturnValueOnce(forgingDelegateRound); - // @ts-ignore - databaseService.forgingDelegates = [forgingDelegate] as any; - - // @ts-ignore - databaseService.blocksInCurrentRound = [{ data: { generatorPublicKey: "delegate public key" } }] as any; - - const delegateWallet = { publicKey: "delegate public key", getAttribute: jest.fn() }; - const dposStateRoundDelegates = [delegateWallet]; - dposState.getRoundDelegates.mockReturnValueOnce(dposStateRoundDelegates); - dposState.getRoundDelegates.mockReturnValueOnce(dposStateRoundDelegates); - - const delegateWalletRound = 2; - delegateWallet.getAttribute.mockReturnValueOnce(delegateWalletRound); - - walletRepository.findByPublicKey.mockReturnValueOnce(delegateWallet); - - const delegateUsername = "test_delegate"; - delegateWallet.getAttribute.mockReturnValueOnce(delegateUsername); - - const height = 51; - await databaseService.applyRound(height); - - expect(dposState.buildDelegateRanking).toBeCalled(); - expect(dposState.setDelegatesRound).toBeCalledWith({ - round: 2, - nextRound: 2, - roundHeight: 52, - maxDelegates: 51, - }); - expect(roundRepository.save).toBeCalledWith(dposStateRoundDelegates); - expect(events.dispatch).toBeCalledWith("round.applied"); - }); - - it("should build delegates, save round, dispatch events when height is 1", async () => { - const databaseService = container.resolve(DatabaseService); - - const forgingDelegate = { getAttribute: jest.fn() }; - const forgingDelegateRound = 1; - forgingDelegate.getAttribute.mockReturnValueOnce(forgingDelegateRound); - // @ts-ignore - databaseService.forgingDelegates = [forgingDelegate] as any; - - // @ts-ignore - databaseService.blocksInCurrentRound = []; - - const delegateWallet = { publicKey: "delegate public key", getAttribute: jest.fn() }; - const dposStateRoundDelegates = [delegateWallet]; - dposState.getRoundDelegates.mockReturnValueOnce(dposStateRoundDelegates); - dposState.getRoundDelegates.mockReturnValueOnce(dposStateRoundDelegates); - - const delegateWalletRound = 1; - delegateWallet.getAttribute.mockReturnValueOnce(delegateWalletRound); - - walletRepository.findByPublicKey.mockReturnValueOnce(delegateWallet); - - const delegateUsername = "test_delegate"; - delegateWallet.getAttribute.mockReturnValueOnce(delegateUsername); - - const height = 1; - await databaseService.applyRound(height); - - expect(dposState.buildDelegateRanking).toBeCalled(); - expect(dposState.setDelegatesRound).toBeCalledWith({ - round: 1, - nextRound: 1, - roundHeight: 1, - maxDelegates: 51, - }); - expect(roundRepository.save).toBeCalledWith(dposStateRoundDelegates); - expect(events.dispatch).toBeCalledWith("round.applied"); - }); - - it("should build delegates, save round, dispatch events, and skip missing round checks when first round has genesis block only", async () => { - const databaseService = container.resolve(DatabaseService); - - const forgingDelegate = { getAttribute: jest.fn() }; - const forgingDelegateRound = 1; - forgingDelegate.getAttribute.mockReturnValueOnce(forgingDelegateRound); - // @ts-ignore - databaseService.forgingDelegates = [forgingDelegate] as any; - - // @ts-ignore - databaseService.blocksInCurrentRound = [{ data: { height: 1 } }] as any; - - const delegateWallet = { publicKey: "delegate public key", getAttribute: jest.fn() }; - const dposStateRoundDelegates = [delegateWallet]; - dposState.getRoundDelegates.mockReturnValueOnce(dposStateRoundDelegates); - dposState.getRoundDelegates.mockReturnValueOnce(dposStateRoundDelegates); - - const delegateWalletRound = 2; - delegateWallet.getAttribute.mockReturnValueOnce(delegateWalletRound); - - walletRepository.findByPublicKey.mockReturnValueOnce(delegateWallet); - - const delegateUsername = "test_delegate"; - delegateWallet.getAttribute.mockReturnValueOnce(delegateUsername); - - const height = 51; - await databaseService.applyRound(height); - - expect(dposState.buildDelegateRanking).toBeCalled(); - expect(dposState.setDelegatesRound).toBeCalledWith({ - round: 2, - nextRound: 2, - roundHeight: 52, - maxDelegates: 51, - }); - expect(roundRepository.save).toBeCalledWith(dposStateRoundDelegates); - expect(events.dispatch).toBeCalledWith("round.applied"); - }); - - it("should delete round and rethrow error when error was thrown", async () => { - const databaseService = container.resolve(DatabaseService); - - dposState.buildDelegateRanking.mockImplementation(() => { - throw new Error("Fail"); - }); - - const height = 51; - const check = () => databaseService.applyRound(height); - - await expect(check()).rejects.toThrowError("Fail"); - expect(roundRepository.delete).toBeCalledWith({ round: 2 }); - }); - - it("should do nothing when next height is same round", async () => { - const databaseService = container.resolve(DatabaseService); - const height = 50; - await databaseService.applyRound(height); - expect(logger.info).not.toBeCalled(); - }); - - it("should warn when, and do nothing when round was already applied", async () => { - const databaseService = container.resolve(DatabaseService); - - const forgingDelegate = { getAttribute: jest.fn() }; - const forgingDelegateRound = 2; - forgingDelegate.getAttribute.mockReturnValueOnce(forgingDelegateRound); - // @ts-ignore - databaseService.forgingDelegates = [forgingDelegate] as any; - - const height = 51; - await databaseService.applyRound(height); - - expect(logger.warning).toBeCalledWith( - "Round 2 has already been applied. This should happen only if you are a forger.", - ); - }); -}); - -describe("DatabaseService.getActiveDelegates", () => { - it("should return shuffled round delegates", async () => { - const databaseService = container.resolve(DatabaseService); - - const lastBlock = Blocks.BlockFactory.fromData(block1760000, getTimeStampForBlock); - - // @ts-ignore - blockRepository.findLatest.mockResolvedValueOnce(lastBlock.data); - // @ts-ignore - transactionRepository.findByBlockIds.mockResolvedValueOnce(lastBlock.transactions); - - const delegatePublicKey = "03287bfebba4c7881a0509717e71b34b63f31e40021c321f89ae04f84be6d6ac37"; - const delegateVoteBalance = Utils.BigNumber.make("100"); - const roundDelegateModel = { publicKey: delegatePublicKey, balance: delegateVoteBalance }; - roundRepository.getRound.mockResolvedValueOnce([roundDelegateModel]); - - const newDelegateWallet = { setAttribute: jest.fn(), clone: jest.fn() }; - walletRepository.createWallet.mockReturnValueOnce(newDelegateWallet); - - const oldDelegateWallet = { getAttribute: jest.fn() }; - walletRepository.findByPublicKey.mockReturnValueOnce(oldDelegateWallet); - - const delegateUsername = "test_delegate"; - oldDelegateWallet.getAttribute.mockReturnValueOnce(delegateUsername); - - const cloneDelegateWallet = {}; - newDelegateWallet.clone.mockReturnValueOnce(cloneDelegateWallet); - - await databaseService.getActiveDelegates(); - - expect(walletRepository.findByPublicKey).toBeCalledWith(delegatePublicKey); - expect(walletRepository.createWallet).toBeCalledWith(Identities.Address.fromPublicKey(delegatePublicKey)); - expect(oldDelegateWallet.getAttribute).toBeCalledWith("delegate.username", ""); - expect(newDelegateWallet.setAttribute).toBeCalledWith("delegate", { - voteBalance: delegateVoteBalance, - username: delegateUsername, - }); - expect(newDelegateWallet.clone).toBeCalled(); - }); - - it("should return cached forgingDelegates when round is the same", async () => { - const databaseService = container.resolve(DatabaseService); - - const forgingDelegate = { getAttribute: jest.fn() }; - const forgingDelegateRound = 2; - forgingDelegate.getAttribute.mockReturnValueOnce(forgingDelegateRound); - // @ts-ignore - databaseService.forgingDelegates = [forgingDelegate] as any; - - const roundInfo = { round: 2 }; - const result = await databaseService.getActiveDelegates(roundInfo as any); - - expect(forgingDelegate.getAttribute).toBeCalledWith("delegate.round"); - // @ts-ignore - expect(result).toBe(databaseService.forgingDelegates); }); }); @@ -665,13 +207,11 @@ describe("DatabaseService.getBlocks", () => { const block101 = { height: 101, transactions: [] }; const block102 = { height: 102, transactions: [] }; - stateStore.getLastBlocksByHeight.mockReturnValueOnce([block101, block102]); - blockRepository.findByHeightRangeWithTransactions.mockResolvedValueOnce([block100]); + blockRepository.findByHeightRangeWithTransactions.mockResolvedValueOnce([block100, block101, block102]); - const result = await databaseService.getBlocks(100, 3); + const result = await databaseService.getBlocks(100, 102); - expect(stateStore.getLastBlocksByHeight).toBeCalledWith(100, 102, undefined); - expect(blockRepository.findByHeightRangeWithTransactions).toBeCalledWith(100, 100); + expect(blockRepository.findByHeightRangeWithTransactions).toBeCalledWith(100, 102); expect(result).toEqual([block100, block101, block102]); }); @@ -682,13 +222,11 @@ describe("DatabaseService.getBlocks", () => { const block101 = { height: 101 }; const block102 = { height: 102 }; - stateStore.getLastBlocksByHeight.mockReturnValueOnce([block101, block102]); - blockRepository.findByHeightRange.mockResolvedValueOnce([block100]); + blockRepository.findByHeightRange.mockResolvedValueOnce([block100, block101, block102]); - const result = await databaseService.getBlocks(100, 3, true); + const result = await databaseService.getBlocks(100, 102, true); - expect(stateStore.getLastBlocksByHeight).toBeCalledWith(100, 102, true); - expect(blockRepository.findByHeightRange).toBeCalledWith(100, 100); + expect(blockRepository.findByHeightRange).toBeCalledWith(100, 102); expect(result).toEqual([block100, block101, block102]); }); }); @@ -701,7 +239,11 @@ describe("DatabaseService.getBlocksForDownload", () => { const block101 = { height: 101, transactions: [] }; const block102 = { height: 102, transactions: [] }; - blockRepository.findByHeightRangeWithTransactionsForDownload.mockResolvedValueOnce([block100, block101, block102]); + blockRepository.findByHeightRangeWithTransactionsForDownload.mockResolvedValueOnce([ + block100, + block101, + block102, + ]); const result = await databaseService.getBlocksForDownload(100, 3); @@ -725,80 +267,6 @@ describe("DatabaseService.getBlocksForDownload", () => { }); }); -describe("DatabaseService.getBlocksByHeight", () => { - it("should return blocks with transactions when full blocks are requested", async () => { - const databaseService = container.resolve(DatabaseService); - - const block100 = { height: 100, transactions: [] }; - const block101 = { height: 101, transactions: [] }; - const block102 = { height: 102, transactions: [] }; - - stateStore.getLastBlocksByHeight.mockReturnValueOnce([block100]); - stateStore.getLastBlocksByHeight.mockReturnValueOnce([]); - stateStore.getLastBlocksByHeight.mockReturnValueOnce([block102]); - - blockRepository.findByHeights.mockResolvedValueOnce([block101]); - - const result = await databaseService.getBlocksByHeight([100, 101, 102]); - - expect(stateStore.getLastBlocksByHeight).toBeCalledWith(100, 100, true); - expect(stateStore.getLastBlocksByHeight).toBeCalledWith(101, 101, true); - expect(stateStore.getLastBlocksByHeight).toBeCalledWith(102, 102, true); - expect(blockRepository.findByHeights).toBeCalledWith([101]); - expect(result).toEqual([block100, block101, block102]); - }); -}); - -describe("DatabaseService.getBlocksForRound", () => { - it("should return empty array if there are no blocks", async () => { - const databaseService = container.resolve(DatabaseService); - - stateStore.getLastBlock.mockReturnValueOnce(undefined); - blockRepository.findLatest.mockResolvedValueOnce(undefined); - - const roundInfo = { roundHeight: 52, maxDelegates: 51 }; - const result = await databaseService.getBlocksForRound(roundInfo as any); - - expect(stateStore.getLastBlock).toBeCalled(); - expect(blockRepository.findLatest).toBeCalled(); - expect(result).toEqual([]); - }); - - it("should return array with genesis block only when last block is genesis block", async () => { - const databaseService = container.resolve(DatabaseService); - - const lastBlock = { data: { height: 1 } }; - stateStore.getLastBlock.mockReturnValueOnce(lastBlock); - - const roundInfo = { roundHeight: 1, maxDelegates: 51 }; - const result = await databaseService.getBlocksForRound(roundInfo as any); - - expect(stateStore.getLastBlock).toBeCalled(); - expect(result).toEqual([lastBlock]); - }); - - it("should return current round blocks", async () => { - const databaseService = container.resolve(DatabaseService); - - const block1 = { data: { height: 1 } }; - const block2 = Blocks.BlockFactory.fromData(block1760000, getTimeStampForBlock); - stateStore.getLastBlock.mockReturnValueOnce(block2); - stateStore.getLastBlocksByHeight.mockReturnValueOnce([ - block1.data, - { ...block2.data, transactions: block2.transactions.map((t) => t.data) }, - ]); - stateStore.getGenesisBlock.mockReturnValueOnce(block1); - - const roundInfo = { roundHeight: 1, maxDelegates: 2 }; - const result = await databaseService.getBlocksForRound(roundInfo as any); - Object.assign(result[1], { getBlockTimeStampLookup: block2["getBlockTimeStampLookup"] }); - - expect(stateStore.getLastBlock).toBeCalled(); - expect(stateStore.getLastBlocksByHeight).toBeCalledWith(1, 2, undefined); - expect(result).toEqual([block1, block2]); - }); -}); - describe("DatabaseService.getLastBlock", () => { it("should return undefined if there are no blocks", async () => { const databaseService = container.resolve(DatabaseService); @@ -827,82 +295,6 @@ describe("DatabaseService.getLastBlock", () => { }); }); -describe("DatabaseService.getCommonBlocks", () => { - it("should return blocks by ids", async () => { - const databaseService = container.resolve(DatabaseService); - - const block100 = { id: "00100", height: 100, transactions: [] }; - const block101 = { id: "00101", height: 101, transactions: [] }; - const block102 = { id: "00102", height: 102, transactions: [] }; - - stateStore.getCommonBlocks.mockReturnValueOnce([block101, block102]); - blockRepository.findByIds.mockResolvedValueOnce([block100, block101, block102]); - - const result = await databaseService.getCommonBlocks([block100.id, block101.id, block102.id]); - - expect(stateStore.getCommonBlocks).toBeCalledWith([block100.id, block101.id, block102.id]); - expect(blockRepository.findByIds).toBeCalledWith([block100.id, block101.id, block102.id]); - expect(result).toEqual([block100, block101, block102]); - }); -}); - -describe("DatabaseService.getRecentBlockIds", () => { - it("should return last 10 block ids", async () => { - const databaseService = container.resolve(DatabaseService); - - const block101 = { id: "00101", height: 101, transactions: [] }; - const block102 = { id: "00102", height: 102, transactions: [] }; - const block103 = { id: "00103", height: 103, transactions: [] }; - const block104 = { id: "00104", height: 104, transactions: [] }; - const block105 = { id: "00105", height: 105, transactions: [] }; - const block106 = { id: "00106", height: 106, transactions: [] }; - const block107 = { id: "00107", height: 107, transactions: [] }; - const block108 = { id: "00108", height: 108, transactions: [] }; - const block109 = { id: "00109", height: 109, transactions: [] }; - const block110 = { id: "00110", height: 110, transactions: [] }; - - stateStore.getLastBlockIds.mockReturnValueOnce([ - block101, - block102, - block103, - block104, - block105, - block106, - block107, - block108, - block109, - ]); - - blockRepository.findRecent.mockResolvedValueOnce([ - block110, - block109, - block108, - block107, - block106, - block105, - block104, - block103, - block102, - block101, - ]); - - const result = await databaseService.getRecentBlockIds(); - - expect(result).toEqual([ - block110.id, - block109.id, - block108.id, - block107.id, - block106.id, - block105.id, - block104.id, - block103.id, - block102.id, - block101.id, - ]); - }); -}); - describe("DatabaseService.getTopBlocks", () => { it("should return top blocks with transactions", async () => { const databaseService = container.resolve(DatabaseService); @@ -953,89 +345,6 @@ describe("DatabaseService.getTransaction", () => { }); }); -describe("DatabaseService.loadBlocksFromCurrentRound", () => { - it("should initialize blocksInCurrentRound property", async () => { - const databaseService = container.resolve(DatabaseService); - - const lastBlock = Blocks.BlockFactory.fromData(block1760000, getTimeStampForBlock); - stateStore.getLastBlock.mockReturnValueOnce(lastBlock); - stateStore.getLastBlocksByHeight.mockReturnValueOnce([lastBlock.data]); - blockRepository.findByHeightRangeWithTransactions.mockReturnValueOnce([lastBlock.data]); - - await databaseService.loadBlocksFromCurrentRound(); - - expect(stateStore.getLastBlock).toBeCalled(); - }); -}); - -describe("DatabaseService.revertBlock", () => { - it("should revert state, and fire events", async () => { - const databaseService = container.resolve(DatabaseService); - - const transaction1 = { data: {} }; - const transaction2 = { data: {} }; - const block = { - data: { id: "123", height: 100 }, - transactions: [transaction1, transaction2], - }; - // @ts-ignore - databaseService.blocksInCurrentRound = [block as any]; - - await databaseService.revertBlock(block as any); - - expect(blockState.revertBlock).toBeCalledWith(block); - expect(events.dispatch).toBeCalledWith("transaction.reverted", transaction1.data); - expect(events.dispatch).toBeCalledWith("transaction.reverted", transaction2.data); - expect(events.dispatch).toBeCalledWith("block.reverted", block.data); - }); -}); - -describe("DatabaseService.revertRound", () => { - it("should revert, and delete round when reverting to previous round", async () => { - const databaseService = container.resolve(DatabaseService); - - const lastBlock = Blocks.BlockFactory.fromData(block1760000, getTimeStampForBlock); - stateStore.getLastBlock.mockReturnValueOnce(lastBlock); - stateStore.getLastBlocksByHeight.mockReturnValueOnce([lastBlock.data]); - blockRepository.findByHeightRangeWithTransactions.mockReturnValueOnce([lastBlock.data]); - - const prevRoundState = { getAllDelegates: jest.fn(), getRoundDelegates: jest.fn(), revert: jest.fn() }; - getDposPreviousRoundState.mockReturnValueOnce(prevRoundState).mockReturnValueOnce(prevRoundState); - - const prevRoundDelegateWallet = { getAttribute: jest.fn() }; - const prevRoundDposStateAllDelegates = [prevRoundDelegateWallet]; - prevRoundState.getAllDelegates.mockReturnValueOnce(prevRoundDposStateAllDelegates); - - const prevRoundDelegateUsername = "test_delegate"; - prevRoundDelegateWallet.getAttribute.mockReturnValueOnce(prevRoundDelegateUsername); - - const delegateWallet = { setAttribute: jest.fn(), getAttribute: jest.fn() }; - walletRepository.findByUsername.mockReturnValueOnce(delegateWallet); - - const prevRoundDelegateRank = 1; - prevRoundDelegateWallet.getAttribute.mockReturnValueOnce(prevRoundDelegateRank); - - const prevRoundDposStateRoundDelegates = [prevRoundDelegateWallet]; - prevRoundState.getRoundDelegates.mockReturnValueOnce(prevRoundDposStateRoundDelegates); - - const dposStateRoundDelegates = [delegateWallet]; - dposState.getRoundDelegates.mockReturnValueOnce(dposStateRoundDelegates); - dposState.getRoundDelegates.mockReturnValueOnce(dposStateRoundDelegates); - - const forgingDelegates = [delegateWallet]; - triggers.call.mockResolvedValue(forgingDelegates); - - await databaseService.revertRound(51); - - expect(getDposPreviousRoundState).toBeCalled(); - expect(walletRepository.findByUsername).toBeCalledWith(prevRoundDelegateUsername); - expect(delegateWallet.setAttribute).toBeCalledWith("delegate.rank", prevRoundDelegateRank); - // @ts-ignore - expect(databaseService.forgingDelegates).toEqual(forgingDelegates); - expect(roundRepository.delete).toBeCalledWith({ round: 2 }); - }); -}); - describe("DatabaseService.saveRound", () => { it("should save delegates to round repository and fire events", async () => { const databaseService = container.resolve(DatabaseService); @@ -1069,9 +378,6 @@ describe("DatabaseService.verifyBlockchain", () => { it("should return false when there are no blocks", async () => { const databaseService = container.resolve(DatabaseService); - const lastBlock = undefined; - stateStore.getLastBlock.mockReturnValueOnce(lastBlock); - const numberOfBlocks = 0; const numberOfTransactions = 0; const totalFee = "0"; @@ -1085,7 +391,6 @@ describe("DatabaseService.verifyBlockchain", () => { const result = await databaseService.verifyBlockchain(); - expect(stateStore.getLastBlock).toBeCalledWith(); expect(blockRepository.getStatistics).toBeCalledWith(); expect(transactionRepository.getStatistics).toBeCalledWith(); expect(result).toBe(false); @@ -1095,7 +400,6 @@ describe("DatabaseService.verifyBlockchain", () => { const databaseService = container.resolve(DatabaseService); const lastBlock = Blocks.BlockFactory.fromData(block1760000, getTimeStampForBlock); - stateStore.getLastBlock.mockReturnValueOnce(lastBlock); const numberOfBlocks = 1760000; const numberOfTransactions = 999999; @@ -1114,9 +418,8 @@ describe("DatabaseService.verifyBlockchain", () => { }; transactionRepository.getStatistics.mockResolvedValueOnce(transactionStats); - const result = await databaseService.verifyBlockchain(); + const result = await databaseService.verifyBlockchain(lastBlock); - expect(stateStore.getLastBlock).toBeCalledWith(); expect(blockRepository.count).toBeCalledWith(); expect(blockRepository.getStatistics).toBeCalledWith(); expect(transactionRepository.getStatistics).toBeCalledWith(); @@ -1127,7 +430,6 @@ describe("DatabaseService.verifyBlockchain", () => { const databaseService = container.resolve(DatabaseService); const lastBlock = Blocks.BlockFactory.fromData(block1760000, getTimeStampForBlock); - stateStore.getLastBlock.mockReturnValueOnce(lastBlock); const numberOfBlocks = 1760000; const numberOfTransactions = 999999; @@ -1142,9 +444,8 @@ describe("DatabaseService.verifyBlockchain", () => { const transactionStats = { totalFee, totalAmount, count: numberOfTransactions }; transactionRepository.getStatistics.mockResolvedValueOnce(transactionStats); - const result = await databaseService.verifyBlockchain(); + const result = await databaseService.verifyBlockchain(lastBlock); - expect(stateStore.getLastBlock).toBeCalled(); expect(blockRepository.count).toBeCalled(); expect(blockRepository.getStatistics).toBeCalled(); expect(transactionRepository.getStatistics).toBeCalled(); diff --git a/__tests__/unit/core-database/service-provider.test.ts b/__tests__/unit/core-database/service-provider.test.ts index ef0fdd5fbf..27210b197a 100644 --- a/__tests__/unit/core-database/service-provider.test.ts +++ b/__tests__/unit/core-database/service-provider.test.ts @@ -1,8 +1,7 @@ -import { Application, Container, Providers } from "@packages/core-kernel"; -import { createConnection, getCustomRepository } from "typeorm"; - import { defaults } from "@packages/core-database/src/defaults"; import { ServiceProvider } from "@packages/core-database/src/service-provider"; +import { Application, Container, Providers } from "@packages/core-kernel"; +import { createConnection, getCustomRepository } from "typeorm"; jest.mock("typeorm", () => { return Object.assign(jest.requireActual("typeorm"), { @@ -18,10 +17,6 @@ const logger = { info: jest.fn(), }; -const triggers = { - bind: jest.fn(), -}; - const events = { dispatch: jest.fn(), }; @@ -29,12 +24,10 @@ const events = { beforeEach(() => { app = new Application(new Container.Container()); app.bind(Container.Identifiers.LogService).toConstantValue(logger); - app.bind(Container.Identifiers.TriggerService).toConstantValue(triggers); app.bind(Container.Identifiers.EventDispatcherService).toConstantValue(events); logger.debug.mockReset(); logger.info.mockReset(); - triggers.bind.mockReset(); events.dispatch.mockReset(); }); @@ -50,7 +43,6 @@ describe("ServiceProvider.register", () => { expect(getCustomRepository).toBeCalledTimes(3); expect(events.dispatch).toBeCalled(); - expect(triggers.bind).toBeCalled(); expect(app.isBound(Container.Identifiers.DatabaseConnection)).toBe(true); expect(app.isBound(Container.Identifiers.DatabaseRoundRepository)).toBe(true); diff --git a/__tests__/unit/core-forger/forger-service.test.ts b/__tests__/unit/core-forger/forger-service.test.ts index a7cef92513..0d163e1e7f 100644 --- a/__tests__/unit/core-forger/forger-service.test.ts +++ b/__tests__/unit/core-forger/forger-service.test.ts @@ -4,11 +4,11 @@ import { ForgeNewBlockAction, IsForgingAllowedAction } from "@arkecosystem/core- import { Contracts } from "@arkecosystem/core-kernel"; import { Sandbox } from "@arkecosystem/core-test-framework"; import { Interfaces } from "@arkecosystem/crypto"; -import { GetActiveDelegatesAction } from "@packages/core-database/src/actions"; import { HostNoResponseError, RelayCommunicationError } from "@packages/core-forger/src/errors"; import { ForgerService } from "@packages/core-forger/src/forger-service"; import { Container, Enums, Services, Utils } from "@packages/core-kernel"; import { NetworkStateStatus } from "@packages/core-p2p"; +import { GetActiveDelegatesAction } from "@packages/core-state/src/actions"; import { Crypto, Managers } from "@packages/crypto"; import { Address } from "@packages/crypto/src/identities"; import { BuilderFactory } from "@packages/crypto/src/transactions"; @@ -139,7 +139,7 @@ describe("ForgerService", () => { }); }); - describe("GetLastForgedBlock", () => { + describe("GetLastForgedBlock", () => { it("should return undefined", async () => { forgerService.register({ hosts: [mockHost] }); diff --git a/__tests__/unit/core-forger/setup.ts b/__tests__/unit/core-forger/setup.ts index 9e8d3deea9..b3043ecbe1 100644 --- a/__tests__/unit/core-forger/setup.ts +++ b/__tests__/unit/core-forger/setup.ts @@ -1,8 +1,8 @@ import "jest-extended"; -import { GetActiveDelegatesAction } from "@packages/core-database/src/actions"; import { DelegateTracker } from "@packages/core-forger/src/delegate-tracker"; import { Container, Services } from "@packages/core-kernel"; +import { GetActiveDelegatesAction } from "@packages/core-state/src/actions"; import { Wallet } from "@packages/core-state/src/wallets"; import { Sandbox } from "@packages/core-test-framework/src"; import { Managers } from "@packages/crypto/src"; @@ -33,6 +33,13 @@ export const setup = async (activeDelegates) => { } } + @Container.injectable() + class MockDatabaseInteraction { + public async getActiveDelegates(): Promise { + return activeDelegates; + } + } + @Container.injectable() class MockWalletRepository { public findByPublicKey(publicKey: string) { @@ -51,6 +58,8 @@ export const setup = async (activeDelegates) => { sandbox.app.bind(Container.Identifiers.DatabaseService).to(MockDatabaseService); + sandbox.app.bind(Container.Identifiers.DatabaseInteraction).to(MockDatabaseInteraction); + sandbox.app.bind(Container.Identifiers.BlockchainService).to(MockBlockchainService); sandbox.app.bind(Container.Identifiers.WalletRepository).to(MockWalletRepository); diff --git a/__tests__/unit/core-kernel/utils/get-blocktime-lookup.test.ts b/__tests__/unit/core-kernel/utils/get-blocktime-lookup.test.ts index 82c80b08e3..8ec8f9fada 100644 --- a/__tests__/unit/core-kernel/utils/get-blocktime-lookup.test.ts +++ b/__tests__/unit/core-kernel/utils/get-blocktime-lookup.test.ts @@ -18,7 +18,7 @@ const mockApp: Application = { // @ts-ignore get: () => { return { - getBlocksByHeight: async (heights: Array): Promise> => { + findBlockByHeights: async (heights: Array): Promise> => { const result = [{ timestamp: 0 }]; switch (heights[0]) { case 2: diff --git a/__tests__/unit/core-p2p/peer-verifier.test.ts b/__tests__/unit/core-p2p/peer-verifier.test.ts index 65135a02a4..3ebcb3feca 100644 --- a/__tests__/unit/core-p2p/peer-verifier.test.ts +++ b/__tests__/unit/core-p2p/peer-verifier.test.ts @@ -1,7 +1,6 @@ -import { Container, Application, Contracts } from "@arkecosystem/core-kernel"; - -import { PeerVerifier, PeerVerificationResult } from "@arkecosystem/core-p2p/src/peer-verifier"; +import { Application, Container, Contracts } from "@arkecosystem/core-kernel"; import { Peer } from "@arkecosystem/core-p2p/src/peer"; +import { PeerVerificationResult, PeerVerifier } from "@arkecosystem/core-p2p/src/peer-verifier"; import { Blocks } from "@arkecosystem/crypto"; describe("PeerVerifier", () => { @@ -13,31 +12,37 @@ describe("PeerVerifier", () => { const logger = { warning: console.log, debug: console.log, info: console.log }; const trigger = { call: jest.fn() }; const stateStore = { getLastBlocks: jest.fn(), getLastHeight: jest.fn() }; - const database = { getBlocksByHeight: jest.fn() }; + const database = {}; + const databaseInteractions = { + getBlocksByHeight: jest.fn(), + }; const dposState = { getRoundInfo: jest.fn(), getRoundDelegates: jest.fn() }; - const blockFromDataMock = (blockData) => ({ - verifySignature: () => true, - data: { - height: blockData.height, - generatorPublicKey: blockData.generatorPublicKey, - }, - } as Blocks.Block); - const blockWithIdFromDataMock = (blockData) => ({ - verifySignature: () => true, - data: { - id: blockData.id, - height: blockData.height, - generatorPublicKey: blockData.generatorPublicKey, - }, - } as Blocks.Block); - const notVerifiedBlockFromDataMock = (blockData) => ({ - verifySignature: () => false, - data: { - height: blockData.height, - generatorPublicKey: blockData.generatorPublicKey, - }, - } as Blocks.Block); + const blockFromDataMock = (blockData) => + ({ + verifySignature: () => true, + data: { + height: blockData.height, + generatorPublicKey: blockData.generatorPublicKey, + }, + } as Blocks.Block); + const blockWithIdFromDataMock = (blockData) => + ({ + verifySignature: () => true, + data: { + id: blockData.id, + height: blockData.height, + generatorPublicKey: blockData.generatorPublicKey, + }, + } as Blocks.Block); + const notVerifiedBlockFromDataMock = (blockData) => + ({ + verifySignature: () => false, + data: { + height: blockData.height, + generatorPublicKey: blockData.generatorPublicKey, + }, + } as Blocks.Block); beforeAll(() => { process.env.CORE_P2P_PEER_VERIFIER_DEBUG_EXTRA = "true"; @@ -48,6 +53,7 @@ describe("PeerVerifier", () => { app.bind(Container.Identifiers.LogService).toConstantValue(logger); app.bind(Container.Identifiers.TriggerService).toConstantValue(trigger); app.bind(Container.Identifiers.StateStore).toConstantValue(stateStore); + app.bind(Container.Identifiers.DatabaseInteraction).toConstantValue(databaseInteractions); app.bind(Container.Identifiers.DatabaseService).toConstantValue(database); app.bind(Container.Identifiers.Application).toConstantValue(app); app.bind(Container.Identifiers.DposState).toConstantValue(dposState); @@ -84,8 +90,8 @@ describe("PeerVerifier", () => { }; expect(await peerVerifier.checkState(claimedState, Date.now() + 2000)).toBeUndefined(); - }) - }) + }); + }); describe("when Case1. Peer height > our height and our highest block is part of the peer's chain", () => { const claimedState: Contracts.P2P.PeerState = { @@ -108,18 +114,16 @@ describe("PeerVerifier", () => { stateStore.getLastBlocks = jest .fn() .mockReturnValue([{ data: { height: ourHeader.height }, getHeader: () => ourHeader }]); - database.getBlocksByHeight = jest.fn().mockImplementation((blockHeights) => + databaseInteractions.getBlocksByHeight = jest.fn().mockImplementation((blockHeights) => blockHeights.map((height: number) => ({ height, id: height.toString().padStart(2, "0").repeat(20), // just using height to mock the id })), ); - peerCommunicator.hasCommonBlocks = jest - .fn() - .mockImplementation((_, ids) => ({ - id: ids[ids.length - 1], - height: parseInt(ids[ids.length - 1].slice(0, 2)), - })); + peerCommunicator.hasCommonBlocks = jest.fn().mockImplementation((_, ids) => ({ + id: ids[ids.length - 1], + height: parseInt(ids[ids.length - 1].slice(0, 2)), + })); trigger.call = jest.fn().mockReturnValue([{ publicKey: generatorPublicKey }]); // getActiveDelegates mock peerCommunicator.getPeerBlocks = jest.fn().mockImplementation((_, options) => { const blocks = []; @@ -128,7 +132,9 @@ describe("PeerVerifier", () => { } return blocks; }); - const spyFromData = jest.spyOn(Blocks.BlockFactory, "fromData").mockImplementation(blockWithIdFromDataMock); + const spyFromData = jest + .spyOn(Blocks.BlockFactory, "fromData") + .mockImplementation(blockWithIdFromDataMock); const result = await peerVerifier.checkState(claimedState, Date.now() + 2000); @@ -142,7 +148,7 @@ describe("PeerVerifier", () => { spyFromData.mockRestore(); peerCommunicator.getPeerBlocks = jest.fn(); - database.getBlocksByHeight = jest.fn(); + databaseInteractions.getBlocksByHeight = jest.fn(); peerCommunicator.hasCommonBlocks = jest.fn(); stateStore.getLastHeight = jest.fn(); trigger.call = jest.fn(); @@ -171,7 +177,7 @@ describe("PeerVerifier", () => { stateStore.getLastBlocks = jest .fn() .mockReturnValueOnce([{ data: { height: ourHeader.height }, getHeader: () => ourHeader }]); - database.getBlocksByHeight = jest.fn().mockImplementation((blockHeights) => + databaseInteractions.getBlocksByHeight = jest.fn().mockImplementation((blockHeights) => blockHeights.map((height: number) => ({ height, id: height.toString().padStart(2, "0").repeat(20), // just using height to mock the id @@ -197,7 +203,7 @@ describe("PeerVerifier", () => { spyFromData.mockRestore(); peerCommunicator.getPeerBlocks = jest.fn(); - database.getBlocksByHeight = jest.fn(); + databaseInteractions.getBlocksByHeight = jest.fn(); peerCommunicator.hasCommonBlocks = jest.fn(); }); }); @@ -220,7 +226,9 @@ describe("PeerVerifier", () => { .mockReturnValueOnce([ { data: { height: claimedState.height }, getHeader: () => claimedState.header }, ]); - database.getBlocksByHeight = jest.fn().mockReturnValueOnce([{ id: claimedState.header.id }]); + databaseInteractions.getBlocksByHeight = jest + .fn() + .mockReturnValueOnce([{ id: claimedState.header.id }]); const result = await peerVerifier.checkState(claimedState, Date.now() + 2000); @@ -244,55 +252,64 @@ describe("PeerVerifier", () => { id: "11165046748333390338", }; - it.each([[true], [false]]) - ("should return PeerVerificationResult forked when claimed state block header is valid", async (delegatesEmpty) => { - const generatorPublicKey = "03c5282b639d0e8f94cfac6c0ed242d1634d8a2c93cbd76c6ed2856a9f19cf6a13"; - stateStore.getLastHeight = jest.fn().mockReturnValueOnce(claimedState.height); - stateStore.getLastBlocks = jest - .fn() - .mockReturnValueOnce([{ data: { height: claimedState.height }, getHeader: () => ourHeader }]); - database.getBlocksByHeight = jest - .fn() - .mockReturnValueOnce([{ id: ourHeader.id }]) - .mockImplementation((blockHeights) => - blockHeights.map((height: number) => ({ - height, - id: height.toString().padStart(2, "0").repeat(20), // just using height to mock the id - })), - ); - peerCommunicator.hasCommonBlocks = jest - .fn() - .mockImplementation((_, ids) => ({ id: ids[0], height: parseInt(ids[0].slice(0, 2)) })); - - if (delegatesEmpty) { - // getActiveDelegates return empty array, should still work using dpos state - trigger.call = jest.fn().mockReturnValueOnce([]); // getActiveDelegates mock - dposState.getRoundInfo = jest.fn().mockReturnValueOnce({ round: 1, maxDelegates: 51 }); - dposState.getRoundDelegates = jest.fn().mockReturnValueOnce([{ publicKey: generatorPublicKey }]); - - } else { - trigger.call = jest.fn().mockReturnValueOnce([{ publicKey: generatorPublicKey }]); // getActiveDelegates mock - } - - peerCommunicator.getPeerBlocks = jest.fn().mockImplementation((_, options) => { - const blocks = []; - for (let i = options.fromBlockHeight + 1; i <= options.fromBlockHeight + options.blockLimit; i++) { - blocks.push({ id: i.toString(), height: i, generatorPublicKey }); + it.each([[true], [false]])( + "should return PeerVerificationResult forked when claimed state block header is valid", + async (delegatesEmpty) => { + const generatorPublicKey = "03c5282b639d0e8f94cfac6c0ed242d1634d8a2c93cbd76c6ed2856a9f19cf6a13"; + stateStore.getLastHeight = jest.fn().mockReturnValueOnce(claimedState.height); + stateStore.getLastBlocks = jest + .fn() + .mockReturnValueOnce([{ data: { height: claimedState.height }, getHeader: () => ourHeader }]); + databaseInteractions.getBlocksByHeight = jest + .fn() + .mockReturnValueOnce([{ id: ourHeader.id }]) + .mockImplementation((blockHeights) => + blockHeights.map((height: number) => ({ + height, + id: height.toString().padStart(2, "0").repeat(20), // just using height to mock the id + })), + ); + peerCommunicator.hasCommonBlocks = jest + .fn() + .mockImplementation((_, ids) => ({ id: ids[0], height: parseInt(ids[0].slice(0, 2)) })); + + if (delegatesEmpty) { + // getActiveDelegates return empty array, should still work using dpos state + trigger.call = jest.fn().mockReturnValueOnce([]); // getActiveDelegates mock + dposState.getRoundInfo = jest.fn().mockReturnValueOnce({ round: 1, maxDelegates: 51 }); + dposState.getRoundDelegates = jest + .fn() + .mockReturnValueOnce([{ publicKey: generatorPublicKey }]); + } else { + trigger.call = jest.fn().mockReturnValueOnce([{ publicKey: generatorPublicKey }]); // getActiveDelegates mock } - return blocks; - }); - const spyFromData = jest.spyOn(Blocks.BlockFactory, "fromData").mockImplementation(blockFromDataMock); - - const result = await peerVerifier.checkState(claimedState, Date.now() + 2000); - expect(result).toBeInstanceOf(PeerVerificationResult); - expect(result.forked).toBeTrue(); - - spyFromData.mockRestore(); - peerCommunicator.getPeerBlocks = jest.fn(); - database.getBlocksByHeight = jest.fn(); - peerCommunicator.hasCommonBlocks = jest.fn(); - }); + peerCommunicator.getPeerBlocks = jest.fn().mockImplementation((_, options) => { + const blocks = []; + for ( + let i = options.fromBlockHeight + 1; + i <= options.fromBlockHeight + options.blockLimit; + i++ + ) { + blocks.push({ id: i.toString(), height: i, generatorPublicKey }); + } + return blocks; + }); + const spyFromData = jest + .spyOn(Blocks.BlockFactory, "fromData") + .mockImplementation(blockFromDataMock); + + const result = await peerVerifier.checkState(claimedState, Date.now() + 2000); + + expect(result).toBeInstanceOf(PeerVerificationResult); + expect(result.forked).toBeTrue(); + + spyFromData.mockRestore(); + peerCommunicator.getPeerBlocks = jest.fn(); + databaseInteractions.getBlocksByHeight = jest.fn(); + peerCommunicator.hasCommonBlocks = jest.fn(); + }, + ); it("should return undefined when claimed state block header is invalid", async () => { stateStore.getLastHeight = jest.fn().mockReturnValueOnce(claimedState.height); @@ -313,7 +330,7 @@ describe("PeerVerifier", () => { stateStore.getLastBlocks = jest .fn() .mockReturnValueOnce([{ data: { height: claimedState.height }, getHeader: () => ourHeader }]); - database.getBlocksByHeight = jest + databaseInteractions.getBlocksByHeight = jest .fn() .mockReturnValueOnce([{ id: ourHeader.id }]) .mockImplementation((blockHeights) => @@ -334,7 +351,7 @@ describe("PeerVerifier", () => { stateStore.getLastBlocks = jest .fn() .mockReturnValueOnce([{ data: { height: claimedState.height }, getHeader: () => ourHeader }]); - database.getBlocksByHeight = jest + databaseInteractions.getBlocksByHeight = jest .fn() .mockReturnValueOnce([{ id: ourHeader.id }]) .mockImplementation((blockHeights) => @@ -354,7 +371,7 @@ describe("PeerVerifier", () => { stateStore.getLastBlocks = jest .fn() .mockReturnValueOnce([{ data: { height: claimedState.height }, getHeader: () => ourHeader }]); - database.getBlocksByHeight = jest + databaseInteractions.getBlocksByHeight = jest .fn() .mockReturnValueOnce([{ id: ourHeader.id }]) .mockImplementation((blockHeights) => @@ -363,7 +380,7 @@ describe("PeerVerifier", () => { id: height.toString().padStart(2, "0").repeat(20), // just using height to mock the id })), ); - peerCommunicator.hasCommonBlocks = jest .fn().mockResolvedValueOnce(undefined); + peerCommunicator.hasCommonBlocks = jest.fn().mockResolvedValueOnce(undefined); jest.spyOn(Blocks.BlockFactory, "fromData").mockImplementation(blockFromDataMock); const result = await peerVerifier.checkState(claimedState, Date.now() + 2000); @@ -376,7 +393,7 @@ describe("PeerVerifier", () => { stateStore.getLastBlocks = jest .fn() .mockReturnValueOnce([{ data: { height: claimedState.height }, getHeader: () => ourHeader }]); - database.getBlocksByHeight = jest + databaseInteractions.getBlocksByHeight = jest .fn() .mockReturnValueOnce([{ id: ourHeader.id }]) .mockImplementation((blockHeights) => @@ -400,7 +417,7 @@ describe("PeerVerifier", () => { stateStore.getLastBlocks = jest .fn() .mockReturnValueOnce([{ data: { height: claimedState.height }, getHeader: () => ourHeader }]); - database.getBlocksByHeight = jest + databaseInteractions.getBlocksByHeight = jest .fn() .mockReturnValueOnce([{ id: ourHeader.id }]) .mockImplementation((blockHeights) => @@ -413,45 +430,46 @@ describe("PeerVerifier", () => { .fn() .mockImplementation((_, ids) => ({ id: ids[0], height: 1000 + parseInt(ids[0].slice(0, 2)) })); jest.spyOn(Blocks.BlockFactory, "fromData").mockImplementation(blockFromDataMock); - + const result = await peerVerifier.checkState(claimedState, Date.now() + 2000); expect(result).toBeUndefined(); }); - it.each([[true], [false]]) - ("should return undefined when getPeerBlocks returns empty or rejects", async (returnEmpty) => { - stateStore.getLastHeight = jest.fn().mockReturnValueOnce(claimedState.height); - stateStore.getLastBlocks = jest - .fn() - .mockReturnValueOnce([{ data: { height: claimedState.height }, getHeader: () => ourHeader }]); - database.getBlocksByHeight = jest - .fn() - .mockReturnValueOnce([{ id: ourHeader.id }]) - .mockImplementation((blockHeights) => - blockHeights.map((height: number) => ({ - height, - id: height.toString().padStart(2, "0").repeat(20), // just using height to mock the id - })), - ); - peerCommunicator.hasCommonBlocks = jest - .fn() - .mockImplementation((_, ids) => ({ id: ids[0], height: parseInt(ids[0].slice(0, 2)) })); - jest.spyOn(Blocks.BlockFactory, "fromData").mockImplementation(blockFromDataMock); - const generatorPublicKey = "03c5282b639d0e8f94cfac6c0ed242d1634d8a2c93cbd76c6ed2856a9f19cf6a13"; - trigger.call = jest.fn().mockReturnValueOnce([{ publicKey: generatorPublicKey }]); // getActiveDelegates mock - - if (returnEmpty) { - peerCommunicator.getPeerBlocks = jest.fn().mockResolvedValueOnce([]); + it.each([[true], [false]])( + "should return undefined when getPeerBlocks returns empty or rejects", + async (returnEmpty) => { + stateStore.getLastHeight = jest.fn().mockReturnValueOnce(claimedState.height); + stateStore.getLastBlocks = jest + .fn() + .mockReturnValueOnce([{ data: { height: claimedState.height }, getHeader: () => ourHeader }]); + databaseInteractions.getBlocksByHeight = jest + .fn() + .mockReturnValueOnce([{ id: ourHeader.id }]) + .mockImplementation((blockHeights) => + blockHeights.map((height: number) => ({ + height, + id: height.toString().padStart(2, "0").repeat(20), // just using height to mock the id + })), + ); + peerCommunicator.hasCommonBlocks = jest + .fn() + .mockImplementation((_, ids) => ({ id: ids[0], height: parseInt(ids[0].slice(0, 2)) })); + jest.spyOn(Blocks.BlockFactory, "fromData").mockImplementation(blockFromDataMock); + const generatorPublicKey = "03c5282b639d0e8f94cfac6c0ed242d1634d8a2c93cbd76c6ed2856a9f19cf6a13"; + trigger.call = jest.fn().mockReturnValueOnce([{ publicKey: generatorPublicKey }]); // getActiveDelegates mock - } else { - peerCommunicator.getPeerBlocks = jest.fn().mockRejectedValueOnce(new Error("timeout")); - } + if (returnEmpty) { + peerCommunicator.getPeerBlocks = jest.fn().mockResolvedValueOnce([]); + } else { + peerCommunicator.getPeerBlocks = jest.fn().mockRejectedValueOnce(new Error("timeout")); + } - const result = await peerVerifier.checkState(claimedState, Date.now() + 2000); + const result = await peerVerifier.checkState(claimedState, Date.now() + 2000); - expect(result).toBeUndefined(); - }); + expect(result).toBeUndefined(); + }, + ); it("should return undefined when peer returns block that does not verify", async () => { const generatorPublicKey = "03c5282b639d0e8f94cfac6c0ed242d1634d8a2c93cbd76c6ed2856a9f19cf6a13"; @@ -459,7 +477,7 @@ describe("PeerVerifier", () => { stateStore.getLastBlocks = jest .fn() .mockReturnValueOnce([{ data: { height: claimedState.height }, getHeader: () => ourHeader }]); - database.getBlocksByHeight = jest + databaseInteractions.getBlocksByHeight = jest .fn() .mockReturnValueOnce([{ id: ourHeader.id }]) .mockImplementation((blockHeights) => @@ -495,7 +513,7 @@ describe("PeerVerifier", () => { stateStore.getLastBlocks = jest .fn() .mockReturnValueOnce([{ data: { height: claimedState.height }, getHeader: () => ourHeader }]); - database.getBlocksByHeight = jest + databaseInteractions.getBlocksByHeight = jest .fn() .mockReturnValueOnce([{ id: ourHeader.id }]) .mockImplementation((blockHeights) => @@ -530,7 +548,7 @@ describe("PeerVerifier", () => { stateStore.getLastBlocks = jest .fn() .mockReturnValueOnce([{ data: { height: claimedState.height }, getHeader: () => ourHeader }]); - database.getBlocksByHeight = jest + databaseInteractions.getBlocksByHeight = jest .fn() .mockReturnValueOnce([{ id: ourHeader.id }]) .mockImplementation((blockHeights) => @@ -564,7 +582,7 @@ describe("PeerVerifier", () => { stateStore.getLastBlocks = jest .fn() .mockReturnValueOnce([{ data: { height: claimedState.height }, getHeader: () => ourHeader }]); - database.getBlocksByHeight = jest + databaseInteractions.getBlocksByHeight = jest .fn() .mockReturnValueOnce([{ id: ourHeader.id }]) .mockImplementation((blockHeights) => @@ -587,12 +605,12 @@ describe("PeerVerifier", () => { const spyFromData = jest.spyOn(Blocks.BlockFactory, "fromData").mockImplementation(blockFromDataMock); await expect(peerVerifier.checkState(claimedState, Date.now() - 1)).rejects.toEqual( - new Error("timeout elapsed before successful completion of the verification") - ) + new Error("timeout elapsed before successful completion of the verification"), + ); spyFromData.mockRestore(); peerCommunicator.getPeerBlocks = jest.fn(); - database.getBlocksByHeight = jest.fn(); + databaseInteractions.getBlocksByHeight = jest.fn(); peerCommunicator.hasCommonBlocks = jest.fn(); }); }); @@ -619,7 +637,9 @@ describe("PeerVerifier", () => { { data: { height: ourHeight }, getHeader: () => ourHeader }, { data: { height: claimedState.height }, getHeader: () => claimedState.header }, ]); - database.getBlocksByHeight = jest.fn().mockReturnValueOnce([{ id: claimedState.header.id }]); + databaseInteractions.getBlocksByHeight = jest + .fn() + .mockReturnValueOnce([{ id: claimedState.header.id }]); const result = await peerVerifier.checkState(claimedState, Date.now() + 2000); @@ -649,7 +669,7 @@ describe("PeerVerifier", () => { stateStore.getLastBlocks = jest .fn() .mockReturnValueOnce([{ data: { height: ourHeader.height }, getHeader: () => ourHeader }]); - database.getBlocksByHeight = jest + databaseInteractions.getBlocksByHeight = jest .fn() .mockReturnValueOnce([{ id: ourHeader.id }]) .mockImplementation((blockHeights) => @@ -678,7 +698,7 @@ describe("PeerVerifier", () => { spyFromData.mockRestore(); peerCommunicator.getPeerBlocks = jest.fn(); - database.getBlocksByHeight = jest.fn(); + databaseInteractions.getBlocksByHeight = jest.fn(); peerCommunicator.hasCommonBlocks = jest.fn(); }); }); diff --git a/__tests__/unit/core-p2p/socket-server/controllers/internal.test.ts b/__tests__/unit/core-p2p/socket-server/controllers/internal.test.ts index 4889431b28..051f3f1ef2 100644 --- a/__tests__/unit/core-p2p/socket-server/controllers/internal.test.ts +++ b/__tests__/unit/core-p2p/socket-server/controllers/internal.test.ts @@ -1,10 +1,9 @@ import { Container, Utils as KernelUtils } from "@arkecosystem/core-kernel"; - +import { NetworkStateStatus } from "@arkecosystem/core-p2p/src/enums"; +import { NetworkState } from "@arkecosystem/core-p2p/src/network-state"; import { InternalController } from "@arkecosystem/core-p2p/src/socket-server/controllers/internal"; -import { Networks, Utils, Blocks } from "@arkecosystem/crypto"; +import { Blocks, Networks, Utils } from "@arkecosystem/crypto"; import { TransactionFactory } from "@arkecosystem/crypto/dist/transactions"; -import { NetworkState } from "@arkecosystem/core-p2p/src/network-state"; -import { NetworkStateStatus } from "@arkecosystem/core-p2p/src/enums"; describe("InternalController", () => { let internalController: InternalController; @@ -16,6 +15,7 @@ describe("InternalController", () => { const networkMonitor = { getNetworkState: jest.fn() }; const emitter = { dispatch: jest.fn() }; const database = { getActiveDelegates: jest.fn() }; + const databaseInteractions = { getActiveDelegates: jest.fn() }; const poolCollator = { getBlockCandidateTransactions: jest.fn() }; const poolService = { getPoolSize: jest.fn() }; const blockchain = { getLastBlock: jest.fn(), forceWakeup: jest.fn() }; @@ -35,6 +35,7 @@ describe("InternalController", () => { container.bind(Container.Identifiers.PeerNetworkMonitor).toConstantValue(networkMonitor); container.bind(Container.Identifiers.EventDispatcherService).toConstantValue(emitter); container.bind(Container.Identifiers.DatabaseService).toConstantValue(database); + container.bind(Container.Identifiers.DatabaseInteraction).toConstantValue(databaseInteractions); container.bind(Container.Identifiers.Application).toConstantValue(app); }); @@ -118,7 +119,7 @@ describe("InternalController", () => { delegate: "delegate2", }, ]; - database.getActiveDelegates = jest.fn().mockReturnValueOnce(delegates); + databaseInteractions.getActiveDelegates = jest.fn().mockReturnValueOnce(delegates); const forgingInfo = { blockTimestamp: 97456, currentForger: 0, diff --git a/__tests__/unit/core-p2p/socket-server/controllers/peer.test.ts b/__tests__/unit/core-p2p/socket-server/controllers/peer.test.ts index b31057c9d7..549f3c1d42 100644 --- a/__tests__/unit/core-p2p/socket-server/controllers/peer.test.ts +++ b/__tests__/unit/core-p2p/socket-server/controllers/peer.test.ts @@ -15,6 +15,10 @@ describe("PeerController", () => { const logger = { warning: jest.fn(), debug: jest.fn(), info: jest.fn() }; const peerStorage = { getPeers: jest.fn() }; const database = { getCommonBlocks: jest.fn(), getBlocksForDownload: jest.fn() }; + const databaseInteractions = { + getCommonBlocks: jest.fn(), + getBlocksForDownload: jest.fn(), + }; const blockchain = { getLastBlock: jest.fn(), handleIncomingBlock: jest.fn(), @@ -62,6 +66,7 @@ describe("PeerController", () => { container.bind(Container.Identifiers.LogService).toConstantValue(logger); container.bind(Container.Identifiers.PeerStorage).toConstantValue(peerStorage); container.bind(Container.Identifiers.DatabaseService).toConstantValue(database); + container.bind(Container.Identifiers.DatabaseInteraction).toConstantValue(databaseInteractions); container.bind(Container.Identifiers.Application).toConstantValue(app); }); @@ -95,7 +100,7 @@ describe("PeerController", () => { describe("getCommonBlocks", () => { it("should return the last common block found and the last height", async () => { const request = { payload: { ids: ["123456789", "111116789"] } }; - database.getCommonBlocks = jest.fn().mockReturnValueOnce(request.payload.ids); + databaseInteractions.getCommonBlocks = jest.fn().mockReturnValueOnce(request.payload.ids); const height = 1433; blockchain.getLastBlock = jest.fn().mockReturnValueOnce({ data: { height } }); const commonBlocks = await peerController.getCommonBlocks(request, {}); @@ -108,7 +113,7 @@ describe("PeerController", () => { it("should throw MissingCommonBlockError when no common block found", async () => { const request = { payload: { ids: ["123456789", "111116789"] } }; - database.getCommonBlocks = jest.fn().mockReturnValueOnce([]); + databaseInteractions.getCommonBlocks = jest.fn().mockReturnValueOnce([]); await expect(peerController.getCommonBlocks(request, {})).rejects.toBeInstanceOf(MissingCommonBlockError); }); diff --git a/__tests__/unit/core-state/__fixtures__/block1760000.ts b/__tests__/unit/core-state/__fixtures__/block1760000.ts new file mode 100644 index 0000000000..acd986961f --- /dev/null +++ b/__tests__/unit/core-state/__fixtures__/block1760000.ts @@ -0,0 +1,118 @@ +import { Utils } from "@arkecosystem/crypto"; + +export default { + id: "17605317082329008056", + version: 0, + height: 1760000, + timestamp: 62222080, + previousBlock: "3112633353705641986", + numberOfTransactions: 7, + totalAmount: Utils.BigNumber.make("10500000000"), + totalFee: Utils.BigNumber.make("70000000"), + reward: Utils.BigNumber.make("200000000"), + payloadLength: 224, + payloadHash: "de56269cae3ab156f6979b94a04c30b82ed7d6f9a97d162583c98215c18c65db", + generatorPublicKey: "03287bfebba4c7881a0509717e71b34b63f31e40021c321f89ae04f84be6d6ac37", + blockSignature: + "30450221008c59bd2379061ad3539b73284fc0bbb57dbc97efd54f55010ba3f198c04dde7402202e482126b3084c6313c1378d686df92a3e2ef5581323de11e74fe07eeab339f3", + transactions: [ + { + version: 1, + network: 30, + type: 0, + timestamp: 62222080, + senderPublicKey: "03287bfebba4c7881a0509717e71b34b63f31e40021c321f89ae04f84be6d6ac37", + fee: Utils.BigNumber.make("10000000"), + amount: Utils.BigNumber.make("1300000000"), + expiration: 0, + recipientId: "DBYyh2vXcigrJGUHfvmYxVxEqeH7vomw6x", + signature: + "30440220714c2627f0e9c3bd6bf13b8b4faa5ec2d677694c27f580e2f9e3875bde9bc36f02201c33faacab9eafd799d9ceecaa153e3b87b4cd04535195261fd366e552652549", + id: "188b4d9d95a58e4e18d9ce9db28f2010323b90b5afd36a474d7ae7bf70772bb0", + }, + { + version: 1, + network: 30, + type: 0, + timestamp: 62222080, + senderPublicKey: "03287bfebba4c7881a0509717e71b34b63f31e40021c321f89ae04f84be6d6ac37", + fee: Utils.BigNumber.make("10000000"), + amount: Utils.BigNumber.make("1700000000"), + expiration: 0, + recipientId: "DBYyh2vXcigrJGUHfvmYxVxEqeH7vomw6x", + signature: + "3045022100e6039f810684515c0d6b31039040a76c98f3624b6454cb156a0a2137e5f8dba7022001ada19bcca5798e1c7cc8cc39bab5d4019525e3d72a42bd2c4129352b8ead87", + id: "23084f2cc566f6144a8f447bc784de64a0b0646776060482d8550856145e11e2", + }, + { + version: 1, + network: 30, + type: 0, + timestamp: 62222080, + senderPublicKey: "03287bfebba4c7881a0509717e71b34b63f31e40021c321f89ae04f84be6d6ac37", + fee: Utils.BigNumber.make("10000000"), + amount: Utils.BigNumber.make("1500000000"), + expiration: 0, + recipientId: "DBYyh2vXcigrJGUHfvmYxVxEqeH7vomw6x", + signature: + "3045022100c2b5ef772b36e468e95ec2e457bfaba7bad0e13b3faf57e229ff5d67a0e017c902202339664595ea5c70ce20e4dd182532f7fa385d86575b0476ff3eda9f9785e1e9", + id: "743ce0a590c2af90e4734db3630b52d7a7cbc2bc228d75ae6409c0b6d184bfad", + }, + { + version: 1, + network: 30, + type: 0, + timestamp: 62222080, + senderPublicKey: "03287bfebba4c7881a0509717e71b34b63f31e40021c321f89ae04f84be6d6ac37", + fee: Utils.BigNumber.make("10000000"), + amount: Utils.BigNumber.make("1600000000"), + expiration: 0, + recipientId: "DBYyh2vXcigrJGUHfvmYxVxEqeH7vomw6x", + signature: + "30450221009ceb56688705e6b12000bde726ca123d84982231d7434f059612ff5f987409c602200d908667877c902e7ba35024951046b883e0bce9103d4717928d94ecc958884a", + id: "877780706b62b437913ef4ea30c6e370f8877ef7a5bac58d8cebca83b7e20060", + }, + { + version: 1, + network: 30, + type: 0, + timestamp: 62222080, + senderPublicKey: "03287bfebba4c7881a0509717e71b34b63f31e40021c321f89ae04f84be6d6ac37", + fee: Utils.BigNumber.make("10000000"), + amount: Utils.BigNumber.make("1200000000"), + expiration: 0, + recipientId: "DBYyh2vXcigrJGUHfvmYxVxEqeH7vomw6x", + signature: + "30440220464beac6d49943ad8afaac4fdc863c9cd7cf3a84f9938c1d7269ed522298f11a02203581bf180de1966f86d914afeb005e1e818c9213514f96a34e1391c2a08514fa", + id: "947fe8745eeed8fa6e5ad62a8dad29bcf3d50ce001907926c486460d1cc1f1c0", + }, + { + version: 1, + network: 30, + type: 0, + timestamp: 62222080, + senderPublicKey: "03287bfebba4c7881a0509717e71b34b63f31e40021c321f89ae04f84be6d6ac37", + fee: Utils.BigNumber.make("10000000"), + amount: Utils.BigNumber.make("1800000000"), + expiration: 0, + recipientId: "DBYyh2vXcigrJGUHfvmYxVxEqeH7vomw6x", + signature: + "3045022100c7b40d7134d909762d18d6bfb7ac1c32be0ee8c047020131f499faea70ca0b2b0220117c0cf026f571f5a85e3ae800a6fd595185076ff38e64c7a4bd14f34e1d4dd1", + id: "98387933d65fabffe2642464d4c7b1ff5fe1fa5a35992f834b0ac145dff462ea", + }, + { + version: 1, + network: 30, + type: 0, + timestamp: 62222080, + senderPublicKey: "03287bfebba4c7881a0509717e71b34b63f31e40021c321f89ae04f84be6d6ac37", + fee: Utils.BigNumber.make("10000000"), + amount: Utils.BigNumber.make("1400000000"), + expiration: 0, + recipientId: "DBYyh2vXcigrJGUHfvmYxVxEqeH7vomw6x", + signature: + "304402206a4a8e4e6918fbc15728653b117f51db716aeb04e5ee1de047f80b0476ee4efb02200f486dfaf0def3f3e8636d46ee75a2c07de9714ce4283a25fde9b6218b5e7923", + id: "e93345dd9a87ac4e84d9bfd892dfbfeb02e546e5bd7822168d0f72c7662e6176", + }, + ], +}; diff --git a/__tests__/unit/core-state/database-interactions.test.ts b/__tests__/unit/core-state/database-interactions.test.ts new file mode 100644 index 0000000000..f03a41b45f --- /dev/null +++ b/__tests__/unit/core-state/database-interactions.test.ts @@ -0,0 +1,817 @@ +import "jest-extended"; + +import { Container, Enums } from "@arkecosystem/core-kernel"; +import { Blocks, Identities, Utils } from "@arkecosystem/crypto"; + +import { DatabaseService } from "../../../packages/core-database"; +import { DatabaseInteraction } from "../../../packages/core-state/src/database-interactions"; +import block1760000 from "./__fixtures__/block1760000"; + +const getTimeStampForBlock = () => { + throw new Error("Unreachable"); +}; +const app = { + get: jest.fn(), + terminate: jest.fn(), +}; +const connection = { + query: jest.fn(), + close: jest.fn(), +}; +const blockRepository = { + findOne: jest.fn(), + findByHeightRange: jest.fn(), + findByHeightRangeWithTransactions: jest.fn(), + findByHeightRangeWithTransactionsForDownload: jest.fn(), + findByHeights: jest.fn(), + findLatest: jest.fn(), + findByIds: jest.fn(), + findRecent: jest.fn(), + findTop: jest.fn(), + count: jest.fn(), + getStatistics: jest.fn(), + saveBlocks: jest.fn(), + deleteBlocks: jest.fn(), +}; +const transactionRepository = { + find: jest.fn(), + findOne: jest.fn(), + findByBlockIds: jest.fn(), + getStatistics: jest.fn(), +}; +const roundRepository = { + getRound: jest.fn(), + save: jest.fn(), + delete: jest.fn(), +}; + +const stateStore = { + setGenesisBlock: jest.fn(), + getGenesisBlock: jest.fn(), + setLastBlock: jest.fn(), + getLastBlock: jest.fn(), + getLastBlocksByHeight: jest.fn(), + getCommonBlocks: jest.fn(), + getLastBlockIds: jest.fn(), +}; + +const stateBlockStore = { + resize: jest.fn(), +}; + +const stateTransactionStore = { + resize: jest.fn(), +}; + +const handlerRegistry = { + getActivatedHandlerForData: jest.fn(), +}; + +const walletRepository = { + createWallet: jest.fn(), + findByPublicKey: jest.fn(), + findByUsername: jest.fn(), +}; + +const blockState = { + applyBlock: jest.fn(), + revertBlock: jest.fn(), +}; + +const dposState = { + buildDelegateRanking: jest.fn(), + setDelegatesRound: jest.fn(), + getRoundDelegates: jest.fn(), +}; + +const getDposPreviousRoundState = jest.fn(); + +const triggers = { + call: jest.fn(), +}; + +const events = { + call: jest.fn(), + dispatch: jest.fn(), +}; +const logger = { + error: jest.fn(), + warning: jest.fn(), + info: jest.fn(), + debug: jest.fn(), +}; +// const databaseService = { +// reset: jest.fn(), +// saveBlocks: jest.fn(), +// }; +const container = new Container.Container(); +container.bind(Container.Identifiers.Application).toConstantValue(app); +container.bind(Container.Identifiers.DatabaseConnection).toConstantValue(connection); +container.bind(Container.Identifiers.DatabaseBlockRepository).toConstantValue(blockRepository); +container.bind(Container.Identifiers.DatabaseTransactionRepository).toConstantValue(transactionRepository); +container.bind(Container.Identifiers.DatabaseRoundRepository).toConstantValue(roundRepository); +container.bind(Container.Identifiers.DatabaseService).to(DatabaseService); +container.bind(Container.Identifiers.StateStore).toConstantValue(stateStore); +container.bind(Container.Identifiers.StateBlockStore).toConstantValue(stateBlockStore); +container.bind(Container.Identifiers.StateTransactionStore).toConstantValue(stateTransactionStore); +container.bind(Container.Identifiers.TransactionHandlerRegistry).toConstantValue(handlerRegistry); +container.bind(Container.Identifiers.WalletRepository).toConstantValue(walletRepository); +container.bind(Container.Identifiers.BlockState).toConstantValue(blockState); +container.bind(Container.Identifiers.DposState).toConstantValue(dposState); +container.bind(Container.Identifiers.DposPreviousRoundStateProvider).toConstantValue(getDposPreviousRoundState); +container.bind(Container.Identifiers.TriggerService).toConstantValue(triggers); +container.bind(Container.Identifiers.EventDispatcherService).toConstantValue(events); +container.bind(Container.Identifiers.LogService).toConstantValue(logger); + +beforeEach(() => { + app.get.mockReset(); + app.terminate.mockReset(); + connection.query.mockReset(); + connection.close.mockReset(); + blockRepository.findOne.mockReset(); + blockRepository.findByHeightRange.mockReset(); + blockRepository.findByHeightRangeWithTransactionsForDownload.mockReset(); + blockRepository.findByHeightRangeWithTransactions.mockReset(); + blockRepository.findByHeights.mockReset(); + blockRepository.findLatest.mockReset(); + blockRepository.findByIds.mockReset(); + blockRepository.findRecent.mockReset(); + blockRepository.findTop.mockReset(); + blockRepository.count.mockReset(); + blockRepository.getStatistics.mockReset(); + blockRepository.saveBlocks.mockReset(); + blockRepository.deleteBlocks.mockReset(); + transactionRepository.find.mockReset(); + transactionRepository.findOne.mockReset(); + transactionRepository.findByBlockIds.mockReset(); + transactionRepository.getStatistics.mockReset(); + roundRepository.getRound.mockReset(); + roundRepository.save.mockReset(); + roundRepository.delete.mockReset(); + + stateStore.setGenesisBlock.mockReset(); + stateStore.getGenesisBlock.mockReset(); + stateStore.setLastBlock.mockReset(); + stateStore.getLastBlock.mockReset(); + stateStore.getLastBlocksByHeight.mockReset(); + stateStore.getCommonBlocks.mockReset(); + stateStore.getLastBlockIds.mockReset(); + + stateBlockStore.resize.mockReset(); + stateTransactionStore.resize.mockReset(); + + handlerRegistry.getActivatedHandlerForData.mockReset(); + + walletRepository.createWallet.mockReset(); + walletRepository.findByPublicKey.mockReset(); + walletRepository.findByUsername.mockReset(); + + blockState.applyBlock.mockReset(); + blockState.revertBlock.mockReset(); + + dposState.buildDelegateRanking.mockReset(); + dposState.setDelegatesRound.mockReset(); + dposState.getRoundDelegates.mockReset(); + + getDposPreviousRoundState.mockReset(); + + triggers.call.mockReset(); + + logger.error.mockReset(); + logger.warning.mockReset(); + logger.info.mockReset(); + logger.debug.mockReset(); + events.call.mockReset(); + events.dispatch.mockReset(); +}); + +describe("DatabaseInteractions", () => { + it("should dispatch starting event", async () => { + const databaseInteraction: DatabaseInteraction = container.resolve(DatabaseInteraction); + await databaseInteraction.initialize(); + expect(events.dispatch).toBeCalledWith(Enums.StateEvent.Starting); + }); + + it("should reset database when CORE_RESET_DATABASE variable is set", async () => { + try { + const databaseInteraction: DatabaseInteraction = container.resolve(DatabaseInteraction); + + process.env.CORE_RESET_DATABASE = "1"; + const genesisBlock = {}; + stateStore.getGenesisBlock.mockReturnValue(genesisBlock); + + await databaseInteraction.initialize(); + // expect(databaseInteraction.reset).toBeCalled(); + expect(stateStore.getGenesisBlock).toBeCalled(); + // expect(databaseInteraction.saveBlocks).toBeCalledWith([genesisBlock]); + expect(stateStore.setGenesisBlock).toBeCalled(); + } finally { + delete process.env.CORE_RESET_DATABASE; + } + }); + + it("should terminate app if exception was raised", async () => { + const databaseInteraction: DatabaseInteraction = container.resolve(DatabaseInteraction); + stateStore.setGenesisBlock.mockImplementationOnce(() => { + throw new Error("Fail"); + }); + await databaseInteraction.initialize(); + expect(app.terminate).toBeCalled(); + }); + + it("should terminate if unable to deserialize last 5 blocks", async () => { + const databaseInteraction: DatabaseInteraction = container.resolve(DatabaseInteraction); + + const block101data = { id: "block101", height: 101 }; + const block102data = { id: "block102", height: 102 }; + const block103data = { id: "block103", height: 103 }; + const block104data = { id: "block104", height: 104 }; + const block105data = { id: "block105", height: 105 }; + const block106data = { id: "block106", height: 105 }; + + blockRepository.findLatest.mockResolvedValueOnce(block106data); + + blockRepository.findLatest.mockResolvedValueOnce(block106data); // this.getLastBlock + transactionRepository.findByBlockIds.mockResolvedValueOnce([]); // this.getLastBlock + + blockRepository.findLatest.mockResolvedValueOnce(block106data); // blockRepository.deleteBlocks + blockRepository.findLatest.mockResolvedValueOnce(block105data); // this.getLastBlock + transactionRepository.findByBlockIds.mockResolvedValueOnce([]); // this.getLastBlock + + blockRepository.findLatest.mockResolvedValueOnce(block105data); // blockRepository.deleteBlocks + blockRepository.findLatest.mockResolvedValueOnce(block104data); // this.getLastBlock + transactionRepository.findByBlockIds.mockResolvedValueOnce([]); // this.getLastBlock + + blockRepository.findLatest.mockResolvedValueOnce(block104data); // blockRepository.deleteBlocks + blockRepository.findLatest.mockResolvedValueOnce(block103data); // this.getLastBlock + transactionRepository.findByBlockIds.mockResolvedValueOnce([]); // this.getLastBlock + + blockRepository.findLatest.mockResolvedValueOnce(block103data); // blockRepository.deleteBlocks + blockRepository.findLatest.mockResolvedValueOnce(block102data); // this.getLastBlock + transactionRepository.findByBlockIds.mockResolvedValueOnce([]); // this.getLastBlock + + blockRepository.findLatest.mockResolvedValueOnce(block102data); // blockRepository.deleteBlocks + blockRepository.findLatest.mockResolvedValueOnce(block101data); // this.getLastBlock + transactionRepository.findByBlockIds.mockResolvedValueOnce([]); // this.getLastBlock + + await databaseInteraction.initialize(); + + expect(stateStore.setGenesisBlock).toBeCalled(); + expect(blockRepository.findLatest).toBeCalledTimes(12); + + expect(transactionRepository.findByBlockIds).toBeCalledWith([block106data.id]); + + expect(blockRepository.deleteBlocks).toBeCalledWith([block106data]); + expect(transactionRepository.findByBlockIds).toBeCalledWith([block105data.id]); + + expect(blockRepository.deleteBlocks).toBeCalledWith([block105data]); + expect(transactionRepository.findByBlockIds).toBeCalledWith([block104data.id]); + + expect(blockRepository.deleteBlocks).toBeCalledWith([block104data]); + expect(transactionRepository.findByBlockIds).toBeCalledWith([block103data.id]); + + expect(blockRepository.deleteBlocks).toBeCalledWith([block103data]); + expect(transactionRepository.findByBlockIds).toBeCalledWith([block102data.id]); + + expect(blockRepository.deleteBlocks).toBeCalledWith([block102data]); + expect(transactionRepository.findByBlockIds).toBeCalledWith([block101data.id]); + + expect(app.terminate).toBeCalled(); + }); +}); + +describe("DatabaseInteraction.restoreCurrentRound", () => { + it("should restore round to its initial state", async () => { + const databaseInteraction: DatabaseInteraction = container.resolve(DatabaseInteraction); + + const lastBlock = Blocks.BlockFactory.fromData(block1760000, getTimeStampForBlock); + stateStore.getLastBlock.mockReturnValueOnce(lastBlock); + + const lastBlocksByHeight = [lastBlock.data]; + stateStore.getLastBlocksByHeight.mockReturnValueOnce(lastBlocksByHeight); + blockRepository.findByHeightRangeWithTransactions.mockReturnValueOnce(lastBlocksByHeight); + + const prevRoundState = { getAllDelegates: jest.fn(), getRoundDelegates: jest.fn(), revert: jest.fn() }; + getDposPreviousRoundState.mockReturnValueOnce(prevRoundState); + + const delegateWallet = { setAttribute: jest.fn(), getAttribute: jest.fn() }; + walletRepository.findByUsername.mockReturnValueOnce(delegateWallet); + + const dposStateRoundDelegates = [delegateWallet]; + dposState.getRoundDelegates.mockReturnValueOnce(dposStateRoundDelegates); + dposState.getRoundDelegates.mockReturnValueOnce(dposStateRoundDelegates); + + const forgingDelegates = [delegateWallet]; + triggers.call.mockResolvedValue(forgingDelegates); + + await databaseInteraction.restoreCurrentRound(1760000); + + expect(getDposPreviousRoundState).not.toBeCalled(); // restoring current round should not need previous round state + // important: getActiveDelegates should be called with only roundInfo (restoreCurrentRound does *not* provide delegates to it) + expect(triggers.call).toHaveBeenLastCalledWith("getActiveDelegates", { + roundInfo: expect.anything(), + delegates: undefined, + }); + // @ts-ignore + expect(databaseInteraction.forgingDelegates).toEqual(forgingDelegates); + }); +}); + +describe("DatabaseInteraction.reset", () => { + it("should reset database", async () => { + const databaseInteraction: DatabaseInteraction = container.resolve(DatabaseInteraction); + + const genesisBlock = {}; + stateStore.getGenesisBlock.mockReturnValueOnce(genesisBlock); + + await databaseInteraction.reset(); + + expect(connection.query).toBeCalledWith("TRUNCATE TABLE blocks, rounds, transactions RESTART IDENTITY;"); + expect(blockRepository.saveBlocks).toBeCalledWith([genesisBlock]); + }); +}); + +describe("DatabaseInteraction.applyBlock", () => { + it("should apply block, round, detect missing blocks, and fire events", async () => { + const databaseInteraction: DatabaseInteraction = container.resolve(DatabaseInteraction); + + const lastBlock = { data: { height: 53, timestamp: 0 } }; + stateStore.getLastBlock.mockReturnValueOnce(lastBlock); + + const delegateWallet = { publicKey: "delegate public key", getAttribute: jest.fn() }; + const delegateUsername = "test_delegate"; + delegateWallet.getAttribute.mockReturnValueOnce(delegateUsername); + + const handler = { emitEvents: jest.fn() }; + handlerRegistry.getActivatedHandlerForData.mockResolvedValueOnce(handler); + + // still previous last block! + stateStore.getLastBlock.mockReturnValueOnce(lastBlock); + + // @ts-ignore + databaseInteraction.blocksInCurrentRound = []; + // @ts-ignore + databaseInteraction.forgingDelegates = [delegateWallet] as any; + + const transaction = {}; + const block = { data: { height: 54, timestamp: 35 }, transactions: [transaction] }; + await databaseInteraction.applyBlock(block as any); + + expect(stateStore.getLastBlock).toBeCalledTimes(1); + expect(blockState.applyBlock).toBeCalledWith(block); + // @ts-ignore + expect(databaseInteraction.blocksInCurrentRound).toEqual([block]); + expect(events.dispatch).toBeCalledWith("forger.missing", { delegate: delegateWallet }); + expect(handler.emitEvents).toBeCalledWith(transaction, events); + expect(events.dispatch).toBeCalledWith("block.applied", block.data); + }); + + it("should apply block, not apply round, and not detect missed blocks when last block height is 1", async () => { + const databaseInteraction: DatabaseInteraction = container.resolve(DatabaseInteraction); + + const lastBlock = { data: { height: 1 } }; + stateStore.getLastBlock.mockReturnValueOnce(lastBlock); + + const handler = { emitEvents: jest.fn() }; + handlerRegistry.getActivatedHandlerForData.mockResolvedValueOnce(handler); + + // still previous last block! + stateStore.getLastBlock.mockReturnValueOnce(lastBlock); + + const transaction = {}; + const block = { data: { height: 2, timestamp: 35 }, transactions: [transaction] }; + await databaseInteraction.applyBlock(block as any); + + expect(stateStore.getLastBlock).toBeCalledTimes(1); + expect(handler.emitEvents).toBeCalledWith(transaction, events); + expect(events.dispatch).toBeCalledWith("block.applied", block.data); + }); +}); + +describe("DatabaseInteraction.applyRound", () => { + it("should build delegates, save round, dispatch events when round changes on next height", async () => { + const databaseInteraction: DatabaseInteraction = container.resolve(DatabaseInteraction); + + const forgingDelegate = { getAttribute: jest.fn() }; + const forgingDelegateRound = 1; + forgingDelegate.getAttribute.mockReturnValueOnce(forgingDelegateRound); + // @ts-ignore + databaseInteraction.forgingDelegates = [forgingDelegate] as any; + + // @ts-ignore + databaseInteraction.blocksInCurrentRound = [{ data: { generatorPublicKey: "delegate public key" } }] as any; + + const delegateWallet = { publicKey: "delegate public key", getAttribute: jest.fn() }; + const dposStateRoundDelegates = [delegateWallet]; + dposState.getRoundDelegates.mockReturnValueOnce(dposStateRoundDelegates); + dposState.getRoundDelegates.mockReturnValueOnce(dposStateRoundDelegates); + + const delegateWalletRound = 2; + delegateWallet.getAttribute.mockReturnValueOnce(delegateWalletRound); + + walletRepository.findByPublicKey.mockReturnValueOnce(delegateWallet); + + const delegateUsername = "test_delegate"; + delegateWallet.getAttribute.mockReturnValueOnce(delegateUsername); + + const height = 51; + await databaseInteraction.applyRound(height); + + expect(dposState.buildDelegateRanking).toBeCalled(); + expect(dposState.setDelegatesRound).toBeCalledWith({ + round: 2, + nextRound: 2, + roundHeight: 52, + maxDelegates: 51, + }); + expect(roundRepository.save).toBeCalledWith(dposStateRoundDelegates); + expect(events.dispatch).toBeCalledWith("round.applied"); + }); + + it("should build delegates, save round, dispatch events when height is 1", async () => { + const databaseInteraction: DatabaseInteraction = container.resolve(DatabaseInteraction); + + const forgingDelegate = { getAttribute: jest.fn() }; + const forgingDelegateRound = 1; + forgingDelegate.getAttribute.mockReturnValueOnce(forgingDelegateRound); + // @ts-ignore + databaseInteraction.forgingDelegates = [forgingDelegate] as any; + + // @ts-ignore + databaseInteraction.blocksInCurrentRound = []; + + const delegateWallet = { publicKey: "delegate public key", getAttribute: jest.fn() }; + const dposStateRoundDelegates = [delegateWallet]; + dposState.getRoundDelegates.mockReturnValueOnce(dposStateRoundDelegates); + dposState.getRoundDelegates.mockReturnValueOnce(dposStateRoundDelegates); + + const delegateWalletRound = 1; + delegateWallet.getAttribute.mockReturnValueOnce(delegateWalletRound); + + walletRepository.findByPublicKey.mockReturnValueOnce(delegateWallet); + + const delegateUsername = "test_delegate"; + delegateWallet.getAttribute.mockReturnValueOnce(delegateUsername); + + const height = 1; + await databaseInteraction.applyRound(height); + + expect(dposState.buildDelegateRanking).toBeCalled(); + expect(dposState.setDelegatesRound).toBeCalledWith({ + round: 1, + nextRound: 1, + roundHeight: 1, + maxDelegates: 51, + }); + expect(roundRepository.save).toBeCalledWith(dposStateRoundDelegates); + expect(events.dispatch).toBeCalledWith("round.applied"); + }); + + it("should build delegates, save round, dispatch events, and skip missing round checks when first round has genesis block only", async () => { + const databaseInteraction: DatabaseInteraction = container.resolve(DatabaseInteraction); + + const forgingDelegate = { getAttribute: jest.fn() }; + const forgingDelegateRound = 1; + forgingDelegate.getAttribute.mockReturnValueOnce(forgingDelegateRound); + // @ts-ignore + databaseInteraction.forgingDelegates = [forgingDelegate] as any; + + // @ts-ignore + databaseInteraction.blocksInCurrentRound = [{ data: { height: 1 } }] as any; + + const delegateWallet = { publicKey: "delegate public key", getAttribute: jest.fn() }; + const dposStateRoundDelegates = [delegateWallet]; + dposState.getRoundDelegates.mockReturnValueOnce(dposStateRoundDelegates); + dposState.getRoundDelegates.mockReturnValueOnce(dposStateRoundDelegates); + + const delegateWalletRound = 2; + delegateWallet.getAttribute.mockReturnValueOnce(delegateWalletRound); + + walletRepository.findByPublicKey.mockReturnValueOnce(delegateWallet); + + const delegateUsername = "test_delegate"; + delegateWallet.getAttribute.mockReturnValueOnce(delegateUsername); + + const height = 51; + await databaseInteraction.applyRound(height); + + expect(dposState.buildDelegateRanking).toBeCalled(); + expect(dposState.setDelegatesRound).toBeCalledWith({ + round: 2, + nextRound: 2, + roundHeight: 52, + maxDelegates: 51, + }); + expect(roundRepository.save).toBeCalledWith(dposStateRoundDelegates); + expect(events.dispatch).toBeCalledWith("round.applied"); + }); + + it("should delete round and rethrow error when error was thrown", async () => { + const databaseInteraction: DatabaseInteraction = container.resolve(DatabaseInteraction); + + dposState.buildDelegateRanking.mockImplementation(() => { + throw new Error("Fail"); + }); + + const height = 51; + const check = () => databaseInteraction.applyRound(height); + + await expect(check()).rejects.toThrowError("Fail"); + expect(roundRepository.delete).toBeCalledWith({ round: 2 }); + }); + + it("should do nothing when next height is same round", async () => { + const databaseInteraction: DatabaseInteraction = container.resolve(DatabaseInteraction); + + const height = 50; + await databaseInteraction.applyRound(height); + expect(logger.info).not.toBeCalled(); + }); + + it("should warn when, and do nothing when round was already applied", async () => { + const databaseInteraction: DatabaseInteraction = container.resolve(DatabaseInteraction); + + const forgingDelegate = { getAttribute: jest.fn() }; + const forgingDelegateRound = 2; + forgingDelegate.getAttribute.mockReturnValueOnce(forgingDelegateRound); + // @ts-ignore + databaseInteraction.forgingDelegates = [forgingDelegate] as any; + + const height = 51; + await databaseInteraction.applyRound(height); + + expect(logger.warning).toBeCalledWith( + "Round 2 has already been applied. This should happen only if you are a forger.", + ); + }); +}); + +describe("DatabaseInteraction.getActiveDelegates", () => { + it("should return shuffled round delegates", async () => { + const databaseInteraction: DatabaseInteraction = container.resolve(DatabaseInteraction); + + const lastBlock = Blocks.BlockFactory.fromData(block1760000, getTimeStampForBlock); + + // @ts-ignore + blockRepository.findLatest.mockResolvedValueOnce(lastBlock.data); + // @ts-ignore + transactionRepository.findByBlockIds.mockResolvedValueOnce(lastBlock.transactions); + + const delegatePublicKey = "03287bfebba4c7881a0509717e71b34b63f31e40021c321f89ae04f84be6d6ac37"; + const delegateVoteBalance = Utils.BigNumber.make("100"); + const roundDelegateModel = { publicKey: delegatePublicKey, balance: delegateVoteBalance }; + roundRepository.getRound.mockResolvedValueOnce([roundDelegateModel]); + + const newDelegateWallet = { setAttribute: jest.fn(), clone: jest.fn() }; + walletRepository.createWallet.mockReturnValueOnce(newDelegateWallet); + + const oldDelegateWallet = { getAttribute: jest.fn() }; + walletRepository.findByPublicKey.mockReturnValueOnce(oldDelegateWallet); + + const delegateUsername = "test_delegate"; + oldDelegateWallet.getAttribute.mockReturnValueOnce(delegateUsername); + + const cloneDelegateWallet = {}; + newDelegateWallet.clone.mockReturnValueOnce(cloneDelegateWallet); + + await databaseInteraction.getActiveDelegates(); + + expect(walletRepository.findByPublicKey).toBeCalledWith(delegatePublicKey); + expect(walletRepository.createWallet).toBeCalledWith(Identities.Address.fromPublicKey(delegatePublicKey)); + expect(oldDelegateWallet.getAttribute).toBeCalledWith("delegate.username", ""); + expect(newDelegateWallet.setAttribute).toBeCalledWith("delegate", { + voteBalance: delegateVoteBalance, + username: delegateUsername, + }); + expect(newDelegateWallet.clone).toBeCalled(); + }); + + it("should return cached forgingDelegates when round is the same", async () => { + const databaseInteraction: DatabaseInteraction = container.resolve(DatabaseInteraction); + + const forgingDelegate = { getAttribute: jest.fn() }; + const forgingDelegateRound = 2; + forgingDelegate.getAttribute.mockReturnValueOnce(forgingDelegateRound); + // @ts-ignore + databaseInteraction.forgingDelegates = [forgingDelegate] as any; + + const roundInfo = { round: 2 }; + const result = await databaseInteraction.getActiveDelegates(roundInfo as any); + + expect(forgingDelegate.getAttribute).toBeCalledWith("delegate.round"); + // @ts-ignore + expect(result).toBe(databaseInteraction.forgingDelegates); + }); +}); + +describe("DatabaseInteraction.getBlocksByHeight", () => { + it("should return blocks with transactions when full blocks are requested", async () => { + const databaseInteraction: DatabaseInteraction = container.resolve(DatabaseInteraction); + + const block100 = { height: 100, transactions: [] }; + const block101 = { height: 101, transactions: [] }; + const block102 = { height: 102, transactions: [] }; + + stateStore.getLastBlocksByHeight.mockReturnValueOnce([block100]); + stateStore.getLastBlocksByHeight.mockReturnValueOnce([]); + stateStore.getLastBlocksByHeight.mockReturnValueOnce([block102]); + + blockRepository.findByHeights.mockResolvedValueOnce([block101]); + + const result = await databaseInteraction.getBlocksByHeight([100, 101, 102]); + + expect(stateStore.getLastBlocksByHeight).toBeCalledWith(100, 100, true); + expect(stateStore.getLastBlocksByHeight).toBeCalledWith(101, 101, true); + expect(stateStore.getLastBlocksByHeight).toBeCalledWith(102, 102, true); + expect(blockRepository.findByHeights).toBeCalledWith([101]); + expect(result).toEqual([block100, block101, block102]); + }); +}); + +describe("DatabaseInteraction.getBlocksForRound", () => { + it("should return empty array if there are no blocks", async () => { + const databaseInteraction: DatabaseInteraction = container.resolve(DatabaseInteraction); + + stateStore.getLastBlock.mockReturnValueOnce(undefined); + blockRepository.findLatest.mockResolvedValueOnce(undefined); + + const roundInfo = { roundHeight: 52, maxDelegates: 51 }; + const result = await databaseInteraction.getBlocksForRound(roundInfo as any); + + expect(stateStore.getLastBlock).toBeCalled(); + expect(blockRepository.findLatest).toBeCalled(); + expect(result).toEqual([]); + }); + + it("should return array with genesis block only when last block is genesis block", async () => { + const databaseInteraction: DatabaseInteraction = container.resolve(DatabaseInteraction); + + const lastBlock = { data: { height: 1 } }; + stateStore.getLastBlock.mockReturnValueOnce(lastBlock); + + const roundInfo = { roundHeight: 1, maxDelegates: 51 }; + const result = await databaseInteraction.getBlocksForRound(roundInfo as any); + + expect(stateStore.getLastBlock).toBeCalled(); + expect(result).toEqual([lastBlock]); + }); +}); + +describe("DatabaseInteraction.getCommonBlocks", () => { + it("should return blocks by ids", async () => { + const databaseInteraction: DatabaseInteraction = container.resolve(DatabaseInteraction); + + const block100 = { id: "00100", height: 100, transactions: [] }; + const block101 = { id: "00101", height: 101, transactions: [] }; + const block102 = { id: "00102", height: 102, transactions: [] }; + + stateStore.getCommonBlocks.mockReturnValueOnce([block101, block102]); + blockRepository.findByIds.mockResolvedValueOnce([block100, block101, block102]); + + const result = await databaseInteraction.getCommonBlocks([block100.id, block101.id, block102.id]); + + expect(stateStore.getCommonBlocks).toBeCalledWith([block100.id, block101.id, block102.id]); + expect(blockRepository.findByIds).toBeCalledWith([block100.id, block101.id, block102.id]); + expect(result).toEqual([block100, block101, block102]); + }); +}); + +describe("DatabaseInteraction.getRecentBlockIds", () => { + it("should return last 10 block ids", async () => { + const databaseInteraction: DatabaseInteraction = container.resolve(DatabaseInteraction); + + const block101 = { id: "00101", height: 101, transactions: [] }; + const block102 = { id: "00102", height: 102, transactions: [] }; + const block103 = { id: "00103", height: 103, transactions: [] }; + const block104 = { id: "00104", height: 104, transactions: [] }; + const block105 = { id: "00105", height: 105, transactions: [] }; + const block106 = { id: "00106", height: 106, transactions: [] }; + const block107 = { id: "00107", height: 107, transactions: [] }; + const block108 = { id: "00108", height: 108, transactions: [] }; + const block109 = { id: "00109", height: 109, transactions: [] }; + const block110 = { id: "00110", height: 110, transactions: [] }; + + stateStore.getLastBlockIds.mockReturnValueOnce([ + block101, + block102, + block103, + block104, + block105, + block106, + block107, + block108, + block109, + ]); + + blockRepository.findRecent.mockResolvedValueOnce([ + block110, + block109, + block108, + block107, + block106, + block105, + block104, + block103, + block102, + block101, + ]); + + const result = await databaseInteraction.getRecentBlockIds(); + + expect(result).toEqual([ + block110.id, + block109.id, + block108.id, + block107.id, + block106.id, + block105.id, + block104.id, + block103.id, + block102.id, + block101.id, + ]); + }); +}); + +describe("DatabaseInteraction.loadBlocksFromCurrentRound", () => { + it("should initialize blocksInCurrentRound property", async () => { + const databaseInteraction: DatabaseInteraction = container.resolve(DatabaseInteraction); + + const lastBlock = Blocks.BlockFactory.fromData(block1760000, getTimeStampForBlock); + stateStore.getLastBlock.mockReturnValueOnce(lastBlock); + stateStore.getLastBlocksByHeight.mockReturnValueOnce([lastBlock.data]); + blockRepository.findByHeightRangeWithTransactions.mockReturnValueOnce([lastBlock.data]); + + await databaseInteraction.loadBlocksFromCurrentRound(); + + expect(stateStore.getLastBlock).toBeCalled(); + }); +}); + +describe("DatabaseInteraction.revertBlock", () => { + it("should revert state, and fire events", async () => { + const databaseInteraction: DatabaseInteraction = container.resolve(DatabaseInteraction); + + const transaction1 = { data: {} }; + const transaction2 = { data: {} }; + const block = { + data: { id: "123", height: 100 }, + transactions: [transaction1, transaction2], + }; + // @ts-ignore + databaseInteraction.blocksInCurrentRound = [block as any]; + + await databaseInteraction.revertBlock(block as any); + + expect(blockState.revertBlock).toBeCalledWith(block); + expect(events.dispatch).toBeCalledWith("transaction.reverted", transaction1.data); + expect(events.dispatch).toBeCalledWith("transaction.reverted", transaction2.data); + expect(events.dispatch).toBeCalledWith("block.reverted", block.data); + }); +}); + +describe("DatabaseInteraction.revertRound", () => { + it("should revert, and delete round when reverting to previous round", async () => { + const databaseInteraction: DatabaseInteraction = container.resolve(DatabaseInteraction); + + const lastBlock = Blocks.BlockFactory.fromData(block1760000, getTimeStampForBlock); + stateStore.getLastBlock.mockReturnValueOnce(lastBlock); + stateStore.getLastBlocksByHeight.mockReturnValueOnce([lastBlock.data]); + blockRepository.findByHeightRangeWithTransactions.mockReturnValueOnce([lastBlock.data]); + + const prevRoundState = { getAllDelegates: jest.fn(), getRoundDelegates: jest.fn(), revert: jest.fn() }; + getDposPreviousRoundState.mockReturnValueOnce(prevRoundState).mockReturnValueOnce(prevRoundState); + + const prevRoundDelegateWallet = { getAttribute: jest.fn() }; + const prevRoundDposStateAllDelegates = [prevRoundDelegateWallet]; + prevRoundState.getAllDelegates.mockReturnValueOnce(prevRoundDposStateAllDelegates); + + const prevRoundDelegateUsername = "test_delegate"; + prevRoundDelegateWallet.getAttribute.mockReturnValueOnce(prevRoundDelegateUsername); + + const delegateWallet = { setAttribute: jest.fn(), getAttribute: jest.fn() }; + walletRepository.findByUsername.mockReturnValueOnce(delegateWallet); + + const prevRoundDelegateRank = 1; + prevRoundDelegateWallet.getAttribute.mockReturnValueOnce(prevRoundDelegateRank); + + const prevRoundDposStateRoundDelegates = [prevRoundDelegateWallet]; + prevRoundState.getRoundDelegates.mockReturnValueOnce(prevRoundDposStateRoundDelegates); + + const dposStateRoundDelegates = [delegateWallet]; + dposState.getRoundDelegates.mockReturnValueOnce(dposStateRoundDelegates); + dposState.getRoundDelegates.mockReturnValueOnce(dposStateRoundDelegates); + + const forgingDelegates = [delegateWallet]; + triggers.call.mockResolvedValue(forgingDelegates); + + await databaseInteraction.revertRound(51); + + expect(getDposPreviousRoundState).toBeCalled(); + expect(walletRepository.findByUsername).toBeCalledWith(prevRoundDelegateUsername); + expect(delegateWallet.setAttribute).toBeCalledWith("delegate.rank", prevRoundDelegateRank); + // @ts-ignore + expect(databaseInteraction.forgingDelegates).toEqual(forgingDelegates); + expect(roundRepository.delete).toBeCalledWith({ round: 2 }); + }); +}); diff --git a/__tests__/unit/core-state/service-provider.test.ts b/__tests__/unit/core-state/service-provider.test.ts index fcdacd0130..905e075dd1 100644 --- a/__tests__/unit/core-state/service-provider.test.ts +++ b/__tests__/unit/core-state/service-provider.test.ts @@ -2,7 +2,6 @@ import "jest-extended"; import { Application, Container, Services } from "@packages/core-kernel"; import { ServiceProvider } from "@packages/core-state/src"; -import { StateBuilder } from "@packages/core-state/src/state-builder"; let app: Application; @@ -20,15 +19,18 @@ describe("ServiceProvider", () => { serviceProvider = app.resolve(ServiceProvider); }); + afterAll(() => jest.clearAllMocks()); + it("should register", async () => { await expect(serviceProvider.register()).toResolve(); }); - it("should call statebuilder on boot", async () => { - const resolveSpy = jest.spyOn(app, "resolve"); + it("should boot correctly", async () => { + const initializeSpy = jest.fn(); + jest.spyOn(app, "get").mockReturnValue({ initialize: initializeSpy, bind: jest.fn() }); await serviceProvider.register(); expect(async () => await serviceProvider.boot()).not.toThrow(); - expect(resolveSpy).toHaveBeenCalledWith(StateBuilder); + expect(initializeSpy).toHaveBeenCalled(); }); it("should boot when the package is core-database", async () => { diff --git a/packages/core-blockchain/src/blockchain.ts b/packages/core-blockchain/src/blockchain.ts index 957ada600d..a556163eb3 100644 --- a/packages/core-blockchain/src/blockchain.ts +++ b/packages/core-blockchain/src/blockchain.ts @@ -1,5 +1,6 @@ import { DatabaseService, Repositories } from "@arkecosystem/core-database"; import { Container, Contracts, Enums, Services, Utils } from "@arkecosystem/core-kernel"; +import { DatabaseInteraction } from "@arkecosystem/core-state"; import { Blocks, Crypto, Interfaces, Managers, Utils as CryptoUtils } from "@arkecosystem/crypto"; import async from "async"; @@ -15,6 +16,9 @@ export class Blockchain implements Contracts.Blockchain.Blockchain { @Container.inject(Container.Identifiers.StateStore) private readonly state!: Contracts.State.StateStore; + @Container.inject(Container.Identifiers.DatabaseInteraction) + private readonly databaseInteraction!: DatabaseInteraction; + @Container.inject(Container.Identifiers.DatabaseService) private readonly database!: DatabaseService; @@ -282,7 +286,7 @@ export class Blockchain implements Contracts.Blockchain.Blockchain { const revertLastBlock = async () => { const lastBlock: Interfaces.IBlock = this.state.getLastBlock(); - await this.database.revertBlock(lastBlock); + await this.databaseInteraction.revertBlock(lastBlock); removedBlocks.push(lastBlock.data); removedTransactions.push(...[...lastBlock.transactions].reverse()); @@ -359,7 +363,7 @@ export class Blockchain implements Contracts.Blockchain.Blockchain { this.logger.info(`Removing top ${Utils.pluralize("block", count, true)}`); await this.blockRepository.deleteTopBlocks(count); - await this.database.loadBlocksFromCurrentRound(); + await this.databaseInteraction.loadBlocksFromCurrentRound(); } /** @@ -440,14 +444,14 @@ export class Blockchain implements Contracts.Blockchain.Blockchain { ); for (const block of acceptedBlocks.reverse()) { - await this.database.revertBlock(block); + await this.databaseInteraction.revertBlock(block); } this.state.setLastBlock(lastBlock); this.resetLastDownloadedBlock(); await this.database.deleteRound(deleteRoundsAfter + 1); - await this.database.loadBlocksFromCurrentRound(); + await this.databaseInteraction.loadBlocksFromCurrentRound(); return undefined; } diff --git a/packages/core-blockchain/src/processor/block-processor.ts b/packages/core-blockchain/src/processor/block-processor.ts index 5f0e05bdea..a24e3db4a7 100644 --- a/packages/core-blockchain/src/processor/block-processor.ts +++ b/packages/core-blockchain/src/processor/block-processor.ts @@ -168,7 +168,7 @@ export class BlockProcessor { if (nonceBySender[sender] === undefined) { nonceBySender[sender] = this.app - .get(Container.Identifiers.DatabaseService) + .get(Container.Identifiers.DatabaseInteraction) .walletRepository.getNonce(sender); } diff --git a/packages/core-blockchain/src/processor/handlers/accept-block-handler.ts b/packages/core-blockchain/src/processor/handlers/accept-block-handler.ts index de8986cb5a..4ea7a11578 100644 --- a/packages/core-blockchain/src/processor/handlers/accept-block-handler.ts +++ b/packages/core-blockchain/src/processor/handlers/accept-block-handler.ts @@ -1,5 +1,5 @@ -import { DatabaseService } from "@arkecosystem/core-database"; import { Container, Contracts } from "@arkecosystem/core-kernel"; +import { DatabaseInteraction } from "@arkecosystem/core-state"; import { Interfaces } from "@arkecosystem/crypto"; import { BlockProcessorResult } from "../block-processor"; @@ -20,15 +20,15 @@ export class AcceptBlockHandler implements BlockHandler { @Container.inject(Container.Identifiers.StateStore) private readonly state!: Contracts.State.StateStore; - @Container.inject(Container.Identifiers.DatabaseService) - private readonly database!: DatabaseService; + @Container.inject(Container.Identifiers.DatabaseInteraction) + private readonly databaseInteraction!: DatabaseInteraction; @Container.inject(Container.Identifiers.TransactionPoolService) private readonly transactionPool!: Contracts.TransactionPool.Service; public async execute(block: Interfaces.IBlock): Promise { try { - await this.database.applyBlock(block); + await this.databaseInteraction.applyBlock(block); // Check if we recovered from a fork if (this.state.forkedBlock && this.state.forkedBlock.data.height === block.data.height) { diff --git a/packages/core-blockchain/src/state-machine/actions/initialize.ts b/packages/core-blockchain/src/state-machine/actions/initialize.ts index 326468394f..c0ff41c49c 100644 --- a/packages/core-blockchain/src/state-machine/actions/initialize.ts +++ b/packages/core-blockchain/src/state-machine/actions/initialize.ts @@ -1,5 +1,6 @@ import { DatabaseService } from "@arkecosystem/core-database"; import { Container, Contracts, Utils as AppUtils } from "@arkecosystem/core-kernel"; +import { DatabaseInteraction } from "@arkecosystem/core-state"; import { Interfaces, Managers } from "@arkecosystem/crypto"; import { Action } from "../contracts"; @@ -24,6 +25,9 @@ export class Initialize implements Action { @Container.inject(Container.Identifiers.DatabaseService) private readonly databaseService!: DatabaseService; + @Container.inject(Container.Identifiers.DatabaseInteraction) + private readonly databaseInteraction!: DatabaseInteraction; + public async handle(): Promise { try { const block: Interfaces.IBlock = this.stateStore.getLastBlock(); @@ -61,7 +65,7 @@ export class Initialize implements Action { await this.databaseService.deleteRound(roundInfo.round + 1); if (this.stateStore.networkStart) { - await this.databaseService.restoreCurrentRound(block.data.height); + await this.databaseInteraction.restoreCurrentRound(block.data.height); await this.transactionPool.readdTransactions(); await this.app.get(Container.Identifiers.PeerNetworkMonitor).boot(); @@ -83,7 +87,7 @@ export class Initialize implements Action { ******************************* */ // Integrity Verification - await this.databaseService.restoreCurrentRound(block.data.height); + await this.databaseInteraction.restoreCurrentRound(block.data.height); await this.transactionPool.readdTransactions(); await this.app.get(Container.Identifiers.PeerNetworkMonitor).boot(); diff --git a/packages/core-database/src/actions/index.ts b/packages/core-database/src/actions/index.ts deleted file mode 100644 index c44a83086d..0000000000 --- a/packages/core-database/src/actions/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { GetActiveDelegatesAction } from "./get-active-delegates"; diff --git a/packages/core-database/src/database-service.ts b/packages/core-database/src/database-service.ts index 780c084c09..ccab98b75e 100644 --- a/packages/core-database/src/database-service.ts +++ b/packages/core-database/src/database-service.ts @@ -1,9 +1,9 @@ -import { Container, Contracts, Enums, Services, Utils as AppUtils } from "@arkecosystem/core-kernel"; -import { Blocks, Crypto, Identities, Interfaces, Managers, Transactions, Utils } from "@arkecosystem/crypto"; -import assert from "assert"; +import { Container, Contracts, Enums, Utils as AppUtils } from "@arkecosystem/core-kernel"; +import { Blocks, Interfaces, Transactions } from "@arkecosystem/crypto"; import { Connection } from "typeorm"; import { DatabaseEvent } from "./events"; +import { Round } from "./models"; import { BlockRepository } from "./repositories/block-repository"; import { RoundRepository } from "./repositories/round-repository"; import { TransactionRepository } from "./repositories/transaction-repository"; @@ -26,40 +26,6 @@ export class DatabaseService { @Container.inject(Container.Identifiers.DatabaseRoundRepository) private readonly roundRepository!: RoundRepository; - @Container.inject(Container.Identifiers.StateStore) - private readonly stateStore!: Contracts.State.StateStore; - - // TODO: StateBlockStore interface - @Container.inject(Container.Identifiers.StateBlockStore) - private readonly stateBlockStore!: any; - - // TODO: StateTransactionStore interface - @Container.inject(Container.Identifiers.StateTransactionStore) - private readonly stateTransactionStore!: any; - - // TODO: TransactionHandlerRegistry interface - @Container.inject(Container.Identifiers.TransactionHandlerRegistry) - @Container.tagged("state", "blockchain") - private readonly handlerRegistry!: any; - - @Container.inject(Container.Identifiers.WalletRepository) - @Container.tagged("state", "blockchain") - private readonly walletRepository!: Contracts.State.WalletRepository; - - @Container.inject(Container.Identifiers.BlockState) - @Container.tagged("state", "blockchain") - private readonly blockState!: Contracts.State.BlockState; - - @Container.inject(Container.Identifiers.DposState) - @Container.tagged("state", "blockchain") - private readonly dposState!: Contracts.State.DposState; - - @Container.inject(Container.Identifiers.DposPreviousRoundStateProvider) - private readonly getDposPreviousRoundState!: Contracts.State.DposPreviousRoundStateProvider; - - @Container.inject(Container.Identifiers.TriggerService) - private readonly triggers!: Services.Triggers.Triggers; - @Container.inject(Container.Identifiers.LogService) private readonly logger!: Contracts.Kernel.Logger; @@ -69,28 +35,11 @@ export class DatabaseService { // TODO: make private readonly public restoredDatabaseIntegrity: boolean = false; - private blocksInCurrentRound: Interfaces.IBlock[] = []; - - private forgingDelegates: Contracts.State.Wallet[] = []; - public async initialize(): Promise { try { - this.events.dispatch(Enums.StateEvent.Starting); - - const genesisBlockJson = Managers.configManager.get("genesisBlock"); - const blockTimeLookup = await AppUtils.forgingInfoCalculator.getBlockTimeLookup( - this.app, - genesisBlockJson.height, - ); - const genesisBlock = Blocks.BlockFactory.fromJson(genesisBlockJson, blockTimeLookup); - this.stateStore.setGenesisBlock(genesisBlock!); - if (process.env.CORE_RESET_DATABASE) { await this.reset(); } - - await this.initializeLastBlock(); - await this.loadBlocksFromCurrentRound(); } catch (error) { this.logger.error(error.stack); this.app.terminate("Failed to initialize database service.", error); @@ -108,133 +57,8 @@ export class DatabaseService { this.logger.debug("Disconnected from database"); } - public async restoreCurrentRound(height: number): Promise { - await this.initializeActiveDelegates(height); - await this.applyRound(height); - } - public async reset(): Promise { await this.connection.query("TRUNCATE TABLE blocks, rounds, transactions RESTART IDENTITY;"); - await this.createGenesisBlock(); - } - - // TODO: move out of core-database to get rid of BlockState dependency - public async applyBlock(block: Interfaces.IBlock): Promise { - await this.blockState.applyBlock(block); - - this.blocksInCurrentRound.push(block); - - await this.detectMissedBlocks(block); - - await this.applyRound(block.data.height); - - for (const transaction of block.transactions) { - await this.emitTransactionEvents(transaction); - } - - this.events.dispatch(Enums.BlockEvent.Applied, block.data); - } - - // TODO: move out of core-database to get rid of WalletState dependency - public async applyRound(height: number): Promise { - // ! this doesn't make sense - // ! next condition should be modified to include height === 1 - const nextHeight: number = height === 1 ? 1 : height + 1; - - if (AppUtils.roundCalculator.isNewRound(nextHeight)) { - const roundInfo: Contracts.Shared.RoundInfo = AppUtils.roundCalculator.calculateRound(nextHeight); - const { round } = roundInfo; - - if ( - nextHeight === 1 || - this.forgingDelegates.length === 0 || - this.forgingDelegates[0].getAttribute("delegate.round") !== round - ) { - this.logger.info(`Starting Round ${roundInfo.round.toLocaleString()}`); - - try { - if (nextHeight > 1) { - this.detectMissedRound(this.forgingDelegates); - } - - this.dposState.buildDelegateRanking(); - this.dposState.setDelegatesRound(roundInfo); - - await this.setForgingDelegatesOfRound(roundInfo, this.dposState.getRoundDelegates().slice()); - await this.saveRound(this.dposState.getRoundDelegates()); - - this.blocksInCurrentRound = []; - - this.events.dispatch(Enums.RoundEvent.Applied); - } catch (error) { - // trying to leave database state has it was - // ! this.saveRound may not have been called - // ! try should be moved below await this.setForgingDelegatesOfRound - await this.deleteRound(round); - - throw error; - } - } else { - // ! then applyRound should not be called at all - this.logger.warning( - `Round ${round.toLocaleString()} has already been applied. This should happen only if you are a forger.`, - ); - } - } - } - - public async getActiveDelegates( - roundInfo?: Contracts.Shared.RoundInfo, - delegates?: Contracts.State.Wallet[], - ): Promise { - if (!roundInfo) { - // ! use this.stateStore.getLastBlock() - const lastBlock = await this.getLastBlock(); - roundInfo = AppUtils.roundCalculator.calculateRound(lastBlock.data.height); - } - - const { round } = roundInfo; - - if (this.forgingDelegates.length && this.forgingDelegates[0].getAttribute("delegate.round") === round) { - return this.forgingDelegates; - } - - // When called during applyRound we already know the delegates, so we don't have to query the database. - if (!delegates) { - delegates = (await this.roundRepository.getRound(round)).map(({ publicKey, balance }) => { - // ! find wallet by public key and clone it - const wallet = this.walletRepository.createWallet(Identities.Address.fromPublicKey(publicKey)); - wallet.publicKey = publicKey; - wallet.setAttribute("delegate", { - voteBalance: Utils.BigNumber.make(balance), - username: this.walletRepository.findByPublicKey(publicKey).getAttribute("delegate.username", ""), // ! default username? - }); - return wallet; - }); - } - - for (const delegate of delegates) { - // ! throw if delegate round doesn't match instead of altering argument - delegate.setAttribute("delegate.round", round); - } - - // ! extracting code below can simplify many call stacks and tests - - const seedSource: string = round.toString(); - let currentSeed: Buffer = Crypto.HashAlgorithms.sha256(seedSource); - - delegates = delegates.map((delegate) => delegate.clone()); - for (let i = 0, delCount = delegates.length; i < delCount; i++) { - for (let x = 0; x < 4 && i < delCount; i++, x++) { - const newIndex = currentSeed[x] % delCount; - const b = delegates[newIndex]; - delegates[newIndex] = delegates[i]; - delegates[i] = b; - } - currentSeed = Crypto.HashAlgorithms.sha256(currentSeed); - } - - return delegates; } public async getBlock(id: string): Promise { @@ -263,29 +87,12 @@ export class DatabaseService { // ! three methods below (getBlocks, getBlocksForDownload, getBlocksByHeight) can be merged into one - public async getBlocks(offset: number, limit: number, headersOnly?: boolean): Promise { - // The functions below return matches in the range [start, end], including both ends. - const start: number = offset; - let end: number = offset + limit - 1; - - let blocks: Interfaces.IBlockData[] = this.stateStore.getLastBlocksByHeight(start, end, headersOnly); - - if (blocks.length !== limit) { - // ! assumes that earlier blocks may be missing - // ! but querying database is unnecessary when later blocks are missing too (aren't forged yet) - - if (blocks.length) { - end = blocks[0].height - 1; - } - - const blocksFromDB = headersOnly - ? await this.blockRepository.findByHeightRange(start, end) - : await this.blockRepository.findByHeightRangeWithTransactions(start, end); - - blocks = [...blocksFromDB, ...blocks]; - } - - return blocks; + public async getBlocks(start: number, end: number, headersOnly?: boolean): Promise { + // ! assumes that earlier blocks may be missing + // ! but querying database is unnecessary when later blocks are missing too (aren't forged yet) + return headersOnly + ? await this.blockRepository.findByHeightRange(start, end) + : await this.blockRepository.findByHeightRangeWithTransactions(start, end); } // TODO: move to block repository @@ -309,93 +116,8 @@ export class DatabaseService { ) as unknown) as Promise; } - /** - * Get the blocks at the given heights. - * The transactions for those blocks will not be loaded like in `getBlocks()`. - * @param {Array} heights array of arbitrary block heights - * @return {Array} array for the corresponding blocks. The element (block) at index `i` - * in the resulting array corresponds to the requested height at index `i` in the input - * array heights[]. For example, if - * heights[0] = 100 - * heights[1] = 200 - * heights[2] = 150 - * then the result array will have the same number of elements (3) and will be: - * result[0] = block at height 100 - * result[1] = block at height 200 - * result[2] = block at height 150 - * If some of the requested blocks do not exist in our chain (requested height is larger than - * the height of our blockchain), then that element will be `undefined` in the resulting array - * @throws Error - */ - public async getBlocksByHeight(heights: number[]) { - // TODO: add type - const blocks: Interfaces.IBlockData[] = []; - - // Map of height -> index in heights[], e.g. if - // heights[5] == 6000000, then - // toGetFromDB[6000000] == 5 - // In this map we only store a subset of the heights - the ones we could not retrieve - // from app/state and need to get from the database. - const toGetFromDB = {}; - - for (const [i, height] of heights.entries()) { - const stateBlocks = this.stateStore.getLastBlocksByHeight(height, height, true); - - if (Array.isArray(stateBlocks) && stateBlocks.length > 0) { - blocks[i] = stateBlocks[0]; - } - - if (blocks[i] === undefined) { - toGetFromDB[height] = i; - } - } - - const heightsToGetFromDB: number[] = Object.keys(toGetFromDB).map((height) => +height); - if (heightsToGetFromDB.length > 0) { - const blocksByHeights = await this.blockRepository.findByHeights(heightsToGetFromDB); - - for (const blockFromDB of blocksByHeights) { - const index = toGetFromDB[blockFromDB.height]; - blocks[index] = blockFromDB; - } - } - - return blocks; - } - - public async getBlocksForRound(roundInfo?: Contracts.Shared.RoundInfo): Promise { - // ! it should check roundInfo before assuming that lastBlock is what's have to be returned - - let lastBlock: Interfaces.IBlock = this.stateStore.getLastBlock(); - - if (!lastBlock) { - lastBlock = await this.getLastBlock(); - } - - if (!lastBlock) { - return []; - } else if (lastBlock.data.height === 1) { - return [lastBlock]; - } - - if (!roundInfo) { - roundInfo = AppUtils.roundCalculator.calculateRound(lastBlock.data.height); - } - - // ? number of blocks in round may not equal roundInfo.maxDelegates - // ? see round-calculator.ts handling milestone change - const blocks = await this.getBlocks(roundInfo.roundHeight, roundInfo.maxDelegates); - - const builtBlockPromises: Promise[] = blocks.map(async (block: Interfaces.IBlockData) => { - if (block.height === 1) { - return this.stateStore.getGenesisBlock(); - } - - const blockTimeLookup = await AppUtils.forgingInfoCalculator.getBlockTimeLookup(this.app, block.height); - return Blocks.BlockFactory.fromData(block, blockTimeLookup, { deserializeTransactionsUnchecked: true })!; - }); - - return Promise.all(builtBlockPromises); + public async findBlockByHeights(heights: number[]) { + return await this.blockRepository.findByHeights(heights); } public async getLastBlock(): Promise { @@ -424,31 +146,6 @@ export class DatabaseService { return lastBlock; } - public async getCommonBlocks(ids: string[]): Promise { - let commonBlocks: Interfaces.IBlockData[] = this.stateStore.getCommonBlocks(ids); - - if (commonBlocks.length < ids.length) { - // ! do not query blocks that were found - // ! why method is called commonBlocks, but is just findByIds? - commonBlocks = ((await this.blockRepository.findByIds(ids)) as unknown) as Interfaces.IBlockData[]; - } - - return commonBlocks; - } - - public async getRecentBlockIds(): Promise { - // ! why getLastBlockIds returns blocks and not ids? - let blocks: any[] = this.stateStore.getLastBlockIds().reverse().slice(0, 10); - - if (blocks.length < 10) { - // ! blockRepository.findRecent returns objects containing single id property in reverse order - // ! where recent block id is first in array - blocks = await this.blockRepository.findRecent(10); - } - - return blocks.map((block) => block.id); - } - public async getTopBlocks(count: number): Promise { // ! blockRepository.findTop returns blocks in reverse order // ! where recent block is first in array @@ -465,43 +162,28 @@ export class DatabaseService { return this.transactionRepository.findOne(id); } - public async loadBlocksFromCurrentRound(): Promise { - // ! this should not be public, this.blocksInCurrentRound is used by DatabaseService only - this.blocksInCurrentRound = await this.getBlocksForRound(); + public async deleteBlocks(blocks: Interfaces.IBlockData[]): Promise { + return await this.blockRepository.deleteBlocks(blocks); } - public async revertBlock(block: Interfaces.IBlock): Promise { - await this.revertRound(block.data.height); - await this.blockState.revertBlock(block); - - // ! blockState is already reverted if this check fails - assert(this.blocksInCurrentRound.pop()!.data.id === block.data.id); - - for (let i = block.transactions.length - 1; i >= 0; i--) { - this.events.dispatch(Enums.TransactionEvent.Reverted, block.transactions[i].data); - } - - this.events.dispatch(Enums.BlockEvent.Reverted, block.data); + public async saveBlocks(blocks: Interfaces.IBlock[]): Promise { + return await this.blockRepository.saveBlocks(blocks); } - public async revertRound(height: number): Promise { - const roundInfo: Contracts.Shared.RoundInfo = AppUtils.roundCalculator.calculateRound(height); - const { round, nextRound, maxDelegates } = roundInfo; - - // ! height >= maxDelegates is always true - if (nextRound === round + 1 && height >= maxDelegates) { - this.logger.info(`Back to previous round: ${round.toLocaleString()}`); + public async findLatestBlock(): Promise { + return await this.blockRepository.findLatest(); + } - this.blocksInCurrentRound = await this.getBlocksForRound(roundInfo); + public async findBlockByID(ids: any[]): Promise { + return ((await this.blockRepository.findByIds(ids)) as unknown) as Interfaces.IBlockData[]; + } - await this.setForgingDelegatesOfRound( - roundInfo, - await this.calcPreviousActiveDelegates(roundInfo, this.blocksInCurrentRound), - ); + public async findRecentBlocks(limit: number): Promise<{ id: string }[]> { + return await this.blockRepository.findRecent(limit); + } - // ! this will only delete one round - await this.deleteRound(nextRound); - } + public async getRound(round: number): Promise { + return await this.roundRepository.getRound(round); } public async saveRound(activeDelegates: readonly Contracts.State.Wallet[]): Promise { @@ -516,22 +198,22 @@ export class DatabaseService { await this.roundRepository.delete({ round }); } - public async verifyBlockchain(): Promise { + public async verifyBlockchain(lastBlock?: Interfaces.IBlock): Promise { const errors: string[] = []; - const lastBlock: Interfaces.IBlock = this.stateStore.getLastBlock(); + const block: Interfaces.IBlock = lastBlock ? lastBlock : await this.getLastBlock(); // Last block is available - if (!lastBlock) { + if (!block) { errors.push("Last block is not available"); } else { // ! can be checked using blockStats.count instead const numberOfBlocks: number = await this.blockRepository.count(); // Last block height equals the number of stored blocks - if (lastBlock.data.height !== +numberOfBlocks) { + if (block.data.height !== +numberOfBlocks) { errors.push( - `Last block height: ${lastBlock.data.height.toLocaleString()}, number of stored blocks: ${numberOfBlocks}`, + `Last block height: ${block.data.height.toLocaleString()}, number of stored blocks: ${numberOfBlocks}`, ); } } @@ -580,81 +262,6 @@ export class DatabaseService { return !hasErrors; } - private async detectMissedBlocks(block: Interfaces.IBlock) { - const lastBlock: Interfaces.IBlock = this.stateStore.getLastBlock(); - - if (lastBlock.data.height === 1) { - return; - } - - const blockTimeLookup = await AppUtils.forgingInfoCalculator.getBlockTimeLookup( - this.app, - lastBlock.data.height, - ); - - const lastSlot: number = Crypto.Slots.getSlotNumber(blockTimeLookup, lastBlock.data.timestamp); - const currentSlot: number = Crypto.Slots.getSlotNumber(blockTimeLookup, block.data.timestamp); - - const missedSlots: number = Math.min(currentSlot - lastSlot - 1, this.forgingDelegates.length); - for (let i = 0; i < missedSlots; i++) { - const missedSlot: number = lastSlot + i + 1; - const delegate: Contracts.State.Wallet = this.forgingDelegates[missedSlot % this.forgingDelegates.length]; - - this.logger.debug( - `Delegate ${delegate.getAttribute("delegate.username")} (${delegate.publicKey}) just missed a block.`, - ); - - this.events.dispatch(Enums.ForgerEvent.Missing, { - delegate, - }); - } - } - - private async initializeLastBlock(): Promise { - // ? attempt to remove potentially corrupt blocks from database - - let lastBlock: Interfaces.IBlock | undefined; - let tries = 5; // ! actually 6, but only 5 will be removed - - // Ensure the config manager is initialized, before attempting to call `fromData` - // which otherwise uses potentially wrong milestones. - let lastHeight: number = 1; - const latest: Interfaces.IBlockData | undefined = await this.blockRepository.findLatest(); - if (latest) { - lastHeight = latest.height; - } - - Managers.configManager.setHeight(lastHeight); - - const getLastBlock = async (): Promise => { - try { - return await this.getLastBlock(); - } catch (error) { - this.logger.error(error.message); - - if (tries > 0) { - const block: Interfaces.IBlockData = (await this.blockRepository.findLatest())!; - await this.blockRepository.deleteBlocks([block]); - tries--; - } else { - this.app.terminate("Unable to deserialize last block from database.", error); - throw new Error("Terminated (unreachable)"); - } - - return getLastBlock(); - } - }; - - lastBlock = await getLastBlock(); - - if (!lastBlock) { - this.logger.warning("No block found in database"); - lastBlock = await this.createGenesisBlock(); - } - - this.configureState(lastBlock); - } - private async loadTransactionsForBlocks(blocks: Interfaces.IBlockData[]): Promise { const dbTransactions: Array<{ id: string; @@ -691,95 +298,4 @@ export class DatabaseService { const ids: string[] = blocks.map((block: Interfaces.IBlockData) => block.id!); return this.transactionRepository.findByBlockIds(ids); } - - private async createGenesisBlock(): Promise { - const genesisBlock = this.stateStore.getGenesisBlock(); - await this.blockRepository.saveBlocks([genesisBlock]); - return genesisBlock; - } - - private configureState(lastBlock: Interfaces.IBlock): void { - this.stateStore.setLastBlock(lastBlock); - const { blocktime, block } = Managers.configManager.getMilestone(); - const blocksPerDay: number = Math.ceil(86400 / blocktime); - this.stateBlockStore.resize(blocksPerDay); - this.stateTransactionStore.resize(blocksPerDay * block.maxTransactions); - } - - private detectMissedRound(delegates: Contracts.State.Wallet[]): void { - if (!delegates || !this.blocksInCurrentRound.length) { - // ! this.blocksInCurrentRound is impossible - // ! otherwise this.blocksInCurrentRound!.length = 0 in applyRound will throw - return; - } - - if (this.blocksInCurrentRound.length === 1 && this.blocksInCurrentRound[0].data.height === 1) { - // ? why skip missed round checks when first round has genesis block only? - return; - } - - for (const delegate of delegates) { - // ! use .some() instead of .fitler() - const producedBlocks: Interfaces.IBlock[] = this.blocksInCurrentRound.filter( - (blockGenerator) => blockGenerator.data.generatorPublicKey === delegate.publicKey, - ); - - if (producedBlocks.length === 0) { - const wallet: Contracts.State.Wallet = this.walletRepository.findByPublicKey(delegate.publicKey!); - - this.logger.debug( - `Delegate ${wallet.getAttribute("delegate.username")} (${wallet.publicKey}) just missed a round.`, - ); - - this.events.dispatch(Enums.RoundEvent.Missed, { - delegate: wallet, - }); - } - } - } - - private async initializeActiveDelegates(height: number): Promise { - // ! may be set to undefined to early if error is raised - this.forgingDelegates = []; - - const roundInfo: Contracts.Shared.RoundInfo = AppUtils.roundCalculator.calculateRound(height); - await this.setForgingDelegatesOfRound(roundInfo); - } - - private async setForgingDelegatesOfRound( - roundInfo: Contracts.Shared.RoundInfo, - delegates?: Contracts.State.Wallet[], - ): Promise { - // ! it's this.getActiveDelegates(roundInfo, delegates); - // ! only last part of that function which reshuffles delegates is used - const result = await this.triggers.call("getActiveDelegates", { roundInfo, delegates }); - this.forgingDelegates = (result as Contracts.State.Wallet[]) || []; - } - - private async calcPreviousActiveDelegates( - roundInfo: Contracts.Shared.RoundInfo, - blocks?: Interfaces.IBlock[], - ): Promise { - // ! make blocks required parameter forcing caller to specify blocks explicitly - blocks = blocks || (await this.getBlocksForRound(roundInfo)); - - const prevRoundState = await this.getDposPreviousRoundState(blocks, roundInfo); - for (const prevRoundDelegateWallet of prevRoundState.getAllDelegates()) { - // ! name suggest that this is pure function - // ! when in fact it is manipulating current wallet repository setting delegate ranks - const username = prevRoundDelegateWallet.getAttribute("delegate.username"); - const delegateWallet = this.walletRepository.findByUsername(username); - delegateWallet.setAttribute("delegate.rank", prevRoundDelegateWallet.getAttribute("delegate.rank")); - } - - // ! return readonly array instead of taking slice - return prevRoundState.getRoundDelegates().slice(); - } - - private async emitTransactionEvents(transaction: Interfaces.ITransaction): Promise { - this.events.dispatch(Enums.TransactionEvent.Applied, transaction.data); - const handler = await this.handlerRegistry.getActivatedHandlerForData(transaction.data); - // ! no reason to pass this.emitter - handler.emitEvents(transaction, this.events); - } } diff --git a/packages/core-database/src/service-provider.ts b/packages/core-database/src/service-provider.ts index b1ddeaccfc..87b3e79c00 100644 --- a/packages/core-database/src/service-provider.ts +++ b/packages/core-database/src/service-provider.ts @@ -1,7 +1,6 @@ -import { Container, Contracts, Providers, Services } from "@arkecosystem/core-kernel"; +import { Container, Contracts, Providers } from "@arkecosystem/core-kernel"; import { Connection, createConnection, getCustomRepository } from "typeorm"; -import { GetActiveDelegatesAction } from "./actions"; import { BlockFilter } from "./block-filter"; import { BlockHistoryService } from "./block-history-service"; import { DatabaseService } from "./database-service"; @@ -36,8 +35,6 @@ export class ServiceProvider extends Providers.ServiceProvider { this.app.bind(Container.Identifiers.DatabaseModelConverter).to(ModelConverter); this.app.bind(Container.Identifiers.DatabaseService).to(DatabaseService).inSingletonScope(); - - this.registerActions(); } public async boot(): Promise { @@ -84,10 +81,4 @@ export class ServiceProvider extends Providers.ServiceProvider { public getTransactionRepository(): TransactionRepository { return getCustomRepository(TransactionRepository); } - - private registerActions(): void { - this.app - .get(Container.Identifiers.TriggerService) - .bind("getActiveDelegates", new GetActiveDelegatesAction(this.app)); - } } diff --git a/packages/core-kernel/src/ioc/identifiers.ts b/packages/core-kernel/src/ioc/identifiers.ts index 2ba54b53a8..92b7436434 100644 --- a/packages/core-kernel/src/ioc/identifiers.ts +++ b/packages/core-kernel/src/ioc/identifiers.ts @@ -59,6 +59,7 @@ export const Identifiers = { DatabaseTransactionRepository: Symbol.for("Database"), DatabaseTransactionFilter: Symbol.for("Database"), DatabaseModelConverter: Symbol.for("Database"), + DatabaseInteraction: Symbol.for("Database"), // Kernel ConfigRepository: Symbol.for("Repository"), diff --git a/packages/core-kernel/src/utils/get-blocktime-lookup.ts b/packages/core-kernel/src/utils/get-blocktime-lookup.ts index 00c04bdc3d..9d908bc432 100644 --- a/packages/core-kernel/src/utils/get-blocktime-lookup.ts +++ b/packages/core-kernel/src/utils/get-blocktime-lookup.ts @@ -38,7 +38,7 @@ export const getBlockTimeLookup = async (app: Application, height: number): Prom const databaseService = app.get(Identifiers.DatabaseService); const getBlockTimestampByHeight = async (height: number): Promise => { - const blocks = await databaseService.getBlocksByHeight([height]); + const blocks = await databaseService.findBlockByHeights([height]); return blocks[0].timestamp; }; diff --git a/packages/core-p2p/src/peer-verifier.ts b/packages/core-p2p/src/peer-verifier.ts index 27663d787d..c00028ab29 100644 --- a/packages/core-p2p/src/peer-verifier.ts +++ b/packages/core-p2p/src/peer-verifier.ts @@ -1,5 +1,5 @@ -import { DatabaseService } from "@arkecosystem/core-database"; import { Container, Contracts, Services, Utils } from "@arkecosystem/core-kernel"; +import { DatabaseInteraction } from "@arkecosystem/core-state"; import { Blocks, Interfaces } from "@arkecosystem/crypto"; import assert from "assert"; import pluralize from "pluralize"; @@ -38,7 +38,7 @@ export class PeerVerifier implements Contracts.P2P.PeerVerifier { private readonly logger!: Contracts.Kernel.Logger; // todo: make use of ioc - private database!: DatabaseService; + private databaseInteraction!: DatabaseInteraction; private logPrefix!: string; private communicator!: Contracts.P2P.PeerCommunicator; @@ -55,7 +55,7 @@ export class PeerVerifier implements Contracts.P2P.PeerVerifier { public initialize(communicator: Contracts.P2P.PeerCommunicator, peer: Contracts.P2P.Peer) { this.communicator = communicator; this.peer = peer; - this.database = this.app.get(Container.Identifiers.DatabaseService); + this.databaseInteraction = this.app.get(Container.Identifiers.DatabaseInteraction); this.logPrefix = `Peer verify ${peer.ip}:`; @@ -218,12 +218,12 @@ export class PeerVerifier implements Contracts.P2P.PeerVerifier { return false; } - const blocks = await this.database.getBlocksByHeight([claimedHeight]); + const blocks = await this.databaseInteraction.getBlocksByHeight([claimedHeight]); assert.strictEqual( blocks.length, 1, - `database.getBlocksByHeight([ ${claimedHeight} ]) returned ${blocks.length} results: ` + + `databaseInteraction.getBlocksByHeight([ ${claimedHeight} ]) returned ${blocks.length} results: ` + this.anyToString(blocks) + ` (our chain is at height ${ourHeight})`, ); @@ -284,7 +284,7 @@ export class PeerVerifier implements Contracts.P2P.PeerVerifier { const nAry = 8; const probe = async (heightsToProbe: number[]): Promise => { - const ourBlocks = await this.database.getBlocksByHeight(heightsToProbe); + const ourBlocks = await this.databaseInteraction.getBlocksByHeight(heightsToProbe); assert.strictEqual(ourBlocks.length, heightsToProbe.length); diff --git a/packages/core-p2p/src/socket-server/controllers/internal.ts b/packages/core-p2p/src/socket-server/controllers/internal.ts index 4aff78dc6a..21cfc4052b 100644 --- a/packages/core-p2p/src/socket-server/controllers/internal.ts +++ b/packages/core-p2p/src/socket-server/controllers/internal.ts @@ -1,5 +1,5 @@ -import { DatabaseService } from "@arkecosystem/core-database"; import { Container, Contracts, Utils } from "@arkecosystem/core-kernel"; +import { DatabaseInteraction } from "@arkecosystem/core-state"; import { Crypto, Interfaces, Managers } from "@arkecosystem/crypto"; import Hapi from "@hapi/hapi"; @@ -12,8 +12,8 @@ export class InternalController extends Controller { @Container.inject(Container.Identifiers.PeerNetworkMonitor) private readonly peerNetworkMonitor!: Contracts.P2P.NetworkMonitor; - @Container.inject(Container.Identifiers.DatabaseService) - private readonly database!: DatabaseService; + @Container.inject(Container.Identifiers.DatabaseInteraction) + private readonly databaseInteraction!: DatabaseInteraction; @Container.inject(Container.Identifiers.EventDispatcherService) private readonly events!: Contracts.Kernel.EventDispatcher; @@ -53,12 +53,12 @@ export class InternalController extends Controller { const roundInfo = Utils.roundCalculator.calculateRound(height); const reward = Managers.configManager.getMilestone(height).reward; - const delegates: Contracts.P2P.DelegateWallet[] = (await this.database.getActiveDelegates(roundInfo)).map( - (wallet) => ({ - ...wallet, - delegate: wallet.getAttribute("delegate"), - }), - ); + const delegates: Contracts.P2P.DelegateWallet[] = ( + await this.databaseInteraction.getActiveDelegates(roundInfo) + ).map((wallet) => ({ + ...wallet, + delegate: wallet.getAttribute("delegate"), + })); const blockTimeLookup = await Utils.forgingInfoCalculator.getBlockTimeLookup(this.app, height); diff --git a/packages/core-p2p/src/socket-server/controllers/peer.ts b/packages/core-p2p/src/socket-server/controllers/peer.ts index c3eba3d28c..659c7c3952 100644 --- a/packages/core-p2p/src/socket-server/controllers/peer.ts +++ b/packages/core-p2p/src/socket-server/controllers/peer.ts @@ -1,5 +1,5 @@ -import { DatabaseService } from "@arkecosystem/core-database"; import { Container, Contracts, Utils } from "@arkecosystem/core-kernel"; +import { DatabaseInteraction } from "@arkecosystem/core-state"; import { Crypto, Interfaces } from "@arkecosystem/crypto"; import Hapi from "@hapi/hapi"; @@ -11,13 +11,13 @@ export class PeerController extends Controller { @Container.inject(Container.Identifiers.PeerStorage) private readonly peerStorage!: Contracts.P2P.PeerStorage; - @Container.inject(Container.Identifiers.DatabaseService) - private readonly database!: DatabaseService; + @Container.inject(Container.Identifiers.DatabaseInteraction) + private readonly databaseInteraction!: DatabaseInteraction; public getPeers(request: Hapi.Request, h: Hapi.ResponseToolkit): Contracts.P2P.PeerBroadcast[] { return this.peerStorage .getPeers() - .filter((peer) => peer.port !== -1 ) + .filter((peer) => peer.port !== -1) .map((peer) => peer.toBroadcast()) .sort((a, b) => { Utils.assert.defined(a.latency); @@ -34,7 +34,9 @@ export class PeerController extends Controller { common: Interfaces.IBlockData; lastBlockHeight: number; }> { - const commonBlocks: Interfaces.IBlockData[] = await this.database.getCommonBlocks((request.payload as any).ids); + const commonBlocks: Interfaces.IBlockData[] = await this.databaseInteraction.getCommonBlocks( + (request.payload as any).ids, + ); if (!commonBlocks.length) { throw new MissingCommonBlockError(); diff --git a/packages/core-database/src/actions/get-active-delegates.ts b/packages/core-state/src/actions/get-active-delegates.ts similarity index 66% rename from packages/core-database/src/actions/get-active-delegates.ts rename to packages/core-state/src/actions/get-active-delegates.ts index d809d7c3b7..54e66ca2aa 100644 --- a/packages/core-database/src/actions/get-active-delegates.ts +++ b/packages/core-state/src/actions/get-active-delegates.ts @@ -1,7 +1,7 @@ import { Container, Contracts, Services } from "@arkecosystem/core-kernel"; import { ActionArguments } from "@arkecosystem/core-kernel/src/types"; -import { DatabaseService } from "../database-service"; +import { DatabaseInteraction } from "../database-interactions"; export class GetActiveDelegatesAction extends Services.Triggers.Action { private app: Contracts.Kernel.Application; @@ -15,8 +15,10 @@ export class GetActiveDelegatesAction extends Services.Triggers.Action { const roundInfo: Contracts.Shared.RoundInfo = args.roundInfo; const delegates: Contracts.State.Wallet[] = args.delegates; - const database: DatabaseService = this.app.get(Container.Identifiers.DatabaseService); + const databaseInteractions: DatabaseInteraction = this.app.get( + Container.Identifiers.DatabaseInteraction, + ); - return database.getActiveDelegates(roundInfo, delegates); + return databaseInteractions.getActiveDelegates(roundInfo, delegates); } } diff --git a/packages/core-state/src/actions/index.ts b/packages/core-state/src/actions/index.ts index 047b0cb56c..2e188a6db4 100644 --- a/packages/core-state/src/actions/index.ts +++ b/packages/core-state/src/actions/index.ts @@ -1 +1,2 @@ export { BuildDelegateRankingAction } from "./build-delegate-ranking"; +export { GetActiveDelegatesAction } from "./get-active-delegates"; diff --git a/packages/core-state/src/database-interactions.ts b/packages/core-state/src/database-interactions.ts new file mode 100644 index 0000000000..93937d3882 --- /dev/null +++ b/packages/core-state/src/database-interactions.ts @@ -0,0 +1,549 @@ +import { DatabaseService } from "@arkecosystem/core-database"; +import { Container, Contracts, Enums, Services, Utils as AppUtils } from "@arkecosystem/core-kernel"; +import { Blocks, Crypto, Identities, Interfaces, Managers, Utils } from "@arkecosystem/crypto"; +import assert from "assert"; + +@Container.injectable() +export class DatabaseInteraction { + @Container.inject(Container.Identifiers.Application) + private readonly app!: Contracts.Kernel.Application; + + @Container.inject(Container.Identifiers.DatabaseService) + private readonly databaseService!: DatabaseService; + + @Container.inject(Container.Identifiers.BlockState) + @Container.tagged("state", "blockchain") + private readonly blockState!: Contracts.State.BlockState; + + @Container.inject(Container.Identifiers.DposState) + @Container.tagged("state", "blockchain") + private readonly dposState!: Contracts.State.DposState; + + @Container.inject(Container.Identifiers.DposPreviousRoundStateProvider) + private readonly getDposPreviousRoundState!: Contracts.State.DposPreviousRoundStateProvider; + + @Container.inject(Container.Identifiers.StateStore) + private readonly stateStore!: Contracts.State.StateStore; + + // TODO: StateTransactionStore interface + @Container.inject(Container.Identifiers.StateTransactionStore) + private readonly stateTransactionStore!: any; + + // TODO: StateBlockStore interface + @Container.inject(Container.Identifiers.StateBlockStore) + private readonly stateBlockStore!: any; + + // TODO: TransactionHandlerRegistry interface + @Container.inject(Container.Identifiers.TransactionHandlerRegistry) + @Container.tagged("state", "blockchain") + private readonly handlerRegistry!: any; + + // core-state + @Container.inject(Container.Identifiers.WalletRepository) + @Container.tagged("state", "blockchain") + private readonly walletRepository!: Contracts.State.WalletRepository; + + @Container.inject(Container.Identifiers.TriggerService) + private readonly triggers!: Services.Triggers.Triggers; + + @Container.inject(Container.Identifiers.EventDispatcherService) + private readonly events!: Contracts.Kernel.EventDispatcher; + + @Container.inject(Container.Identifiers.LogService) + private readonly logger!: Contracts.Kernel.Logger; + + private blocksInCurrentRound: Interfaces.IBlock[] = []; + + private forgingDelegates: Contracts.State.Wallet[] = []; + + public async initialize(): Promise { + try { + this.events.dispatch(Enums.StateEvent.Starting); + + const genesisBlockJson = Managers.configManager.get("genesisBlock"); + const blockTimeLookup = await AppUtils.forgingInfoCalculator.getBlockTimeLookup( + this.app, + genesisBlockJson.height, + ); + const genesisBlock = Blocks.BlockFactory.fromJson(genesisBlockJson, blockTimeLookup); + this.stateStore.setGenesisBlock(genesisBlock!); + + if (process.env.CORE_RESET_DATABASE) { + await this.reset(); + } + + await this.initializeLastBlock(); + await this.loadBlocksFromCurrentRound(); + } catch (error) { + this.logger.error(error.stack); + this.app.terminate("Failed to initialize database service.", error); + } + } + + public async reset(): Promise { + await this.databaseService.reset(); + await this.createGenesisBlock(); + } + + public async restoreCurrentRound(height: number): Promise { + await this.initializeActiveDelegates(height); + await this.applyRound(height); + } + + public async applyBlock(block: Interfaces.IBlock): Promise { + await this.blockState.applyBlock(block); + + this.blocksInCurrentRound.push(block); + + await this.detectMissedBlocks(block); + + await this.applyRound(block.data.height); + + for (const transaction of block.transactions) { + await this.emitTransactionEvents(transaction); + } + + this.events.dispatch(Enums.BlockEvent.Applied, block.data); + } + + public async applyRound(height: number): Promise { + // ! this doesn't make sense + // ! next condition should be modified to include height === 1 + const nextHeight: number = height === 1 ? 1 : height + 1; + + if (AppUtils.roundCalculator.isNewRound(nextHeight)) { + const roundInfo: Contracts.Shared.RoundInfo = AppUtils.roundCalculator.calculateRound(nextHeight); + const { round } = roundInfo; + + if ( + nextHeight === 1 || + this.forgingDelegates.length === 0 || + this.forgingDelegates[0].getAttribute("delegate.round") !== round + ) { + this.logger.info(`Starting Round ${roundInfo.round.toLocaleString()}`); + + try { + if (nextHeight > 1) { + this.detectMissedRound(this.forgingDelegates); + } + + this.dposState.buildDelegateRanking(); + this.dposState.setDelegatesRound(roundInfo); + + await this.setForgingDelegatesOfRound(roundInfo, this.dposState.getRoundDelegates().slice()); + await this.databaseService.saveRound(this.dposState.getRoundDelegates()); + + this.blocksInCurrentRound = []; + + this.events.dispatch(Enums.RoundEvent.Applied); + } catch (error) { + // trying to leave database state has it was + // ! this.saveRound may not have been called + // ! try should be moved below await this.setForgingDelegatesOfRound + await this.databaseService.deleteRound(round); + + throw error; + } + } else { + // ! then applyRound should not be called at all + this.logger.warning( + `Round ${round.toLocaleString()} has already been applied. This should happen only if you are a forger.`, + ); + } + } + } + + public async revertRound(height: number): Promise { + const roundInfo: Contracts.Shared.RoundInfo = AppUtils.roundCalculator.calculateRound(height); + const { round, nextRound, maxDelegates } = roundInfo; + + // ! height >= maxDelegates is always true + if (nextRound === round + 1 && height >= maxDelegates) { + this.logger.info(`Back to previous round: ${round.toLocaleString()}`); + + this.blocksInCurrentRound = await this.getBlocksForRound(roundInfo); + + await this.setForgingDelegatesOfRound( + roundInfo, + await this.calcPreviousActiveDelegates(roundInfo, this.blocksInCurrentRound), + ); + + // ! this will only delete one round + await this.databaseService.deleteRound(nextRound); + } + } + + public async loadBlocksFromCurrentRound(): Promise { + // ! this should not be public, this.blocksInCurrentRound is used by DatabaseService only + this.blocksInCurrentRound = await this.getBlocksForRound(); + } + + public async revertBlock(block: Interfaces.IBlock): Promise { + await this.revertRound(block.data.height); + await this.blockState.revertBlock(block); + + // ! blockState is already reverted if this check fails + assert(this.blocksInCurrentRound.pop()!.data.id === block.data.id); + + for (let i = block.transactions.length - 1; i >= 0; i--) { + this.events.dispatch(Enums.TransactionEvent.Reverted, block.transactions[i].data); + } + + this.events.dispatch(Enums.BlockEvent.Reverted, block.data); + } + + public async getBlocksForRound(roundInfo?: Contracts.Shared.RoundInfo): Promise { + // ! it should check roundInfo before assuming that lastBlock is what's have to be returned + + let lastBlock: Interfaces.IBlock = this.stateStore.getLastBlock(); + + if (!lastBlock) { + lastBlock = await this.databaseService.getLastBlock(); + } + + if (!lastBlock) { + return []; + } else if (lastBlock.data.height === 1) { + return [lastBlock]; + } + + if (!roundInfo) { + roundInfo = AppUtils.roundCalculator.calculateRound(lastBlock.data.height); + } + + // ? number of blocks in round may not equal roundInfo.maxDelegates + // ? see round-calculator.ts handling milestone change + const blocks = await this.databaseService.getBlocks(roundInfo.roundHeight, roundInfo.maxDelegates); + + const builtBlockPromises: Promise[] = blocks.map(async (block: Interfaces.IBlockData) => { + if (block.height === 1) { + return this.stateStore.getGenesisBlock(); + } + + const blockTimeLookup = await AppUtils.forgingInfoCalculator.getBlockTimeLookup(this.app, block.height); + return Blocks.BlockFactory.fromData(block, blockTimeLookup, { deserializeTransactionsUnchecked: true })!; + }); + + return Promise.all(builtBlockPromises); + } + + public async getActiveDelegates( + roundInfo?: Contracts.Shared.RoundInfo, + delegates?: Contracts.State.Wallet[], + ): Promise { + if (!roundInfo) { + // ! use this.stateStore.getLastBlock() + const lastBlock = await this.databaseService.getLastBlock(); + roundInfo = AppUtils.roundCalculator.calculateRound(lastBlock.data.height); + } + + const { round } = roundInfo; + + if (this.forgingDelegates.length && this.forgingDelegates[0].getAttribute("delegate.round") === round) { + return this.forgingDelegates; + } + + // When called during applyRound we already know the delegates, so we don't have to query the database. + if (!delegates) { + delegates = (await this.databaseService.getRound(round)).map(({ publicKey, balance }) => { + // ! find wallet by public key and clone it + const wallet = this.walletRepository.createWallet(Identities.Address.fromPublicKey(publicKey)); + wallet.publicKey = publicKey; + wallet.setAttribute("delegate", { + voteBalance: Utils.BigNumber.make(balance), + username: this.walletRepository.findByPublicKey(publicKey).getAttribute("delegate.username", ""), // ! default username? + }); + return wallet; + }); + } + + for (const delegate of delegates) { + // ! throw if delegate round doesn't match instead of altering argument + delegate.setAttribute("delegate.round", round); + } + + // ! extracting code below can simplify many call stacks and tests + + const seedSource: string = round.toString(); + let currentSeed: Buffer = Crypto.HashAlgorithms.sha256(seedSource); + + delegates = delegates.map((delegate) => delegate.clone()); + for (let i = 0, delCount = delegates.length; i < delCount; i++) { + for (let x = 0; x < 4 && i < delCount; i++, x++) { + const newIndex = currentSeed[x] % delCount; + const b = delegates[newIndex]; + delegates[newIndex] = delegates[i]; + delegates[i] = b; + } + currentSeed = Crypto.HashAlgorithms.sha256(currentSeed); + } + + return delegates; + } + + public async verifyBlockchain(): Promise { + const lastBlock = this.stateStore.getLastBlock(); + return this.databaseService.verifyBlockchain(lastBlock); + } + + public async getCommonBlocks(ids: string[]): Promise { + let commonBlocks: Interfaces.IBlockData[] = this.stateStore.getCommonBlocks(ids); + + if (commonBlocks.length < ids.length) { + // ! do not query blocks that were found + // ! why method is called commonBlocks, but is just findByIds? + commonBlocks = ((await this.databaseService.findBlockByID(ids)) as unknown) as Interfaces.IBlockData[]; + } + + return commonBlocks; + } + + public async getBlocks(offset: number, limit: number, headersOnly?: boolean): Promise { + // The functions below return matches in the range [start, end], including both ends. + const start: number = offset; + const end: number = offset + limit - 1; + + let blocks: Interfaces.IBlockData[] = this.stateStore.getLastBlocksByHeight(start, end, headersOnly); + + if (blocks.length !== limit) { + // ! assumes that earlier blocks may be missing + // ! but querying database is unnecessary when later blocks are missing too (aren't forged yet) + blocks = await this.databaseService.getBlocks(start, end, headersOnly); + } + + return blocks; + } + + public async getRecentBlockIds(): Promise { + // ! why getLastBlockIds returns blocks and not ids? + let blocks: any[] = this.stateStore.getLastBlockIds().reverse().slice(0, 10); + + if (blocks.length < 10) { + // ! blockRepository.findRecent returns objects containing single id property in reverse order + // ! where recent block id is first in array + blocks = await this.databaseService.findRecentBlocks(10); + } + + return blocks.map((block) => block.id); + } + + /** + * Get the blocks at the given heights. + * The transactions for those blocks will not be loaded like in `getBlocks()`. + * @param {Array} heights array of arbitrary block heights + * @return {Array} array for the corresponding blocks. The element (block) at index `i` + * in the resulting array corresponds to the requested height at index `i` in the input + * array heights[]. For example, if + * heights[0] = 100 + * heights[1] = 200 + * heights[2] = 150 + * then the result array will have the same number of elements (3) and will be: + * result[0] = block at height 100 + * result[1] = block at height 200 + * result[2] = block at height 150 + * If some of the requested blocks do not exist in our chain (requested height is larger than + * the height of our blockchain), then that element will be `undefined` in the resulting array + * @throws Error + */ + public async getBlocksByHeight(heights: number[]) { + // TODO: add type + const blocks: Interfaces.IBlockData[] = []; + + // Map of height -> index in heights[], e.g. if + // heights[5] == 6000000, then + // toGetFromDB[6000000] == 5 + // In this map we only store a subset of the heights - the ones we could not retrieve + // from app/state and need to get from the database. + const toGetFromDB = {}; + + for (const [i, height] of heights.entries()) { + const stateBlocks = this.stateStore.getLastBlocksByHeight(height, height, true); + + if (Array.isArray(stateBlocks) && stateBlocks.length > 0) { + blocks[i] = stateBlocks[0]; + } + + if (blocks[i] === undefined) { + toGetFromDB[height] = i; + } + } + + const heightsToGetFromDB: number[] = Object.keys(toGetFromDB).map((height) => +height); + if (heightsToGetFromDB.length > 0) { + const blocksByHeights = await this.databaseService.findBlockByHeights(heightsToGetFromDB); + + for (const blockFromDB of blocksByHeights) { + const index = toGetFromDB[blockFromDB.height]; + blocks[index] = blockFromDB; + } + } + + return blocks; + } + + private async detectMissedBlocks(block: Interfaces.IBlock) { + const lastBlock: Interfaces.IBlock = this.stateStore.getLastBlock(); + + if (lastBlock.data.height === 1) { + return; + } + + const blockTimeLookup = await AppUtils.forgingInfoCalculator.getBlockTimeLookup( + this.app, + lastBlock.data.height, + ); + + const lastSlot: number = Crypto.Slots.getSlotNumber(blockTimeLookup, lastBlock.data.timestamp); + const currentSlot: number = Crypto.Slots.getSlotNumber(blockTimeLookup, block.data.timestamp); + + const missedSlots: number = Math.min(currentSlot - lastSlot - 1, this.forgingDelegates.length); + for (let i = 0; i < missedSlots; i++) { + const missedSlot: number = lastSlot + i + 1; + const delegate: Contracts.State.Wallet = this.forgingDelegates[missedSlot % this.forgingDelegates.length]; + + this.logger.debug( + `Delegate ${delegate.getAttribute("delegate.username")} (${delegate.publicKey}) just missed a block.`, + ); + + this.events.dispatch(Enums.ForgerEvent.Missing, { + delegate, + }); + } + } + + private async initializeLastBlock(): Promise { + // ? attempt to remove potentially corrupt blocks from database + + let lastBlock: Interfaces.IBlock | undefined; + let tries = 5; // ! actually 6, but only 5 will be removed + + // Ensure the config manager is initialized, before attempting to call `fromData` + // which otherwise uses potentially wrong milestones. + let lastHeight: number = 1; + const latest: Interfaces.IBlockData | undefined = await this.databaseService.findLatestBlock(); + if (latest) { + lastHeight = latest.height; + } + + Managers.configManager.setHeight(lastHeight); + + const getLastBlock = async (): Promise => { + try { + return await this.databaseService.getLastBlock(); + } catch (error) { + this.logger.error(error.message); + + if (tries > 0) { + const block: Interfaces.IBlockData = (await this.databaseService.findLatestBlock())!; + await this.databaseService.deleteBlocks([block]); + tries--; + } else { + this.app.terminate("Unable to deserialize last block from database.", error); + throw new Error("Terminated (unreachable)"); + } + + return getLastBlock(); + } + }; + + lastBlock = await getLastBlock(); + + if (!lastBlock) { + this.logger.warning("No block found in database"); + lastBlock = await this.createGenesisBlock(); + } + + this.configureState(lastBlock); + } + + private async createGenesisBlock(): Promise { + const genesisBlock = this.stateStore.getGenesisBlock(); + await this.databaseService.saveBlocks([genesisBlock]); + return genesisBlock; + } + + private configureState(lastBlock: Interfaces.IBlock): void { + this.stateStore.setLastBlock(lastBlock); + const { blocktime, block } = Managers.configManager.getMilestone(); + const blocksPerDay: number = Math.ceil(86400 / blocktime); + this.stateBlockStore.resize(blocksPerDay); + this.stateTransactionStore.resize(blocksPerDay * block.maxTransactions); + } + + private detectMissedRound(delegates: Contracts.State.Wallet[]): void { + if (!delegates || !this.blocksInCurrentRound.length) { + // ! this.blocksInCurrentRound is impossible + // ! otherwise this.blocksInCurrentRound!.length = 0 in applyRound will throw + return; + } + + if (this.blocksInCurrentRound.length === 1 && this.blocksInCurrentRound[0].data.height === 1) { + // ? why skip missed round checks when first round has genesis block only? + return; + } + + for (const delegate of delegates) { + // ! use .some() instead of .fitler() + const producedBlocks: Interfaces.IBlock[] = this.blocksInCurrentRound.filter( + (blockGenerator) => blockGenerator.data.generatorPublicKey === delegate.publicKey, + ); + + if (producedBlocks.length === 0) { + const wallet: Contracts.State.Wallet = this.walletRepository.findByPublicKey(delegate.publicKey!); + + this.logger.debug( + `Delegate ${wallet.getAttribute("delegate.username")} (${wallet.publicKey}) just missed a round.`, + ); + + this.events.dispatch(Enums.RoundEvent.Missed, { + delegate: wallet, + }); + } + } + } + + private async initializeActiveDelegates(height: number): Promise { + // ! may be set to undefined to early if error is raised + this.forgingDelegates = []; + + const roundInfo: Contracts.Shared.RoundInfo = AppUtils.roundCalculator.calculateRound(height); + await this.setForgingDelegatesOfRound(roundInfo); + } + + private async setForgingDelegatesOfRound( + roundInfo: Contracts.Shared.RoundInfo, + delegates?: Contracts.State.Wallet[], + ): Promise { + // ! it's this.getActiveDelegates(roundInfo, delegates); + // ! only last part of that function which reshuffles delegates is used + const result = await this.triggers.call("getActiveDelegates", { roundInfo, delegates }); + this.forgingDelegates = (result as Contracts.State.Wallet[]) || []; + } + + private async calcPreviousActiveDelegates( + roundInfo: Contracts.Shared.RoundInfo, + blocks?: Interfaces.IBlock[], + ): Promise { + // ! make blocks required parameter forcing caller to specify blocks explicitly + blocks = blocks || (await this.getBlocksForRound(roundInfo)); + + const prevRoundState = await this.getDposPreviousRoundState(blocks, roundInfo); + for (const prevRoundDelegateWallet of prevRoundState.getAllDelegates()) { + // ! name suggest that this is pure function + // ! when in fact it is manipulating current wallet repository setting delegate ranks + const username = prevRoundDelegateWallet.getAttribute("delegate.username"); + const delegateWallet = this.walletRepository.findByUsername(username); + delegateWallet.setAttribute("delegate.rank", prevRoundDelegateWallet.getAttribute("delegate.rank")); + } + + // ! return readonly array instead of taking slice + return prevRoundState.getRoundDelegates().slice(); + } + + private async emitTransactionEvents(transaction: Interfaces.ITransaction): Promise { + this.events.dispatch(Enums.TransactionEvent.Applied, transaction.data); + const handler = await this.handlerRegistry.getActivatedHandlerForData(transaction.data); + // ! no reason to pass this.emitter + handler.emitEvents(transaction, this.events); + } +} diff --git a/packages/core-state/src/index.ts b/packages/core-state/src/index.ts index 681720f216..be1edfc2c3 100644 --- a/packages/core-state/src/index.ts +++ b/packages/core-state/src/index.ts @@ -1,2 +1,3 @@ export * from "./service-provider"; export * as Wallets from "./wallets"; +export { DatabaseInteraction } from "./database-interactions"; diff --git a/packages/core-state/src/service-provider.ts b/packages/core-state/src/service-provider.ts index b57a5423ce..53a4546843 100644 --- a/packages/core-state/src/service-provider.ts +++ b/packages/core-state/src/service-provider.ts @@ -1,8 +1,9 @@ import { Container, Contracts, Providers, Services } from "@arkecosystem/core-kernel"; import { Interfaces } from "@arkecosystem/crypto"; -import { BuildDelegateRankingAction } from "./actions"; +import { BuildDelegateRankingAction, GetActiveDelegatesAction } from "./actions"; import { BlockState } from "./block-state"; +import { DatabaseInteraction } from "./database-interactions"; import { DposPreviousRoundState, DposState } from "./dpos"; import { StateBuilder } from "./state-builder"; import { BlockStore } from "./stores/blocks"; @@ -64,10 +65,13 @@ export class ServiceProvider extends Providers.ServiceProvider { .bind(Container.Identifiers.TransactionValidatorFactory) .toAutoFactory(Container.Identifiers.TransactionValidator); + this.app.bind(Container.Identifiers.DatabaseInteraction).to(DatabaseInteraction); + this.registerActions(); } public async boot(): Promise { + await this.app.get(Container.Identifiers.DatabaseInteraction).initialize(); await this.app.resolve(StateBuilder).run(); } @@ -79,5 +83,9 @@ export class ServiceProvider extends Providers.ServiceProvider { this.app .get(Container.Identifiers.TriggerService) .bind("buildDelegateRanking", new BuildDelegateRankingAction()); + + this.app + .get(Container.Identifiers.TriggerService) + .bind("getActiveDelegates", new GetActiveDelegatesAction(this.app)); } } diff --git a/packages/core/bin/config/devnet/app.json b/packages/core/bin/config/devnet/app.json index a066587477..12f8bb696d 100644 --- a/packages/core/bin/config/devnet/app.json +++ b/packages/core/bin/config/devnet/app.json @@ -87,6 +87,9 @@ { "package": "@arkecosystem/core-logger-pino" }, + { + "package": "@arkecosystem/core-database" + }, { "package": "@arkecosystem/core-forger" } @@ -115,4 +118,4 @@ } ] } -} +} \ No newline at end of file diff --git a/packages/core/bin/config/mainnet/app.json b/packages/core/bin/config/mainnet/app.json index 502df55e36..c30ae92807 100644 --- a/packages/core/bin/config/mainnet/app.json +++ b/packages/core/bin/config/mainnet/app.json @@ -81,6 +81,9 @@ { "package": "@arkecosystem/core-logger-pino" }, + { + "package": "@arkecosystem/core-database" + }, { "package": "@arkecosystem/core-forger" } @@ -109,4 +112,4 @@ } ] } -} +} \ No newline at end of file diff --git a/packages/core/bin/config/testnet/app.json b/packages/core/bin/config/testnet/app.json index a066587477..12f8bb696d 100644 --- a/packages/core/bin/config/testnet/app.json +++ b/packages/core/bin/config/testnet/app.json @@ -87,6 +87,9 @@ { "package": "@arkecosystem/core-logger-pino" }, + { + "package": "@arkecosystem/core-database" + }, { "package": "@arkecosystem/core-forger" } @@ -115,4 +118,4 @@ } ] } -} +} \ No newline at end of file