diff --git a/__tests__/integration/core-forger/client.test.ts b/__tests__/integration/core-forger/client.test.ts index 46ff598d21..2baf2aac02 100644 --- a/__tests__/integration/core-forger/client.test.ts +++ b/__tests__/integration/core-forger/client.test.ts @@ -5,6 +5,7 @@ import { httpie } from "@arkecosystem/core-utils"; import "jest-extended"; import nock from "nock"; import { Client } from "../../../packages/core-forger/src/client"; +import { HostNoResponseError } from "../../../packages/core-forger/src/errors"; import { sampleBlocks } from "./__fixtures__/blocks"; jest.setTimeout(30000); @@ -52,7 +53,7 @@ describe("Client", () => { return requestBody; }); - await client.__chooseHost(); + await client.selectHost(); const wasBroadcasted = await client.broadcast(sampleBlock.toJson()); expect(wasBroadcasted).toBeTruthy(); @@ -84,7 +85,7 @@ describe("Client", () => { .get("/internal/transactions/forging") .reply(200, { data: expectedResponse }); - await client.__chooseHost(); + await client.selectHost(); const response = await client.getTransactions(); expect(response).toEqual(expectedResponse); @@ -100,7 +101,7 @@ describe("Client", () => { .get("/internal/network/state") .reply(200, { data: expectedResponse }); - await client.__chooseHost(); + await client.selectHost(); const response = await client.getNetworkState(); expect(response).toEqual(expectedResponse); @@ -115,23 +116,22 @@ describe("Client", () => { .get("/internal/blockchain/sync") .reply(200); + await client.selectHost(); await client.syncCheck(); expect(httpie.get).toHaveBeenCalledWith(`${host}/internal/blockchain/sync`, expect.any(Object)); }); }); - describe("getUsernames", () => { - it("should fetch usernames", async () => { - jest.spyOn(httpie, "get"); - const expectedResponse = { foo: "bar" }; - nock(host) - .get("/internal/utils/usernames") - .reply(200, { data: expectedResponse }); - - const response = await client.getUsernames(); + describe("selectHost", () => { + it("should fallback to responsive host", async () => { + client = new Client(["http://127.0.0.2:4000", "http://127.0.0.3:4000", host]); + await expect(client.selectHost()).toResolve(); + }); - expect(response).toEqual(expectedResponse); + it("should throw error when no host is responsive", async () => { + client = new Client(["http://127.0.0.2:4000", "http://127.0.0.3:4000"]); + await expect(client.selectHost()).rejects.toThrowError(HostNoResponseError); }); }); @@ -145,7 +145,7 @@ describe("Client", () => { return [200]; }); - await client.__chooseHost(); + await client.selectHost(); await client.emitEvent("foo", "bar"); expect(httpie.post).toHaveBeenCalledWith(`${host}/internal/utils/events`, { @@ -153,7 +153,7 @@ describe("Client", () => { headers: { "Content-Type": "application/json", nethash: {}, - port: "4000", + port: 4000, version: "2.3.0", "x-auth": "forger", }, diff --git a/__tests__/unit/core-forger/manager.test.ts b/__tests__/unit/core-forger/manager.test.ts index d2d89abffe..443b3ce3b1 100644 --- a/__tests__/unit/core-forger/manager.test.ts +++ b/__tests__/unit/core-forger/manager.test.ts @@ -35,18 +35,15 @@ describe("Forger Manager", () => { it("should be ok with configured delegates", async () => { const secret = "a secret"; forgeManager.secrets = [secret]; - // @ts-ignore - forgeManager.client.getUsernames.mockReturnValue([]); const delegates = await forgeManager.loadDelegates(); expect(delegates).toBeArray(); delegates.forEach(value => expect(value).toBeInstanceOf(Delegate)); - expect(forgeManager.client.getUsernames).toHaveBeenCalled(); }); }); - describe("__forgeNewBlock", () => { + describe("forgeNewBlock", () => { it("should forge a block", async () => { // NOTE: make sure we have valid transactions from an existing wallet const transactions = generateTransfer( @@ -68,7 +65,7 @@ describe("Forger Manager", () => { reward: 2 * 1e8, }; - await forgeManager.__forgeNewBlock(del, round, { + await forgeManager.forgeNewBlock(del, round, { lastBlockId: round.lastBlock.id, nodeHeight: round.lastBlock.height, }); @@ -86,7 +83,7 @@ describe("Forger Manager", () => { describe("__monitor", () => { it("should emit failed event if error while monitoring", async () => { - forgeManager.client.getUsernames.mockRejectedValue(new Error("oh bollocks")); + forgeManager.client.getRound.mockRejectedValue(new Error("oh bollocks")); setTimeout(() => forgeManager.stop(), 1000); await forgeManager.__monitor(); @@ -95,12 +92,12 @@ describe("Forger Manager", () => { }); }); - describe("__getTransactionsForForging", () => { + describe("getTransactionsForForging", () => { it("should return zero transactions if none to forge", async () => { // @ts-ignore forgeManager.client.getTransactions.mockReturnValue({}); - const transactions = await forgeManager.__getTransactionsForForging(); + const transactions = await forgeManager.getTransactionsForForging(); expect(transactions).toHaveLength(0); expect(forgeManager.client.getTransactions).toHaveBeenCalled(); @@ -111,7 +108,7 @@ describe("Forger Manager", () => { transactions: [Transaction.fromData(sampleTransaction).serialized.toString("hex")], }); - const transactions = await forgeManager.__getTransactionsForForging(); + const transactions = await forgeManager.getTransactionsForForging(); expect(transactions).toHaveLength(1); expect(forgeManager.client.getTransactions).toHaveBeenCalled(); @@ -120,31 +117,12 @@ describe("Forger Manager", () => { }); }); - describe("__isDelegateActivated", () => { - it("should be ok", async () => { - forgeManager.delegates = [ - { - username: "arkxdev", - publicKey: "0310ad026647eed112d1a46145eed58b8c19c67c505a67f1199361a511ce7860c0", - }, - ]; - - const forger = await forgeManager.__isDelegateActivated( - "0310ad026647eed112d1a46145eed58b8c19c67c505a67f1199361a511ce7860c0", - ); - - expect(forger).toBeObject(); - expect(forger.username).toBe("arkxdev"); - expect(forger.publicKey).toBe("0310ad026647eed112d1a46145eed58b8c19c67c505a67f1199361a511ce7860c0"); - }); - }); - - describe("__parseNetworkState", () => { + describe("parseNetworkState", () => { it("should be TRUE when quorum > 0.66", async () => { const networkState = new NetworkState(NetworkStateStatus.Default); Object.assign(networkState, { getQuorum: () => 0.9, nodeHeight: 100, lastBlockId: "1233443" }); - const canForge = await forgeManager.__parseNetworkState(networkState, delegate); + const canForge = await forgeManager.parseNetworkState(networkState, delegate); expect(canForge).toBeTrue(); }); @@ -153,7 +131,7 @@ describe("Forger Manager", () => { const networkState = new NetworkState(NetworkStateStatus.Unknown); Object.assign(networkState, { getQuorum: () => 1, nodeHeight: 100, lastBlockId: "1233443" }); - const canForge = await forgeManager.__parseNetworkState(networkState, delegate); + const canForge = await forgeManager.parseNetworkState(networkState, delegate); expect(canForge).toBeFalse(); }); @@ -162,21 +140,21 @@ describe("Forger Manager", () => { const networkState = new NetworkState(NetworkStateStatus.Default); Object.assign(networkState, { getQuorum: () => 0.65, nodeHeight: 100, lastBlockId: "1233443" }); - const canForge = await forgeManager.__parseNetworkState(networkState, delegate); + const canForge = await forgeManager.parseNetworkState(networkState, delegate); expect(canForge).toBeFalse(); }); it("should be FALSE when coldStart is active", async () => { const networkState = new NetworkState(NetworkStateStatus.ColdStart); - const canForge = await forgeManager.__parseNetworkState(networkState, delegate); + const canForge = await forgeManager.parseNetworkState(networkState, delegate); expect(canForge).toBeFalse(); }); it("should be FALSE when minimumNetworkReach is not sufficient", async () => { const networkState = new NetworkState(NetworkStateStatus.BelowMinimumPeers); - const canForge = await forgeManager.__parseNetworkState(networkState, delegate); + const canForge = await forgeManager.parseNetworkState(networkState, delegate); expect(canForge).toBeFalse(); }); @@ -200,7 +178,7 @@ describe("Forger Manager", () => { }, }); - const canForge = await forgeManager.__parseNetworkState(networkState, delegate); + const canForge = await forgeManager.parseNetworkState(networkState, delegate); expect(canForge).toBeFalse(); }); }); diff --git a/packages/core-api/src/versions/1/delegates/controller.ts b/packages/core-api/src/versions/1/delegates/controller.ts index 4ee2ce26f1..7af553d2f6 100644 --- a/packages/core-api/src/versions/1/delegates/controller.ts +++ b/packages/core-api/src/versions/1/delegates/controller.ts @@ -95,15 +95,14 @@ export class DelegatesController extends Controller { const delegatesCount = this.config.getMilestone(lastBlock).activeDelegates; const currentSlot = slots.getSlotNumber(lastBlock.data.timestamp); - let activeDelegates = await this.databaseService.getActiveDelegates(lastBlock.data.height); - activeDelegates = activeDelegates.map(delegate => delegate.publicKey); - + const activeDelegates = await this.databaseService.getActiveDelegates(lastBlock.data.height); const nextForgers = []; + for (let i = 1; i <= delegatesCount && i <= limit; i++) { const delegate = activeDelegates[(currentSlot + i) % delegatesCount]; if (delegate) { - nextForgers.push(delegate); + nextForgers.push(delegate.publicKey); } } diff --git a/packages/core-database-postgres/src/repositories/rounds.ts b/packages/core-database-postgres/src/repositories/rounds.ts index fc90b309f5..f8508b5c45 100644 --- a/packages/core-database-postgres/src/repositories/rounds.ts +++ b/packages/core-database-postgres/src/repositories/rounds.ts @@ -11,7 +11,7 @@ export class RoundsRepository extends Repository implements Database.IRoundsRepo * @param {Number} round * @return {Promise} */ - public async findById(round) { + public async findById(round: number): Promise { return this.db.manyOrNone(sql.find, { round }); } @@ -20,7 +20,7 @@ export class RoundsRepository extends Repository implements Database.IRoundsRepo * @param {Number} round * @return {Promise} */ - public async delete(round) { + public async delete(round): Promise { return this.db.none(sql.delete, { round }); } @@ -28,7 +28,7 @@ export class RoundsRepository extends Repository implements Database.IRoundsRepo * Get the model related to this repository. * @return {Round} */ - public getModel() { + public getModel(): Round { return new Round(this.pgp); } } diff --git a/packages/core-database/src/database-service.ts b/packages/core-database/src/database-service.ts index 3f9ff76790..ecc9f769f2 100644 --- a/packages/core-database/src/database-service.ts +++ b/packages/core-database/src/database-service.ts @@ -24,7 +24,7 @@ export class DatabaseService implements Database.IDatabaseService { public blocksInCurrentRound: any[] = null; public stateStarted: boolean = false; public restoredDatabaseIntegrity: boolean = false; - public forgingDelegates: any[] = null; + public forgingDelegates: Database.IDelegateWallet[] = null; public cache: Map = new Map(); constructor( @@ -141,7 +141,7 @@ export class DatabaseService implements Database.IDatabaseService { this.connection.enqueueDeleteRound(height); } - public async getActiveDelegates(height: number, delegates?: any[]) { + public async getActiveDelegates(height: number, delegates?: Database.IDelegateWallet[]) { const maxDelegates = this.config.getMilestone(height).activeDelegates; const round = Math.floor((height - 1) / maxDelegates) + 1; @@ -151,7 +151,9 @@ export class DatabaseService implements Database.IDatabaseService { // When called during applyRound we already know the delegates, so we don't have to query the database. if (!delegates || delegates.length === 0) { - delegates = await this.connection.roundsRepository.findById(round); + delegates = ((await this.connection.roundsRepository.findById( + round, + )) as unknown) as Database.IDelegateWallet[]; } const seedSource = round.toString(); @@ -169,6 +171,7 @@ export class DatabaseService implements Database.IDatabaseService { this.forgingDelegates = delegates.map(delegate => { delegate.round = +delegate.round; + delegate.username = this.walletManager.findByPublicKey(delegate.publicKey).username; return delegate; }); diff --git a/packages/core-database/src/wallet-manager.ts b/packages/core-database/src/wallet-manager.ts index 64da614419..57efe4ecbe 100644 --- a/packages/core-database/src/wallet-manager.ts +++ b/packages/core-database/src/wallet-manager.ts @@ -187,7 +187,7 @@ export class WalletManager implements Database.IWalletManager { * @param height * @return {Array} */ - public loadActiveDelegateList(maxDelegates: number, height?: number): any[] { + public loadActiveDelegateList(maxDelegates: number, height?: number): Database.IDelegateWallet[] { if (height > 1 && height % maxDelegates !== 1) { throw new Error("Trying to build delegates outside of round change"); } @@ -252,7 +252,7 @@ export class WalletManager implements Database.IWalletManager { this.logger.debug(`Loaded ${delegates.length} active ${pluralize("delegate", delegates.length)}`); - return delegates; + return delegates as Database.IDelegateWallet[]; } /** diff --git a/packages/core-forger/package.json b/packages/core-forger/package.json index b528c1af43..ba8dd47de4 100644 --- a/packages/core-forger/package.json +++ b/packages/core-forger/package.json @@ -26,15 +26,12 @@ "@arkecosystem/core-p2p": "^2.3.0-next.3", "@arkecosystem/core-utils": "^2.3.0-next.3", "@arkecosystem/crypto": "^2.3.0-next.3", - "delay": "^4.1.0", "lodash.isempty": "^4.4.0", - "lodash.sample": "^4.2.1", "lodash.uniq": "^4.5.0", "pluralize": "^7.0.0" }, "devDependencies": { "@types/lodash.isempty": "^4.4.6", - "@types/lodash.sample": "^4.2.6", "@types/lodash.uniq": "^4.5.6", "@types/pluralize": "^0.0.29" }, diff --git a/packages/core-forger/src/client.ts b/packages/core-forger/src/client.ts index d1fae486e5..09451d3c06 100644 --- a/packages/core-forger/src/client.ts +++ b/packages/core-forger/src/client.ts @@ -1,15 +1,22 @@ import { app } from "@arkecosystem/core-container"; import { Logger } from "@arkecosystem/core-interfaces"; -import { NetworkState, NetworkStateStatus } from "@arkecosystem/core-p2p"; -import { httpie } from "@arkecosystem/core-utils"; -import delay from "delay"; -import sample from "lodash.sample"; +import { ICurrentRound, IForgingTransactions, IResponse, NetworkState } from "@arkecosystem/core-p2p"; +import { httpie, IHttpieResponse } from "@arkecosystem/core-utils"; +import { ITransactionData, models } from "@arkecosystem/crypto"; import { URL } from "url"; +import { HostNoResponseError, RelayCommunicationError } from "./errors"; export class Client { public hosts: string[]; - private host: any; - private headers: any; + private host: string; + private headers: { + version: string; + port: number; + nethash: string; + "x-auth": "forger"; + "Content-Type": "application/json"; + }; + private logger: Logger.ILogger; /** @@ -28,7 +35,7 @@ export class Client { this.headers = { version: app.getVersion(), - port, + port: +port, nethash: app.getConfig().get("network.nethash"), "x-auth": "forger", "Content-Type": "application/json", @@ -37,102 +44,54 @@ export class Client { /** * Send the given block to the relay. - * @param {(Block|Object)} block - * @return {Object} */ - public async broadcast(block) { + public async broadcast(block: models.IBlockData): Promise> { this.logger.debug( `Broadcasting forged block id:${block.id} at height:${block.height.toLocaleString()} with ${ block.numberOfTransactions } transactions to ${this.host}`, ); - return this.__post(`${this.host}/internal/blocks`, { block }); + return this.post(`${this.host}/internal/blocks`, { block }); } /** * Sends the WAKEUP signal to the to relay hosts to check if synced and sync if necesarry */ - public async syncCheck() { - await this.__chooseHost(); - + public async syncCheck(): Promise { this.logger.debug(`Sending wake-up check to relay node ${this.host}`); - - try { - await this.__get(`${this.host}/internal/blockchain/sync`); - } catch (error) { - this.logger.error(`Could not sync check: ${error.message}`); - } + await this.get(`${this.host}/internal/blockchain/sync`); } /** * Get the current round. - * @return {Object} */ - public async getRound() { - try { - await this.__chooseHost(); - - const response = await this.__get(`${this.host}/internal/rounds/current`); - - return response.body.data; - } catch (e) { - return {}; - } + public async getRound(): Promise { + await this.selectHost(); + const response = await this.get>(`${this.host}/internal/rounds/current`); + return response.body.data; } /** * Get the current network quorum. - * @return {NetworkState} */ public async getNetworkState(): Promise { - try { - const response = await this.__get(`${this.host}/internal/network/state`, 4000); - - return NetworkState.parse(response.body.data); - } catch (e) { - this.logger.error(`Could not retrieve network state: ${this.host}/internal/network/state: ${e.message}`); - return new NetworkState(NetworkStateStatus.Unknown); - } + const response = await this.get>(`${this.host}/internal/network/state`, 4000); + return NetworkState.parse(response.body.data); } /** * Get all transactions that are ready to be forged. - * @return {Object} */ - public async getTransactions() { - try { - const response = await this.__get(`${this.host}/internal/transactions/forging`); - - return response.body.data; - } catch (e) { - return {}; - } - } - - /** - * Get a list of all active delegate usernames. - * @return {Object} - */ - public async getUsernames(wait = 0) { - await this.__chooseHost(wait); - - try { - const response = await this.__get(`${this.host}/internal/utils/usernames`); - - return response.body.data; - } catch (e) { - return {}; - } + public async getTransactions(): Promise { + const response = await this.get>(`${this.host}/internal/transactions/forging`); + return response.body.data; } /** * Emit the given event and payload to the local host. - * @param {String} event - * @param {Object} body - * @return {Object} */ - public async emitEvent(event, body) { + public async emitEvent(event: string, body: string | models.IBlockData | ITransactionData): Promise { // NOTE: Events need to be emitted to the localhost. If you need to trigger // actions on a remote host based on events you should be using webhooks // that get triggered by the events you wish to react to. @@ -142,43 +101,47 @@ export class Client { const host = this.hosts.find(item => allowedHosts.some(allowedHost => item.includes(allowedHost))); if (!host) { - return this.logger.error("Was unable to find any local hosts."); + this.logger.error("emitEvent: unable to find any local hosts."); + return; } - try { - await this.__post(`${host}/internal/utils/events`, { event, body }); - } catch (error) { - this.logger.error(`Failed to emit "${event}" to "${host}"`); - } + await this.post(`${host}/internal/utils/events`, { event, body }); } /** * Chose a responsive host. - * @return {void} */ - public async __chooseHost(wait = 0) { - const host = sample(this.hosts); - - try { - await this.__get(`${host}/peer/status`); - - this.host = host; - } catch (error) { - this.logger.debug(`${host} didn't respond to the forger. Trying another host`); - - if (wait > 0) { - await delay(wait); + public async selectHost(): Promise { + let queriedHosts = 0; + for (const host of this.hosts) { + try { + await this.get(`${host}/peer/status`); + this.host = host; + } catch (error) { + if (queriedHosts === this.hosts.length - 1) { + throw new HostNoResponseError(host); + } else { + this.logger.warn(`Failed to get response from ${host}. Trying another host.`); + } + } finally { + queriedHosts++; } - - await this.__chooseHost(wait); } } - public async __get(url, timeout: number = 2000) { - return httpie.get(url, { headers: this.headers, timeout }); + private async get(url, timeout: number = 2000): Promise> { + try { + return httpie.get(url, { headers: this.headers, timeout }); + } catch (error) { + throw new RelayCommunicationError(url, error.message); + } } - public async __post(url, body) { - return httpie.post(url, { body, headers: this.headers, timeout: 2000 }); + private async post(url, body): Promise> { + try { + return httpie.post(url, { body, headers: this.headers, timeout: 2000 }); + } catch (error) { + throw new RelayCommunicationError(url, error.message); + } } } diff --git a/packages/core-forger/src/errors.ts b/packages/core-forger/src/errors.ts new file mode 100644 index 0000000000..db2c4b2a6c --- /dev/null +++ b/packages/core-forger/src/errors.ts @@ -0,0 +1,31 @@ +// tslint:disable:max-classes-per-file + +export class ForgerError extends Error { + constructor(message: string) { + super(message); + + Object.defineProperty(this, "message", { + enumerable: false, + value: message, + }); + + Object.defineProperty(this, "name", { + enumerable: false, + value: this.constructor.name, + }); + + Error.captureStackTrace(this, this.constructor); + } +} + +export class RelayCommunicationError extends ForgerError { + constructor(endpoint: string, message: string) { + super(`Request to ${endpoint} failed, because of '${message}'.`); + } +} + +export class HostNoResponseError extends ForgerError { + constructor(host: string) { + super(`${host} didn't respond. Trying again later.`); + } +} diff --git a/packages/core-forger/src/index.ts b/packages/core-forger/src/index.ts index 9ca9a319e5..319b548318 100644 --- a/packages/core-forger/src/index.ts +++ b/packages/core-forger/src/index.ts @@ -9,7 +9,7 @@ export const plugin: Container.PluginDescriptor = { alias: "forger", async register(container: Container.IContainer, options) { const forgerManager = new ForgerManager(options); - const forgers = await forgerManager.loadDelegates(options.bip38, options.password); + const forgers = await forgerManager.loadDelegates(options.bip38 as string, options.password as string); const logger = container.resolvePlugin("logger"); if (!forgers) { diff --git a/packages/core-forger/src/manager.ts b/packages/core-forger/src/manager.ts index a5a806f128..664373e651 100644 --- a/packages/core-forger/src/manager.ts +++ b/packages/core-forger/src/manager.ts @@ -1,25 +1,28 @@ import { app } from "@arkecosystem/core-container"; import { Logger } from "@arkecosystem/core-interfaces"; -import { NetworkState, NetworkStateStatus } from "@arkecosystem/core-p2p"; -import { configManager, ITransactionData, models, slots, Transaction } from "@arkecosystem/crypto"; -import delay from "delay"; +import { ICurrentRound, NetworkState, NetworkStateStatus } from "@arkecosystem/core-p2p"; +import { configManager, ITransactionData, models, networks, slots, Transaction } from "@arkecosystem/crypto"; import isEmpty from "lodash.isempty"; import uniq from "lodash.uniq"; import pluralize from "pluralize"; import { Client } from "./client"; +import { HostNoResponseError } from "./errors"; const { Delegate } = models; export class ForgerManager { private logger = app.resolvePlugin("logger"); private config = app.getConfig(); - private secrets: any; - private network: any; - private client: any; - private delegates: any; - private usernames: any; - private isStopped: any; + + private secrets: string[]; + private network: networks.INetwork; + private client: Client; + private delegates: models.Delegate[]; + private usernames: { [key: string]: string }; + private isStopped: boolean; + private round: ICurrentRound; + private initialized: boolean; /** * Create a new forger manager instance. @@ -33,14 +36,11 @@ export class ForgerManager { /** * Load all delegates that forge. - * @param {String} bip38 - * @param {String} password - * @return {Array} */ - public async loadDelegates(bip38, password) { + public async loadDelegates(bip38: string, password: string): Promise { if (!bip38 && (!this.secrets || !this.secrets.length || !Array.isArray(this.secrets))) { this.logger.warn('No delegate found! Please check your "delegates.json" file and try again.'); - return; + return null; } this.secrets = uniq(this.secrets.map(secret => secret.trim())); @@ -52,129 +52,104 @@ export class ForgerManager { this.delegates.push(new Delegate(bip38, this.network, password)); } - await this.__loadUsernames(2000); - - const delegates = this.delegates.map( - delegate => `${this.usernames[delegate.publicKey]} (${delegate.publicKey})`, - ); - - this.logger.debug(`Loaded ${pluralize("delegate", delegates.length, true)}: ${delegates.join(", ")}`); + try { + await this.loadRound(); + } catch (error) { + this.logger.warn("Waiting for a responsive host."); + } return this.delegates; } /** * Start forging on the given node. - * @return {Object} */ - public async startForging() { - const slot = slots.getSlotNumber(); - - while (slots.getSlotNumber() === slot) { - await delay(100); - } - - return this.__monitor(null); + public async startForging(): Promise { + return this.checkLater(slots.getTimeInMsUntilNextSlot()); } /** * Stop forging on the given node. - * @return {void} */ - public async stop() { + public async stop(): Promise { this.isStopped = true; } /** * Monitor the node for any actions that trigger forging. - * @param {Object} round - * @return {Function} */ - public async __monitor(round): Promise { + public async __monitor(): Promise { try { if (this.isStopped) { - return false; + return; } - await this.__loadUsernames(); + await this.loadRound(); - round = await this.client.getRound(); - if (!round.canForge) { - // this.logger.debug('Block already forged in current slot') - // technically it is possible to compute doing shennanigan with arkjs.slots lib - - await delay(200); // basically looping until we lock at beginning of next slot - - return this.__monitor(round); + if (!this.round.canForge) { + // basically looping until we lock at beginning of next slot + return this.checkLater(200); } - const delegate = this.__isDelegateActivated(round.currentForger.publicKey); + const delegate = this.getDelegateByPublicKey(this.round.currentForger.publicKey); if (!delegate) { // this.logger.debug(`Current forging delegate ${ // round.currentForger.publicKey // } is not configured on this node.`) - if (this.__isDelegateActivated(round.nextForger.publicKey)) { - const username = this.usernames[round.nextForger.publicKey]; + if (this.getDelegateByPublicKey(this.round.nextForger.publicKey)) { + const username = this.usernames[this.round.nextForger.publicKey]; this.logger.info( - `Next forging delegate ${username} (${round.nextForger.publicKey}) is active on this node.`, + `Next forging delegate ${username} (${ + this.round.nextForger.publicKey + }) is active on this node.`, ); await this.client.syncCheck(); } - await delay(slots.getTimeInMsUntilNextSlot()); // we will check at next slot - - return this.__monitor(round); + return this.checkLater(slots.getTimeInMsUntilNextSlot()); } const networkState = await this.client.getNetworkState(); - if (networkState.nodeHeight !== round.lastBlock.height) { + if (networkState.nodeHeight !== this.round.lastBlock.height) { this.logger.warn( `The NetworkState height (${networkState.nodeHeight}) and round height (${ - round.lastBlock.height + this.round.lastBlock.height }) are out of sync. This indicates delayed blocks on the network.`, ); } - if (this.__parseNetworkState(networkState, delegate)) { - await this.__forgeNewBlock(delegate, round, networkState); + if (this.parseNetworkState(networkState, delegate)) { + await this.forgeNewBlock(delegate, this.round, networkState); } - await delay(slots.getTimeInMsUntilNextSlot()); // we will check at next slot - - return this.__monitor(round); + return this.checkLater(slots.getTimeInMsUntilNextSlot()); } catch (error) { - // README: The Blockchain is not ready, monitor until it is instead of crashing. - if (error.response && error.response.status === 503) { - this.logger.warn(`Blockchain not ready - ${error.response.status} ${error.response.statusText}`); - - await delay(2000); + if (error instanceof HostNoResponseError) { + this.logger.warn(error.message); + } else { + this.logger.error(JSON.stringify(error.stack)); + this.logger.error(`Forging failed: ${error.message}`); - return this.__monitor(round); - } - - // README: The Blockchain is ready but an action still failed. - this.logger.error(`Forging failed: ${error.message}`); + if (!isEmpty(this.round)) { + this.logger.info( + `Round: ${this.round.current.toLocaleString()}, height: ${this.round.lastBlock.height.toLocaleString()}`, + ); + } - if (!isEmpty(round)) { - this.logger.info( - `Round: ${round.current.toLocaleString()}, height: ${round.lastBlock.height.toLocaleString()}`, - ); + this.client.emitEvent("forger.failed", error.message); } - await delay(2000); // no idea when this will be ok, so waiting 2s before checking again - - this.client.emitEvent("forger.failed", error.message); - - return this.__monitor(round); + // no idea when this will be ok, so waiting 2s before checking again + return this.checkLater(2000); } } /** * Creates new block by the delegate and sends it to relay node for verification and broadcast */ - public async __forgeNewBlock(delegate: models.Delegate, round, networkState: NetworkState) { - const transactions = await this.__getTransactionsForForging(); + public async forgeNewBlock(delegate: models.Delegate, round, networkState: NetworkState) { + const transactions = await this.getTransactionsForForging(); const previousBlock = { id: networkState.lastBlockId, @@ -208,9 +183,8 @@ export class ForgerManager { /** * Gets the unconfirmed transactions from the relay nodes transaction pool */ - public async __getTransactionsForForging(): Promise { + public async getTransactionsForForging(): Promise { const response = await this.client.getTransactions(); - const transactions = response.transactions ? response.transactions.map(serializedTx => Transaction.fromHex(serializedTx).data) : []; @@ -228,21 +202,10 @@ export class ForgerManager { return transactions; } - /** - * Checks if delegate public key is in the loaded (active) delegates list - * @param {Object} PublicKey - * @return {Object} - */ - public __isDelegateActivated(queryPublicKey) { - return this.delegates.find(delegate => delegate.publicKey === queryPublicKey); - } - /** * Parses the given network state and decides if forging is allowed. - * @param {Object} networkState internal response - * @param {Booolean} isAllowedToForge */ - public __parseNetworkState(networkState, currentForger) { + public parseNetworkState(networkState: NetworkState, delegate: models.Delegate): boolean { if (networkState.status === NetworkStateStatus.Unknown) { this.logger.info("Failed to get network state from client. Will not forge."); return false; @@ -269,10 +232,10 @@ export class ForgerManager { ); for (const overHeightBlockHeader of overHeightBlockHeaders) { - if (overHeightBlockHeader.generatorPublicKey === currentForger.publicKey) { - const username = this.usernames[currentForger.publicKey]; + if (overHeightBlockHeader.generatorPublicKey === delegate.publicKey) { + const username = this.usernames[delegate.publicKey]; this.logger.warn( - `Possible double forging delegate: ${username} (${currentForger.publicKey}) - Block: ${ + `Possible double forging delegate: ${username} (${delegate.publicKey}) - Block: ${ overHeightBlockHeader.id }. Will not forge.`, ); @@ -292,10 +255,31 @@ export class ForgerManager { } /** - * Get a list of all active delegate usernames. - * @return {Object} + * Checks if delegate public key is in the loaded (active) delegates list */ - public async __loadUsernames(wait = 0) { - this.usernames = await this.client.getUsernames(wait); + private getDelegateByPublicKey(publicKey: string): models.Delegate | null { + return this.delegates.find(delegate => delegate.publicKey === publicKey); + } + + private async loadRound(): Promise { + this.round = await this.client.getRound(); + + this.usernames = this.round.delegates.reduce( + (acc, delegate) => Object.assign(acc, { [delegate.publicKey]: delegate.username }), + {}, + ); + + if (!this.initialized) { + const delegates = this.delegates.map( + delegate => `${this.usernames[delegate.publicKey]} (${delegate.publicKey})`, + ); + + this.logger.debug(`Loaded ${pluralize("delegate", delegates.length, true)}: ${delegates.join(", ")}`); + this.initialized = true; + } + } + + private checkLater(timeout: number): void { + setTimeout(() => this.__monitor(), timeout); } } diff --git a/packages/core-interfaces/src/core-blockchain/blockchain.ts b/packages/core-interfaces/src/core-blockchain/blockchain.ts index 6c7ac31f7c..048d1153ba 100644 --- a/packages/core-interfaces/src/core-blockchain/blockchain.ts +++ b/packages/core-interfaces/src/core-blockchain/blockchain.ts @@ -124,10 +124,10 @@ export interface IBlockchain { * @return {Object} */ getUnconfirmedTransactions( - blockSize: any, + blockSize: number, ): { - transactions: any[]; - poolSize: any; + transactions: string[]; + poolSize: number; count: number; }; diff --git a/packages/core-interfaces/src/core-database/database-repository/rounds-repository.ts b/packages/core-interfaces/src/core-database/database-repository/rounds-repository.ts index 468ca091c1..a1c4983c01 100644 --- a/packages/core-interfaces/src/core-database/database-repository/rounds-repository.ts +++ b/packages/core-interfaces/src/core-database/database-repository/rounds-repository.ts @@ -1,13 +1,20 @@ import { IRepository } from "./repository"; +export interface IRound { + id: number; + publicKey: string; + balance: string; + round: number; +} + export interface IRoundsRepository extends IRepository { /** * Find a round by its ID. */ - findById(id: number): Promise; + findById(id: number): Promise; /** * Delete the round from the database. */ - delete(id: number): Promise; + delete(id: number): Promise; } diff --git a/packages/core-interfaces/src/core-database/database-service.ts b/packages/core-interfaces/src/core-database/database-service.ts index 2122b700c2..845afeacf7 100644 --- a/packages/core-interfaces/src/core-database/database-service.ts +++ b/packages/core-interfaces/src/core-database/database-service.ts @@ -7,7 +7,7 @@ import { IWalletsBusinessRepository, } from "./business-repository"; import { IDatabaseConnection } from "./database-connection"; -import { IWalletManager } from "./wallet-manager"; +import { IDelegateWallet, IWalletManager } from "./wallet-manager"; export interface IDatabaseService { walletManager: IWalletManager; @@ -36,7 +36,7 @@ export interface IDatabaseService { verifyBlockchain(): Promise<{ valid: boolean; errors: any[] }>; - getActiveDelegates(height: number, delegates?: any[]): Promise; + getActiveDelegates(height: number, delegates?: any[]): Promise; buildWallets(): Promise; diff --git a/packages/core-interfaces/src/core-database/wallet-manager.ts b/packages/core-interfaces/src/core-database/wallet-manager.ts index f692735b1d..93c123e027 100644 --- a/packages/core-interfaces/src/core-database/wallet-manager.ts +++ b/packages/core-interfaces/src/core-database/wallet-manager.ts @@ -21,6 +21,8 @@ export interface IWallet { verifySignatures(transaction: ITransactionData, multisignature: IMultiSignatureAsset): boolean; } +export type IDelegateWallet = IWallet & { rate: number; round: number }; + export interface IWalletManager { logger: Logger.ILogger; @@ -48,7 +50,7 @@ export interface IWalletManager { clear(): void; - loadActiveDelegateList(maxDelegateCount: number, height?: number): any[]; + loadActiveDelegateList(maxDelegateCount: number, height?: number): IDelegateWallet[]; buildVoteBalances(): void; diff --git a/packages/core-p2p/src/index.ts b/packages/core-p2p/src/index.ts index 5c15e09dbd..88319ca6b2 100644 --- a/packages/core-p2p/src/index.ts +++ b/packages/core-p2p/src/index.ts @@ -3,3 +3,4 @@ export * from "./peer"; export * from "./court"; export * from "./plugin"; export * from "./network-state"; +export * from "./server/types"; diff --git a/packages/core-p2p/src/server/types.ts b/packages/core-p2p/src/server/types.ts new file mode 100644 index 0000000000..0ebe68d9ca --- /dev/null +++ b/packages/core-p2p/src/server/types.ts @@ -0,0 +1,22 @@ +import { Database } from "@arkecosystem/core-interfaces"; +import { models } from "@arkecosystem/crypto"; + +export interface IResponse { + data: T; +} + +export interface ICurrentRound { + current: number; + reward: string; + timestamp: number; + delegates: Database.IDelegateWallet[]; + currentForger: Database.IDelegateWallet; + nextForger: Database.IDelegateWallet; + lastBlock: models.IBlockData; + canForge: boolean; +} +export interface IForgingTransactions { + transactions: string[]; + poolSize: number; + count: number; +} diff --git a/packages/core-p2p/src/server/versions/internal/handlers/utils.ts b/packages/core-p2p/src/server/versions/internal/handlers/utils.ts index 2e153079bd..6eab6b6a76 100644 --- a/packages/core-p2p/src/server/versions/internal/handlers/utils.ts +++ b/packages/core-p2p/src/server/versions/internal/handlers/utils.ts @@ -1,36 +1,10 @@ import { app } from "@arkecosystem/core-container"; -import { Blockchain, EventEmitter } from "@arkecosystem/core-interfaces"; +import { EventEmitter } from "@arkecosystem/core-interfaces"; const emitter = app.resolvePlugin("event-emitter"); import * as schema from "../schemas/utils"; -/** - * @type {Object} - */ -export const usernames = { - /** - * @param {Hapi.Request} request - * @param {Hapi.Toolkit} h - * @return {Hapi.Response} - */ - async handler(request, h) { - const blockchain = app.resolvePlugin("blockchain"); - const database = blockchain.database; - const walletManager = database.walletManager; - - const lastBlock = blockchain.getLastBlock(); - const delegates = await database.getActiveDelegates(lastBlock ? lastBlock.data.height + 1 : 1); - - const data = {}; - for (const delegate of delegates) { - data[delegate.publicKey] = walletManager.findByPublicKey(delegate.publicKey).username; - } - - return { data }; - }, -}; - /** * Emit the given event and payload to the local host. * @type {Object} diff --git a/packages/core-p2p/src/server/versions/internal/index.ts b/packages/core-p2p/src/server/versions/internal/index.ts index 243215c5a8..b7be30049a 100644 --- a/packages/core-p2p/src/server/versions/internal/index.ts +++ b/packages/core-p2p/src/server/versions/internal/index.ts @@ -24,7 +24,6 @@ const register = async (server, options) => { { method: "POST", path: "/transactions/verify", ...transactions.verify }, { method: "GET", path: "/transactions/forging", ...transactions.forging }, - { method: "GET", path: "/utils/usernames", ...utils.usernames }, { method: "POST", path: "/utils/events", ...utils.emitEvent }, ]); }; diff --git a/packages/core-utils/src/httpie.ts b/packages/core-utils/src/httpie.ts index 1dbfc2e11b..67057a47a8 100644 --- a/packages/core-utils/src/httpie.ts +++ b/packages/core-utils/src/httpie.ts @@ -32,32 +32,38 @@ export class HttpieError extends Error { } } +export interface IHttpieResponse { + body: T; + headers: { [key: string]: string }; + status: number; +} + class Httpie { - public async get(url: string, opts?): Promise { + public async get(url: string, opts?): Promise> { return this.sendRequest("get", url, opts); } - public async post(url: string, opts?): Promise { + public async post(url: string, opts?): Promise> { return this.sendRequest("post", url, opts); } - public async put(url: string, opts?): Promise { + public async put(url: string, opts?): Promise> { return this.sendRequest("put", url, opts); } - public async patch(url: string, opts?): Promise { + public async patch(url: string, opts?): Promise> { return this.sendRequest("patch", url, opts); } - public async head(url: string, opts?): Promise { + public async head(url: string, opts?): Promise> { return this.sendRequest("head", url, opts); } - public async delete(url: string, opts?): Promise { + public async delete(url: string, opts?): Promise> { return this.sendRequest("delete", url, opts); } - private async sendRequest(method: string, url: string, opts?): Promise { + private async sendRequest(method: string, url: string, opts?): Promise> { if (!opts) { opts = {}; } diff --git a/packages/core-utils/src/index.ts b/packages/core-utils/src/index.ts index 7f6ee374d8..d2f481a050 100644 --- a/packages/core-utils/src/index.ts +++ b/packages/core-utils/src/index.ts @@ -3,7 +3,7 @@ import { CappedSet } from "./capped-set"; import { calculateApproval, calculateForgedTotal } from "./delegate-calculator"; import { formatTimestamp } from "./format-timestamp"; import { hasSomeProperty } from "./has-some-property"; -import { httpie } from "./httpie"; +import { httpie, IHttpieResponse } from "./httpie"; import { NSect } from "./nsect"; import { calculateRound, isNewRound } from "./round-calculator"; import { calculate } from "./supply-calculator"; @@ -18,6 +18,7 @@ export { bignumify, delegateCalculator, formatTimestamp, + IHttpieResponse, httpie, hasSomeProperty, roundCalculator,