From 03ddb4792f749ba73281ac231f04f81817e816db Mon Sep 17 00:00:00 2001 From: Brian Faust Date: Fri, 26 Apr 2019 12:22:15 +0300 Subject: [PATCH] feat(core-state): in-memory storage for last N blocks and transactions --- .../core-p2p/mocks/core-container.ts | 9 +- .../unit/core-blockchain/mocks/container.ts | 2 +- .../core-blockchain/stubs/state-storage.ts | 4 +- .../__fixtures__/state-storage-stub.ts | 4 +- .../core-database/mocks/core-container.ts | 2 +- __tests__/unit/core-p2p/mocks/state.ts | 6 +- .../unit/core-p2p/network-monitor.test.ts | 7 +- __tests__/unit/core-state/mocks/container.ts | 7 - .../unit/core-state/state-storage.test.ts | 4 +- .../unit/core-transaction-pool/mocks/state.ts | 6 +- .../core-utils/ordered-capped-map.test.ts | 174 ++++++++++++++++++ packages/core-blockchain/src/blockchain.ts | 4 +- packages/core-blockchain/src/plugin.ts | 5 +- packages/core-blockchain/src/state-machine.ts | 4 +- .../src/database-service-factory.ts | 5 +- .../core-database/src/database-service.ts | 38 +++- .../src/core-blockchain/blockchain.ts | 6 +- .../core-interfaces/src/core-state/index.ts | 3 +- .../core-interfaces/src/core-state/service.ts | 7 + .../{state-storage.ts => state-store.ts} | 2 +- packages/core-p2p/src/network-monitor.ts | 7 +- packages/core-p2p/src/peer-guard.ts | 2 +- packages/core-p2p/src/peer-verifier.ts | 5 +- packages/core-state/package.json | 4 +- packages/core-state/src/plugin.ts | 28 ++- packages/core-state/src/service.ts | 35 ++++ packages/core-state/src/stores/blocks.ts | 16 ++ .../src/{state-storage.ts => stores/state.ts} | 2 +- .../core-state/src/stores/transactions.ts | 12 ++ .../core-transaction-pool/src/connection.ts | 8 +- .../core-transaction-pool/src/processor.ts | 7 +- packages/core-utils/package.json | 3 +- packages/core-utils/src/index.ts | 12 +- packages/core-utils/src/ordered-capped-map.ts | 64 +++++++ 34 files changed, 429 insertions(+), 75 deletions(-) create mode 100644 __tests__/unit/core-utils/ordered-capped-map.test.ts create mode 100644 packages/core-interfaces/src/core-state/service.ts rename packages/core-interfaces/src/core-state/{state-storage.ts => state-store.ts} (98%) create mode 100644 packages/core-state/src/service.ts create mode 100644 packages/core-state/src/stores/blocks.ts rename packages/core-state/src/{state-storage.ts => stores/state.ts} (99%) create mode 100644 packages/core-state/src/stores/transactions.ts create mode 100644 packages/core-utils/src/ordered-capped-map.ts diff --git a/__tests__/integration/core-p2p/mocks/core-container.ts b/__tests__/integration/core-p2p/mocks/core-container.ts index e59d1d1b95..4194de1d40 100644 --- a/__tests__/integration/core-p2p/mocks/core-container.ts +++ b/__tests__/integration/core-p2p/mocks/core-container.ts @@ -104,9 +104,12 @@ jest.mock("@arkecosystem/core-container", () => { if (name === "state") { return { - getLastBlock: () => genesisBlock, - cacheTransactions: jest.fn().mockImplementation(txs => ({ notAdded: txs, added: [] })), - removeCachedTransactionIds: jest.fn().mockReturnValue(null), + getStore: () => ({ + getLastBlock: () => genesisBlock, + getLastHeight: () => genesisBlock.data.height, + cacheTransactions: jest.fn().mockImplementation(txs => ({ notAdded: txs, added: [] })), + removeCachedTransactionIds: jest.fn().mockReturnValue(null), + }), }; } diff --git a/__tests__/unit/core-blockchain/mocks/container.ts b/__tests__/unit/core-blockchain/mocks/container.ts index cc87ae6a30..6a3030e019 100644 --- a/__tests__/unit/core-blockchain/mocks/container.ts +++ b/__tests__/unit/core-blockchain/mocks/container.ts @@ -53,7 +53,7 @@ export const container = { if (name === "state") { stateStorageStub.blockchain = blockchainMachine.initialState; - return stateStorageStub; + return { getStore: () => stateStorageStub }; } return null; diff --git a/__tests__/unit/core-blockchain/stubs/state-storage.ts b/__tests__/unit/core-blockchain/stubs/state-storage.ts index 821576651e..0b13eb4d8a 100644 --- a/__tests__/unit/core-blockchain/stubs/state-storage.ts +++ b/__tests__/unit/core-blockchain/stubs/state-storage.ts @@ -2,7 +2,7 @@ import { State } from "@arkecosystem/core-interfaces"; import { Blocks, Interfaces } from "@arkecosystem/crypto"; -export class StateStorageStub implements State.IStateStorage { +export class StateStoreStub implements State.IStateStore { public blockchain: any; public lastDownloadedBlock: Interfaces.IBlock | null; public blockPing: any; @@ -76,4 +76,4 @@ export class StateStorageStub implements State.IStateStorage { } } -export const stateStorageStub = new StateStorageStub(); +export const stateStorageStub = new StateStoreStub(); diff --git a/__tests__/unit/core-database/__fixtures__/state-storage-stub.ts b/__tests__/unit/core-database/__fixtures__/state-storage-stub.ts index 9a21bb9085..095a49add5 100644 --- a/__tests__/unit/core-database/__fixtures__/state-storage-stub.ts +++ b/__tests__/unit/core-database/__fixtures__/state-storage-stub.ts @@ -2,7 +2,7 @@ import { State } from "@arkecosystem/core-interfaces"; import { Blocks, Interfaces } from "@arkecosystem/crypto"; -export class StateStorageStub implements State.IStateStorage { +export class StateStoreStub implements State.IStateStore { public blockchain: any; public lastDownloadedBlock: Interfaces.IBlock | null; public blockPing: any; @@ -65,4 +65,4 @@ export class StateStorageStub implements State.IStateStorage { public setLastBlock(block: Blocks.Block): void {} } -export const stateStorageStub = new StateStorageStub(); +export const stateStorageStub = new StateStoreStub(); diff --git a/__tests__/unit/core-database/mocks/core-container.ts b/__tests__/unit/core-database/mocks/core-container.ts index 8e3534f39a..1fe48f2c7e 100644 --- a/__tests__/unit/core-database/mocks/core-container.ts +++ b/__tests__/unit/core-database/mocks/core-container.ts @@ -30,7 +30,7 @@ jest.mock("@arkecosystem/core-container", () => { } if (name === "state") { - return stateStorageStub; + return { getStore: () => stateStorageStub }; } return {}; diff --git a/__tests__/unit/core-p2p/mocks/state.ts b/__tests__/unit/core-p2p/mocks/state.ts index 58b7e6e575..d9c7f414de 100644 --- a/__tests__/unit/core-p2p/mocks/state.ts +++ b/__tests__/unit/core-p2p/mocks/state.ts @@ -1,6 +1,8 @@ import { genesisBlock } from "../../../utils/fixtures/unitnet/block-model"; export const state = { - getLastBlock: () => genesisBlock, - forkedBlock: null, + getStore: () => ({ + getLastBlock: () => genesisBlock, + forkedBlock: null, + }), }; diff --git a/__tests__/unit/core-p2p/network-monitor.test.ts b/__tests__/unit/core-p2p/network-monitor.test.ts index 0f0b77d3d0..08d4ef3200 100644 --- a/__tests__/unit/core-p2p/network-monitor.test.ts +++ b/__tests__/unit/core-p2p/network-monitor.test.ts @@ -135,13 +135,18 @@ describe("NetworkMonitor", () => { const spySuspend = jest.spyOn(processor, "suspend"); - state.forkedBlock = { ip: "1.1.1.1" }; + const spyStateStore = jest.spyOn(state, "getStore").mockReturnValueOnce({ + ...state.getStore(), + ...{ forkedBlock: { ip: "1.1.1.1" } }, + }); await monitor.refreshPeersAfterFork(); expect(monitor.resetSuspendedPeers).toHaveBeenCalled(); expect(spySuspend).toHaveBeenCalledWith("1.1.1.1"); expect(connector.disconnect).toHaveBeenCalled(); + + spyStateStore.mockRestore(); }); }); diff --git a/__tests__/unit/core-state/mocks/container.ts b/__tests__/unit/core-state/mocks/container.ts index c7acb7bd0f..5f99d2e1f9 100644 --- a/__tests__/unit/core-state/mocks/container.ts +++ b/__tests__/unit/core-state/mocks/container.ts @@ -17,13 +17,6 @@ export const container = { }), }; }, - resolve: name => { - if (name === "state") { - return {}; - } - - return {}; - }, resolvePlugin: name => { if (name === "logger") { return logger; diff --git a/__tests__/unit/core-state/state-storage.test.ts b/__tests__/unit/core-state/state-storage.test.ts index 53472b9a25..6678524db2 100644 --- a/__tests__/unit/core-state/state-storage.test.ts +++ b/__tests__/unit/core-state/state-storage.test.ts @@ -5,7 +5,7 @@ import { logger } from "./mocks/logger"; import { Blocks as cBlocks, Interfaces } from "@arkecosystem/crypto"; import delay from "delay"; import { defaults } from "../../../packages/core-state/src/defaults"; -import { StateStorage } from "../../../packages/core-state/src/state-storage"; +import { StateStore } from "../../../packages/core-state/src/stores/state"; import "../../utils"; import { blocks101to155 } from "../../utils/fixtures/testnet/blocks101to155"; import { blocks2to100 } from "../../utils/fixtures/testnet/blocks2to100"; @@ -15,7 +15,7 @@ const blocks = blocks2to100.concat(blocks101to155).map(block => BlockFactory.fro let stateStorage; beforeAll(async () => { - stateStorage = new StateStorage(); + stateStorage = new StateStore(); }); beforeEach(() => { diff --git a/__tests__/unit/core-transaction-pool/mocks/state.ts b/__tests__/unit/core-transaction-pool/mocks/state.ts index decd939eb2..acd68b4a0c 100644 --- a/__tests__/unit/core-transaction-pool/mocks/state.ts +++ b/__tests__/unit/core-transaction-pool/mocks/state.ts @@ -1,4 +1,6 @@ export const state = { - cacheTransactions: () => null, - removeCachedTransactionIds: () => null, + getStore: () => ({ + cacheTransactions: () => null, + removeCachedTransactionIds: () => null, + }), }; diff --git a/__tests__/unit/core-utils/ordered-capped-map.test.ts b/__tests__/unit/core-utils/ordered-capped-map.test.ts new file mode 100644 index 0000000000..7e17903939 --- /dev/null +++ b/__tests__/unit/core-utils/ordered-capped-map.test.ts @@ -0,0 +1,174 @@ +import "jest-extended"; + +import { OrderedCappedMap } from "../../../packages/core-utils/src/ordered-capped-map"; + +describe("Ordered Capped Map", () => { + it("should set and get an entry", () => { + const store = new OrderedCappedMap(100); + store.set("foo", 1); + store.set("bar", 2); + + expect(store.get("foo")).toBe(1); + expect(store.count()).toBe(2); + }); + + it("should get an entry", () => { + const store = new OrderedCappedMap(2); + store.set("1", 1); + store.set("2", 2); + + expect(store.get("1")).toBe(1); + expect(store.get("3")).toBeUndefined(); + + store.set("3", 3); + + expect(store.has("1")).toBeFalse(); + expect(store.has("2")).toBeTrue(); + expect(store.has("3")).toBeTrue(); + }); + + it("should set entries and remove ones that exceed the maximum size", () => { + const store = new OrderedCappedMap(2); + store.set("foo", 1); + store.set("bar", 2); + + expect(store.get("foo")).toBe(1); + expect(store.get("bar")).toBe(2); + + store.set("baz", 3); + store.set("faz", 4); + + expect(store.has("foo")).toBeFalse(); + expect(store.has("bar")).toBeFalse(); + expect(store.has("baz")).toBeTrue(); + expect(store.has("faz")).toBeTrue(); + expect(store.count()).toBe(2); + }); + + it("should update an entry", () => { + const store = new OrderedCappedMap(100); + store.set("foo", 1); + + expect(store.get("foo")).toBe(1); + + store.set("foo", 2); + + expect(store.get("foo")).toBe(2); + expect(store.count()).toBe(1); + }); + + it("should return if an entry exists", () => { + const store = new OrderedCappedMap(100); + store.set("1", 1); + + expect(store.has("1")).toBeTrue(); + }); + + it("should remove the specified entrys", () => { + const store = new OrderedCappedMap(100); + store.set("1", 1); + store.set("2", 2); + + expect(store.delete("1")).toBeTrue(); + expect(store.has("1")).toBeFalse(); + expect(store.has("2")).toBeTrue(); + expect(store.delete("1")).toBeFalse(); + expect(store.count()).toBe(1); + }); + + it("should remove the specified entrys", () => { + const store = new OrderedCappedMap(2); + store.set("1", 1); + store.set("2", 2); + + expect(store.count()).toBe(2); + expect(store.delete("1")).toBeTrue(); + expect(store.has("1")).toBeFalse(); + expect(store.has("2")).toBeTrue(); + + store.delete("2"); + + expect(store.count()).toBe(0); + }); + + it("should remove all entrys", () => { + const store = new OrderedCappedMap(3); + store.set("1", 1); + store.set("2", 2); + store.set("3", 3); + + expect(store.count()).toBe(3); + + store.clear(); + + expect(store.count()).toBe(0); + }); + + it("should return the first value", () => { + const store = new OrderedCappedMap(2); + store.set("1", 1); + store.set("2", 2); + + expect(store.first()).toBe(1); + }); + + it("should return the last value", () => { + const store = new OrderedCappedMap(2); + store.set("1", 1); + store.set("2", 2); + + expect(store.last()).toBe(2); + }); + + it("should return the keys", () => { + const store = new OrderedCappedMap(3); + store.set("1", 1); + store.set("2", 2); + store.set("3", 3); + + expect(store.keys()).toEqual(["1", "2", "3"]); + }); + + it("should return the values", () => { + const store = new OrderedCappedMap(3); + store.set("1", 1); + store.set("2", 2); + store.set("3", 3); + + expect(store.values()).toEqual([1, 2, 3]); + }); + + it("should return the entry count", () => { + const store = new OrderedCappedMap(100); + store.set("1", 1); + store.set("2", 2); + + expect(store.count()).toBe(2); + + store.delete("1"); + + expect(store.count()).toBe(1); + + store.set("3", 3); + + expect(store.count()).toBe(2); + }); + + it("should resize the map", () => { + const store = new OrderedCappedMap(3); + store.set("1", 1); + store.set("2", 2); + store.set("3", 3); + + expect(store.count()).toBe(3); + + store.set("4", 4); + + expect(store.count()).toBe(3); + + store.resize(4); + store.set("5", 5); + + expect(store.count()).toBe(4); + }); +}); diff --git a/packages/core-blockchain/src/blockchain.ts b/packages/core-blockchain/src/blockchain.ts index a77e57d488..79f342002c 100644 --- a/packages/core-blockchain/src/blockchain.ts +++ b/packages/core-blockchain/src/blockchain.ts @@ -25,9 +25,9 @@ const { BlockFactory } = Blocks; export class Blockchain implements blockchain.IBlockchain { /** * Get the state of the blockchain. - * @return {IStateStorage} + * @return {IStateStore} */ - get state(): State.IStateStorage { + get state(): State.IStateStore { return stateMachine.state; } diff --git a/packages/core-blockchain/src/plugin.ts b/packages/core-blockchain/src/plugin.ts index b46be3d7dd..8c13708295 100644 --- a/packages/core-blockchain/src/plugin.ts +++ b/packages/core-blockchain/src/plugin.ts @@ -14,7 +14,10 @@ export const plugin: Container.PluginDescriptor = { async register(container: Container.IContainer, options: Container.IPluginOptions) { const blockchain = new Blockchain(options); - container.resolvePlugin("state").reset(blockchainMachine); + container + .resolvePlugin("state") + .getStore() + .reset(blockchainMachine); if (!process.env.CORE_SKIP_BLOCKCHAIN) { await blockchain.start(); diff --git a/packages/core-blockchain/src/state-machine.ts b/packages/core-blockchain/src/state-machine.ts index c5d161efa0..b1a31093b3 100644 --- a/packages/core-blockchain/src/state-machine.ts +++ b/packages/core-blockchain/src/state-machine.ts @@ -15,10 +15,10 @@ const { BlockFactory } = Blocks; const config = app.getConfig(); const emitter = app.resolvePlugin("event-emitter"); const logger = app.resolvePlugin("logger"); -const stateStorage = app.resolvePlugin("state"); +const stateStorage = app.resolvePlugin("state").getStore(); /** - * @type {IStateStorage} + * @type {IStateStore} */ blockchainMachine.state = stateStorage; diff --git a/packages/core-database/src/database-service-factory.ts b/packages/core-database/src/database-service-factory.ts index d0847a2afe..e8df178ed3 100644 --- a/packages/core-database/src/database-service-factory.ts +++ b/packages/core-database/src/database-service-factory.ts @@ -1,5 +1,4 @@ -import { app } from "@arkecosystem/core-container"; -import { Database, State } from "@arkecosystem/core-interfaces"; +import { Database } from "@arkecosystem/core-interfaces"; import { DatabaseService } from "./database-service"; import { BlocksBusinessRepository } from "./repositories/blocks-business-repository"; import { DelegatesBusinessRepository } from "./repositories/delegates-business-repository"; @@ -24,7 +23,5 @@ export const databaseServiceFactory = async ( await databaseService.init(); - app.resolvePlugin("state").setLastBlock(await databaseService.getLastBlock()); - return databaseService; }; diff --git a/packages/core-database/src/database-service.ts b/packages/core-database/src/database-service.ts index b8852113fb..14d526ccce 100644 --- a/packages/core-database/src/database-service.ts +++ b/packages/core-database/src/database-service.ts @@ -44,14 +44,15 @@ export class DatabaseService implements Database.IDatabaseService { } public async init(): Promise { + await this.createGenesisBlock(); + const lastBlock: Interfaces.IBlock = await this.getLastBlock(); - if (lastBlock) { - Managers.configManager.setHeight(lastBlock.data.height); - } + Managers.configManager.setHeight(lastBlock.data.height); await this.loadBlocksFromCurrentRound(); - await this.createGenesisBlock(); + + await this.configureState(lastBlock); } public async restoreCurrentRound(height: number): Promise { @@ -215,7 +216,8 @@ export class DatabaseService implements Database.IDatabaseService { const end: number = offset + limit - 1; let blocks: Interfaces.IBlockData[] = app - .resolvePlugin("state") + .resolvePlugin("state") + .getStore() .getLastBlocksByHeight(start, end); if (blocks.length !== limit) { @@ -256,7 +258,10 @@ export class DatabaseService implements Database.IDatabaseService { const toGetFromDB = {}; for (const [i, height] of heights.entries()) { - const stateBlocks = app.resolvePlugin("state").getLastBlocksByHeight(height, height); + const stateBlocks = app + .resolvePlugin("state") + .getStore() + .getLastBlocksByHeight(height, height); if (Array.isArray(stateBlocks) && stateBlocks.length > 0) { blocks[i] = stateBlocks[0]; @@ -281,7 +286,10 @@ export class DatabaseService implements Database.IDatabaseService { } public async getBlocksForRound(roundInfo?: Shared.IRoundInfo): Promise { - let lastBlock: Interfaces.IBlock = app.resolvePlugin("state").getLastBlock(); + let lastBlock: Interfaces.IBlock = app + .resolvePlugin("state") + .getStore() + .getLastBlock(); if (!lastBlock) { lastBlock = await this.getLastBlock(); @@ -331,7 +339,8 @@ export class DatabaseService implements Database.IDatabaseService { public async getCommonBlocks(ids: string[]): Promise { let commonBlocks: Interfaces.IBlockData[] = app - .resolvePlugin("state") + .resolvePlugin("state") + .getStore() .getCommonBlocks(ids); if (commonBlocks.length < ids.length) { @@ -344,6 +353,7 @@ export class DatabaseService implements Database.IDatabaseService { public async getRecentBlockIds(): Promise { let blocks: Interfaces.IBlockData[] = app .resolvePlugin("state") + .getStore() .getLastBlockIds() .reverse() .slice(0, 10); @@ -566,6 +576,18 @@ export class DatabaseService implements Database.IDatabaseService { } } + private configureState(lastBlock: Interfaces.IBlock): void { + const state: State.IStateService = app.resolvePlugin("state"); + + state.getStore().setLastBlock(lastBlock); + + const { blocktime, block } = Managers.configManager.getMilestone(); + + const blocksPerDay: number = Math.ceil(86400 / blocktime); + state.getBlocks().resize(blocksPerDay); + state.getTransactions().resize(blocksPerDay * block.maxTransactions); + } + private async initializeActiveDelegates(height: number): Promise { this.forgingDelegates = null; diff --git a/packages/core-interfaces/src/core-blockchain/blockchain.ts b/packages/core-interfaces/src/core-blockchain/blockchain.ts index 106d04098c..8acdd11e15 100644 --- a/packages/core-interfaces/src/core-blockchain/blockchain.ts +++ b/packages/core-interfaces/src/core-blockchain/blockchain.ts @@ -1,15 +1,15 @@ import { Interfaces } from "@arkecosystem/crypto"; import { IDatabaseService } from "../core-database"; import { IPeerService } from "../core-p2p"; -import { IStateStorage } from "../core-state"; +import { IStateStore } from "../core-state"; import { IConnection } from "../core-transaction-pool"; export interface IBlockchain { /** * Get the state of the blockchain. - * @return {IStateStorage} + * @return {IStateStore} */ - readonly state: IStateStorage; + readonly state: IStateStore; /** * Get the network (p2p) interface. diff --git a/packages/core-interfaces/src/core-state/index.ts b/packages/core-interfaces/src/core-state/index.ts index 848c19a0b2..158c980869 100644 --- a/packages/core-interfaces/src/core-state/index.ts +++ b/packages/core-interfaces/src/core-state/index.ts @@ -1 +1,2 @@ -export * from "./state-storage"; +export * from "./service"; +export * from "./state-store"; diff --git a/packages/core-interfaces/src/core-state/service.ts b/packages/core-interfaces/src/core-state/service.ts new file mode 100644 index 0000000000..b291570fb1 --- /dev/null +++ b/packages/core-interfaces/src/core-state/service.ts @@ -0,0 +1,7 @@ +import { IStateStore } from "./state-store"; + +export interface IStateService { + getBlocks(): any; // @TODO: add type + getTransactions(): any; // @TODO: add type + getStore(): IStateStore; +} diff --git a/packages/core-interfaces/src/core-state/state-storage.ts b/packages/core-interfaces/src/core-state/state-store.ts similarity index 98% rename from packages/core-interfaces/src/core-state/state-storage.ts rename to packages/core-interfaces/src/core-state/state-store.ts index c9bc4a7f81..d11006faca 100644 --- a/packages/core-interfaces/src/core-state/state-storage.ts +++ b/packages/core-interfaces/src/core-state/state-store.ts @@ -1,6 +1,6 @@ import { Interfaces } from "@arkecosystem/crypto"; -export interface IStateStorage { +export interface IStateStore { blockchain: any; lastDownloadedBlock: Interfaces.IBlock | null; blockPing: any; diff --git a/packages/core-p2p/src/network-monitor.ts b/packages/core-p2p/src/network-monitor.ts index 649ccc519b..52631bb10f 100644 --- a/packages/core-p2p/src/network-monitor.ts +++ b/packages/core-p2p/src/network-monitor.ts @@ -208,7 +208,7 @@ export class NetworkMonitor implements P2P.INetworkMonitor { await this.resetSuspendedPeers(); // Ban peer who caused the fork - const forkedBlock = app.resolvePlugin("state").forkedBlock; + const forkedBlock = app.resolvePlugin("state").getStore().forkedBlock; if (forkedBlock) { this.processor.suspend(forkedBlock.ip); } @@ -220,7 +220,10 @@ export class NetworkMonitor implements P2P.INetworkMonitor { await this.resetSuspendedPeers(); } - const lastBlock = app.resolvePlugin("state").getLastBlock(); + const lastBlock = app + .resolvePlugin("state") + .getStore() + .getLastBlock(); const allPeers: P2P.IPeer[] = [ ...this.storage.getPeers(), diff --git a/packages/core-p2p/src/peer-guard.ts b/packages/core-p2p/src/peer-guard.ts index ed71e8847b..6b6aa1b5bb 100644 --- a/packages/core-p2p/src/peer-guard.ts +++ b/packages/core-p2p/src/peer-guard.ts @@ -62,7 +62,7 @@ export class PeerGuard implements P2P.IPeerGuard { } public analyze(peer: P2P.IPeer): P2P.IPunishment { - const state = app.resolvePlugin("state"); + const state = app.resolvePlugin("state").getStore(); if (state.forkedBlock && peer.ip === state.forkedBlock.ip) { return this.createPunishment(this.offences.fork); diff --git a/packages/core-p2p/src/peer-verifier.ts b/packages/core-p2p/src/peer-verifier.ts index cec255e2de..df33f4726d 100644 --- a/packages/core-p2p/src/peer-verifier.ts +++ b/packages/core-p2p/src/peer-verifier.ts @@ -103,7 +103,10 @@ export class PeerVerifier { * @return {Number} chain height */ private async ourHeight(): Promise { - const height: number = app.resolvePlugin("state").getLastBlock().data.height; + const height: number = app + .resolvePlugin("state") + .getStore() + .getLastHeight(); assert(Number.isInteger(height), `Couldn't derive our chain height: ${height}`); diff --git a/packages/core-state/package.json b/packages/core-state/package.json index 4edbbfc9cd..2d868428f6 100644 --- a/packages/core-state/package.json +++ b/packages/core-state/package.json @@ -23,8 +23,8 @@ "dependencies": { "@arkecosystem/core-container": "^2.3.18", "@arkecosystem/core-interfaces": "^2.3.18", - "@arkecosystem/crypto": "^2.3.18", - "immutable": "^4.0.0-rc.12" + "@arkecosystem/core-utils": "^2.3.18", + "@arkecosystem/crypto": "^2.3.18" }, "publishConfig": { "access": "public" diff --git a/packages/core-state/src/plugin.ts b/packages/core-state/src/plugin.ts index 375556cc42..e30bf5e1d9 100644 --- a/packages/core-state/src/plugin.ts +++ b/packages/core-state/src/plugin.ts @@ -1,25 +1,19 @@ -import { Container, Logger } from "@arkecosystem/core-interfaces"; +import { Container } from "@arkecosystem/core-interfaces"; import { defaults } from "./defaults"; -import { StateStorage } from "./state-storage"; - -/** - * @TODO - * - * 1. Move the wallet manager into core-state as it only holds the in-memory state of wallets and doesn't - * depend on the database even though it is in that package. - * - * 2. Seal the storage or make all props private - * - * 3. Implement https://github.com/ArkEcosystem/core/issues/2114 - */ +import { StateService } from "./service"; +import { BlockStore } from "./stores/blocks"; +import { StateStore } from "./stores/state"; +import { TransactionStore } from "./stores/transactions"; export const plugin: Container.PluginDescriptor = { pkg: require("../package.json"), defaults, alias: "state", - async register(container: Container.IContainer) { - container.resolvePlugin("logger").info("State initialised."); - - return new StateStorage(); + async register() { + return new StateService({ + blocks: new BlockStore(1000), + transactions: new TransactionStore(1000), + storage: new StateStore(), + }); }, }; diff --git a/packages/core-state/src/service.ts b/packages/core-state/src/service.ts new file mode 100644 index 0000000000..c93ffe7e72 --- /dev/null +++ b/packages/core-state/src/service.ts @@ -0,0 +1,35 @@ +import { State } from "@arkecosystem/core-interfaces"; +import { BlockStore } from "./stores/blocks"; +import { TransactionStore } from "./stores/transactions"; + +export class StateService implements State.IStateService { + private readonly blocks: BlockStore; + private readonly transactions: TransactionStore; + private readonly storage: State.IStateStore; + + public constructor({ + blocks, + transactions, + storage, + }: { + blocks: BlockStore; + transactions: TransactionStore; + storage: State.IStateStore; + }) { + this.blocks = blocks; + this.transactions = transactions; + this.storage = storage; + } + + public getBlocks(): BlockStore { + return this.blocks; + } + + public getTransactions(): TransactionStore { + return this.transactions; + } + + public getStore(): State.IStateStore { + return this.storage; + } +} diff --git a/packages/core-state/src/stores/blocks.ts b/packages/core-state/src/stores/blocks.ts new file mode 100644 index 0000000000..6715325f4d --- /dev/null +++ b/packages/core-state/src/stores/blocks.ts @@ -0,0 +1,16 @@ +import { OrderedCappedMap } from "@arkecosystem/core-utils"; +import { Interfaces } from "@arkecosystem/crypto"; + +export class BlockStore extends OrderedCappedMap { + public getIds(): string[] { + return this.store + .valueSeq() + .reverse() + .map((block: Interfaces.IBlockData) => block.id) + .toArray(); + } + + public lastHeight(): number { + return this.last().height; + } +} diff --git a/packages/core-state/src/state-storage.ts b/packages/core-state/src/stores/state.ts similarity index 99% rename from packages/core-state/src/state-storage.ts rename to packages/core-state/src/stores/state.ts index 4278168690..156508f3c6 100644 --- a/packages/core-state/src/state-storage.ts +++ b/packages/core-state/src/stores/state.ts @@ -9,7 +9,7 @@ import { OrderedMap, OrderedSet, Seq } from "immutable"; /** * Represents an in-memory storage for state machine data. */ -export class StateStorage implements State.IStateStorage { +export class StateStore implements State.IStateStore { // @TODO: make all properties private and expose them one-by-one through a getter if used outside of this class public blockchain: any = {}; public lastDownloadedBlock: Interfaces.IBlock | null = null; diff --git a/packages/core-state/src/stores/transactions.ts b/packages/core-state/src/stores/transactions.ts new file mode 100644 index 0000000000..33c917491b --- /dev/null +++ b/packages/core-state/src/stores/transactions.ts @@ -0,0 +1,12 @@ +import { OrderedCappedMap } from "@arkecosystem/core-utils"; +import { Interfaces } from "@arkecosystem/crypto"; + +export class TransactionStore extends OrderedCappedMap { + public getIds(): string[] { + return this.store + .valueSeq() + .reverse() + .map((transaction: Interfaces.ITransactionData) => transaction.id) + .toArray(); + } +} diff --git a/packages/core-transaction-pool/src/connection.ts b/packages/core-transaction-pool/src/connection.ts index 81823454f1..d4bdb75ee5 100644 --- a/packages/core-transaction-pool/src/connection.ts +++ b/packages/core-transaction-pool/src/connection.ts @@ -310,7 +310,9 @@ export class Connection implements TransactionPool.IConnection { delegateWallet.balance = delegateWallet.balance.plus(block.data.reward.plus(block.data.totalFee)); } - app.resolvePlugin("state").removeCachedTransactionIds(block.transactions.map(tx => tx.id)); + app.resolvePlugin("state") + .getStore() + .removeCachedTransactionIds(block.transactions.map(tx => tx.id)); } public async buildWallets(): Promise { @@ -318,7 +320,9 @@ export class Connection implements TransactionPool.IConnection { const transactionIds: string[] = await this.getTransactionIdsForForging(0, this.getPoolSize()); - app.resolvePlugin("state").removeCachedTransactionIds(transactionIds); + app.resolvePlugin("state") + .getStore() + .removeCachedTransactionIds(transactionIds); for (const transactionId of transactionIds) { const transaction: Interfaces.ITransaction = this.getTransaction(transactionId); diff --git a/packages/core-transaction-pool/src/processor.ts b/packages/core-transaction-pool/src/processor.ts index 1165e2339f..be55ab73fb 100644 --- a/packages/core-transaction-pool/src/processor.ts +++ b/packages/core-transaction-pool/src/processor.ts @@ -67,7 +67,8 @@ export class Processor implements TransactionPool.IProcessor { private cacheTransactions(transactions: Interfaces.ITransactionData[]): void { const { added, notAdded }: ITransactionsCached = app - .resolvePlugin("state") + .resolvePlugin("state") + .getStore() .cacheTransactions(transactions); this.transactions = added; @@ -84,7 +85,9 @@ export class Processor implements TransactionPool.IProcessor { .resolvePlugin("database") .getForgedTransactionsIds([...new Set([...this.accept.keys(), ...this.broadcast.keys()])]); - app.resolvePlugin("state").removeCachedTransactionIds(forgedIdsSet); + app.resolvePlugin("state") + .getStore() + .removeCachedTransactionIds(forgedIdsSet); for (const id of forgedIdsSet) { this.pushError(this.accept.get(id).data, "ERR_FORGED", "Already forged."); diff --git a/packages/core-utils/package.json b/packages/core-utils/package.json index f370737a56..28daf23b45 100644 --- a/packages/core-utils/package.json +++ b/packages/core-utils/package.json @@ -24,7 +24,8 @@ "@faustbrian/dato": "^0.3.0", "cli-table3": "^0.5.1", "fast-json-parse": "^1.0.3", - "got": "^9.6.0" + "got": "^9.6.0", + "immutable": "^4.0.0-rc.12" }, "publishConfig": { "access": "public" diff --git a/packages/core-utils/src/index.ts b/packages/core-utils/src/index.ts index 0919d98c40..3ae7180ad5 100644 --- a/packages/core-utils/src/index.ts +++ b/packages/core-utils/src/index.ts @@ -5,6 +5,7 @@ import { hasSomeProperty } from "./has-some-property"; import { httpie, IHttpieResponse } from "./httpie"; import { isBlockChained } from "./is-block-chained"; import { NSect } from "./nsect"; +import { OrderedCappedMap } from "./ordered-capped-map"; import { calculateRound, isNewRound } from "./round-calculator"; import { calculate } from "./supply-calculator"; @@ -12,4 +13,13 @@ export const delegateCalculator = { calculateApproval, calculateForgedTotal }; export const roundCalculator = { calculateRound, isNewRound }; export const supplyCalculator = { calculate }; -export { CappedSet, formatTimestamp, hasSomeProperty, httpie, IHttpieResponse, isBlockChained, NSect }; +export { + CappedSet, + formatTimestamp, + hasSomeProperty, + httpie, + IHttpieResponse, + isBlockChained, + NSect, + OrderedCappedMap, +}; diff --git a/packages/core-utils/src/ordered-capped-map.ts b/packages/core-utils/src/ordered-capped-map.ts new file mode 100644 index 0000000000..c2bf82dd95 --- /dev/null +++ b/packages/core-utils/src/ordered-capped-map.ts @@ -0,0 +1,64 @@ +import { OrderedMap } from "immutable"; + +export class OrderedCappedMap { + protected store: OrderedMap = OrderedMap(); + private maxSize: number; + + constructor(maxSize: number) { + this.resize(maxSize); + } + + public get(key: K): V { + return this.store.get(key); + } + + public set(key: K, value: V): void { + if (this.store.size >= this.maxSize) { + this.store = this.store.delete(this.store.keyOf(this.first())); + } + + this.store = this.store.set(key, value); + } + + public has(key: K): boolean { + return this.store.has(key); + } + + public delete(key: K): boolean { + if (!this.store.has(key)) { + return false; + } + + this.store = this.store.delete(key); + + return !this.store.has(key); + } + + public clear(): void { + this.store = this.store.clear(); + } + + public resize(maxSize: number): void { + this.maxSize = maxSize; + } + + public first(): V { + return this.store.first(); + } + + public last(): V { + return this.store.last(); + } + + public keys(): K[] { + return this.store.keySeq().toArray(); + } + + public values(): V[] { + return this.store.valueSeq().toArray(); + } + + public count(): number { + return this.store.size; + } +}