From 00dbdc650db868b45ef50e51bb266ddf373ea6b0 Mon Sep 17 00:00:00 2001 From: cgeek <cem.moreau@gmail.com> Date: Sat, 21 Jul 2018 11:29:15 +0200 Subject: [PATCH] [enh] allow WS2P sync on a single WS2P peer --- app/lib/common-libs/constants.ts | 3 + app/lib/common-libs/errors.ts | 2 + app/lib/common-libs/timeout-promise.ts | 18 ++ app/modules/crawler/index.ts | 2 +- app/modules/crawler/lib/pulling.ts | 2 +- .../crawler/lib/sync/P2PSyncDownloader.ts | 168 ++++++++++-------- .../crawler/lib/sync/RemoteSynchronizer.ts | 32 ++-- app/modules/ws2p/lib/WS2PCluster.ts | 8 +- app/modules/ws2p/lib/WS2PServer.ts | 2 +- app/modules/ws2p/lib/constants.ts | 4 +- test/integration/misc/cli.ts | 16 +- test/integration/tools/toolbox.ts | 2 +- 12 files changed, 147 insertions(+), 112 deletions(-) create mode 100644 app/lib/common-libs/timeout-promise.ts diff --git a/app/lib/common-libs/constants.ts b/app/lib/common-libs/constants.ts index 98bef6194..9aa102688 100755 --- a/app/lib/common-libs/constants.ts +++ b/app/lib/common-libs/constants.ts @@ -311,6 +311,9 @@ export const CommonConstants = { BLOCKS_COLLECT_THRESHOLD: 30, // Number of blocks to wait before trimming the loki data DEFAULT_NON_WOT_PEERS_LIMIT: 100, // Number of non-wot peers accepted in our peer document pool + + REJECT_WAIT_FOR_AVAILABLE_NODES_IN_SYNC_AFTER: 20000, // Reject after 20 seconds without any change + REJECT_WAIT_FOR_AVAILABLE_NODES_IN_SYNC_MAX_FAILS: 5, // Maximum number of rejections of waiting for an available node } function exact (regexpContent:string) { diff --git a/app/lib/common-libs/errors.ts b/app/lib/common-libs/errors.ts index 358152eda..a3b754a49 100755 --- a/app/lib/common-libs/errors.ts +++ b/app/lib/common-libs/errors.ts @@ -1,5 +1,7 @@ export enum DataErrors { + REJECT_WAIT_FOR_AVAILABLE_NODES_BUT_CONTINUE, + NO_NODE_FOUND_TO_DOWNLOAD_CHUNK, WRONG_CURRENCY_DETECTED, NO_PEERING_AVAILABLE_FOR_SYNC, REMOTE_HAS_NO_CURRENT_BLOCK, diff --git a/app/lib/common-libs/timeout-promise.ts b/app/lib/common-libs/timeout-promise.ts new file mode 100644 index 000000000..9705d3b2c --- /dev/null +++ b/app/lib/common-libs/timeout-promise.ts @@ -0,0 +1,18 @@ +// Source file from duniter: Crypto-currency software to manage libre currency such as Äž1 +// Copyright (C) 2018 Cedric Moreau <cem.moreau@gmail.com> +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +export function newRejectTimeoutPromise(timeout: number) { + return new Promise((res, rej) => { + setTimeout(rej, timeout) + }) +} diff --git a/app/modules/crawler/index.ts b/app/modules/crawler/index.ts index 9c281406b..25a509dd1 100644 --- a/app/modules/crawler/index.ts +++ b/app/modules/crawler/index.ts @@ -96,7 +96,7 @@ export const CrawlerDependency = { onDatabaseExecute: async (server:Server, conf:ConfDTO, program:any, params:any): Promise<any> => { const source = params[0] let currency = params[1] - const to = params.upTo + const to = program.upTo const HOST_PATTERN = /^[^:/]+(:[0-9]{1,5})?$/ const FILE_PATTERN = /^(\/.+)$/ if (!source || !(source.match(HOST_PATTERN) || source.match(FILE_PATTERN))) { diff --git a/app/modules/crawler/lib/pulling.ts b/app/modules/crawler/lib/pulling.ts index 52af56791..e82e5c2e9 100644 --- a/app/modules/crawler/lib/pulling.ts +++ b/app/modules/crawler/lib/pulling.ts @@ -137,7 +137,7 @@ export abstract class AbstractDAO extends PullingDao { const applyCoroutine = async (peer:PeerDTO, blocks:BlockDTO[]) => { if (blocks.length > 0) { let isFork = localCurrent - // && localCurrent.number !== -1 + && localCurrent.number !== -1 && !(blocks[0].previousHash == localCurrent.hash && blocks[0].number == localCurrent.number + 1); if (!isFork) { diff --git a/app/modules/crawler/lib/sync/P2PSyncDownloader.ts b/app/modules/crawler/lib/sync/P2PSyncDownloader.ts index 3dacd0368..014522747 100644 --- a/app/modules/crawler/lib/sync/P2PSyncDownloader.ts +++ b/app/modules/crawler/lib/sync/P2PSyncDownloader.ts @@ -10,6 +10,12 @@ import {Keypair} from "../../../../lib/dto/ConfDTO"; import {IRemoteContacter} from "./IRemoteContacter"; import {Querable} from "../../../../lib/common-libs/querable"; import {cat} from "shelljs"; +import {ManualPromise, newManualPromise} from "../../../../lib/common-libs/manual-promise" +import {GlobalFifoPromise} from "../../../../service/GlobalFifoPromise" +import {getNanosecondsTime} from "../../../../ProcessCpuProfiler" +import {CommonConstants} from "../../../../lib/common-libs/constants" +import {DataErrors} from "../../../../lib/common-libs/errors" +import {newRejectTimeoutPromise} from "../../../../lib/common-libs/timeout-promise" const makeQuerablePromise = require('querablep'); @@ -17,8 +23,6 @@ export class P2PSyncDownloader implements ISyncDownloader { private PARALLEL_PER_CHUNK = 1; private MAX_DELAY_PER_DOWNLOAD = cliprogram.slow ? 15000 : 5000; - private WAIT_DELAY_WHEN_MAX_DOWNLOAD_IS_REACHED = 3000; - private NO_NODES_AVAILABLE = "No node available for download"; private TOO_LONG_TIME_DOWNLOAD:string private nbBlocksToDownload:number private numberOfChunksToDownload:number @@ -29,6 +33,8 @@ export class P2PSyncDownloader implements ISyncDownloader { private nbDownloading = 0 private lastAvgDelay:number private downloads: { [chunk: number]: any } = {} + private fifoPromise = new GlobalFifoPromise() + private nbWaitFailed = 0 constructor( private currency: string, @@ -49,84 +55,96 @@ export class P2PSyncDownloader implements ISyncDownloader { // Create slots of download, in a ready stage this.lastAvgDelay = this.MAX_DELAY_PER_DOWNLOAD; - } - - /** - * Get a list of P2P nodes to use for download. - * If a node is not yet correctly initialized (we can test a node before considering it good for downloading), then - * this method would not return it. - */ - private async getP2Pcandidates(): Promise<any[]> { - let promises = this.peers.reduce((chosens:Querable<ProfiledNode>[], thePeer, index:number) => { - if (!this.nodes[index]) { - // Create the node - let p = PeerDTO.fromJSONObject(thePeer) - this.nodes[index] = makeQuerablePromise((async () => { - const bmaAPI = p.getBMA() - const ws2pAPI = p.getFirstNonTorWS2P() - const apis: { host: string, port: number, path?: string }[] = [] - const bmaHost = bmaAPI.dns || bmaAPI.ipv4 || bmaAPI.ipv6 - if (bmaAPI.port && bmaHost) { - apis.push({ - port: bmaAPI.port, - host: bmaHost - }) - } - if (ws2pAPI) { - apis.push(ws2pAPI) - } - let syncApi: any = null - try { - syncApi = await RemoteSynchronizer.getSyncAPI(this.currency, apis, this.keypair) - } catch (e) { - } + for (const thePeer of peers) { + // Create the node + let p = PeerDTO.fromJSONObject(thePeer) + this.nodes.push(makeQuerablePromise((async () => { + const bmaAPI = p.getBMA() + const ws2pAPI = p.getFirstNonTorWS2P() + const apis: { isBMA?: boolean, isWS2P?: boolean, host: string, port: number, path?: string }[] = [] + const bmaHost = bmaAPI.dns || bmaAPI.ipv4 || bmaAPI.ipv6 + if (bmaAPI.port && bmaHost) { + apis.push({ + isBMA: true, + port: bmaAPI.port, + host: bmaHost + }) + } + if (ws2pAPI) { + apis.push({ + isWS2P: true, + host: ws2pAPI.host, + port: ws2pAPI.port, + path: ws2pAPI.path, + }) + } + let syncApi: any = null + try { + syncApi = await RemoteSynchronizer.getSyncAPI(this.currency, apis, this.keypair) + const manualp = newManualPromise<boolean>() + manualp.resolve(true) const node: ProfiledNode = { - api: syncApi && syncApi.api, - connected: !!syncApi, + api: syncApi.api, tta: 1, ttas: [], nbSuccess: 1, - excluded: false, - downloading: false, + readyForDownload: manualp, hostName: syncApi && syncApi.api.hostName || '', } if (node.hostName.match(/^(localhost|192|127)/)) { node.tta = this.MAX_DELAY_PER_DOWNLOAD } return node - })()) - chosens.push(this.nodes[index]); - } else { - chosens.push(this.nodes[index]); - } - // Continue - return chosens; - }, []); - const eventuals:ProfiledNode[] = await Promise.all(promises) - const candidates: ProfiledNode[] = eventuals.filter(c => c.connected) as ProfiledNode[] - candidates.forEach((c) => { - c.tta = c.tta || 0; // By default we say a node is super slow to answer - c.ttas = c.ttas || []; // Memorize the answer delays - }); - if (candidates.length === 0) { - throw this.NO_NODES_AVAILABLE; - } - // We remove the nodes impossible to reach (timeout) - let withGoodDelays = Underscore.filter(candidates, (c) => c.tta <= this.MAX_DELAY_PER_DOWNLOAD && !c.excluded && !c.downloading); - if (withGoodDelays.length === 0) { - await new Promise(res => setTimeout(res, this.WAIT_DELAY_WHEN_MAX_DOWNLOAD_IS_REACHED)) // We wait a bit before continuing the downloads - // We reinitialize the nodes - this.nodes = [] - // And try it all again - return this.getP2Pcandidates(); + } catch (e) { + this.logger.warn(e) + return newManualPromise() // Which never resolves, so this node won't be used + } + })())) } - const parallelMax = Math.min(this.PARALLEL_PER_CHUNK, withGoodDelays.length); - withGoodDelays = Underscore.sortBy(withGoodDelays, (c:any) => c.tta); - withGoodDelays = withGoodDelays.slice(0, parallelMax); - // We temporarily augment the tta to avoid asking several times to the same node in parallel - withGoodDelays.forEach((c:any) => c.tta = this.MAX_DELAY_PER_DOWNLOAD); - return withGoodDelays; + } + + private async wait4AnAvailableNode(): Promise<any> { + let promises: Promise<any>[] = this.nodes + return await Promise.race(promises.concat(newRejectTimeoutPromise(CommonConstants.REJECT_WAIT_FOR_AVAILABLE_NODES_IN_SYNC_AFTER) + .catch(() => { + if (this.nbWaitFailed >= CommonConstants.REJECT_WAIT_FOR_AVAILABLE_NODES_IN_SYNC_MAX_FAILS) { + this.logger.error("Impossible sync: no more compliant nodes to download from") + process.exit(2) + } + else { + throw Error(DataErrors[DataErrors.REJECT_WAIT_FOR_AVAILABLE_NODES_BUT_CONTINUE]) + } + }))) + } + + /** + * Get a list of P2P nodes to use for download. + * If a node is not yet correctly initialized (we can test a node before considering it good for downloading), then + * this method would not return it. + */ + private async getP2Pcandidates(): Promise<ProfiledNode[]> { + return this.fifoPromise.pushFIFOPromise('getP2Pcandidates_' + getNanosecondsTime(), async () => { + // We wait to have at least 1 available node + await this.wait4AnAvailableNode() + // We filter on all the available nodes, since serveral can be ready at the same time + const readyNodes:ProfiledNode[] = await Promise.all(this.nodes.filter(p => p.isResolved())) + // We remove the nodes impossible to reach (timeout) + let withGoodDelays = Underscore.filter(readyNodes, (c) => c.tta <= this.MAX_DELAY_PER_DOWNLOAD) + if (withGoodDelays.length === 0) { + readyNodes.map(c => { + if (c.tta >= this.MAX_DELAY_PER_DOWNLOAD) { + c.tta = this.MAX_DELAY_PER_DOWNLOAD - 1 + } + }) + } + const parallelMax = Math.min(this.PARALLEL_PER_CHUNK, withGoodDelays.length) + withGoodDelays = Underscore.sortBy(withGoodDelays, c => c.tta) + withGoodDelays = withGoodDelays.slice(0, parallelMax) + // We temporarily augment the tta to avoid asking several times to the same node in parallel + withGoodDelays.forEach(c => c.tta = this.MAX_DELAY_PER_DOWNLOAD) + return withGoodDelays + }) } /** @@ -143,12 +161,16 @@ export class P2PSyncDownloader implements ISyncDownloader { this.logger.warn('Excluding node %s as it returns unchainable chunks', [lastSupplier.host, lastSupplier.port].join(':')) } let candidates = await this.getP2Pcandidates(); + if (candidates.length === 0) { + this.logger.warn('No node found to download this chunk.') + throw Error(DataErrors[DataErrors.NO_NODE_FOUND_TO_DOWNLOAD_CHUNK]) + } // Book the nodes return await this.raceOrCancelIfTimeout(this.MAX_DELAY_PER_DOWNLOAD, candidates.map(async (node:ProfiledNode) => { try { const start = Date.now(); this.handler[chunkIndex] = node; - node.downloading = true; + node.readyForDownload = newManualPromise() this.nbDownloading++; this.watcher.writeStatus('Getting chunck #' + chunkIndex + '/' + (this.numberOfChunksToDownload - 1) + ' from ' + from + ' to ' + (from + count - 1) + ' on peer ' + node.hostName); let blocks = await node.api.getBlocks(count, from); @@ -173,12 +195,12 @@ export class P2PSyncDownloader implements ISyncDownloader { this.nbDownloadsTried++; this.nbDownloading--; - node.downloading = false; + node.readyForDownload.resolve(true) return blocks; } catch (e) { this.nbDownloading--; - node.downloading = false; + node.readyForDownload.resolve(true) this.nbDownloadsTried++; node.ttas.push(this.MAX_DELAY_PER_DOWNLOAD + 1); // No more ask on this node // Average time to answer @@ -244,7 +266,5 @@ interface ProfiledNode { ttas: number[] nbSuccess: number hostName: string - connected: boolean - excluded: boolean - downloading: boolean + readyForDownload: ManualPromise<boolean> } diff --git a/app/modules/crawler/lib/sync/RemoteSynchronizer.ts b/app/modules/crawler/lib/sync/RemoteSynchronizer.ts index 3628fd6aa..1636ec0fc 100644 --- a/app/modules/crawler/lib/sync/RemoteSynchronizer.ts +++ b/app/modules/crawler/lib/sync/RemoteSynchronizer.ts @@ -31,7 +31,7 @@ import {pullSandboxToLocalServer} from "../sandbox" import * as path from 'path' import {IRemoteContacter} from "./IRemoteContacter"; import {BMARemoteContacter} from "./BMARemoteContacter"; -import {WS2PConnection, WS2PPubkeyLocalAuth, WS2PPubkeyRemoteAuth} from "../../../ws2p/lib/WS2PConnection"; +import {WS2PConnection, WS2PPubkeyRemoteAuth, WS2PPubkeySyncLocalAuth} from "../../../ws2p/lib/WS2PConnection"; import {WS2PRequester} from "../../../ws2p/lib/WS2PRequester"; import {WS2PMessageHandler} from "../../../ws2p/lib/impl/WS2PMessageHandler"; import {WS2PResponse} from "../../../ws2p/lib/impl/WS2PResponse"; @@ -114,7 +114,7 @@ export class RemoteSynchronizer extends AbstractSynchronizer { ;(this.node as any).pubkey = this.peer.pubkey } - public static async getSyncAPI(currency: string, hosts: { host: string, port: number, path?: string }[], keypair: Keypair) { + public static async getSyncAPI(currency: string, hosts: { isBMA?: boolean, isWS2P?: boolean, host: string, port: number, path?: string }[], keypair: Keypair) { let api: IRemoteContacter|undefined let peering: any for (const access of hosts) { @@ -122,16 +122,20 @@ export class RemoteSynchronizer extends AbstractSynchronizer { const port = access.port const path = access.path logger.info(`Connecting to address ${host} :${port}...`) - try { - const contacter = await connect(PeerDTO.fromJSONObject({ endpoints: [`BASIC_MERKLED_API ${host} ${port}${path && ' ' + path || ''}`]}), 3000) - peering = await contacter.getPeer() - api = new BMARemoteContacter(contacter) - } catch (e) { - logger.warn(`Node does not support BMA at address ${host} :${port}, trying WS2P...`) + + // If we know this is a WS2P connection, don't try BMA + if (access.isWS2P !== true) { + try { + const contacter = await connect(PeerDTO.fromJSONObject({ endpoints: [`BASIC_MERKLED_API ${host} ${port}${path && ' ' + path || ''}`]}), 3000) + peering = await contacter.getPeer() + api = new BMARemoteContacter(contacter) + } catch (e) { + logger.warn(`Node does not support BMA at address ${host} :${port}, trying WS2P...`) + } } - // If BMA is unreachable, let's try WS2P - if (!api) { + // If BMA is unreachable and the connection is not marked as strict BMA, let's try WS2P + if (!api && access.isBMA !== true) { const pair = KeyGen(keypair.pub, keypair.sec) const connection = WS2PConnection.newConnectionToAddress(1, `ws://${host}:${port}${path && ' ' + path || ''}`, @@ -143,13 +147,9 @@ export class RemoteSynchronizer extends AbstractSynchronizer { logger.warn('Receiving push messages, which are not allowed during a SYNC.', json) } }), - new WS2PPubkeyLocalAuth(currency, pair, '00000000'), + new WS2PPubkeySyncLocalAuth(currency, pair, '00000000'), new WS2PPubkeyRemoteAuth(currency, pair), - undefined, - { - connectionTimeout: 1500, - requestTimeout: 1500, - } + undefined ) try { const requester = WS2PRequester.fromConnection(connection) diff --git a/app/modules/ws2p/lib/WS2PCluster.ts b/app/modules/ws2p/lib/WS2PCluster.ts index 372bac013..4456010c4 100644 --- a/app/modules/ws2p/lib/WS2PCluster.ts +++ b/app/modules/ws2p/lib/WS2PCluster.ts @@ -772,13 +772,13 @@ export class WS2PCluster { // Sync case is specific if (isSync) { + // OK for reconnection period of time + if (this.ok4reconnect[pub]) { + return true + } if (this.banned4Sync[pub]) { return false } - // Already connected - if (syncConnectedPubkeys.indexOf(pub) !== -1) { - return !!this.ok4reconnect[pub] - } const limit = (this.server.conf.ws2p && this.server.conf.ws2p.syncLimit) || WS2PConstants.WS2P_SYNC_LIMIT const ok = syncConnectedPubkeys.length < limit if (ok) { diff --git a/app/modules/ws2p/lib/WS2PServer.ts b/app/modules/ws2p/lib/WS2PServer.ts index 9ee4847df..1e11603ce 100644 --- a/app/modules/ws2p/lib/WS2PServer.ts +++ b/app/modules/ws2p/lib/WS2PServer.ts @@ -129,7 +129,7 @@ export class WS2PServer extends events.EventEmitter { } }) // We close the connection after a given delay - setTimeout(() => c.close(), WS2PConstants.SYNC_CONNECTION_DURATION_IN_SECONDS) + setTimeout(() => c.close(), 1000 * WS2PConstants.SYNC_CONNECTION_DURATION_IN_SECONDS) // We don't broadcast or pipe data return } diff --git a/app/modules/ws2p/lib/constants.ts b/app/modules/ws2p/lib/constants.ts index ac58306db..0782d6b52 100644 --- a/app/modules/ws2p/lib/constants.ts +++ b/app/modules/ws2p/lib/constants.ts @@ -57,7 +57,7 @@ export const WS2PConstants = { }, BAN_DURATION_IN_SECONDS: 120, - SYNC_BAN_DURATION_IN_SECONDS: 240, + SYNC_BAN_DURATION_IN_SECONDS: 2400, BAN_ON_REPEAT_THRESHOLD: 5, ERROR_RECALL_DURATION_IN_SECONDS: 60, SINGLE_RECORD_PROTECTION_IN_SECONDS: 60, @@ -98,5 +98,5 @@ export const WS2PConstants = { HEADS_SPREAD_TIMEOUT: 100, // Wait 100ms before sending a bunch of signed heads WS2P_SYNC_LIMIT: 15, // Number of concurrent peers for sync - SYNC_CONNECTION_DURATION_IN_SECONDS: 120, // Duration of the SYNC connection + SYNC_CONNECTION_DURATION_IN_SECONDS: 1200, // Duration of the SYNC connection } \ No newline at end of file diff --git a/test/integration/misc/cli.ts b/test/integration/misc/cli.ts index 3f22278ec..e7a1b3c8f 100644 --- a/test/integration/misc/cli.ts +++ b/test/integration/misc/cli.ts @@ -11,9 +11,7 @@ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. -import {MerkleDTO} from "../../../app/lib/dto/MerkleDTO" import {hashf} from "../../../app/lib/common" -import {processForURL} from "../../../app/lib/helpers/merkle" import {fakeSyncServer} from "../tools/toolbox" import {Underscore} from "../../../app/lib/common-libs/underscore" @@ -39,13 +37,7 @@ describe("CLI", function() { */ const onReadBlockchainChunk = (count:number, from:number) => Promise.resolve(blockchain.blocks.slice(from, from + count)); const onReadParticularBlock = (number:number) => Promise.resolve(blockchain.blocks[number]); - const onPeersRequested = async (req:any) => { - const merkle = new MerkleDTO(); - merkle.initialize(leaves); - return processForURL(req, merkle, async () => { - return peersMap; - }) - } + const onPeersRequested = async () => [] /** * The fake hash in the blockchain @@ -134,14 +126,14 @@ describe("CLI", function() { it('sync 7 blocks (fast)', async () => { await execute(['reset', 'data']); - await execute(['sync', fakeServer.host + ':' + String(fakeServer.port), '7', 'duniter_unit_test_currency', '--nocautious', '--nointeractive', '--noshuffle']); + await execute(['sync', fakeServer.host + ':' + String(fakeServer.port), 'duniter_unit_test_currency', '--nocautious', '--nointeractive', '--noshuffle', '--up-to', '7']); const res = await execute(['export-bc', '--nostdout']); res[res.length - 1].should.have.property('number').equal(7); res.should.have.length(7 + 1); // blocks #0..#7 }) it('sync 4 blocks (cautious)', async () => { - await execute(['sync', fakeServer.host + ':' + String(fakeServer.port), '11', 'duniter_unit_test_currency', '--nointeractive']); + await execute(['sync', fakeServer.host + ':' + String(fakeServer.port), 'duniter_unit_test_currency', '--nointeractive', '--up-to', '11']); const res = await execute(['export-bc', '--nostdout']); res[res.length - 1].should.have.property('number').equal(11); res.should.have.length(11 + 1); @@ -154,7 +146,7 @@ describe("CLI", function() { }) it('[spawn] sync 10 first blocks --memory', async () => { - await execute(['sync', fakeServer.host + ':' + String(fakeServer.port), '10', 'duniter_unit_test_currency', '--memory', '--cautious', '--nointeractive']); + await execute(['sync', fakeServer.host + ':' + String(fakeServer.port), 'duniter_unit_test_currency', '--memory', '--cautious', '--nointeractive', '--up-to', '10']); }) }); diff --git a/test/integration/tools/toolbox.ts b/test/integration/tools/toolbox.ts index e4e7cf88d..907414f1a 100644 --- a/test/integration/tools/toolbox.ts +++ b/test/integration/tools/toolbox.ts @@ -188,7 +188,7 @@ export const fakeSyncServer = async (currency: string, readBlocksMethod:any, rea }, noLimit); // Mock BMA method for sync mocking - httpMethods.httpGET('/network/peering/peers', onPeersRequested, noLimit); + httpMethods.httpGET('/network/peers', onPeersRequested, noLimit); // Another mock BMA method for sync mocking httpMethods.httpGET('/blockchain/blocks/:count/:from', (req:any) => { -- GitLab