From 4f8043d259e7db535d50d2c26e86694b012f9db4 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Mon, 30 Sep 2019 18:20:25 +0200 Subject: [PATCH 01/92] Add streaming iterables guide (#459) * docs: add streaming iterables guide placeholder * chore: move peer discovery readme to doc fold:wqer * docs: add link to async refactor issue --- README.md | 2 +- PEER_DISCOVERY.md => doc/PEER_DISCOVERY.md | 0 doc/STREAMING_ITERABLES.md | 32 ++++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) rename PEER_DISCOVERY.md => doc/PEER_DISCOVERY.md (100%) create mode 100644 doc/STREAMING_ITERABLES.md diff --git a/README.md b/README.md index d9f75882dd..de08e1b0c0 100644 --- a/README.md +++ b/README.md @@ -336,7 +336,7 @@ Required keys in the `options` object: > Peer has been discovered. If `autoDial` is `true`, applications should **not** attempt to connect to the peer -unless they are performing a specific action. See [peer discovery and auto dial](./PEER_DISCOVERY.md) for more information. +unless they are performing a specific action. See [peer discovery and auto dial](./doc/PEER_DISCOVERY.md) for more information. - `peer`: instance of [PeerInfo][] diff --git a/PEER_DISCOVERY.md b/doc/PEER_DISCOVERY.md similarity index 100% rename from PEER_DISCOVERY.md rename to doc/PEER_DISCOVERY.md diff --git a/doc/STREAMING_ITERABLES.md b/doc/STREAMING_ITERABLES.md new file mode 100644 index 0000000000..a58727be9e --- /dev/null +++ b/doc/STREAMING_ITERABLES.md @@ -0,0 +1,32 @@ +# Iterable Streams + +> This document is a guide on how to use Iterable Streams in Libp2p. As a part of the [refactor away from callbacks](https://github.com/ipfs/js-ipfs/issues/1670), we have also moved to using Iterable Streams instead of [pull-streams](https://pull-stream.github.io/). If there are missing usage guides you feel should be added, please submit a PR! + +## Table of Contents + +- [Iterable Streams](#iterable-streams) + - [Table of Contents](#table-of-contents) + - [Usage Guide](#usage-guide) + - [Iterable Modules](#iterable-modules) + +## Usage Guide + +**Coming Soon!** + +## Iterable Modules + +- [it-handshake][it-handshake] Handshakes for binary protocols with iterable streams. +- [it-length-prefixed][it-length-prefixed] Streaming length prefixed buffers with async iterables. +- [it-pair][it-pair] Paired streams that are internally connected. +- [it-pipe][it-pipe] Create a pipeline of iterables. Works with duplex streams. +- [it-pushable][it-pushable] An iterable that you can push values into. +- [it-reader][it-reader] Read an exact number of bytes from a binary, async iterable. +- [streaming-iterables][streaming-iterables] A Swiss army knife for async iterables. + +[it-handshake]: https://github.com/jacobheun/it-handshake +[it-length-prefixed]: https://github.com/alanshaw/it-length-prefixed +[it-pair]: https://github.com/alanshaw/it-pair +[it-pipe]: https://github.com/alanshaw/it-pipe +[it-pushable]: https://github.com/alanshaw/it-pushable +[it-reader]: https://github.com/alanshaw/it-reader +[streaming-iterables]: https://github.com/bustle/streaming-iterables From a7d5e67e06a5a147ceab57f29e1027d80fc09629 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Wed, 2 Oct 2019 13:31:28 +0200 Subject: [PATCH 02/92] refactor(async): update transports subsystem (#461) * test: remove all tests for a clean slate The refactor will require a large number of updates to the tests. In order to ensure we have done a decent deduplication, and have a cleaner suite of tests we've removed all tests. This will also allow us to more easily see tests for the refactored systems. We have a record of the latest test suites in master, so we are not losing any history. * chore: update tcp and websockets * chore: remove other transports until they are converted * chore: use mafmt and multiaddr async versions * chore: add and fix dependencies * chore: clean up travis file * feat: add new transport manager * docs: add constructor jsdocs * refactor(config): check that transports exist This also removes the other logic, it can be added when those subsystems are refactored * chore(deps): use async peer-id and peer-info * feat: wire up the transport manager with libp2p * chore: remove superstruct dep --- .aegir.js | 131 +-- .travis.yml | 6 +- package.json | 24 +- src/config.js | 63 +- src/errors.js | 6 +- src/index.js | 168 +--- src/transport-manager.js | 177 ++++ test/browser.js | 6 - test/circuit-relay.browser.js | 93 -- test/circuit-relay.node.js | 215 ----- test/circuit/dialer.spec.js | 303 ------ test/circuit/fixtures/nodes.js | 25 - test/circuit/helpers/test-node.js | 22 - test/circuit/helpers/utils.js | 78 -- test/circuit/hop.spec.js | 433 --------- test/circuit/listener.spec.js | 292 ------ test/circuit/proto.spec.js | 50 - test/circuit/stop.spec.js | 85 -- test/config.spec.js | 358 ------- test/connection-manager/default.js | 19 - test/connection-manager/max-data.js | 36 - .../max-event-loop-delay.js | 59 -- .../max-peer-per-protocol.js | 37 - test/connection-manager/max-peers.js | 35 - test/connection-manager/max-received-data.js | 36 - test/connection-manager/max-sent-data.js | 36 - test/connection-manager/node.js | 10 - test/connection-manager/set-peer-value.js | 44 - test/connection-manager/utils/connect-all.js | 17 - .../utils/create-libp2p-node.js | 50 - test/connection-manager/utils/prepare.js | 83 -- .../utils/try-connect-all.js | 27 - test/content-routing.node.js | 404 -------- test/create.spec.js | 143 --- test/dht.node.js | 168 ---- test/fixtures/browser.js | 7 + test/fixtures/peers.js | 27 + test/fixtures/test-data/test-id.json | 5 - test/fixtures/test-peer.json | 5 - test/fsm.spec.js | 168 ---- test/get-peer-info.spec.js | 134 --- test/identify/basic.spec.js | 15 - test/identify/dialer.spec.js | 192 ---- test/identify/listener.spec.js | 70 -- test/multiaddr-trim.node.js | 41 - test/node.js | 17 +- test/peer-discovery.node.js | 494 ---------- test/peer-routing.node.js | 294 ------ test/ping.node.js | 61 -- test/ping/node.js | 3 - test/ping/test-ping.js | 118 --- test/pnet.node.js | 92 -- test/pnet/fixtures/peer-a.json | 5 - test/pnet/fixtures/peer-b.json | 5 - test/pnet/pnet.spec.js | 105 -- test/promisify.node.js | 87 -- test/pubsub.node.js | 467 --------- test/stats.js | 29 - test/stream-muxing.node.js | 338 ------- test/switch/browser.js | 12 - test/switch/connection.node.js | 451 --------- test/switch/constructor.spec.js | 15 - test/switch/dial-fsm.node.js | 405 -------- test/switch/dialSelf.spec.js | 88 -- test/switch/dialer.spec.js | 230 ----- test/switch/identify.node.js | 173 ---- test/switch/limit-dialer.node.js | 93 -- test/switch/node.js | 13 - test/switch/pnet.node.js | 152 --- test/switch/secio.node.js | 116 --- test/switch/stats.node.js | 280 ------ test/switch/stream-muxers.node.js | 155 --- .../swarm-muxing+webrtc-star.browser.js | 153 --- .../switch/swarm-muxing+websockets.browser.js | 74 -- test/switch/swarm-muxing.node.js | 248 ----- test/switch/swarm-no-muxing.node.js | 90 -- test/switch/switch.spec.js | 37 - test/switch/t-webrtc-star.browser.js | 83 -- test/switch/test-data/id-1.json | 5 - test/switch/test-data/id-2.json | 5 - test/switch/test-data/ids.json | 904 ------------------ test/switch/transport-manager.spec.js | 295 ------ test/switch/transports.browser.js | 52 - test/switch/transports.node.js | 237 ----- test/switch/utils.js | 76 -- test/transports.browser.js | 450 --------- test/transports.node.js | 723 -------------- test/transports/transport-manager.node.js | 56 ++ test/transports/transport-manager.spec.js | 139 +++ test/utils/bundle-browser.js | 103 -- test/utils/bundle-nodejs.js | 96 -- test/utils/constants.js | 41 - test/utils/create-node.js | 41 - test/utils/echo.js | 11 - test/utils/mockUpgrader.js | 6 + test/utils/try-echo.js | 21 - 96 files changed, 482 insertions(+), 12165 deletions(-) create mode 100644 src/transport-manager.js delete mode 100644 test/browser.js delete mode 100644 test/circuit-relay.browser.js delete mode 100644 test/circuit-relay.node.js delete mode 100644 test/circuit/dialer.spec.js delete mode 100644 test/circuit/fixtures/nodes.js delete mode 100644 test/circuit/helpers/test-node.js delete mode 100644 test/circuit/helpers/utils.js delete mode 100644 test/circuit/hop.spec.js delete mode 100644 test/circuit/listener.spec.js delete mode 100644 test/circuit/proto.spec.js delete mode 100644 test/circuit/stop.spec.js delete mode 100644 test/config.spec.js delete mode 100644 test/connection-manager/default.js delete mode 100644 test/connection-manager/max-data.js delete mode 100644 test/connection-manager/max-event-loop-delay.js delete mode 100644 test/connection-manager/max-peer-per-protocol.js delete mode 100644 test/connection-manager/max-peers.js delete mode 100644 test/connection-manager/max-received-data.js delete mode 100644 test/connection-manager/max-sent-data.js delete mode 100644 test/connection-manager/node.js delete mode 100644 test/connection-manager/set-peer-value.js delete mode 100644 test/connection-manager/utils/connect-all.js delete mode 100644 test/connection-manager/utils/create-libp2p-node.js delete mode 100644 test/connection-manager/utils/prepare.js delete mode 100644 test/connection-manager/utils/try-connect-all.js delete mode 100644 test/content-routing.node.js delete mode 100644 test/create.spec.js delete mode 100644 test/dht.node.js create mode 100644 test/fixtures/browser.js create mode 100644 test/fixtures/peers.js delete mode 100644 test/fixtures/test-data/test-id.json delete mode 100644 test/fixtures/test-peer.json delete mode 100644 test/fsm.spec.js delete mode 100644 test/get-peer-info.spec.js delete mode 100644 test/identify/basic.spec.js delete mode 100644 test/identify/dialer.spec.js delete mode 100644 test/identify/listener.spec.js delete mode 100644 test/multiaddr-trim.node.js delete mode 100644 test/peer-discovery.node.js delete mode 100644 test/peer-routing.node.js delete mode 100644 test/ping.node.js delete mode 100644 test/ping/node.js delete mode 100644 test/ping/test-ping.js delete mode 100644 test/pnet.node.js delete mode 100644 test/pnet/fixtures/peer-a.json delete mode 100644 test/pnet/fixtures/peer-b.json delete mode 100644 test/pnet/pnet.spec.js delete mode 100644 test/promisify.node.js delete mode 100644 test/pubsub.node.js delete mode 100644 test/stats.js delete mode 100644 test/stream-muxing.node.js delete mode 100644 test/switch/browser.js delete mode 100644 test/switch/connection.node.js delete mode 100644 test/switch/constructor.spec.js delete mode 100644 test/switch/dial-fsm.node.js delete mode 100644 test/switch/dialSelf.spec.js delete mode 100644 test/switch/dialer.spec.js delete mode 100644 test/switch/identify.node.js delete mode 100644 test/switch/limit-dialer.node.js delete mode 100644 test/switch/node.js delete mode 100644 test/switch/pnet.node.js delete mode 100644 test/switch/secio.node.js delete mode 100644 test/switch/stats.node.js delete mode 100644 test/switch/stream-muxers.node.js delete mode 100644 test/switch/swarm-muxing+webrtc-star.browser.js delete mode 100644 test/switch/swarm-muxing+websockets.browser.js delete mode 100644 test/switch/swarm-muxing.node.js delete mode 100644 test/switch/swarm-no-muxing.node.js delete mode 100644 test/switch/switch.spec.js delete mode 100644 test/switch/t-webrtc-star.browser.js delete mode 100644 test/switch/test-data/id-1.json delete mode 100644 test/switch/test-data/id-2.json delete mode 100644 test/switch/test-data/ids.json delete mode 100644 test/switch/transport-manager.spec.js delete mode 100644 test/switch/transports.browser.js delete mode 100644 test/switch/transports.node.js delete mode 100644 test/switch/utils.js delete mode 100644 test/transports.browser.js delete mode 100644 test/transports.node.js create mode 100644 test/transports/transport-manager.node.js create mode 100644 test/transports/transport-manager.spec.js delete mode 100644 test/utils/bundle-browser.js delete mode 100644 test/utils/bundle-nodejs.js delete mode 100644 test/utils/constants.js delete mode 100644 test/utils/create-node.js delete mode 100644 test/utils/echo.js create mode 100644 test/utils/mockUpgrader.js delete mode 100644 test/utils/try-echo.js diff --git a/.aegir.js b/.aegir.js index d562f4be70..6895593bd0 100644 --- a/.aegir.js +++ b/.aegir.js @@ -1,130 +1,23 @@ 'use strict' -const pull = require('pull-stream') -const WebSocketStarRendezvous = require('libp2p-websocket-star-rendezvous') -const sigServer = require('libp2p-webrtc-star/src/sig-server') -const promisify = require('promisify-es6') -const mplex = require('pull-mplex') -const spdy = require('libp2p-spdy') -const PeerBook = require('peer-book') -const PeerId = require('peer-id') -const PeerInfo = require('peer-info') -const path = require('path') -const Switch = require('./src/switch') -const WebSockets = require('libp2p-websockets') - -const Node = require('./test/utils/bundle-nodejs.js') -const { - getPeerRelay, - WRTC_RENDEZVOUS_MULTIADDR, - WS_RENDEZVOUS_MULTIADDR -} = require('./test/utils/constants') - -let wrtcRendezvous -let wsRendezvous -let node -let peerInfo -let switchA -let switchB - -function echo (protocol, conn) { pull(conn, conn) } -function idJSON (id) { - const p = path.join(__dirname, `./test/switch/test-data/id-${id}.json`) - return require(p) -} +const TransportManager = require('./src/transport-manager') +const mockUpgrader = require('./test/utils/mockUpgrader') +const { MULTIADDRS_WEBSOCKETS } = require('./test/fixtures/browser') +let tm -function createSwitchA () { - return new Promise((resolve, reject) => { - PeerId.createFromJSON(idJSON(1), (err, id) => { - if (err) { return reject(err) } - - const peerA = new PeerInfo(id) - const maA = '/ip4/127.0.0.1/tcp/15337/ws' - - peerA.multiaddrs.add(maA) - const sw = new Switch(peerA, new PeerBook()) - - sw.transport.add('ws', new WebSockets()) - sw.start((err) => { - if (err) { return reject(err) } - resolve(sw) - }) - }) - }) -} - -function createSwitchB () { - return new Promise((resolve, reject) => { - PeerId.createFromJSON(idJSON(2), (err, id) => { - if (err) { return reject(err) } - - const peerB = new PeerInfo(id) - const maB = '/ip4/127.0.0.1/tcp/15347/ws' - - peerB.multiaddrs.add(maB) - const sw = new Switch(peerB, new PeerBook()) - - sw.transport.add('ws', new WebSockets()) - sw.connection.addStreamMuxer(mplex) - sw.connection.addStreamMuxer(spdy) - sw.connection.reuse() - sw.handle('/echo/1.0.0', echo) - sw.start((err) => { - if (err) { return reject(err) } - resolve(sw) - }) - }) - }) -} +const WebSockets = require('libp2p-websockets') const before = async () => { - [ - wrtcRendezvous, - wsRendezvous, - peerInfo, - switchA, - switchB - ] = await Promise.all([ - sigServer.start({ - port: WRTC_RENDEZVOUS_MULTIADDR.nodeAddress().port - // cryptoChallenge: true TODO: needs https://github.com/libp2p/js-libp2p-webrtc-star/issues/128 - }), - WebSocketStarRendezvous.start({ - port: WS_RENDEZVOUS_MULTIADDR.nodeAddress().port, - refreshPeerListIntervalMS: 1000, - strictMultiaddr: false, - cryptoChallenge: true - }), - getPeerRelay(), - createSwitchA(), - createSwitchB() - ]) - - node = new Node({ - peerInfo, - config: { - relay: { - enabled: true, - hop: { - enabled: true, - active: true - } - } - } + tm = new TransportManager({ + upgrader: mockUpgrader, + onConnection: () => {} }) - - node.handle('/echo/1.0.0', (protocol, conn) => pull(conn, conn)) - await node.start() + tm.add(WebSockets.prototype[Symbol.toStringTag], WebSockets) + await tm.listen(MULTIADDRS_WEBSOCKETS) } -const after = () => { - return Promise.all([ - wrtcRendezvous.stop(), - wsRendezvous.stop(), - node.stop(), - promisify(switchA.stop, { context: switchA })(), - promisify(switchB.stop, { context: switchB })() - ]) +const after = async () => { + await tm.close() } module.exports = { diff --git a/.travis.yml b/.travis.yml index 8fe36cb4f8..5eb222c1ec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,16 +29,14 @@ jobs: addons: chrome: stable script: - - npx aegir test -t browser - - npx aegir test -t webworker + - npx aegir test -t browser -t webworker - stage: test name: firefox addons: firefox: latest script: - - npx aegir test -t browser -- --browsers FirefoxHeadless - - npx aegir test -t webworker -- --browsers FirefoxHeadless + - npx aegir test -t browser -t webworker -- --browsers FirefoxHeadless notifications: email: false \ No newline at end of file diff --git a/package.json b/package.json index 406134ff68..db94adc390 100644 --- a/package.json +++ b/package.json @@ -36,9 +36,6 @@ }, "homepage": "https://libp2p.io", "license": "MIT", - "browser": { - "./test/utils/bundle-nodejs": "./test/utils/bundle-browser" - }, "engines": { "node": ">=10.0.0", "npm": ">=6.0.0" @@ -54,25 +51,24 @@ "interface-connection": "~0.3.3", "latency-monitor": "~0.2.1", "libp2p-crypto": "^0.16.2", - "libp2p-websockets": "^0.12.2", - "mafmt": "^6.0.7", + "mafmt": "^7.0.0", "merge-options": "^1.0.1", "moving-average": "^1.0.0", - "multiaddr": "^6.1.0", + "multiaddr": "^7.1.0", "multistream-select": "~0.14.6", "once": "^1.4.0", + "p-settle": "^3.1.0", "peer-book": "^0.9.1", - "peer-id": "^0.12.2", - "peer-info": "~0.15.1", + "peer-id": "^0.13.3", + "peer-info": "^0.17.0", + "promisify-es6": "^1.0.3", + "protons": "^1.0.1", "pull-cat": "^1.1.11", "pull-defer": "~0.2.3", "pull-handshake": "^1.1.4", "pull-reader": "^1.3.1", "pull-stream": "^3.6.9", - "promisify-es6": "^1.0.3", - "protons": "^1.0.1", "retimer": "^2.0.0", - "superstruct": "^0.6.0", "xsalsa20": "^1.0.2" }, "devDependencies": { @@ -96,10 +92,8 @@ "libp2p-pnet": "~0.1.0", "libp2p-secio": "^0.11.1", "libp2p-spdy": "^0.13.2", - "libp2p-tcp": "^0.13.0", - "libp2p-webrtc-star": "^0.16.1", - "libp2p-websocket-star": "~0.10.2", - "libp2p-websocket-star-rendezvous": "~0.4.1", + "libp2p-tcp": "^0.14.1", + "libp2p-websockets": "^0.13.0", "lodash.times": "^4.3.2", "nock": "^10.0.6", "portfinder": "^1.0.20", diff --git a/src/config.js b/src/config.js index 587167cf5f..8f87b602b9 100644 --- a/src/config.js +++ b/src/config.js @@ -1,8 +1,6 @@ 'use strict' const mergeOptions = require('merge-options') -const { struct, superstruct } = require('superstruct') -const { optional, list } = struct const DefaultConfig = { connectionManager: { @@ -38,67 +36,10 @@ const DefaultConfig = { } } -// Define custom types -const s = superstruct({ - types: { - transport: value => { - if (value.length === 0) return 'ERROR_EMPTY' - value.forEach(i => { - if (!i.dial) return 'ERR_NOT_A_TRANSPORT' - }) - return true - }, - protector: value => { - if (!value.protect) return 'ERR_NOT_A_PROTECTOR' - return true - } - } -}) - -const modulesSchema = s({ - connEncryption: optional(list([s('object|function')])), - // this is hacky to simulate optional because interface doesnt work correctly with it - // change to optional when fixed upstream - connProtector: s('undefined|protector'), - contentRouting: optional(list(['object'])), - dht: optional(s('null|function|object')), - pubsub: optional(s('null|function|object')), - peerDiscovery: optional(list([s('object|function')])), - peerRouting: optional(list(['object'])), - streamMuxer: optional(list([s('object|function')])), - transport: 'transport' -}) - -const configSchema = s({ - peerDiscovery: 'object?', - relay: 'object?', - dht: 'object?', - pubsub: 'object?' -}) - -const optionsSchema = s({ - switch: 'object?', - connectionManager: 'object?', - datastore: 'object?', - peerInfo: 'object', - peerBook: 'object?', - modules: modulesSchema, - config: configSchema -}) - module.exports.validate = (opts) => { opts = mergeOptions(DefaultConfig, opts) - const [error, options] = optionsSchema.validate(opts) - // Improve errors throwed, reduce stack by throwing here and add reason to the message - if (error) { - throw new Error(`${error.message}${error.reason ? ' - ' + error.reason : ''}`) - } else { - // Throw when dht is enabled but no dht module provided - if (options.config.dht.enabled) { - s('function|object')(options.modules.dht) - } - } + if (opts.modules.transport.length < 1) throw new Error("'options.modules.transport' must contain at least 1 transport") - return options + return opts } diff --git a/src/errors.js b/src/errors.js index 19fb8530e1..5219891a3b 100644 --- a/src/errors.js +++ b/src/errors.js @@ -9,5 +9,9 @@ exports.codes = { DHT_DISABLED: 'ERR_DHT_DISABLED', PUBSUB_NOT_STARTED: 'ERR_PUBSUB_NOT_STARTED', ERR_NODE_NOT_STARTED: 'ERR_NODE_NOT_STARTED', - ERR_DISCOVERED_SELF: 'ERR_DISCOVERED_SELF' + ERR_NO_VALID_ADDRESSES: 'ERR_NO_VALID_ADDRESSES', + ERR_DISCOVERED_SELF: 'ERR_DISCOVERED_SELF', + ERR_DUPLICATE_TRANSPORT: 'ERR_DUPLICATE_TRANSPORT', + ERR_INVALID_KEY: 'ERR_INVALID_KEY', + ERR_TRANSPORT_UNAVAILABLE: 'ERR_TRANSPORT_UNAVAILABLE' } diff --git a/src/index.js b/src/index.js index 62815a9ee3..b973739b6c 100644 --- a/src/index.js +++ b/src/index.js @@ -9,15 +9,12 @@ const errCode = require('err-code') const promisify = require('promisify-es6') const each = require('async/each') -const series = require('async/series') -const parallel = require('async/parallel') const nextTick = require('async/nextTick') const PeerBook = require('peer-book') const PeerInfo = require('peer-info') const Switch = require('./switch') const Ping = require('./ping') -const WebSockets = require('libp2p-websockets') const ConnectionManager = require('./connection-manager') const { emitFirst } = require('./util') @@ -29,6 +26,8 @@ const { getPeerInfoRemote } = require('./get-peer-info') const validateConfig = require('./config').validate const { codes } = require('./errors') +const TransportManager = require('./transport-manager') + const notStarted = (action, state) => { return errCode( new Error(`libp2p cannot ${action} when not started; state is ${state}`), @@ -67,6 +66,21 @@ class Libp2p extends EventEmitter { this.stats = this._switch.stats this.connectionManager = new ConnectionManager(this, this._options.connectionManager) + // Setup the transport manager + this.transportManager = new TransportManager({ + libp2p: this, + // TODO: set the actual upgrader + upgrader: { + upgradeInbound: (maConn) => maConn, + upgradeOutbound: (maConn) => maConn + }, + // TODO: Route incoming connections to a multiplex protocol router + onConnection: () => {} + }) + this._modules.transport.forEach((Transport) => { + this.transportManager.add(Transport.prototype[Symbol.toStringTag], Transport) + }) + // Attach stream multiplexers if (this._modules.streamMuxer) { const muxers = this._modules.streamMuxer @@ -336,146 +350,42 @@ class Libp2p extends EventEmitter { this._switch.unhandle(protocol) } - _onStarting () { + async _onStarting () { if (!this._modules.transport) { this.emit('error', new Error('no transports were present')) return this.state('abort') } - let ws - - // so that we can have webrtc-star addrs without adding manually the id - const maOld = [] - const maNew = [] - this.peerInfo.multiaddrs.toArray().forEach((ma) => { - if (!ma.getPeerId()) { - maOld.push(ma) - maNew.push(ma.encapsulate('/p2p/' + this.peerInfo.id.toB58String())) - } - }) - this.peerInfo.multiaddrs.replace(maOld, maNew) - const multiaddrs = this.peerInfo.multiaddrs.toArray() - this._modules.transport.forEach((Transport) => { - let t - - if (typeof Transport === 'function') { - t = new Transport({ libp2p: this }) - } else { - t = Transport - } - - if (t.filter(multiaddrs).length > 0) { - this._switch.transport.add(t.tag || t[Symbol.toStringTag], t) - } else if (WebSockets.isWebSockets(t)) { - // TODO find a cleaner way to signal that a transport is always used - // for dialing, even if no listener - ws = t - } - this._transport.push(t) - }) - - series([ - (cb) => { - this.connectionManager.start() - this._switch.start(cb) - }, - (cb) => { - if (ws) { - // always add dialing on websockets - this._switch.transport.add(ws.tag || ws.constructor.name, ws) - } - - // detect which multiaddrs we don't have a transport for and remove them - const multiaddrs = this.peerInfo.multiaddrs.toArray() + // Start parallel tasks + try { + await Promise.all([ + this.transportManager.listen(multiaddrs) + ]) + } catch (err) { + log.error(err) + this.emit('error', err) + return this.state('stop') + } - multiaddrs.forEach((multiaddr) => { - if (!multiaddr.toString().match(/\/p2p-circuit($|\/)/) && - !this._transport.find((transport) => transport.filter(multiaddr).length > 0)) { - this.peerInfo.multiaddrs.delete(multiaddr) - } - }) - cb() - }, - (cb) => { - if (this._dht) { - this._dht.start(() => { - this._dht.on('peer', this._peerDiscovered) - cb() - }) - } else { - cb() - } - }, - (cb) => { - if (this.pubsub) { - return this.pubsub.start(cb) - } - cb() - }, - // Peer Discovery - (cb) => { - if (this._modules.peerDiscovery) { - this._setupPeerDiscovery(cb) - } else { - cb() - } - } - ], (err) => { - if (err) { - log.error(err) - this.emit('error', err) - return this.state('stop') - } - this.state('done') - }) + // libp2p has started + this.state('done') } - _onStopping () { - series([ - (cb) => { - // stop all discoveries before continuing with shutdown - parallel( - this._discovery.map((d) => { - d.removeListener('peer', this._peerDiscovered) - return (_cb) => d.stop((err) => { - log.error('an error occurred stopping the discovery service', err) - _cb() - }) - }), - cb - ) - }, - (cb) => { - if (this.pubsub) { - return this.pubsub.stop(cb) - } - cb() - }, - (cb) => { - if (this._dht) { - this._dht.removeListener('peer', this._peerDiscovered) - return this._dht.stop(cb) - } - cb() - }, - (cb) => { - this.connectionManager.stop() - this._switch.stop(cb) - }, - (cb) => { - // Ensures idempotent restarts, ignore any errors - // from removeAll, they're not useful at this point - this._switch.transport.removeAll(() => cb()) - } - ], (err) => { + async _onStopping () { + // Start parallel tasks + try { + await this.transportManager.close() + } catch (err) { if (err) { log.error(err) this.emit('error', err) } - this.state('done') - }) + } + + // libp2p has stopped + this.state('done') } /** diff --git a/src/transport-manager.js b/src/transport-manager.js new file mode 100644 index 0000000000..371f2f57b8 --- /dev/null +++ b/src/transport-manager.js @@ -0,0 +1,177 @@ +'use strict' + +const pSettle = require('p-settle') +const { codes } = require('./errors') +const errCode = require('err-code') +const debug = require('debug') +const log = debug('libp2p:transports') +log.error = debug('libp2p:transports:error') + +class TransportManager { + /** + * @constructor + * @param {object} options + * @param {Libp2p} options.libp2p The Libp2p instance. It will be passed to the transports. + * @param {Upgrader} options.upgrader The upgrader to provide to the transports + * @param {function(Connection)} options.onConnection Called whenever an incoming connection is received + */ + constructor ({ libp2p, upgrader, onConnection }) { + this.libp2p = libp2p + this.upgrader = upgrader + this._transports = new Map() + this._listeners = new Map() + this.onConnection = onConnection + } + + /** + * Adds a `Transport` to the manager + * + * @param {String} key + * @param {Transport} Transport + * @returns {void} + */ + add (key, Transport) { + log('adding %s', key) + if (!key) { + throw errCode(new Error(`Transport must have a valid key, was given '${key}'`), codes.ERR_INVALID_KEY) + } + if (this._transports.has(key)) { + throw errCode(new Error('There is already a transport with this key'), codes.ERR_DUPLICATE_TRANSPORT) + } + + const transport = new Transport({ + libp2p: this.libp2p, + upgrader: this.upgrader + }) + + this._transports.set(key, transport) + this._listeners.set(key, []) + } + + /** + * Stops all listeners + * @async + */ + async close () { + const tasks = [] + for (const [key, listeners] of this._listeners) { + log('closing listeners for %s', key) + while (listeners.length) { + tasks.push(listeners.pop().close()) + } + } + + await Promise.all(tasks) + this._listeners.clear() + } + + /** + * Dials the given Multiaddr over it's supported transport + * @param {Multiaddr} ma + * @param {*} options + * @returns {Promise} + */ + async dial (ma, options) { + const transport = this.transportForMultiaddr(ma) + if (!transport) { + throw errCode(new Error(`No transport available for address ${String(ma)}`), codes.ERR_TRANSPORT_UNAVAILABLE) + } + const conn = await transport.dial(ma, options) + return conn + } + + /** + * Returns all Multiaddr's the listeners are using + * @returns {Multiaddr[]} + */ + getAddrs () { + let addrs = [] + for (const listeners of this._listeners.values()) { + for (const listener of listeners) { + addrs = [...addrs, ...listener.getAddrs()] + } + } + return addrs + } + + /** + * Finds a transport that matches the given Multiaddr + * @param {Multiaddr} ma + * @returns {Transport|null} + */ + transportForMultiaddr (ma) { + for (const transport of this._transports.values()) { + const addrs = transport.filter([ma]) + if (addrs.length) return transport + } + return null + } + + /** + * Starts listeners for each given Multiaddr. + * @async + * @param {Multiaddr[]} addrs + */ + async listen (addrs) { + for (const [key, transport] of this._transports.entries()) { + const supportedAddrs = transport.filter(addrs) + const tasks = [] + + // For each supported multiaddr, create a listener + for (const addr of supportedAddrs) { + log('creating listener for %s on %s', key, addr) + const listener = transport.createListener({}, this.onConnection) + this._listeners.get(key).push(listener) + + // We need to attempt to listen on everything + tasks.push(listener.listen(addr)) + } + + const results = await pSettle(tasks) + // If we are listening on at least 1 address, succeed. + // TODO: we should look at adding a retry (`p-retry`) here to better support + // listening on remote addresses as they may be offline. We could then potentially + // just wait for any (`p-any`) listener to succeed on each transport before returning + const isListening = results.find(r => r.isFulfilled === true) + if (!isListening) { + throw errCode(new Error(`Transport (${key}) could not listen on any available address`), codes.ERR_NO_VALID_ADDRESSES) + } + } + } + + /** + * Removes the given transport from the manager. + * If a transport has any running listeners, they will be closed. + * + * @async + * @param {string} key + */ + async remove (key) { + log('removing %s', key) + if (this._listeners.has(key)) { + // Close any running listeners + for (const listener of this._listeners.get(key)) { + await listener.close() + } + } + + this._transports.delete(key) + this._listeners.delete(key) + } + + /** + * Removes all transports from the manager. + * If any listeners are running, they will be closed. + * @async + */ + async removeAll () { + const tasks = [] + for (const key of this._transports.keys()) { + tasks.push(this.remove(key)) + } + + await Promise.all(tasks) + } +} + +module.exports = TransportManager diff --git a/test/browser.js b/test/browser.js deleted file mode 100644 index cac1fae1c1..0000000000 --- a/test/browser.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict' - -require('./circuit-relay.browser') -require('./transports.browser') - -require('./switch/browser') diff --git a/test/circuit-relay.browser.js b/test/circuit-relay.browser.js deleted file mode 100644 index 6455f3c6ff..0000000000 --- a/test/circuit-relay.browser.js +++ /dev/null @@ -1,93 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -const expect = chai.expect - -const createNode = require('./utils/create-node') -const tryEcho = require('./utils/try-echo') -const echo = require('./utils/echo') - -const { - getPeerRelay -} = require('./utils/constants') - -function setupNodeWithRelay (addrs, options = {}) { - options = { - config: { - relay: { - enabled: true - }, - ...options.config - }, - ...options - } - - return new Promise((resolve) => { - createNode(addrs, options, (err, node) => { - expect(err).to.not.exist() - - node.handle(echo.multicodec, echo) - node.start((err) => { - expect(err).to.not.exist() - resolve(node) - }) - }) - }) -} - -describe('circuit relay', () => { - let browserNode1 - let browserNode2 - let peerRelay - - before('get peer relay', async () => { - peerRelay = await getPeerRelay() - }) - - before('create the browser nodes', async () => { - [browserNode1, browserNode2] = await Promise.all([ - setupNodeWithRelay([]), - setupNodeWithRelay([]) - ]) - }) - - before('connect to the relay node', async () => { - await Promise.all( - [browserNode1, browserNode2].map((node) => { - return new Promise(resolve => { - node.dialProtocol(peerRelay, (err) => { - expect(err).to.not.exist() - resolve() - }) - }) - }) - ) - }) - - before('give time for HOP support to be determined', async () => { - await new Promise(resolve => { - setTimeout(resolve, 1e3) - }) - }) - - after(async () => { - await Promise.all( - [browserNode1, browserNode2].map((node) => { - return new Promise((resolve) => { - node.stop(resolve) - }) - }) - ) - }) - - it('should be able to echo over relay', (done) => { - browserNode1.dialProtocol(browserNode2.peerInfo, echo.multicodec, (err, conn) => { - expect(err).to.not.exist() - expect(conn).to.exist() - - tryEcho(conn, done) - }) - }) -}) diff --git a/test/circuit-relay.node.js b/test/circuit-relay.node.js deleted file mode 100644 index e93f1d7dc8..0000000000 --- a/test/circuit-relay.node.js +++ /dev/null @@ -1,215 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -const expect = chai.expect -const sinon = require('sinon') -const waterfall = require('async/waterfall') -const series = require('async/series') -const parallel = require('async/parallel') -const Circuit = require('../src/circuit') -const multiaddr = require('multiaddr') - -const createNode = require('./utils/create-node') -const tryEcho = require('./utils/try-echo') -const echo = require('./utils/echo') - -describe('circuit relay', () => { - const handlerSpies = [] - let relayNode1 - let relayNode2 - let nodeWS1 - let nodeWS2 - let nodeTCP1 - let nodeTCP2 - - function setupNode (addrs, options, callback) { - if (typeof options === 'function') { - callback = options - options = {} - } - - options = options || {} - - return createNode(addrs, options, (err, node) => { - expect(err).to.not.exist() - - node.handle('/echo/1.0.0', echo) - node.start((err) => { - expect(err).to.not.exist() - - handlerSpies.push(sinon.spy( - node._switch.transports[Circuit.tag].listeners[0].hopHandler, 'handle' - )) - - callback(node) - }) - }) - } - - before(function (done) { - this.timeout(20 * 1000) - - waterfall([ - // set up passive relay - (cb) => setupNode([ - '/ip4/0.0.0.0/tcp/0/ws', - '/ip4/0.0.0.0/tcp/0' - ], { - config: { - relay: { - enabled: true, - hop: { - enabled: true, - active: false // passive relay - } - } - } - }, (node) => { - relayNode1 = node - cb() - }), - // setup active relay - (cb) => setupNode([ - '/ip4/0.0.0.0/tcp/0/ws', - '/ip4/0.0.0.0/tcp/0' - ], { - config: { - relay: { - enabled: true, - hop: { - enabled: true, - active: false // passive relay - } - } - } - }, (node) => { - relayNode2 = node - cb() - }), - // setup node with WS - (cb) => setupNode([ - '/ip4/0.0.0.0/tcp/0/ws' - ], { - config: { - relay: { - enabled: true - } - } - }, (node) => { - nodeWS1 = node - cb() - }), - // setup node with WS - (cb) => setupNode([ - '/ip4/0.0.0.0/tcp/0/ws' - ], { - config: { - relay: { - enabled: true - } - } - }, (node) => { - nodeWS2 = node - cb() - }), - // set up node with TCP - (cb) => setupNode([ - '/ip4/0.0.0.0/tcp/0' - ], { - config: { - relay: { - enabled: true - } - } - }, (node) => { - nodeTCP1 = node - cb() - }), - // set up node with TCP - (cb) => setupNode([ - '/ip4/0.0.0.0/tcp/0' - ], { - config: { - relay: { - enabled: true - } - } - }, (node) => { - nodeTCP2 = node - cb() - }) - ], (err) => { - expect(err).to.not.exist() - - series([ - (cb) => nodeWS1.dial(relayNode1.peerInfo, cb), - (cb) => nodeWS1.dial(relayNode2.peerInfo, cb), - (cb) => nodeTCP1.dial(relayNode1.peerInfo, cb), - (cb) => nodeTCP2.dial(relayNode2.peerInfo, cb) - ], done) - }) - }) - - after((done) => { - parallel([ - (cb) => relayNode1.stop(cb), - (cb) => relayNode2.stop(cb), - (cb) => nodeWS1.stop(cb), - (cb) => nodeWS2.stop(cb), - (cb) => nodeTCP1.stop(cb), - (cb) => nodeTCP2.stop(cb) - ], done) - }) - - describe('any relay', function () { - this.timeout(20 * 1000) - - it('dial from WS1 to TCP1 over any R', (done) => { - nodeWS1.dialProtocol(nodeTCP1.peerInfo, '/echo/1.0.0', (err, conn) => { - expect(err).to.not.exist() - expect(conn).to.exist() - tryEcho(conn, done) - }) - }) - - it('fail to dial - no R from WS2 to TCP1', (done) => { - nodeWS2.dialProtocol(nodeTCP2.peerInfo, '/echo/1.0.0', (err, conn) => { - expect(err).to.exist() - expect(conn).to.not.exist() - done() - }) - }) - }) - - describe('explicit relay', function () { - this.timeout(20 * 1000) - - it('dial from WS1 to TCP1 over R1', (done) => { - nodeWS1.dialProtocol(nodeTCP1.peerInfo, '/echo/1.0.0', (err, conn) => { - expect(err).to.not.exist() - expect(conn).to.exist() - - tryEcho(conn, () => { - const addr = multiaddr(handlerSpies[0].args[2][0].dstPeer.addrs[0]).toString() - expect(addr).to.equal(`/ipfs/${nodeTCP1.peerInfo.id.toB58String()}`) - done() - }) - }) - }) - - it('dial from WS1 to TCP2 over R2', (done) => { - nodeWS1.dialProtocol(nodeTCP2.peerInfo, '/echo/1.0.0', (err, conn) => { - expect(err).to.not.exist() - expect(conn).to.exist() - - tryEcho(conn, () => { - const addr = multiaddr(handlerSpies[1].args[2][0].dstPeer.addrs[0]).toString() - expect(addr).to.equal(`/ipfs/${nodeTCP2.peerInfo.id.toB58String()}`) - done() - }) - }) - }) - }) -}) diff --git a/test/circuit/dialer.spec.js b/test/circuit/dialer.spec.js deleted file mode 100644 index 610c836405..0000000000 --- a/test/circuit/dialer.spec.js +++ /dev/null @@ -1,303 +0,0 @@ -/* eslint-env mocha */ -/* eslint max-nested-callbacks: ["error", 5] */ - -'use strict' - -const Dialer = require('../../src/circuit/circuit/dialer') -const nodes = require('./fixtures/nodes') -const Connection = require('interface-connection').Connection -const multiaddr = require('multiaddr') -const PeerInfo = require('peer-info') -const PeerId = require('peer-id') -const pull = require('pull-stream/pull') -const values = require('pull-stream/sources/values') -const asyncMap = require('pull-stream/throughs/async-map') -const pair = require('pull-pair/duplex') -const pb = require('pull-protocol-buffers') - -const proto = require('../../src/circuit/protocol') -const utilsFactory = require('../../src/circuit/circuit/utils') - -const sinon = require('sinon') -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) - -describe('dialer tests', function () { - let dialer - - beforeEach(() => { - dialer = sinon.createStubInstance(Dialer) - }) - - afterEach(() => { - sinon.restore() - }) - - describe('.dial', function () { - beforeEach(function () { - dialer.relayPeers = new Map() - dialer.relayPeers.set(nodes.node2.id, new Connection()) - dialer.relayPeers.set(nodes.node3.id, new Connection()) - dialer.dial.callThrough() - }) - - it('fail on non circuit addr', function () { - const dstMa = multiaddr(`/ipfs/${nodes.node4.id}`) - expect(() => dialer.dial(dstMa, (err) => { - err.to.match(/invalid circuit address/) - })) - }) - - it('dial a peer', function (done) { - const dstMa = multiaddr(`/p2p-circuit/ipfs/${nodes.node3.id}`) - dialer._dialPeer.callsFake(function (dstMa, relay, callback) { - return callback(null, dialer.relayPeers.get(nodes.node3.id)) - }) - - dialer.dial(dstMa, (err, conn) => { - expect(err).to.not.exist() - expect(conn).to.be.an.instanceOf(Connection) - done() - }) - }) - - it('dial a peer over the specified relay', function (done) { - const dstMa = multiaddr(`/ipfs/${nodes.node3.id}/p2p-circuit/ipfs/${nodes.node4.id}`) - dialer._dialPeer.callsFake(function (dstMa, relay, callback) { - expect(relay.toString()).to.equal(`/ipfs/${nodes.node3.id}`) - return callback(null, new Connection()) - }) - - dialer.dial(dstMa, (err, conn) => { - expect(err).to.not.exist() - expect(conn).to.be.an.instanceOf(Connection) - done() - }) - }) - }) - - describe('.canHop', function () { - let fromConn = null - const peer = new PeerInfo(PeerId.createFromB58String('QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA')) - - let p = null - beforeEach(function () { - p = pair() - fromConn = new Connection(p[0]) - - dialer.relayPeers = new Map() - dialer.relayConns = new Map() - dialer.utils = utilsFactory({}) - dialer.canHop.callThrough() - dialer._dialRelayHelper.callThrough() - }) - - it('should handle successful CAN_HOP', (done) => { - dialer._dialRelay.callsFake((_, cb) => { - pull( - values([{ - type: proto.CircuitRelay.type.HOP, - code: proto.CircuitRelay.Status.SUCCESS - }]), - pb.encode(proto.CircuitRelay), - p[1] - ) - cb(null, fromConn) - }) - - dialer.canHop(peer, (err) => { - expect(err).to.not.exist() - expect(dialer.relayPeers.has(peer.id.toB58String())).to.be.ok() - done() - }) - }) - - it('should handle failed CAN_HOP', function (done) { - dialer._dialRelay.callsFake((_, cb) => { - pull( - values([{ - type: proto.CircuitRelay.type.HOP, - code: proto.CircuitRelay.Status.HOP_CANT_SPEAK_RELAY - }]), - pb.encode(proto.CircuitRelay), - p[1] - ) - cb(null, fromConn) - }) - - dialer.canHop(peer, (err) => { - expect(err).to.exist() - expect(dialer.relayPeers.has(peer.id.toB58String())).not.to.be.ok() - done() - }) - }) - }) - - describe('._dialPeer', function () { - beforeEach(function () { - dialer.relayPeers = new Map() - dialer.relayPeers.set(nodes.node1.id, new Connection()) - dialer.relayPeers.set(nodes.node2.id, new Connection()) - dialer.relayPeers.set(nodes.node3.id, new Connection()) - dialer._dialPeer.callThrough() - }) - - it('should dial a peer over any relay', function (done) { - const dstMa = multiaddr(`/ipfs/${nodes.node4.id}`) - dialer._negotiateRelay.callsFake(function (conn, dstMa, callback) { - if (conn === dialer.relayPeers.get(nodes.node3.id)) { - return callback(null, dialer.relayPeers.get(nodes.node3.id)) - } - - callback(new Error('error')) - }) - - dialer._dialPeer(dstMa, (err, conn) => { - expect(err).to.not.exist() - expect(conn).to.be.an.instanceOf(Connection) - expect(conn).to.deep.equal(dialer.relayPeers.get(nodes.node3.id)) - done() - }) - }) - - it('should fail dialing a peer over any relay', function (done) { - const dstMa = multiaddr(`/ipfs/${nodes.node4.id}`) - dialer._negotiateRelay.callsFake(function (conn, dstMa, callback) { - callback(new Error('error')) - }) - - dialer._dialPeer(dstMa, (err, conn) => { - expect(conn).to.be.undefined() - expect(err).to.not.be.null() - expect(err).to.equal('no relay peers were found or all relays failed to dial') - done() - }) - }) - }) - - describe('._negotiateRelay', function () { - const dstMa = multiaddr(`/ipfs/${nodes.node4.id}`) - - let conn = null - let peer = null - let p = null - - before((done) => { - PeerId.createFromJSON(nodes.node4, (_, peerId) => { - PeerInfo.create(peerId, (err, peerInfo) => { - peer = peerInfo - peer.multiaddrs.add('/p2p-circuit/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE') - done(err) - }) - }) - }) - - beforeEach(() => { - dialer.swarm = { - _peerInfo: peer - } - dialer.utils = utilsFactory({}) - dialer.relayConns = new Map() - dialer._negotiateRelay.callThrough() - dialer._dialRelayHelper.callThrough() - peer = new PeerInfo(PeerId.createFromB58String('QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE')) - p = pair() - conn = new Connection(p[1]) - }) - - it('should write the correct dst addr', function (done) { - dialer._dialRelay.callsFake((_, cb) => { - pull( - p[0], - pb.decode(proto.CircuitRelay), - asyncMap((msg, cb) => { - expect(msg.dstPeer.addrs[0]).to.deep.equal(dstMa.buffer) - cb(null, { - type: proto.CircuitRelay.Type.STATUS, - code: proto.CircuitRelay.Status.SUCCESS - }) - }), - pb.encode(proto.CircuitRelay), - p[0] - ) - cb(null, conn) - }) - - dialer._negotiateRelay(peer, dstMa, done) - }) - - it('should negotiate relay', function (done) { - dialer._dialRelay.callsFake((_, cb) => { - pull( - p[0], - pb.decode(proto.CircuitRelay), - asyncMap((msg, cb) => { - expect(msg.dstPeer.addrs[0]).to.deep.equal(dstMa.buffer) - cb(null, { - type: proto.CircuitRelay.Type.STATUS, - code: proto.CircuitRelay.Status.SUCCESS - }) - }), - pb.encode(proto.CircuitRelay), - p[0] - ) - cb(null, conn) - }) - - dialer._negotiateRelay(peer, dstMa, (err, conn) => { - expect(err).to.not.exist() - expect(conn).to.be.instanceOf(Connection) - done() - }) - }) - - it('should fail with an invalid peer id', function (done) { - const dstMa = multiaddr('/ip4/127.0.0.1/tcp/4001') - dialer._dialRelay.callsFake((_, cb) => { - pull( - p[0], - pb.decode(proto.CircuitRelay), - asyncMap((msg, cb) => { - expect(msg.dstPeer.addrs[0]).to.deep.equal(dstMa.buffer) - cb(null, { - type: proto.CircuitRelay.Type.STATUS, - code: proto.CircuitRelay.Status.SUCCESS - }) - }), - pb.encode(proto.CircuitRelay), - p[0] - ) - cb(null, conn) - }) - - dialer._negotiateRelay(peer, dstMa, (err, conn) => { - expect(err).to.exist() - expect(conn).to.not.exist() - done() - }) - }) - - it('should handle failed relay negotiation', function (done) { - dialer._dialRelay.callsFake((_, cb) => { - cb(null, conn) - pull( - values([{ - type: proto.CircuitRelay.Type.STATUS, - code: proto.CircuitRelay.Status.MALFORMED_MESSAGE - }]), - pb.encode(proto.CircuitRelay), - p[0] - ) - }) - - dialer._negotiateRelay(peer, dstMa, (err, conn) => { - expect(err).to.not.be.null() - expect(err).to.be.an.instanceOf(Error) - expect(err.message).to.be.equal('Got 400 error code trying to dial over relay') - done() - }) - }) - }) -}) diff --git a/test/circuit/fixtures/nodes.js b/test/circuit/fixtures/nodes.js deleted file mode 100644 index 71a274d91d..0000000000 --- a/test/circuit/fixtures/nodes.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict' - -exports.node1 = { - id: 'QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE', - privKey: 'CAASpwkwggSjAgEAAoIBAQDJwzJPar4nylKY71Mm5q2BOED8uPf1ILvIi15VwVZWqter6flnlii/RKEcBypPbFqJHHa56MvybgQgrFmHKwDjnJvq4jyOZfR+o/D/99Ft1p2FAEBjImSXAgNpK4YsbyV5r0Q1+Avcj++aWWlLu6enUrL9WGzeUkf0U5L6XwXEPRUQdEojAIQi241P1hyqXX5gKAZVGqcPtKb6p1db3fcXodkS1G6JR90TopJHCqTCECp3SB9c6LlG7KXU92sIHJBlhOEEzGkEI1pM1SWnNnW5VLEypU7P56ifzzp4QxPNiJeC+cmE5SrgR3cXP44iKOuNVRJwBpCh5oNYqECzgqJ9AgMBAAECggEBAJpCdqXHrAmKJCqv2HiGqCODGhTfax1s4IYNIJwaTOPIjUrwgfKUGSVb2H4wcEX3RyVLsO6lMcFyIg/FFlJFK9HavE8SmFAbXZqxx6I9HE+JZjf5IEFrW1Mlg+wWDejNNe7adSF6O79wATaWo+32VNGWZilTQTGd4UvJ1jc9DZCh8zZeNhm4C6exXD45gMB0HI1t2ZNl47scsBEE4rV+s7F7y8Yk/tIsf0wSI/H8KSXS5I9aFxr3Z9c3HOfbVwhnIfNUDqcFTeU5BnhByYNLJ4v9xGj7puidcabVXkt2zLmm/LHbKVeGzec9LW5D+KkuB/pKaslsCXN6bVlu+SbVr9UCgYEA7MXfzZw36vDyfn4LPCN0wgzz11uh3cm31QzOPlWpA7hIsL/eInpvc8wa9yBRC1sRk41CedPHn913MR6EJi0Ne6/B1QOmRYBUjr60VPRNdTXCAiLykjXg6+TZ+AKnxlUGK1hjTo8krhpWq7iD/JchVlLoqDAXGFHvSxN0H3WEUm8CgYEA2iWC9w1v+YHfT2PXcLxYde9EuLVkIS4TM7Kb0N3wr/4+K4xWjVXuaJJLJoAbihNAZw0Y+2s1PswDUEpSG0jXeNXLs6XcQxYSEAu/pFdvHFeg2BfwVQoeEFlWyTJR29uti9/APaXMo8FSVAPPR5lKZLStJDM9hEfAPfUaHyic39MCgYAKQbwjNQw7Ejr+/cjQzxxkt5jskFyftfhPs2FP0/ghYB9OANHHnpQraQEWCYFZQ5WsVac2jdUM+NQL/a1t1e/Klt+HscPHKPsAwAQh1f9w/2YrH4ZwjQL0VRKYKs1HyzEcOZT7tzm4jQ2KHNEi5Q0dpzPK7WJivFHoZ6xVHIsh4wKBgAQq20mk9BKsLHvzyFXbA0WdgI6WyIbpvmwqaVegJcz26nEiiTTCA3/z64OcxunoXD6bvXJwJeBBPX73LIJg7dzdGLsh3AdcEJRF5S9ajEDaW7RFIM4/FzvwuPu2/mFY3QPjDmUfGb23H7+DIx6XCxjJatVaNT6lsEJ+wDUALZ8JAoGAO0YJSEziA7y0dXPK5azkJUMJ5yaN+zRDmoBnEggza34rQW0s16NnIR0EBzKGwbpNyePlProv4dQEaLF1kboKsSYvV2rW2ftLVdNqBHEUYFRC9ofPctCxwM1YU21TI2/k1squ+swApg2EHMev2+WKd+jpVPIbCIvJ3AjiAKZtiGQ=', - pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDJwzJPar4nylKY71Mm5q2BOED8uPf1ILvIi15VwVZWqter6flnlii/RKEcBypPbFqJHHa56MvybgQgrFmHKwDjnJvq4jyOZfR+o/D/99Ft1p2FAEBjImSXAgNpK4YsbyV5r0Q1+Avcj++aWWlLu6enUrL9WGzeUkf0U5L6XwXEPRUQdEojAIQi241P1hyqXX5gKAZVGqcPtKb6p1db3fcXodkS1G6JR90TopJHCqTCECp3SB9c6LlG7KXU92sIHJBlhOEEzGkEI1pM1SWnNnW5VLEypU7P56ifzzp4QxPNiJeC+cmE5SrgR3cXP44iKOuNVRJwBpCh5oNYqECzgqJ9AgMBAAE=' -} - -exports.node2 = { - id: 'QmYJjAri5soV8RbeQcHaYYcTAYTET17QTvcoFMyKvRDTXe', - privKey: 'CAASpgkwggSiAgEAAoIBAQDt7YgUeBQsoN/lrgo690mB7yEh8G9iXhZiDecgZCLRRSl3v2cH9w4WjhoW9erfnVbdoTqkCK+se8uK01ySi/ubQQDPcrjacXTa6wAuRTbCG/0bUR9RxKtxZZBS1HaY7L923ulgGDTiVaRQ3JQqhzmQkaU0ikNcluSGaw0kmhXP6JmcL+wndKgW5VD9etcp2Qlk8uUFC/GAO90cOAuER3wnI3ocHGm9on9zyb97g4TDzIfjSaTW4Wanmx2yVbURQxmCba16X3LT9IMPqQaGOzq3+EewMLeCESbUm/uJaJLdqWrWRK4oNzxcMgmUkzav+s476HdA9CRo72am+g3Vdq+lAgMBAAECggEAcByKD6MZVoIjnlVo6qoVUA1+3kAuK/rLrz5/1wp4QYXGaW+eO+mVENm6v3D3UJESGnLbb+nL5Ymbunmn2EHvuBNkL1wOcJgfiPxM5ICmscaAeHu8N0plwpQp8m28yIheG8Qj0az2VmQmfhfCFVwMquuGHgC8hwdu/Uu6MLIObx1xjtaGbY9kk7nzAeXHeJ4RDeuNN0QrYuQVKwrIz1NtPNDR/cli298ZXJcm+HEhBCIHVIYpAq6BHSuiXVqPGEOYWYXo+yVhEtDJ8BmNqlN1Y1s6bnfu/tFkKUN6iQQ46vYnQEGTGR9lg7J/c6tqfRs9FcywWb9J1SX6HxPO8184zQKBgQD6vDYl20UT4ZtrzhFfMyV/1QUqFM/TdwNuiOsIewHBol9o7aOjrxrrbYVa1HOxETyBjmFsW+iIfOVl61SG2HcU4CG+O2s9WBo4JdRlOm4YQ8/83xO3YfbXzuTx8BMCyP/i1uPIZTKQFFAN0HiL96r4L60xHoWB7tQsbZiEbIO/2wKBgQDy7HnkgVeTld6o0+sT84FYRUotjDB00oUWiSeGtj0pFC4yIxhMhD8QjKiWoJyJItcoCsQ/EncuuwwRtuXi83793lJQR1DBYd+TSPg0M8J1pw97fUIPi/FU+jHtrsx7Vn/7Bk9voictsYVLAfbi68tYdsZpAaYOWYMY9NUfVuAmfwKBgCYZDwk1hgt9TkZVK2KRvPLthTldrC5veQAEoeHJ/vxTFbg105V9d9Op8odYnLOc8NqmrbrvRCfpAlo4JcHPhliPrdDf6m2Jw4IgjWNMO4pIU4QSyUYmBoHIGBWC6wCTVf47tKSwa7xkub0/nfF2km3foKtD/fk+NtMBXBlS+7ndAoGAJo6GIlCtN82X07AfJcGGjB4jUetoXYJ0gUkvruAKARUk5+xOFQcAg33v3EiNz+5pu/9JesFRjWc+2Sjwf/8p7t10ry1Ckg8Yz2XLj22PteDYQj91VsZdfaFgf1s5NXJbSdqMjSltkoEUqP0c1JOcaOQhRdVvJ+PpPPLPSPQfC70CgYBvJE1I06s7BEM1DOli3VyfNaJDI4k9W2dCJOU6Bh2MNmbdRjM3xnpOKH5SqRlCz/oI9pn4dxgbX6WPg331MD9CNYy2tt5KBQRrSuDj8p4jlzMIpX36hsyTTrzYU6WWSIPz6jXW8IexXKvXEmr8TVb78ZPiQfbG012cdUhAJniNgg==', - pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDt7YgUeBQsoN/lrgo690mB7yEh8G9iXhZiDecgZCLRRSl3v2cH9w4WjhoW9erfnVbdoTqkCK+se8uK01ySi/ubQQDPcrjacXTa6wAuRTbCG/0bUR9RxKtxZZBS1HaY7L923ulgGDTiVaRQ3JQqhzmQkaU0ikNcluSGaw0kmhXP6JmcL+wndKgW5VD9etcp2Qlk8uUFC/GAO90cOAuER3wnI3ocHGm9on9zyb97g4TDzIfjSaTW4Wanmx2yVbURQxmCba16X3LT9IMPqQaGOzq3+EewMLeCESbUm/uJaJLdqWrWRK4oNzxcMgmUkzav+s476HdA9CRo72am+g3Vdq+lAgMBAAE=' -} - -exports.node3 = { - id: 'QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA', - privKey: 'CAASpwkwggSjAgEAAoIBAQDdnGp0X7Pix5dIawfyuffVryRDRS5JXdyjayKUkgikJLYoiijB5TakrFKhx1SDKpmVLxxqAGz8m5iA2cHwetIQXTZvdYx7XXxv332En3ji8TiGRUiEFM8KQ5WCJ5G7yw8R2pv/pYdnMrPd04QbtSCn0cFVCiiA2Zkl5KnwBo/lf+sVI/TEeiwmVD9nxi13qWgBTmCysqH8Ppyu8fq+bQgqRZSlalVDswyIhgWlepPkD0uYakJJhhOxY+2RlbNhGY0qjRyMTYou2uR/hfd6j8uR++WdB0v3+DYWG2Kc3sWa4BLYb5r4trvQGO1Iagnwuk3AVoi7PldsaInekzWEVljDAgMBAAECggEAXx0jE49/xXWkmJBXePYYSL5C8hxfIV4HtJvm251R2CFpjTy/AXk/Wq4bSRQkUaeXA1CVAWntXP3rFmJfurb8McnP80agZNJa9ikV1jYbzEt71yUlWosT0XPwV0xkYBVnAmKxUafZ1ZENYcfGi53RxjVgpP8XIzZBZOIfjcVDPVw9NAOzQmq4i3DJEz5xZAkaeSM8mn5ZFl1JMBUOgyOHB7d4BWd3zuLyvnn0/08HlsaSUl0mZa3f2Lm2NlsjOiNfMCJTOIT+xDEP9THm5n2cqieSjvtpAZzV4kcoD0rB8OsyHQlFAEXzkgELDr5dVXji0rrIdVz8stYAKGfi996OAQKBgQDuviV1sc+ClJQA59vqbBiKxWqcuCKMzvmL4Yk1e/AkQeRt+JX9kALWzBx65fFmHTj4Lus8AIQoiruPxa0thtqh/m3SlucWnrdaW410xbz3KqQWS7bx+0sFWZIEi4N+PESrIYhtVbFuRiabYgliqdSU9shxtXXnvfhjl+9quZltiwKBgQDtoUCKqrZbm0bmzLvpnKdNodg1lUHaKGgEvWgza2N1t3b/GE07iha2KO3hBDta3bdfIEEOagY8o13217D0VIGsYNKpiEGLEeNIjfcXBEqAKiTfa/sXUfTprpWBZQ/7ZS+eZIYtQjq14EHa7ifAby1v3yDrMIuxphz5JfKdXFgYqQKBgHr47FikPwu2tkmFJCyqgzWvnEufOQSoc7eOc1tePIKggiX2/mM+M4gqWJ0hJeeAM+D6YeZlKa2sUBItMxeZN7JrWGw5mEx5cl4TfFhipgP2LdDiLRiVZL4bte+rYQ67wm8XdatDkYIIlkhBBi6Q5dPZDcQsQNAedPvvvb2OXi4jAoGBAKp06FpP+L2fle2LYSRDlhNvDCvrpDA8mdkEkRGJb/AKKdb09LnH5WDH3VNy+KzGrHoVJfWUAmNPAOFHeYzabaZcUeEAd5utui7afytIjbSABrEpwRTKWneiH2aROzSnMdBZ5ZHjlz/N3Q+RlHxKg/piwTdUPHCzasch/HX6vsr5AoGAGvhCNPKyCwpu8Gg5GQdx0yN6ZPar9wieD345cLanDZWKkLRQbo4SfkfoS+PDfOLzDbWFdPRnWQ0qhdPm3D/N1YD/nudHqbeDlx0dj/6lEHmmPKFFO2kiNFEhn8DycNGbvWyVBKksacuRXav21+LvW+TatUkRMhi8fgRoypnbJjg=', - pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDdnGp0X7Pix5dIawfyuffVryRDRS5JXdyjayKUkgikJLYoiijB5TakrFKhx1SDKpmVLxxqAGz8m5iA2cHwetIQXTZvdYx7XXxv332En3ji8TiGRUiEFM8KQ5WCJ5G7yw8R2pv/pYdnMrPd04QbtSCn0cFVCiiA2Zkl5KnwBo/lf+sVI/TEeiwmVD9nxi13qWgBTmCysqH8Ppyu8fq+bQgqRZSlalVDswyIhgWlepPkD0uYakJJhhOxY+2RlbNhGY0qjRyMTYou2uR/hfd6j8uR++WdB0v3+DYWG2Kc3sWa4BLYb5r4trvQGO1Iagnwuk3AVoi7PldsaInekzWEVljDAgMBAAE=' -} - -exports.node4 = { - id: 'QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy', - privKey: 'CAASqAkwggSkAgEAAoIBAQC6pg6LYWbY+49SOYdYap6RPqKqZxg80IXeo3hiTUbiGtTruxVYZpnz3UbernL9J9mwlXJGRUQJUKmXmi1yePTQiyclpH0KyPefaWLbpxJQdCBI1TPZpDWo2hutWSPqhKBU1QyH2FLKQPWLdxuIX1cNFPtIlSl5gCxN6oiDIwh7++kxNM1G+d5XgJX6iHLlLoNv3Wn6XYX+VtYdyZRFk8gYyT2BrISbxmsrSjSOodwUUzF8TYTjsqW6ksL2x0mrRm2cMM9evULktqwU+I8l9ulASDbFWBXUToXaZSL9M+Oq5JvZO0WIjPeYVpAgWladtayhdxg5dBv8aTbDaM5DZvyRAgMBAAECggEAR65YbZz1k6Vg0HI5kXI4/YzxicHYJBrtHqjnJdGJxHILjZCmzPFydJ5phkG29ZRlXRS381bMn0s0Jn3WsFzVoHWgjitSvl6aAsXFapgKR42hjHcc15vh47wH3xYZ3gobTRkZG96vRO+XnX0bvM7orqR9MM3gRMI9wZqt3LcKnhpiqSlyEZ3Zehu7ZZ8B+XcUw42H6ZTXgmg5mCFEjS/1rVt+EsdZl7Ll7jHigahPA6qMjyRiZB6T20qQ0FFYfmaNuRuuC6cWUXf8DOgnEjMB/Mi/Feoip9bTqNBrVYn2XeDxdMv5pDznNKXpalsMkZwx5FpNOMKnIMdQFyAGtkeQ9QKBgQD3rjTiulitpbbQBzF8VXeymtMJAbR1TAqNv2yXoowhL3JZaWICM7nXHjjsJa3UzJygbi8bO0KWrw7tY0nUbPy5SmHtNYhmUsEjiTjqEnNRrYN68tEKr0HlgX+9rArsjOcwucl2svFSfk+rTYDHU5neZkDDhu1QmnZm/pQI92Lo4wKBgQDA6wpMd53fmX9DhWegs3xelRStcqBTw1ucWVRyPgY1hO1cJ0oReYIXKEw9CHNLW0RHvnVM26kRnqCl+dTcg7dhLuqrckuyQyY1KcRYG1ryJnz3euucaSF2UCsZCHvFNV7Vz8dszUMUVCogWmroVP6HE/BoazUCNh25s/dNwE+i+wKBgEfa1WL1luaBzgCaJaQhk4FQY2sYgIcLEYDACTwQn0C9aBpCdXmYEhEzpmX0JHM5DTOJ48atsYrPrK/3/yJOoB8NUk2kGzc8SOYLWGSoB6aphRx1N2o3IBH6ONoJAH5R/nxnWehCz7oUBP74lCS/v0MDPUS8bzrUJQeKUd4sDxjrAoGBAIRO7rJA+1qF+J1DWi4ByxNHJXZLfh/UhPj23w628SU1dGDWZVsUvZ7KOXdGW2RcRLj7q5E5uXtnEoCillViVJtnRPSun7Gzkfm2Gn3ezQH0WZKVkA+mnpd5JgW2JsS69L6pEPnS0OWZT4b+3AFZgXL8vs2ucR2CJeLdxYdilHuPAoGBAPLCzBkAboXZZtvEWqzqtVNqdMrjLHihFrpg4TXSsk8+ZQZCVN+sRyTGTvBX8+Jvx4at6ClaSgT3eJ/412fEH6CHvrFXjUE9W9y6X0axxaT63y1OXmFiB/hU3vjLWZKZWSDGNS7St02fYri4tWmGtJDjYG1maLRhMSzcoj4fP1xz', - pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6pg6LYWbY+49SOYdYap6RPqKqZxg80IXeo3hiTUbiGtTruxVYZpnz3UbernL9J9mwlXJGRUQJUKmXmi1yePTQiyclpH0KyPefaWLbpxJQdCBI1TPZpDWo2hutWSPqhKBU1QyH2FLKQPWLdxuIX1cNFPtIlSl5gCxN6oiDIwh7++kxNM1G+d5XgJX6iHLlLoNv3Wn6XYX+VtYdyZRFk8gYyT2BrISbxmsrSjSOodwUUzF8TYTjsqW6ksL2x0mrRm2cMM9evULktqwU+I8l9ulASDbFWBXUToXaZSL9M+Oq5JvZO0WIjPeYVpAgWladtayhdxg5dBv8aTbDaM5DZvyRAgMBAAE=' -} diff --git a/test/circuit/helpers/test-node.js b/test/circuit/helpers/test-node.js deleted file mode 100644 index bba5662288..0000000000 --- a/test/circuit/helpers/test-node.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict' - -const Libp2p = require('../../../src') -const secio = require('libp2p-secio') - -class TestNode extends Libp2p { - constructor (peerInfo, transports, muxer, options) { - options = options || {} - - const modules = { - transport: transports, - connection: { - muxer: [muxer], - crypto: options.isCrypto ? [secio] : null - }, - discovery: [] - } - super(modules, peerInfo, null, options) - } -} - -module.exports = TestNode diff --git a/test/circuit/helpers/utils.js b/test/circuit/helpers/utils.js deleted file mode 100644 index 30482de502..0000000000 --- a/test/circuit/helpers/utils.js +++ /dev/null @@ -1,78 +0,0 @@ -'use strict' - -const TestNode = require('./test-node') -const PeerInfo = require('peer-info') -const PeerId = require('peer-id') -const eachAsync = require('async/each') - -exports.createNodes = function createNodes (configNodes, callback) { - const nodes = {} - eachAsync(Object.keys(configNodes), (key, cb1) => { - const config = configNodes[key] - - const setup = (err, peer) => { - if (err) { - callback(err) - } - - eachAsync(config.addrs, (addr, cb2) => { - peer.multiaddrs.add(addr) - cb2() - }, (err) => { - if (err) { - return callback(err) - } - - nodes[key] = new TestNode(peer, config.transports, config.muxer, config.config) - cb1() - }) - } - - if (config.id) { - PeerId.createFromJSON(config.id, (err, peerId) => { - if (err) return callback(err) - PeerInfo.create(peerId, setup) - }) - } else { - PeerInfo.create(setup) - } - }, (err) => { - if (err) { - return callback(err) - } - - startNodes(nodes, (err) => { - if (err) { - callback(err) - } - - callback(null, nodes) - }) - }) -} - -function startNodes (nodes, callback) { - eachAsync(Object.keys(nodes), - (key, cb) => { - nodes[key].start(cb) - }, - (err) => { - if (err) { - return callback(err) - } - callback(null) - }) -} - -exports.stopNodes = function stopNodes (nodes, callback) { - eachAsync(Object.keys(nodes), - (key, cb) => { - nodes[key].stop(cb) - }, - (err) => { - if (err) { - return callback(err) - } - callback() - }) -} diff --git a/test/circuit/hop.spec.js b/test/circuit/hop.spec.js deleted file mode 100644 index 0b30809e0c..0000000000 --- a/test/circuit/hop.spec.js +++ /dev/null @@ -1,433 +0,0 @@ -/* eslint-env mocha */ -/* eslint max-nested-callbacks: ["error", 5] */ -'use strict' - -const Hop = require('../../src/circuit/circuit/hop') -const nodes = require('./fixtures/nodes') -const Connection = require('interface-connection').Connection -const handshake = require('pull-handshake') -const waterfall = require('async/waterfall') -const PeerInfo = require('peer-info') -const PeerId = require('peer-id') -const multiaddr = require('multiaddr') -const pull = require('pull-stream/pull') -const values = require('pull-stream/sources/values') -const collect = require('pull-stream/sinks/collect') -const lp = require('pull-length-prefixed') -const proto = require('../../src/circuit/protocol') -const StreamHandler = require('../../src/circuit/circuit/stream-handler') - -const sinon = require('sinon') -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) - -describe('relay', () => { - describe('.handle', () => { - let relay - let swarm - let fromConn - let stream - let shake - - beforeEach((done) => { - stream = handshake({ timeout: 1000 * 60 }) - shake = stream.handshake - fromConn = new Connection(stream) - const peerInfo = new PeerInfo(PeerId.createFromB58String('QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA')) - fromConn.setPeerInfo(peerInfo) - - const peers = { - QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE: - new PeerInfo(PeerId.createFromB58String('QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE')), - QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA: - new PeerInfo(PeerId.createFromB58String('QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA')), - QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy: - new PeerInfo(PeerId.createFromB58String('QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy')) - } - - Object.keys(peers).forEach((key) => { peers[key]._connectedMultiaddr = true }) // make it truthy - - waterfall([ - (cb) => PeerId.createFromJSON(nodes.node4, cb), - (peerId, cb) => PeerInfo.create(peerId, cb), - (peer, cb) => { - peer.multiaddrs.add('/p2p-circuit/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE') - swarm = { - _peerInfo: peer, - conns: { - QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE: new Connection(), - QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA: new Connection(), - QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy: new Connection() - }, - _peerBook: { - get: (peer) => { - if (!peers[peer]) { - throw new Error() - } - - return peers[peer] - } - } - } - - cb() - } - ], () => { - relay = new Hop(swarm, { enabled: true }) - relay._circuit = sinon.stub() - relay._circuit.callsArgWith(2, null, new Connection()) - done() - }) - }) - - afterEach(() => { - relay._circuit.reset() - }) - - it('should handle a valid circuit request', (done) => { - const relayMsg = { - type: proto.CircuitRelay.Type.HOP, - srcPeer: { - id: PeerId.createFromB58String('QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE').id, - addrs: [multiaddr('/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE').buffer] - }, - dstPeer: { - id: PeerId.createFromB58String('QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA').id, - addrs: [multiaddr('/ipfs/QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA').buffer] - } - } - - relay.on('circuit:success', () => { - expect(relay._circuit.calledWith(sinon.match.any, relayMsg)).to.be.ok() - done() - }) - - relay.handle(relayMsg, new StreamHandler(fromConn)) - }) - - it('should handle a request to passive circuit', (done) => { - const relayMsg = { - type: proto.CircuitRelay.Type.HOP, - srcPeer: { - id: PeerId.createFromB58String('QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA').id, - addrs: [multiaddr('/ipfs/QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA').buffer] - }, - dstPeer: { - id: PeerId.createFromB58String('QmYJjAri5soV8RbeQcHaYYcTAYTET17QTvcoFMyKvRDTXe').id, - addrs: [multiaddr('/ipfs/QmYJjAri5soV8RbeQcHaYYcTAYTET17QTvcoFMyKvRDTXe').buffer] - } - } - - relay.active = false - lp.decodeFromReader( - shake, - (err, msg) => { - expect(err).to.not.exist() - - const response = proto.CircuitRelay.decode(msg) - expect(response.code).to.equal(proto.CircuitRelay.Status.HOP_NO_CONN_TO_DST) - expect(response.type).to.equal(proto.CircuitRelay.Type.STATUS) - done() - }) - - relay.handle(relayMsg, new StreamHandler(fromConn)) - }) - - it('should handle a request to active circuit', (done) => { - const relayMsg = { - type: proto.CircuitRelay.Type.HOP, - srcPeer: { - id: PeerId.createFromB58String('QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA').id, - addrs: [multiaddr('/ipfs/QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA').buffer] - }, - dstPeer: { - id: PeerId.createFromB58String('QmYJjAri5soV8RbeQcHaYYcTAYTET17QTvcoFMyKvRDTXe').id, - addrs: [multiaddr('/ipfs/QmYJjAri5soV8RbeQcHaYYcTAYTET17QTvcoFMyKvRDTXe').buffer] - } - } - - relay.active = true - relay.on('circuit:success', () => { - expect(relay._circuit.calledWith(sinon.match.any, relayMsg)).to.be.ok() - done() - }) - - relay.on('circuit:error', (err) => { - done(err) - }) - - relay.handle(relayMsg, new StreamHandler(fromConn)) - }) - - it('not dial to self', (done) => { - const relayMsg = { - type: proto.CircuitRelay.Type.HOP, - srcPeer: { - id: PeerId.createFromB58String('QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE').id, - addrs: [multiaddr('/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE').buffer] - }, - dstPeer: { - id: PeerId.createFromB58String('QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE').id, - addrs: [multiaddr('/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE').buffer] - } - } - - lp.decodeFromReader( - shake, - (err, msg) => { - expect(err).to.not.exist() - - const response = proto.CircuitRelay.decode(msg) - expect(response.code).to.equal(proto.CircuitRelay.Status.HOP_CANT_RELAY_TO_SELF) - expect(response.type).to.equal(proto.CircuitRelay.Type.STATUS) - done() - }) - - relay.handle(relayMsg, new StreamHandler(fromConn)) - }) - - it('fail on invalid src address', (done) => { - const relayMsg = { - type: proto.CircuitRelay.Type.HOP, - srcPeer: { - id: 'sdfkjsdnfkjdsb', - addrs: ['sdfkjsdnfkjdsb'] - }, - dstPeer: { - id: PeerId.createFromB58String('QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA').id, - addrs: [multiaddr('/ipfs/QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA').buffer] - } - } - - lp.decodeFromReader( - shake, - (err, msg) => { - expect(err).to.not.exist() - - const response = proto.CircuitRelay.decode(msg) - expect(response.code).to.equal(proto.CircuitRelay.Status.HOP_SRC_MULTIADDR_INVALID) - expect(response.type).to.equal(proto.CircuitRelay.Type.STATUS) - done() - }) - - relay.handle(relayMsg, new StreamHandler(fromConn)) - }) - - it('fail on invalid dst address', (done) => { - const relayMsg = { - type: proto.CircuitRelay.Type.HOP, - srcPeer: { - id: PeerId.createFromB58String('QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA').id, - addrs: [multiaddr('/ipfs/QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA').buffer] - }, - dstPeer: { - id: PeerId.createFromB58String('QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE').id, - addrs: ['sdfkjsdnfkjdsb'] - } - } - - lp.decodeFromReader( - shake, - (err, msg) => { - expect(err).to.not.exist() - - const response = proto.CircuitRelay.decode(msg) - expect(response.code).to.equal(proto.CircuitRelay.Status.HOP_DST_MULTIADDR_INVALID) - expect(response.type).to.equal(proto.CircuitRelay.Type.STATUS) - done() - }) - - relay.handle(relayMsg, new StreamHandler(fromConn)) - }) - }) - - describe('._circuit', () => { - let relay - let swarm - let srcConn - let dstConn - let srcStream - let dstStream - let srcShake - let dstShake - - before((done) => { - srcStream = handshake({ timeout: 1000 * 60 }) - srcShake = srcStream.handshake - srcConn = new Connection(srcStream) - dstStream = handshake({ timeout: 1000 * 60 }) - dstShake = dstStream.handshake - dstConn = new Connection(dstStream) - const peerInfo = new PeerInfo(PeerId.createFromB58String('QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA')) - srcConn.setPeerInfo(peerInfo) - - const peers = { - QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE: - new PeerInfo(PeerId.createFromB58String('QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE')), - QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA: - new PeerInfo(PeerId.createFromB58String('QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA')), - QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy: - new PeerInfo(PeerId.createFromB58String('QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy')) - } - - Object.keys(peers).forEach((key) => { peers[key]._connectedMultiaddr = true }) // make it truthy - - waterfall([ - (cb) => PeerId.createFromJSON(nodes.node4, cb), - (peerId, cb) => PeerInfo.create(peerId, cb), - (peer, cb) => { - peer.multiaddrs.add('/p2p-circuit/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE') - swarm = { - _peerInfo: peer, - conns: { - QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE: new Connection(), - QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA: new Connection(), - QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy: new Connection() - }, - _peerBook: { - get: (peer) => { - if (!peers[peer]) { - throw new Error() - } - - return peers[peer] - } - } - } - - cb() - } - ], () => { - relay = new Hop(swarm, { enabled: true }) - relay._dialPeer = sinon.stub() - relay._dialPeer.callsArgWith(1, null, dstConn) - - done() - }) - }) - - after(() => relay._dialPeer.reset()) - - describe('should correctly dial destination node', () => { - const msg = { - type: proto.CircuitRelay.Type.STOP, - srcPeer: { - id: Buffer.from('QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA'), - addrs: [Buffer.from('dsfsdfsdf')] - }, - dstPeer: { - id: Buffer.from('QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE'), - addrs: [Buffer.from('sdflksdfndsklfnlkdf')] - } - } - - before(() => { - relay._circuit( - new StreamHandler(srcConn), - msg, - (err) => { - expect(err).to.not.exist() - }) - }) - - it('should respond with SUCCESS to source node', (done) => { - lp.decodeFromReader( - srcShake, - (err, msg) => { - expect(err).to.not.exist() - - const response = proto.CircuitRelay.decode(msg) - expect(response.type).to.equal(proto.CircuitRelay.Type.STATUS) - expect(response.code).to.equal(proto.CircuitRelay.Status.SUCCESS) - done() - }) - }) - - it('should send STOP message to destination node', (done) => { - lp.decodeFromReader( - dstShake, - (err, _msg) => { - expect(err).to.not.exist() - - const response = proto.CircuitRelay.decode(_msg) - expect(response.type).to.deep.equal(msg.type) - expect(response.srcPeer).to.deep.equal(msg.srcPeer) - expect(response.dstPeer).to.deep.equal(msg.dstPeer) - done() - }) - }) - - it('should create circuit', (done) => { - pull( - values([proto.CircuitRelay.encode({ - type: proto.CircuitRelay.Type.STATUS, - code: proto.CircuitRelay.Status.SUCCESS - })]), - lp.encode(), - collect((err, encoded) => { - expect(err).to.not.exist() - - encoded.forEach((e) => dstShake.write(e)) - pull( - values([Buffer.from('hello')]), - lp.encode(), - collect((err, encoded) => { - expect(err).to.not.exist() - - encoded.forEach((e) => srcShake.write(e)) - lp.decodeFromReader( - dstShake, - (err, _msg) => { - expect(err).to.not.exist() - expect(_msg.toString()).to.equal('hello') - - done() - }) - }) - ) - }) - ) - }) - }) - - describe('should fail creating circuit', () => { - const msg = { - type: proto.CircuitRelay.Type.STOP, - srcPeer: { - id: Buffer.from('QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA'), - addrs: [Buffer.from('dsfsdfsdf')] - }, - dstPeer: { - id: Buffer.from('QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE'), - addrs: [Buffer.from('sdflksdfndsklfnlkdf')] - } - } - - it('should not create circuit', (done) => { - relay._circuit( - new StreamHandler(srcConn), - msg, - (err) => { - expect(err).to.exist() - expect(err).to.match(/Unable to create circuit!/) - done() - }) - - pull( - values([proto.CircuitRelay.encode({ - type: proto.CircuitRelay.Type.STATUS, - code: proto.CircuitRelay.Status.STOP_RELAY_REFUSED - })]), - lp.encode(), - collect((err, encoded) => { - expect(err).to.not.exist() - - encoded.forEach((e) => dstShake.write(e)) - }) - ) - }) - }) - }) -}) diff --git a/test/circuit/listener.spec.js b/test/circuit/listener.spec.js deleted file mode 100644 index 2e821380b7..0000000000 --- a/test/circuit/listener.spec.js +++ /dev/null @@ -1,292 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const Listener = require('../../src/circuit/listener') -const nodes = require('./fixtures/nodes') -const waterfall = require('async/waterfall') -const PeerInfo = require('peer-info') -const PeerId = require('peer-id') -const multiaddr = require('multiaddr') -const handshake = require('pull-handshake') -const Connection = require('interface-connection').Connection -const proto = require('../../src/circuit/protocol') -const lp = require('pull-length-prefixed') -const pull = require('pull-stream/pull') -const values = require('pull-stream/sources/values') -const collect = require('pull-stream/sinks/collect') -const multicodec = require('../../src/circuit/multicodec') - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) -const sinon = require('sinon') - -describe('listener', function () { - describe('listen', function () { - let swarm = null - let handlerSpy = null - let listener = null - let stream = null - let shake = null - let conn = null - - beforeEach(function (done) { - stream = handshake({ timeout: 1000 * 60 }) - shake = stream.handshake - conn = new Connection(stream) - conn.setPeerInfo(new PeerInfo(PeerId - .createFromB58String('QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE'))) - - waterfall([ - (cb) => PeerId.createFromJSON(nodes.node4, cb), - (peerId, cb) => PeerInfo.create(peerId, cb), - (peer, cb) => { - swarm = { - _peerInfo: peer, - handle: sinon.spy((proto, h) => { - handlerSpy = sinon.spy(h) - }), - conns: { - QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE: new Connection() - } - } - - listener = Listener(swarm, {}, () => {}) - listener.listen() - cb() - } - ], done) - }) - - afterEach(() => { - listener = null - }) - - it('should handle HOP', function (done) { - handlerSpy(multicodec.relay, conn) - - const relayMsg = { - type: proto.CircuitRelay.Type.HOP, - srcPeer: { - id: 'QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE', - addrs: ['/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE'] - }, - dstPeer: { - id: 'QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy', - addrs: ['/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy'] - } - } - - listener.hopHandler.handle = (message, conn) => { - expect(message.type).to.equal(proto.CircuitRelay.Type.HOP) - - expect(message.srcPeer.id.toString()).to.equal(relayMsg.srcPeer.id) - expect(message.srcPeer.addrs[0].toString()).to.equal(relayMsg.srcPeer.addrs[0]) - - expect(message.dstPeer.id.toString()).to.equal(relayMsg.dstPeer.id) - expect(message.dstPeer.addrs[0].toString()).to.equal(relayMsg.dstPeer.addrs[0]) - - done() - } - - pull( - values([proto.CircuitRelay.encode(relayMsg)]), - lp.encode(), - collect((err, encoded) => { - expect(err).to.not.exist() - encoded.forEach((e) => shake.write(e)) - }) - ) - }) - - it('should handle STOP', function (done) { - handlerSpy(multicodec.relay, conn) - - const relayMsg = { - type: proto.CircuitRelay.Type.STOP, - srcPeer: { - id: 'QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE', - addrs: ['/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE'] - }, - dstPeer: { - id: 'QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy', - addrs: ['/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy'] - } - } - - listener.stopHandler.handle = (message, conn) => { - expect(message.type).to.equal(proto.CircuitRelay.Type.STOP) - - expect(message.srcPeer.id.toString()).to.equal(relayMsg.srcPeer.id) - expect(message.srcPeer.addrs[0].toString()).to.equal(relayMsg.srcPeer.addrs[0]) - - expect(message.dstPeer.id.toString()).to.equal(relayMsg.dstPeer.id) - expect(message.dstPeer.addrs[0].toString()).to.equal(relayMsg.dstPeer.addrs[0]) - - done() - } - - pull( - values([proto.CircuitRelay.encode(relayMsg)]), - lp.encode(), - collect((err, encoded) => { - expect(err).to.not.exist() - encoded.forEach((e) => shake.write(e)) - }) - ) - }) - - it('should emit \'connection\'', function (done) { - handlerSpy(multicodec.relay, conn) - - const relayMsg = { - type: proto.CircuitRelay.Type.STOP, - srcPeer: { - id: 'QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE', - addrs: ['/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE'] - }, - dstPeer: { - id: 'QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy', - addrs: ['/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy'] - } - } - - listener.stopHandler.handle = (message, sh) => { - const newConn = new Connection(sh.rest()) - listener.stopHandler.emit('connection', newConn) - } - - listener.on('connection', (conn) => { - expect(conn).to.be.instanceof(Connection) - done() - }) - - pull( - values([proto.CircuitRelay.encode(relayMsg)]), - lp.encode(), - collect((err, encoded) => { - expect(err).to.not.exist() - encoded.forEach((e) => shake.write(e)) - }) - ) - }) - - it('should handle CAN_HOP', function (done) { - handlerSpy(multicodec.relay, conn) - - const relayMsg = { - type: proto.CircuitRelay.Type.CAN_HOP, - srcPeer: { - id: 'QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE', - addrs: ['/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE'] - }, - dstPeer: { - id: 'QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy', - addrs: ['/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy'] - } - } - - listener.hopHandler.handle = (message, conn) => { - expect(message.type).to.equal(proto.CircuitRelay.Type.CAN_HOP) - - expect(message.srcPeer.id.toString()).to.equal(relayMsg.srcPeer.id) - expect(message.srcPeer.addrs[0].toString()).to.equal(relayMsg.srcPeer.addrs[0]) - - expect(message.dstPeer.id.toString()).to.equal(relayMsg.dstPeer.id) - expect(message.dstPeer.addrs[0].toString()).to.equal(relayMsg.dstPeer.addrs[0]) - - done() - } - - pull( - values([proto.CircuitRelay.encode(relayMsg)]), - lp.encode(), - collect((err, encoded) => { - expect(err).to.not.exist() - encoded.forEach((e) => shake.write(e)) - }) - ) - }) - - it('should handle invalid message correctly', function (done) { - handlerSpy(multicodec.relay, conn) - - const relayMsg = { - type: 100000, - srcPeer: { - id: Buffer.from('QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE'), - addrs: [multiaddr('/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE').buffer] - }, - dstPeer: { - id: Buffer.from('QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy'), - addrs: [multiaddr('/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy').buffer] - } - } - - pull( - values([Buffer.from([relayMsg])]), - lp.encode(), - collect((err, encoded) => { - expect(err).to.not.exist() - encoded.forEach((e) => shake.write(e)) - }), - lp.decodeFromReader(shake, { maxLength: this.maxLength }, (err, msg) => { - expect(err).to.not.exist() - expect(proto.CircuitRelay.decode(msg).type).to.equal(proto.CircuitRelay.Type.STATUS) - expect(proto.CircuitRelay.decode(msg).code).to.equal(proto.CircuitRelay.Status.MALFORMED_MESSAGE) - done() - }) - ) - }) - }) - - describe('getAddrs', function () { - let swarm = null - let listener = null - let peerInfo = null - - beforeEach(function (done) { - waterfall([ - (cb) => PeerId.createFromJSON(nodes.node4, cb), - (peerId, cb) => PeerInfo.create(peerId, cb), - (peer, cb) => { - swarm = { - _peerInfo: peer - } - - peerInfo = peer - listener = Listener(swarm, {}, () => {}) - cb() - } - ], done) - }) - - afterEach(() => { - peerInfo = null - }) - - it('should return correct addrs', function () { - peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/4002') - peerInfo.multiaddrs.add('/ip4/127.0.0.1/tcp/4003/ws') - - listener.getAddrs((err, addrs) => { - expect(err).to.not.exist() - expect(addrs).to.deep.equal([ - multiaddr('/p2p-circuit/ip4/0.0.0.0/tcp/4002/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy'), - multiaddr('/p2p-circuit/ip4/127.0.0.1/tcp/4003/ws/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy')]) - }) - }) - - it('don\'t return default addrs in an explicit p2p-circuit addres', function () { - peerInfo.multiaddrs.add('/ip4/127.0.0.1/tcp/4003/ws') - peerInfo.multiaddrs.add('/p2p-circuit/ip4/0.0.0.0/tcp/4002') - listener.getAddrs((err, addrs) => { - expect(err).to.not.exist() - expect(addrs[0] - .toString()) - .to.equal('/p2p-circuit/ip4/0.0.0.0/tcp/4002/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy') - }) - }) - }) -}) diff --git a/test/circuit/proto.spec.js b/test/circuit/proto.spec.js deleted file mode 100644 index bb3a667127..0000000000 --- a/test/circuit/proto.spec.js +++ /dev/null @@ -1,50 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) - -const multiaddr = require('multiaddr') - -const proto = require('../../src/circuit/protocol') - -describe('protocol', function () { - let msgObject = null - let message = null - - before(() => { - msgObject = { - type: proto.CircuitRelay.Type.HOP, - srcPeer: { - id: Buffer.from('QmSource'), - addrs: [ - multiaddr('/p2p-circuit/ipfs/QmSource').buffer, - multiaddr('/p2p-circuit/ip4/0.0.0.0/tcp/9000/ipfs/QmSource').buffer, - multiaddr('/ip4/0.0.0.0/tcp/9000/ipfs/QmSource').buffer - ] - }, - dstPeer: { - id: Buffer.from('QmDest'), - addrs: [ - multiaddr('/p2p-circuit/ipfs/QmDest').buffer, - multiaddr('/p2p-circuit/ip4/1.1.1.1/tcp/9000/ipfs/QmDest').buffer, - multiaddr('/ip4/1.1.1.1/tcp/9000/ipfs/QmDest').buffer - ] - } - } - - const buff = proto.CircuitRelay.encode(msgObject) - message = proto.CircuitRelay.decode(buff) - }) - - it('should source and dest', () => { - expect(message.srcPeer).to.deep.equal(msgObject.srcPeer) - expect(message.dstPeer).to.deep.equal(msgObject.dstPeer) - }) - - it('should encode message', () => { - expect(message.message).to.deep.equal(msgObject.message) - }) -}) diff --git a/test/circuit/stop.spec.js b/test/circuit/stop.spec.js deleted file mode 100644 index 55a34817a7..0000000000 --- a/test/circuit/stop.spec.js +++ /dev/null @@ -1,85 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const Stop = require('../../src/circuit/circuit/stop') -const nodes = require('./fixtures/nodes') -const Connection = require('interface-connection').Connection -const handshake = require('pull-handshake') -const waterfall = require('async/waterfall') -const PeerInfo = require('peer-info') -const PeerId = require('peer-id') -const StreamHandler = require('../../src/circuit/circuit/stream-handler') -const proto = require('../../src/circuit/protocol') - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) - -describe('stop', function () { - describe('handle relayed connections', function () { - let stopHandler - - let swarm - let conn - let stream - - beforeEach(function (done) { - stream = handshake({ timeout: 1000 * 60 }) - conn = new Connection(stream) - const peerId = PeerId.createFromB58String('QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE') - conn.setPeerInfo(new PeerInfo(peerId)) - - waterfall([ - (cb) => PeerId.createFromJSON(nodes.node4, cb), - (peerId, cb) => PeerInfo.create(peerId, cb), - (peer, cb) => { - peer.multiaddrs.add('/p2p-circuit/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE') - swarm = { - _peerInfo: peer, - conns: { - QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE: new Connection() - } - } - - stopHandler = new Stop(swarm) - cb() - } - ], done) - }) - - it('handle request with a valid multiaddr', function (done) { - stopHandler.handle({ - type: proto.CircuitRelay.Type.STOP, - srcPeer: { - id: 'QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE', - addrs: ['/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE'] - }, - dstPeer: { - id: 'QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy', - addrs: ['/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy'] - } - }, new StreamHandler(conn), (conn) => { // multistream handler doesn't expect errors... - expect(conn).to.be.instanceOf(Connection) - done() - }) - }) - - it('handle request with invalid multiaddr', function (done) { - stopHandler.handle({ - type: proto.CircuitRelay.Type.STOP, - srcPeer: { - id: 'QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE', - addrs: ['dsfsdfsdf'] - }, - dstPeer: { - id: 'QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy', - addrs: ['sdflksdfndsklfnlkdf'] - } - }, new StreamHandler(conn), (conn) => { - expect(conn).to.not.exist() - done() - }) - }) - }) -}) diff --git a/test/config.spec.js b/test/config.spec.js deleted file mode 100644 index 146f2e3dae..0000000000 --- a/test/config.spec.js +++ /dev/null @@ -1,358 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -const expect = chai.expect -const PeerInfo = require('peer-info') -const PeerId = require('peer-id') -const waterfall = require('async/waterfall') -const WS = require('libp2p-websockets') -const Bootstrap = require('libp2p-bootstrap') -const DelegatedPeerRouter = require('libp2p-delegated-peer-routing') -const DelegatedContentRouter = require('libp2p-delegated-content-routing') -const DHT = require('libp2p-kad-dht') - -const validateConfig = require('../src/config').validate - -describe('configuration', () => { - let peerInfo - - before((done) => { - waterfall([ - (cb) => PeerId.create({ bits: 512 }, cb), - (peerId, cb) => PeerInfo.create(peerId, cb), - (info, cb) => { - peerInfo = info - cb() - } - ], () => done()) - }) - - it('should throw an error if peerInfo is missing', () => { - expect(() => { - validateConfig({ - modules: { - transport: [WS] - } - }) - }).to.throw() - }) - - it('should throw an error if modules is missing', () => { - expect(() => { - validateConfig({ - peerInfo - }) - }).to.throw() - }) - - it('should throw an error if there are no transports', () => { - expect(() => { - validateConfig({ - peerInfo, - modules: { - transport: [] - } - }) - }).to.throw('ERROR_EMPTY') - }) - - it('should add defaults to config', () => { - const options = { - peerInfo, - modules: { - transport: [WS], - peerDiscovery: [Bootstrap], - dht: DHT - } - } - - const expected = { - peerInfo, - connectionManager: { - minPeers: 25 - }, - modules: { - transport: [WS], - peerDiscovery: [Bootstrap], - dht: DHT - }, - config: { - peerDiscovery: { - autoDial: true - }, - pubsub: { - enabled: true, - emitSelf: true, - signMessages: true, - strictSigning: true - }, - dht: { - kBucketSize: 20, - enabled: false, - randomWalk: { - enabled: false, - queriesPerPeriod: 1, - interval: 300000, - timeout: 10000 - } - }, - relay: { - enabled: true, - hop: { - active: false, - enabled: false - } - } - } - } - - expect(validateConfig(options)).to.deep.equal(expected) - }) - - it('should add defaults to missing items', () => { - const options = { - peerInfo, - modules: { - transport: [WS], - peerDiscovery: [Bootstrap], - dht: DHT - }, - config: { - peerDiscovery: { - bootstrap: { - interval: 1000, - enabled: true - } - }, - dht: { - enabled: false - }, - relay: { - enabled: true - }, - pubsub: { - enabled: true - } - } - } - - const expected = { - peerInfo, - connectionManager: { - minPeers: 25 - }, - modules: { - transport: [WS], - peerDiscovery: [Bootstrap], - dht: DHT - }, - config: { - peerDiscovery: { - autoDial: true, - bootstrap: { - interval: 1000, - enabled: true - } - }, - pubsub: { - enabled: true, - emitSelf: true, - signMessages: true, - strictSigning: true - }, - dht: { - kBucketSize: 20, - enabled: false, - randomWalk: { - enabled: false, - queriesPerPeriod: 1, - interval: 300000, - timeout: 10000 - } - }, - relay: { - enabled: true, - hop: { - active: false, - enabled: false - } - } - } - } - - expect(validateConfig(options)).to.deep.equal(expected) - }) - - it('should allow for configuring the switch', () => { - const options = { - peerInfo, - switch: { - denyTTL: 60e3, - denyAttempts: 5, - maxParallelDials: 100, - maxColdCalls: 50, - dialTimeout: 30e3 - }, - modules: { - transport: [WS], - peerDiscovery: [] - } - } - - expect(validateConfig(options)).to.deep.include({ - switch: { - denyTTL: 60e3, - denyAttempts: 5, - maxParallelDials: 100, - maxColdCalls: 50, - dialTimeout: 30e3 - } - }) - }) - - it('should allow for delegated content and peer routing', () => { - const peerRouter = new DelegatedPeerRouter() - const contentRouter = new DelegatedContentRouter(peerInfo) - - const options = { - peerInfo, - modules: { - transport: [WS], - peerDiscovery: [Bootstrap], - peerRouting: [peerRouter], - contentRouting: [contentRouter], - dht: DHT - }, - config: { - peerDiscovery: { - bootstrap: { - interval: 1000, - enabled: true - } - } - } - } - - expect(validateConfig(options).modules).to.deep.include({ - peerRouting: [peerRouter], - contentRouting: [contentRouter] - }) - }) - - it('should not allow for dht to be enabled without it being provided', () => { - const options = { - peerInfo, - modules: { - transport: [WS] - }, - config: { - dht: { - enabled: true - } - } - } - - expect(() => validateConfig(options)).to.throw() - }) - - it('should be able to add validators and selectors for dht', () => { - const selectors = {} - const validators = {} - - const options = { - peerInfo, - modules: { - transport: [WS], - dht: DHT - }, - config: { - dht: { - selectors, - validators - } - } - } - const expected = { - peerInfo, - connectionManager: { - minPeers: 25 - }, - modules: { - transport: [WS], - dht: DHT - }, - config: { - pubsub: { - enabled: true, - emitSelf: true, - signMessages: true, - strictSigning: true - }, - peerDiscovery: { - autoDial: true - }, - relay: { - enabled: true, - hop: { - active: false, - enabled: false - } - }, - dht: { - kBucketSize: 20, - enabled: false, - randomWalk: { - enabled: false, - queriesPerPeriod: 1, - interval: 300000, - timeout: 10000 - }, - selectors, - validators - } - } - } - expect(validateConfig(options)).to.deep.equal(expected) - }) - - it('should support new properties for the dht config', () => { - const options = { - peerInfo, - modules: { - transport: [WS], - dht: DHT - }, - config: { - dht: { - kBucketSize: 20, - enabled: false, - myNewDHTConfigProperty: true, - randomWalk: { - enabled: false, - queriesPerPeriod: 1, - interval: 300000, - timeout: 10000 - } - } - } - } - - const expected = { - kBucketSize: 20, - enabled: false, - myNewDHTConfigProperty: true, - randomWalk: { - enabled: false, - queriesPerPeriod: 1, - interval: 300000, - timeout: 10000 - } - } - - const actual = validateConfig(options).config.dht - - expect(actual).to.deep.equal(expected) - }) -}) diff --git a/test/connection-manager/default.js b/test/connection-manager/default.js deleted file mode 100644 index f4bbfad3d2..0000000000 --- a/test/connection-manager/default.js +++ /dev/null @@ -1,19 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const Prepare = require('./utils/prepare') - -describe('default', function () { - const prepare = Prepare(3, { pollInterval: 1000 }) - before(prepare.before) - after(prepare.after) - - it('does not kick out any peer', (done) => { - prepare.connManagers().forEach((connManager) => { - connManager.on('disconnected', () => { - throw new Error('should not have disconnected') - }) - }) - setTimeout(done, 1900) - }) -}) diff --git a/test/connection-manager/max-data.js b/test/connection-manager/max-data.js deleted file mode 100644 index 1511b31fe5..0000000000 --- a/test/connection-manager/max-data.js +++ /dev/null @@ -1,36 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -const expect = chai.expect - -const Prepare = require('./utils/prepare') - -const PEER_COUNT = 3 - -describe('maxData', function () { - const prepare = Prepare(PEER_COUNT, { - maxData: 100, - minPeers: 1 - }) - before(prepare.create) - after(prepare.after) - - it('kicks out peer after maxData reached', function (done) { - this.timeout(10000) - - let disconnects = 0 - const manager = prepare.connManagers()[0] - manager.on('disconnected', () => { - disconnects++ - expect(disconnects).to.be.most(PEER_COUNT - 2) - manager.removeAllListeners('disconnected') - done() - }) - - prepare.tryConnectAll((err) => { - expect(err).to.not.exist() - }) - }) -}) diff --git a/test/connection-manager/max-event-loop-delay.js b/test/connection-manager/max-event-loop-delay.js deleted file mode 100644 index 7896c96382..0000000000 --- a/test/connection-manager/max-event-loop-delay.js +++ /dev/null @@ -1,59 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -const expect = chai.expect - -const Prepare = require('./utils/prepare') - -const PEER_COUNT = 3 - -describe('maxEventLoopDelay', function () { - const prepare = Prepare(PEER_COUNT, [{ - pollInterval: 1000, - maxEventLoopDelay: 5, - minPeers: 1 - }]) - before(prepare.create) - after(prepare.after) - - it('kicks out peer after maxEventLoopDelay reached', function (done) { - this.timeout(10000) - let stopped = false - - let disconnects = 0 - const manager = prepare.connManagers()[0] - manager.on('disconnected', () => { - disconnects++ - expect(disconnects).to.be.most(PEER_COUNT - 2) - manager.removeAllListeners('disconnected') - stopped = true - done() - }) - - prepare.tryConnectAll((err) => { - expect(err).to.not.exist() - makeDelay() - }) - - function makeDelay () { - let sum = 0 - for (let i = 0; i < 1000000; i++) { - sum += Math.random() - } - debug(sum) - - if (!stopped) { - setTimeout(makeDelay, 0) - } - } - }) -}) - -function debug (what) { - if (what === 0) { - // never true but the compiler doesn't know that - throw new Error('something went wrong') - } -} diff --git a/test/connection-manager/max-peer-per-protocol.js b/test/connection-manager/max-peer-per-protocol.js deleted file mode 100644 index 7cc4947db1..0000000000 --- a/test/connection-manager/max-peer-per-protocol.js +++ /dev/null @@ -1,37 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -const expect = chai.expect - -const Prepare = require('./utils/prepare') - -const PEER_COUNT = 3 - -describe('maxPeers', function () { - const prepare = Prepare(PEER_COUNT, [{ - maxPeersPerProtocol: { - tcp: 1 - } - }]) - before(prepare.create) - after(prepare.after) - - it('kicks out peers in excess', function (done) { - this.timeout(10000) - - let disconnects = 0 - const manager = prepare.connManagers()[0] - manager.on('disconnected', () => { - disconnects++ - expect(disconnects).to.be.most(PEER_COUNT - 2) - manager.removeAllListeners('disconnected') - done() - }) - - prepare.tryConnectAll((err) => { - expect(err).to.not.exist() - }) - }) -}) diff --git a/test/connection-manager/max-peers.js b/test/connection-manager/max-peers.js deleted file mode 100644 index 751286f6ef..0000000000 --- a/test/connection-manager/max-peers.js +++ /dev/null @@ -1,35 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -const expect = chai.expect - -const Prepare = require('./utils/prepare') - -const PEER_COUNT = 3 - -describe('maxPeers', function () { - const prepare = Prepare(PEER_COUNT, [{ - maxPeers: 1 - }]) - before(prepare.create) - after(prepare.after) - - it('kicks out peers in excess', function (done) { - this.timeout(10000) - - let disconnects = 0 - const manager = prepare.connManagers()[0] - manager.on('disconnected', () => { - disconnects++ - expect(disconnects).to.be.most(PEER_COUNT - 2) - manager.removeAllListeners('disconnected') - done() - }) - - prepare.tryConnectAll((err, eachNodeConnections) => { - expect(err).to.not.exist() - }) - }) -}) diff --git a/test/connection-manager/max-received-data.js b/test/connection-manager/max-received-data.js deleted file mode 100644 index 908b720fa0..0000000000 --- a/test/connection-manager/max-received-data.js +++ /dev/null @@ -1,36 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -const expect = chai.expect - -const Prepare = require('./utils/prepare') - -const PEER_COUNT = 3 - -describe('maxReceivedData', function () { - const prepare = Prepare(PEER_COUNT, { - maxReceivedData: 50, - minPeers: 1 - }) - before(prepare.create) - after(prepare.after) - - it('kicks out peer after maxReceivedData reached', function (done) { - this.timeout(10000) - - let disconnects = 0 - const manager = prepare.connManagers()[0] - manager.on('disconnected', () => { - disconnects++ - expect(disconnects).to.be.most(PEER_COUNT - 2) - manager.removeAllListeners('disconnected') - done() - }) - - prepare.tryConnectAll((err, eachNodeConnections) => { - expect(err).to.not.exist() - }) - }) -}) diff --git a/test/connection-manager/max-sent-data.js b/test/connection-manager/max-sent-data.js deleted file mode 100644 index ac1885b6de..0000000000 --- a/test/connection-manager/max-sent-data.js +++ /dev/null @@ -1,36 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -const expect = chai.expect - -const Prepare = require('./utils/prepare') - -const PEER_COUNT = 3 - -describe('maxSentData', function () { - const prepare = Prepare(PEER_COUNT, [{ - maxSentData: 50, - minPeers: 1 - }]) - before(prepare.create) - after(prepare.after) - - it('kicks out peer after maxSentData reached', function (done) { - this.timeout(10000) - - let disconnects = 0 - const manager = prepare.connManagers()[0] - manager.on('disconnected', () => { - disconnects++ - expect(disconnects).to.be.most(PEER_COUNT - 2) - manager.removeAllListeners('disconnected') - done() - }) - - prepare.tryConnectAll((err, eachNodeConnections) => { - expect(err).to.not.exist() - }) - }) -}) diff --git a/test/connection-manager/node.js b/test/connection-manager/node.js deleted file mode 100644 index a307d49068..0000000000 --- a/test/connection-manager/node.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict' - -require('./default') -require('./max-data') -require('./max-event-loop-delay') -require('./max-peer-per-protocol') -require('./max-peers') -require('./max-received-data') -require('./max-sent-data') -require('./set-peer-value') diff --git a/test/connection-manager/set-peer-value.js b/test/connection-manager/set-peer-value.js deleted file mode 100644 index c0adb91ced..0000000000 --- a/test/connection-manager/set-peer-value.js +++ /dev/null @@ -1,44 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -const expect = chai.expect - -const Prepare = require('./utils/prepare') - -const PEER_COUNT = 3 - -describe('setPeerValue', function () { - const prepare = Prepare(PEER_COUNT, [{ - maxPeers: 1, - defaultPeerValue: 0 - }]) - before(prepare.create) - after(prepare.after) - - it('kicks out lower valued peer first', function (done) { - let disconnects = 0 - let firstConnectedPeer - const manager = prepare.connManagers()[0] - - manager.once('connected', (peerId) => { - if (!firstConnectedPeer) { - firstConnectedPeer = peerId - manager.setPeerValue(peerId, 1) - } - }) - - manager.on('disconnected', (peerId) => { - disconnects++ - expect(disconnects).to.be.most(PEER_COUNT - 2) - expect(peerId).to.not.be.equal(firstConnectedPeer) - manager.removeAllListeners('disconnected') - done() - }) - - prepare.tryConnectAll((err) => { - expect(err).to.not.exist() - }) - }) -}) diff --git a/test/connection-manager/utils/connect-all.js b/test/connection-manager/utils/connect-all.js deleted file mode 100644 index e89f3fc87f..0000000000 --- a/test/connection-manager/utils/connect-all.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict' - -const eachSeries = require('async/eachSeries') - -module.exports = (nodes, callback) => { - eachSeries( - nodes, - (node, cb) => { - eachSeries( - nodes.filter(n => node !== n), - (otherNode, cb) => node.dial(otherNode.peerInfo, cb), - cb - ) - }, - callback - ) -} diff --git a/test/connection-manager/utils/create-libp2p-node.js b/test/connection-manager/utils/create-libp2p-node.js deleted file mode 100644 index 0fcdab894c..0000000000 --- a/test/connection-manager/utils/create-libp2p-node.js +++ /dev/null @@ -1,50 +0,0 @@ -'use strict' - -const TCP = require('libp2p-tcp') -const Multiplex = require('libp2p-mplex') -const SECIO = require('libp2p-secio') -const libp2p = require('../../../src') -const waterfall = require('async/waterfall') -const PeerInfo = require('peer-info') -const PeerId = require('peer-id') - -const ConnManager = require('../../../src/connection-manager') - -class Node extends libp2p { - constructor (peerInfo) { - const modules = { - transport: [TCP], - streamMuxer: [Multiplex], - connEncryption: [SECIO] - } - - super({ - peerInfo, - modules, - config: { - peerDiscovery: { - autoDial: false - } - } - }) - } -} - -function createLibp2pNode (options, callback) { - let node - - waterfall([ - (cb) => PeerId.create({ bits: 1024 }, cb), - (id, cb) => PeerInfo.create(id, cb), - (peerInfo, cb) => { - peerInfo.multiaddrs.add('/ip4/127.0.0.1/tcp/0') - node = new Node(peerInfo) - // Replace the connection manager so we use source code instead of dep code - node.connectionManager = new ConnManager(node, options) - node.start(cb) - } - ], (err) => callback(err, node)) -} - -exports = module.exports = createLibp2pNode -exports.bundle = Node diff --git a/test/connection-manager/utils/prepare.js b/test/connection-manager/utils/prepare.js deleted file mode 100644 index 84c305a295..0000000000 --- a/test/connection-manager/utils/prepare.js +++ /dev/null @@ -1,83 +0,0 @@ -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -const expect = chai.expect - -const series = require('async/series') -const each = require('async/each') - -const createLibp2pNode = require('./create-libp2p-node') -const connectAll = require('./connect-all') -const tryConnectAll = require('./try-connect-all') - -module.exports = (count, options) => { - let nodes - - if (!Array.isArray(options)) { - const opts = options - options = [] - for (let n = 0; n < count; n++) { - options[n] = opts - } - } - - const create = (done) => { - const tasks = [] - for (let i = 0; i < count; i++) { - tasks.push((cb) => createLibp2pNode(options.shift() || {}, cb)) - } - - series(tasks, (err, things) => { - if (!err) { - nodes = things - expect(things.length).to.equal(count) - } - done(err) - }) - } - - const connect = function (done) { - if (this && this.timeout) { - this.timeout(10000) - } - connectAll(nodes, done) - } - - const tryConnectAllFn = function (done) { - if (this && this.timeout) { - this.timeout(10000) - } - tryConnectAll(nodes, done) - } - - const before = (done) => { - if (this && this.timeout) { - this.timeout(10000) - } - series([create, connect], done) - } - - const after = function (done) { - if (this && this.timeout) { - this.timeout(10000) - } - if (!nodes) { return done() } - - each(nodes, (node, cb) => { - series([ - (cb) => node.stop(cb) - ], cb) - }, done) - } - - return { - create, - connect, - tryConnectAll: tryConnectAllFn, - before, - after, - things: () => nodes, - connManagers: () => nodes.map((node) => node.connectionManager) - } -} diff --git a/test/connection-manager/utils/try-connect-all.js b/test/connection-manager/utils/try-connect-all.js deleted file mode 100644 index 2d755c8776..0000000000 --- a/test/connection-manager/utils/try-connect-all.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict' - -const mapSeries = require('async/mapSeries') -const eachSeries = require('async/eachSeries') - -module.exports = (nodes, callback) => { - mapSeries( - nodes, - (node, cb) => { - const connectedTo = [] - eachSeries( - nodes.filter(n => node !== n), - (otherNode, cb) => { - const otherNodePeerInfo = otherNode.peerInfo - node.dial(otherNodePeerInfo, (err) => { - if (!err) { - connectedTo.push(otherNodePeerInfo.id.toB58String()) - } - cb() - }) - }, - (err) => cb(err, connectedTo) - ) - }, - callback - ) -} diff --git a/test/content-routing.node.js b/test/content-routing.node.js deleted file mode 100644 index 43534f128f..0000000000 --- a/test/content-routing.node.js +++ /dev/null @@ -1,404 +0,0 @@ -/* eslint-env mocha */ -/* eslint max-nested-callbacks: ["error", 8] */ - -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -const expect = chai.expect -const parallel = require('async/parallel') -const waterfall = require('async/waterfall') -const _times = require('lodash.times') -const CID = require('cids') -const DelegatedContentRouter = require('libp2p-delegated-content-routing') -const sinon = require('sinon') -const nock = require('nock') -const ma = require('multiaddr') -const Node = require('./utils/bundle-nodejs') - -const createNode = require('./utils/create-node') -const createPeerInfo = createNode.createPeerInfo - -describe('.contentRouting', () => { - describe('via the dht', () => { - let nodeA - let nodeB - let nodeC - let nodeD - let nodeE - - before(function (done) { - this.timeout(5 * 1000) - const tasks = _times(5, () => (cb) => { - createNode('/ip4/0.0.0.0/tcp/0', (err, node) => { - expect(err).to.not.exist() - node.start((err) => cb(err, node)) - }) - }) - - parallel(tasks, (err, nodes) => { - expect(err).to.not.exist() - nodeA = nodes[0] - nodeB = nodes[1] - nodeC = nodes[2] - nodeD = nodes[3] - nodeE = nodes[4] - - parallel([ - (cb) => nodeA.dial(nodeB.peerInfo, cb), - (cb) => nodeB.dial(nodeC.peerInfo, cb), - (cb) => nodeC.dial(nodeD.peerInfo, cb), - (cb) => nodeD.dial(nodeE.peerInfo, cb), - (cb) => nodeE.dial(nodeA.peerInfo, cb) - ], done) - }) - }) - - after((done) => { - parallel([ - (cb) => nodeA.stop(cb), - (cb) => nodeB.stop(cb), - (cb) => nodeC.stop(cb), - (cb) => nodeD.stop(cb), - (cb) => nodeE.stop(cb) - ], done) - }) - - it('should use the nodes dht to provide', (done) => { - const stub = sinon.stub(nodeA._dht, 'provide').callsFake(() => { - stub.restore() - done() - }) - - nodeA.contentRouting.provide() - }) - - it('should use the nodes dht to find providers', (done) => { - const stub = sinon.stub(nodeA._dht, 'findProviders').callsFake(() => { - stub.restore() - done() - }) - - nodeA.contentRouting.findProviders() - }) - - describe('le ring', () => { - const cid = new CID('QmTp9VkYvnHyrqKQuFPiuZkiX9gPcqj6x5LJ1rmWuSySnL') - - it('let kbucket get filled', (done) => { - setTimeout(() => done(), 250) - }) - - it('nodeA.contentRouting.provide', (done) => { - nodeA.contentRouting.provide(cid, done) - }) - - it('nodeE.contentRouting.findProviders for existing record', (done) => { - nodeE.contentRouting.findProviders(cid, { maxTimeout: 5000 }, (err, providers) => { - expect(err).to.not.exist() - expect(providers).to.have.length.above(0) - done() - }) - }) - - it('nodeE.contentRouting.findProviders with limited number of providers', (done) => { - parallel([ - (cb) => nodeA.contentRouting.provide(cid, cb), - (cb) => nodeB.contentRouting.provide(cid, cb), - (cb) => nodeC.contentRouting.provide(cid, cb) - ], (err) => { - expect(err).to.not.exist() - - nodeE.contentRouting.findProviders(cid, { maxNumProviders: 2 }, (err, providers) => { - expect(err).to.not.exist() - expect(providers).to.have.length(2) - done() - }) - }) - }) - - it('nodeC.contentRouting.findProviders for non existing record (timeout)', (done) => { - const cid = new CID('QmTp9VkYvnHyrqKQuFPiuZkiX9gPcqj6x5LJ1rmWuSnnnn') - - nodeE.contentRouting.findProviders(cid, { maxTimeout: 5000 }, (err, providers) => { - expect(err).to.exist() - expect(err.code).to.eql('ERR_NOT_FOUND') - expect(providers).to.not.exist() - done() - }) - }) - }) - }) - - describe('via a delegate', () => { - let nodeA - let delegate - - before((done) => { - waterfall([ - (cb) => { - createPeerInfo(cb) - }, - // Create the node using the delegate - (peerInfo, cb) => { - delegate = new DelegatedContentRouter(peerInfo.id, { - host: '0.0.0.0', - protocol: 'http', - port: 60197 - }, [ - ma('/ip4/0.0.0.0/tcp/60194') - ]) - nodeA = new Node({ - peerInfo, - modules: { - contentRouting: [delegate] - }, - config: { - dht: { - enabled: false - }, - relay: { - enabled: true, - hop: { - enabled: true, - active: false - } - } - } - }) - nodeA.start(cb) - } - ], done) - }) - - after((done) => nodeA.stop(done)) - afterEach(() => nock.cleanAll()) - - describe('provide', () => { - it('should use the delegate router to provide', (done) => { - const stub = sinon.stub(delegate, 'provide').callsFake(() => { - stub.restore() - done() - }) - nodeA.contentRouting.provide() - }) - - it('should be able to register as a provider', (done) => { - const cid = new CID('QmU621oD8AhHw6t25vVyfYKmL9VV3PTgc52FngEhTGACFB') - const mockApi = nock('http://0.0.0.0:60197') - // mock the refs call - .post('/api/v0/refs') - .query({ - recursive: false, - arg: cid.toBaseEncodedString(), - 'stream-channels': true - }) - .reply(200, null, [ - 'Content-Type', 'application/json', - 'X-Chunked-Output', '1' - ]) - - nodeA.contentRouting.provide(cid, (err) => { - expect(err).to.not.exist() - expect(mockApi.isDone()).to.equal(true) - done() - }) - }) - - it('should handle errors when registering as a provider', (done) => { - const cid = new CID('QmU621oD8AhHw6t25vVyfYKmL9VV3PTgc52FngEhTGACFB') - const mockApi = nock('http://0.0.0.0:60197') - // mock the refs call - .post('/api/v0/refs') - .query({ - recursive: false, - arg: cid.toBaseEncodedString(), - 'stream-channels': true - }) - .reply(502, 'Bad Gateway', ['Content-Type', 'application/json']) - - nodeA.contentRouting.provide(cid, (err) => { - expect(err).to.exist() - expect(mockApi.isDone()).to.equal(true) - done() - }) - }) - }) - - describe('find providers', () => { - it('should use the delegate router to find providers', (done) => { - const stub = sinon.stub(delegate, 'findProviders').callsFake(() => { - stub.restore() - done() - }) - nodeA.contentRouting.findProviders() - }) - - it('should be able to find providers', (done) => { - const cid = new CID('QmU621oD8AhHw6t25vVyfYKmL9VV3PTgc52FngEhTGACFB') - const provider = 'QmZNgCqZCvTsi3B4Vt7gsSqpkqDpE7M2Y9TDmEhbDb4ceF' - const mockApi = nock('http://0.0.0.0:60197') - .post('/api/v0/dht/findprovs') - .query({ - arg: cid.toBaseEncodedString(), - timeout: '1000ms', - 'stream-channels': true - }) - .reply(200, `{"Extra":"","ID":"QmWKqWXCtRXEeCQTo3FoZ7g4AfnGiauYYiczvNxFCHicbB","Responses":[{"Addrs":["/ip4/0.0.0.0/tcp/0"],"ID":"${provider}"}],"Type":4}\n`, [ - 'Content-Type', 'application/json', - 'X-Chunked-Output', '1' - ]) - - nodeA.contentRouting.findProviders(cid, 1000, (err, response) => { - expect(err).to.not.exist() - expect(response).to.have.length(1) - expect(response[0].id.toB58String()).to.equal(provider) - expect(mockApi.isDone()).to.equal(true) - done() - }) - }) - - it('should handle errors when finding providers', (done) => { - const cid = new CID('QmU621oD8AhHw6t25vVyfYKmL9VV3PTgc52FngEhTGACFB') - const mockApi = nock('http://0.0.0.0:60197') - .post('/api/v0/dht/findprovs') - .query({ - arg: cid.toBaseEncodedString(), - timeout: '30000ms', - 'stream-channels': true - }) - .reply(502, 'Bad Gateway', [ - 'X-Chunked-Output', '1' - ]) - - nodeA.contentRouting.findProviders(cid, (err) => { - expect(err).to.exist() - expect(mockApi.isDone()).to.equal(true) - done() - }) - }) - }) - }) - - describe('via the dht and a delegate', () => { - let nodeA - let delegate - - before((done) => { - waterfall([ - (cb) => { - createPeerInfo(cb) - }, - // Create the node using the delegate - (peerInfo, cb) => { - delegate = new DelegatedContentRouter(peerInfo.id, { - host: '0.0.0.0', - protocol: 'http', - port: 60197 - }, [ - ma('/ip4/0.0.0.0/tcp/60194') - ]) - nodeA = new Node({ - peerInfo, - modules: { - contentRouting: [delegate] - }, - config: { - relay: { - enabled: true, - hop: { - enabled: true, - active: false - } - } - } - }) - nodeA.start(cb) - } - ], done) - }) - - after((done) => nodeA.stop(done)) - - describe('provide', () => { - it('should use both the dht and delegate router to provide', (done) => { - const dhtStub = sinon.stub(nodeA._dht, 'provide').callsFake(() => {}) - const delegateStub = sinon.stub(delegate, 'provide').callsFake(() => { - expect(dhtStub.calledOnce).to.equal(true) - expect(delegateStub.calledOnce).to.equal(true) - delegateStub.restore() - dhtStub.restore() - done() - }) - nodeA.contentRouting.provide() - }) - }) - - describe('findProviders', () => { - it('should only use the dht if it finds providers', (done) => { - const results = [true] - const dhtStub = sinon.stub(nodeA._dht, 'findProviders').callsArgWith(2, null, results) - const delegateStub = sinon.stub(delegate, 'findProviders').throws(() => { - return new Error('the delegate should not have been called') - }) - - nodeA.contentRouting.findProviders('a cid', { maxTimeout: 5000 }, (err, results) => { - expect(err).to.not.exist() - expect(results).to.equal(results) - expect(dhtStub.calledOnce).to.equal(true) - expect(delegateStub.notCalled).to.equal(true) - delegateStub.restore() - dhtStub.restore() - done() - }) - }) - - it('should use the delegate if the dht fails to find providers', (done) => { - const results = [true] - const dhtStub = sinon.stub(nodeA._dht, 'findProviders').callsArgWith(2, null, []) - const delegateStub = sinon.stub(delegate, 'findProviders').callsArgWith(2, null, results) - - nodeA.contentRouting.findProviders('a cid', { maxTimeout: 5000 }, (err, results) => { - expect(err).to.not.exist() - expect(results).to.deep.equal(results) - expect(dhtStub.calledOnce).to.equal(true) - expect(delegateStub.calledOnce).to.equal(true) - delegateStub.restore() - dhtStub.restore() - done() - }) - }) - }) - }) - - describe('no routers', () => { - let nodeA - before((done) => { - createNode('/ip4/0.0.0.0/tcp/0', { - config: { - dht: { - enabled: false - } - } - }, (err, node) => { - expect(err).to.not.exist() - nodeA = node - done() - }) - }) - - it('.findProviders should return an error with no options', (done) => { - nodeA.contentRouting.findProviders('a cid', (err) => { - expect(err).to.exist() - done() - }) - }) - - it('.findProviders should return an error with options', (done) => { - nodeA.contentRouting.findProviders('a cid', { maxTimeout: 5000 }, (err) => { - expect(err).to.exist() - done() - }) - }) - }) -}) diff --git a/test/create.spec.js b/test/create.spec.js deleted file mode 100644 index d73582a60a..0000000000 --- a/test/create.spec.js +++ /dev/null @@ -1,143 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -const expect = chai.expect -const series = require('async/series') -const createNode = require('./utils/create-node') -const sinon = require('sinon') -const { createLibp2p } = require('../src') -const WS = require('libp2p-websockets') -const PeerInfo = require('peer-info') - -describe('libp2p creation', () => { - afterEach(() => { - sinon.restore() - }) - - it('should be able to start and stop successfully', (done) => { - createNode([], { - config: { - pubsub: { - enabled: true - }, - dht: { - enabled: true - } - } - }, (err, node) => { - expect(err).to.not.exist() - - const sw = node._switch - const cm = node.connectionManager - const dht = node._dht - const pub = node.pubsub - - sinon.spy(sw, 'start') - sinon.spy(cm, 'start') - sinon.spy(dht, 'start') - sinon.spy(dht.randomWalk, 'start') - sinon.spy(pub, 'start') - sinon.spy(sw, 'stop') - sinon.spy(cm, 'stop') - sinon.spy(dht, 'stop') - sinon.spy(dht.randomWalk, 'stop') - sinon.spy(pub, 'stop') - sinon.spy(node, 'emit') - - series([ - (cb) => node.start(cb), - (cb) => { - expect(sw.start.calledOnce).to.equal(true) - expect(cm.start.calledOnce).to.equal(true) - expect(dht.start.calledOnce).to.equal(true) - expect(dht.randomWalk.start.calledOnce).to.equal(true) - expect(pub.start.calledOnce).to.equal(true) - expect(node.emit.calledWith('start')).to.equal(true) - - cb() - }, - (cb) => node.stop(cb) - ], (err) => { - expect(err).to.not.exist() - - expect(sw.stop.calledOnce).to.equal(true) - expect(cm.stop.calledOnce).to.equal(true) - expect(dht.stop.calledOnce).to.equal(true) - expect(dht.randomWalk.stop.called).to.equal(true) - expect(pub.stop.calledOnce).to.equal(true) - expect(node.emit.calledWith('stop')).to.equal(true) - - done() - }) - }) - }) - - it('should not create disabled modules', (done) => { - createNode([], { - config: { - pubsub: { - enabled: false - } - } - }, (err, node) => { - expect(err).to.not.exist() - expect(node._pubsub).to.not.exist() - done() - }) - }) - - it('should not throw errors from switch if node has no error listeners', (done) => { - createNode([], {}, (err, node) => { - expect(err).to.not.exist() - - node._switch.emit('error', new Error('bad things')) - done() - }) - }) - - it('should emit errors from switch if node has error listeners', (done) => { - const error = new Error('bad things') - createNode([], {}, (err, node) => { - expect(err).to.not.exist() - node.once('error', (err) => { - expect(err).to.eql(error) - done() - }) - node._switch.emit('error', error) - }) - }) - - it('createLibp2p should create a peerInfo instance', function (done) { - this.timeout(10e3) - createLibp2p({ - modules: { - transport: [WS] - } - }, (err, libp2p) => { - expect(err).to.not.exist() - expect(libp2p).to.exist() - done() - }) - }) - - it('createLibp2p should allow for a provided peerInfo instance', function (done) { - this.timeout(10e3) - PeerInfo.create((err, peerInfo) => { - expect(err).to.not.exist() - sinon.spy(PeerInfo, 'create') - createLibp2p({ - peerInfo, - modules: { - transport: [WS] - } - }, (err, libp2p) => { - expect(err).to.not.exist() - expect(libp2p).to.exist() - expect(PeerInfo.create.callCount).to.eql(0) - done() - }) - }) - }) -}) diff --git a/test/dht.node.js b/test/dht.node.js deleted file mode 100644 index cecd7b7222..0000000000 --- a/test/dht.node.js +++ /dev/null @@ -1,168 +0,0 @@ -/* eslint-env mocha */ - -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -const expect = chai.expect - -const MemoryStore = require('interface-datastore').MemoryDatastore - -const createNode = require('./utils/create-node') - -describe('.dht', () => { - describe('enabled', () => { - let nodeA - const datastore = new MemoryStore() - - before(function (done) { - createNode('/ip4/0.0.0.0/tcp/0', { - datastore - }, (err, node) => { - expect(err).to.not.exist() - nodeA = node - - // Rewrite validators - nodeA._dht.validators.v = { - func (key, publicKey, callback) { - setImmediate(callback) - }, - sign: false - } - - // Rewrite selectors - nodeA._dht.selectors.v = () => 0 - - // Start - nodeA.start(done) - }) - }) - - after((done) => { - nodeA.stop(done) - }) - - it('should be able to dht.put a value to the DHT', (done) => { - const key = Buffer.from('key') - const value = Buffer.from('value') - - nodeA.dht.put(key, value, (err) => { - expect(err).to.not.exist() - done() - }) - }) - - it('should be able to dht.get a value from the DHT with options', (done) => { - const key = Buffer.from('/v/hello') - const value = Buffer.from('world') - - nodeA.dht.put(key, value, (err) => { - expect(err).to.not.exist() - - nodeA.dht.get(key, { maxTimeout: 3000 }, (err, res) => { - expect(err).to.not.exist() - expect(res).to.eql(value) - done() - }) - }) - }) - - it('should be able to dht.get a value from the DHT with no options defined', (done) => { - const key = Buffer.from('/v/hello') - const value = Buffer.from('world') - - nodeA.dht.put(key, value, (err) => { - expect(err).to.not.exist() - - nodeA.dht.get(key, (err, res) => { - expect(err).to.not.exist() - expect(res).to.eql(value) - done() - }) - }) - }) - - it('should be able to dht.getMany a value from the DHT with options', (done) => { - const key = Buffer.from('/v/hello') - const value = Buffer.from('world') - - nodeA.dht.put(key, value, (err) => { - expect(err).to.not.exist() - - nodeA.dht.getMany(key, 1, { maxTimeout: 3000 }, (err, res) => { - expect(err).to.not.exist() - expect(res).to.exist() - done() - }) - }) - }) - - it('should be able to dht.getMany a value from the DHT with no options defined', (done) => { - const key = Buffer.from('/v/hello') - const value = Buffer.from('world') - - nodeA.dht.put(key, value, (err) => { - expect(err).to.not.exist() - - nodeA.dht.getMany(key, 1, (err, res) => { - expect(err).to.not.exist() - expect(res).to.exist() - done() - }) - }) - }) - }) - - describe('disabled', () => { - let nodeA - - before(function (done) { - createNode('/ip4/0.0.0.0/tcp/0', { - config: { - dht: { - enabled: false - } - } - }, (err, node) => { - expect(err).to.not.exist() - nodeA = node - nodeA.start(done) - }) - }) - - after((done) => { - nodeA.stop(done) - }) - - it('should receive an error on dht.put if the dht is disabled', (done) => { - const key = Buffer.from('key') - const value = Buffer.from('value') - - nodeA.dht.put(key, value, (err) => { - expect(err).to.exist() - expect(err.code).to.equal('ERR_DHT_DISABLED') - done() - }) - }) - - it('should receive an error on dht.get if the dht is disabled', (done) => { - const key = Buffer.from('key') - - nodeA.dht.get(key, (err) => { - expect(err).to.exist() - expect(err.code).to.equal('ERR_DHT_DISABLED') - done() - }) - }) - - it('should receive an error on dht.getMany if the dht is disabled', (done) => { - const key = Buffer.from('key') - - nodeA.dht.getMany(key, 10, (err) => { - expect(err).to.exist() - expect(err.code).to.equal('ERR_DHT_DISABLED') - done() - }) - }) - }) -}) diff --git a/test/fixtures/browser.js b/test/fixtures/browser.js new file mode 100644 index 0000000000..57c9628a64 --- /dev/null +++ b/test/fixtures/browser.js @@ -0,0 +1,7 @@ +'use strict' + +const multiaddr = require('multiaddr') + +module.exports.MULTIADDRS_WEBSOCKETS = [ + multiaddr('/ip4/127.0.0.1/tcp/15001/ws') +] diff --git a/test/fixtures/peers.js b/test/fixtures/peers.js new file mode 100644 index 0000000000..fad0d23e8b --- /dev/null +++ b/test/fixtures/peers.js @@ -0,0 +1,27 @@ +'use strict' + +module.exports = [{ + id: 'QmNMMAqSxPetRS1cVMmutW5BCN1qQQyEr4u98kUvZjcfEw', + privKey: 'CAASpQkwggShAgEAAoIBAQDPek2aeHMa0blL42RTKd6xgtkk4Zkldvq4LHxzcag5uXepiQzWANEUvoD3KcUTmMRmx14PvsxdLCNst7S2JSa0R2n5wSRs14zGy6892lx4H4tLBD1KSpQlJ6vabYM1CJhIQRG90BtzDPrJ/X1iJ2HA0PPDz0Mflam2QUMDDrU0IuV2m7gSCJ5r4EmMs3U0xnH/1gShkVx4ir0WUdoWf5KQUJOmLn1clTRHYPv4KL9A/E38+imNAXfkH3c2T7DrCcYRkZSpK+WecjMsH1dCX15hhhggNqfp3iulO1tGPxHjm7PDGTPUjpCWKpD5e50sLqsUwexac1ja6ktMfszIR+FPAgMBAAECggEAB2H2uPRoRCAKU+T3gO4QeoiJaYKNjIO7UCplE0aMEeHDnEjAKC1HQ1G0DRdzZ8sb0fxuIGlNpFMZv5iZ2ZFg2zFfV//DaAwTek9tIOpQOAYHUtgHxkj5FIlg2BjlflGb+ZY3J2XsVB+2HNHkUEXOeKn2wpTxcoJE07NmywkO8Zfr1OL5oPxOPlRN1gI4ffYH2LbfaQVtRhwONR2+fs5ISfubk5iKso6BX4moMYkxubYwZbpucvKKi/rIjUA3SK86wdCUnno1KbDfdXSgCiUlvxt/IbRFXFURQoTV6BOi3sP5crBLw8OiVubMr9/8WE6KzJ0R7hPd5+eeWvYiYnWj4QKBgQD6jRlAFo/MgPO5NZ/HRAk6LUG+fdEWexA+GGV7CwJI61W/Dpbn9ZswPDhRJKo3rquyDFVZPdd7+RlXYg1wpmp1k54z++L1srsgj72vlg4I8wkZ4YLBg0+zVgHlQ0kxnp16DvQdOgiRFvMUUMEgetsoIx1CQWTd67hTExGsW+WAZQKBgQDT/WaHWvwyq9oaZ8G7F/tfeuXvNTk3HIJdfbWGgRXB7lJ7Gf6FsX4x7PeERfL5a67JLV6JdiLLVuYC2CBhipqLqC2DB962aKMvxobQpSljBBZvZyqP1IGPoKskrSo+2mqpYkeCLbDMuJ1nujgMP7gqVjabs2zj6ACKmmpYH/oNowJ/T0ZVtvFsjkg+1VsiMupUARRQuPUWMwa9HOibM1NIZcoQV2NGXB5Z++kR6JqxQO0DZlKArrviclderUdY+UuuY4VRiSEprpPeoW7ZlbTku/Ap8QZpWNEzZorQDro7bnfBW91fX9/81ets/gCPGrfEn+58U3pdb9oleCOQc/ifpQKBgBTYGbi9bYbd9vgZs6bd2M2um+VFanbMytS+g5bSIn2LHXkVOT2UEkB+eGf9KML1n54QY/dIMmukA8HL1oNAyalpw+/aWj+9Ui5kauUhGEywHjSeBEVYM9UXizxz+m9rsoktLLLUI0o97NxCJzitG0Kub3gn0FEogsUeIc7AdinZAoGBANnM1vcteSQDs7x94TDEnvvqwSkA2UWyLidD2jXgE0PG4V6tTkK//QPBmC9eq6TIqXkzYlsErSw4XeKO91knFofmdBzzVh/ddgx/NufJV4tXF+a2iTpqYBUJiz9wpIKgf43/Ob+P1EA99GAhSdxz1ess9O2aTqf3ANzn6v6g62Pv', + pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPek2aeHMa0blL42RTKd6xgtkk4Zkldvq4LHxzcag5uXepiQzWANEUvoD3KcUTmMRmx14PvsxdLCNst7S2JSa0R2n5wSRs14zGy6892lx4H4tLBD1KSpQlJ6vabYM1CJhIQRG90BtzDPrJ/X1iJ2HA0PPDz0Mflam2QUMDDrU0IuV2m7gSCJ5r4EmMs3U0xnH/1gShkVx4ir0WUdoWf5KQUJOmLn1clTRHYPv4KL9A/E38+imNAXfkH3c2T7DrCcYRkZSpK+WecjMsH1dCX15hhhggNqfp3iulO1tGPxHjm7PDGTPUjpCWKpD5e50sLqsUwexac1ja6ktMfszIR+FPAgMBAAE=' +}, { + id: 'QmW8rAgaaA6sRydK1k6vonShQME47aDxaFidbtMevWs73t', + privKey: 'CAASpwkwggSjAgEAAoIBAQCTU3gVDv3SRXLOsFln9GEf1nJ/uCEDhOG10eC0H9l9IPpVxjuPT1ep+ykFUdvefq3D3q+W3hbmiHm81o8dYv26RxZIEioToUWp7Ec5M2B/niYoE93za9/ZDwJdl7eh2hNKwAdxTmdbXUPjkIU4vLyHKRFbJIn9X8w9djldz8hoUvC1BK4L1XrT6F2l0ruJXErH2ZwI1youfSzo87TdXIoFKdrQLuW6hOtDCGKTiS+ab/DkMODc6zl8N47Oczv7vjzoWOJMUJs1Pg0ZsD1zmISY38P0y/QyEhatZn0B8BmSWxlLQuukatzOepQI6k+HtfyAAjn4UEqnMaXTP1uwLldVAgMBAAECggEAHq2f8MqpYjLiAFZKl9IUs3uFZkEiZsgx9BmbMAb91Aec+WWJG4OLHrNVTG1KWp+IcaQablEa9bBvoToQnS7y5OpOon1d066egg7Ymfmv24NEMM5KRpktCNcOSA0CySpPIB6yrg6EiUr3ixiaFUGABKkxmwgVz/Q15IqM0ZMmCUsC174PMAz1COFZxD0ZX0zgHblOJQW3dc0X3XSzhht8vU02SMoVObQHQfeXEHv3K/RiVj/Ax0bTc5JVkT8dm8xksTtsFCNOzRBqFS6MYqX6U/u0Onz3Jm5Jt7fLWb5n97gZR4SleyGrqxYNb46d9X7mP0ie7E6bzFW0DsWBIeAqVQKBgQDW0We2L1n44yOvJaMs3evpj0nps13jWidt2I3RlZXjWzWHiYQfvhWUWqps/xZBnAYgnN/38xbKzHZeRNhrqOo+VB0WK1IYl0lZVE4l6TNKCsLsUfQzsb1pePkd1eRZA+TSqsi+I/IOQlQU7HA0bMrah/5FYyUBP0jYvCOvYTlZuwKBgQCvkcVRydVlzjUgv7lY5lYvT8IHV5iYO4Qkk2q6Wjv9VUKAJZauurMdiy05PboWfs5kbETdwFybXMBcknIvZO4ihxmwL8mcoNwDVZHI4bXapIKMTCyHgUKvJ9SeTcKGC7ZuQJ8mslRmYox/HloTOXEJgQgPRxXcwa3amzvdZI+6LwKBgQCLsnQqgxKUi0m6bdR2qf7vzTH4258z6X34rjpT0F5AEyF1edVFOz0XU/q+lQhpNEi7zqjLuvbYfSyA026WXKuwSsz7jMJ/oWqev/duKgAjp2npesY/E9gkjfobD+zGgoS9BzkyhXe1FCdP0A6L2S/1+zg88WOwMvJxl6/xLl24XwKBgCm60xSajX8yIQyUpWBM9yUtpueJ2Xotgz4ST+bVNbcEAddll8gWFiaqgug9FLLuFu5lkYTHiPtgc1RNdphvO+62/9MRuLDixwh/2TPO+iNqwKDKJjda8Nei9vVddCPaOtU/xNQ0xLzFJbG9LBmvqH9izOCcu8SJwGHaTcNUeJj/AoGADCJ26cY30c13F/8awAAmFYpZWCuTP5ppTsRmjd63ixlrqgkeLGpJ7kYb5fXkcTycRGYgP0e1kssBGcmE7DuG955fx3ZJESX3GQZ+XfMHvYGONwF1EiK1f0p6+GReC2VlQ7PIkoD9o0hojM6SnWvv9EXNjCPALEbfPFFvcniKVsE=', + pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCTU3gVDv3SRXLOsFln9GEf1nJ/uCEDhOG10eC0H9l9IPpVxjuPT1ep+ykFUdvefq3D3q+W3hbmiHm81o8dYv26RxZIEioToUWp7Ec5M2B/niYoE93za9/ZDwJdl7eh2hNKwAdxTmdbXUPjkIU4vLyHKRFbJIn9X8w9djldz8hoUvC1BK4L1XrT6F2l0ruJXErH2ZwI1youfSzo87TdXIoFKdrQLuW6hOtDCGKTiS+ab/DkMODc6zl8N47Oczv7vjzoWOJMUJs1Pg0ZsD1zmISY38P0y/QyEhatZn0B8BmSWxlLQuukatzOepQI6k+HtfyAAjn4UEqnMaXTP1uwLldVAgMBAAE=' +}, { + id: 'QmZqCdSzgpsmB3Qweb9s4fojAoqELWzqku21UVrqtVSKi4', + privKey: 'CAASpgkwggSiAgEAAoIBAQCdbSEsTmw7lp5HagRcx57DaLiSUEkh4iBcKc7Y+jHICEIA8NIVi9FlfGEZj9G21FpiTR4Cy+BLVEuf8Nm90bym4iV+cSumeS21fvD8xGTEbeKGljs6OYHy3M45JhWF85gqHQJOqZufI2NRDuRgMZEO2+qGEXmSlv9mMXba/+9ecze8nSpB7bG2Z2pnKDeYwhF9Cz+ElMyn7TBWDjJERGVgFbTpdM3rBnbhB/TGpvs732QqZmIBlxnDb/Jn0l1gNZCgkEDcJ/0NDMBJTQ8vbvcdmaw3eaMPLkn1ix4wdu9QWCA0IBtuY1R7vSUtf4irnLJG7DnAw2GfM5QrF3xF1GLXAgMBAAECggEAQ1N0qHoxl5pmvqv8iaFlqLSUmx5y6GbI6CGJMQpvV9kQQU68yjItr3VuIXx8d/CBZyEMAK4oko7OeOyMcr3MLKLy3gyQWnXgsopDjhZ/8fH8uwps8g2+IZuFJrO+6LaxEPGvFu06fOiphPUVfn40R2KN/iBjGeox+AaXijmCqaV2vEdNJJPpMfz6VKZBDLTrbiqvo/3GN1U99PUqfPWpOWR29oAhh/Au6blSqvqTUPXB2+D/X6e1JXv31mxMPK68atDHSUjZWKB9lE4FMK1bkSKJRbyXmNIlbZ9V8X4/0r8/6T7JnW7ZT8ugRkquohmwgG7KkDXB1YsOCKXYUqzVYQKBgQDtnopFXWYl7XUyePJ/2MA5i7eoko9jmF44L31irqmHc5unNf6JlNBjlxTNx3WyfzhUzrn3c18psnGkqtow0tkBj5hmqn8/WaPbc5UA/5R1FNaNf8W5khn7MDm6KtYRPjN9djqTDiVHyC6ljONYd+5S+MqyKVWZ3t/xvG60sw85qwKBgQCpmpDtL+2JBwkfeUr3LyDcQxvbfzcv8lXj2otopWxWiLiZF1HzcqgAa2CIwu9kCGEt9Zr+9E4uINbe1To0b01/FhvR6xKO/ukceGA/mBB3vsKDcRmvpBUp+3SmnhY0nOk+ArQl4DhJ34k8pDM3EDPrixPf8SfVdU/8IM32lsdHhQKBgHLgpvCKCwxjFLnmBzcPzz8C8TOqR3BbBZIcQ34l+wflOGdKj1hsfaLoM8KYn6pAHzfBCd88A9Hg11hI0VuxVACRL5jS7NnvuGwsIOluppNEE8Ys86aXn7/0vLPoab3EWJhbRE48FIHzobmft3nZ4XpzlWs02JGfUp1IAC2UM9QpAoGAeWy3pZhSr2/iEC5+hUmwdQF2yEbj8+fDpkWo2VrVnX506uXPPkQwE1zM2Bz31t5I9OaJ+U5fSpcoPpDaAwBMs1fYwwlRWB8YNdHY1q6/23svN3uZsC4BGPV2JnO34iMUudilsRg+NGVdk5TbNejbwx7nM8Urh59djFzQGGMKeSECgYA0QMCARPpdMY50Mf2xQaCP7HfMJhESSPaBq9V3xY6ToEOEnXgAR5pNjnU85wnspHp+82r5XrKfEQlFxGpj2YA4DRRmn239sjDa29qP42UNAFg1+C3OvXTht1d5oOabaGhU0udwKmkEKUbb0bG5xPQJ5qeSJ5T1gLzLk3SIP0GlSw==', + pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdbSEsTmw7lp5HagRcx57DaLiSUEkh4iBcKc7Y+jHICEIA8NIVi9FlfGEZj9G21FpiTR4Cy+BLVEuf8Nm90bym4iV+cSumeS21fvD8xGTEbeKGljs6OYHy3M45JhWF85gqHQJOqZufI2NRDuRgMZEO2+qGEXmSlv9mMXba/+9ecze8nSpB7bG2Z2pnKDeYwhF9Cz+ElMyn7TBWDjJERGVgFbTpdM3rBnbhB/TGpvs732QqZmIBlxnDb/Jn0l1gNZCgkEDcJ/0NDMBJTQ8vbvcdmaw3eaMPLkn1ix4wdu9QWCA0IBtuY1R7vSUtf4irnLJG7DnAw2GfM5QrF3xF1GLXAgMBAAE=' +}, { + id: 'QmR5VwgsL7jyfZHAGyp66tguVrQhCRQuRc3NokocsCZ3fA', + privKey: 'CAASpwkwggSjAgEAAoIBAQCGXYU+uc2nn1zuJhfdFOl34upztnrD1gpHu58ousgHdGlGgYgbqLBAvIAauXdEL0+e30HofjA634SQxE+9nV+0FQBam1DDzHQlXsuwHV+2SKvSDkk4bVllMFpu2SJtts6VH+OXC/2ANJOm+eTALykQPYXgLIBxrhp/eD+Jz5r6wW2nq3k6OmYyK/4pgGzFjo5UyX+fa/171AJ68UPboFpDy6BZCcUjS0ondxPvD7cv5jMNqqMKIB/7rpi8n+Q3oeccRqVL56wH+FE3/QLjwYHwY6ILNRyvNXRqHjwBEXB2R5moXN0AFUWTw9rt3KhFiEjR1U81BTw5/xS7W2Iu0FgZAgMBAAECggEAS64HK8JZfE09eYGJNWPe8ECmD1C7quw21BpwVe+GVPSTizvQHswPohbKDMNj0srXDMPxCnNw1OgqcaOwyjsGuZaOoXoTroTM8nOHRIX27+PUqzaStS6aCG2IsiCozKUHjGTuupftS7XRaF4eIsUtWtFcQ1ytZ9pJYHypRQTi5NMSrTze5ThjnWxtHilK7gnBXik+aR0mYEVfSn13czQEC4rMOs+b9RAc/iibDNoLopfIdvmCCvfxzmySnR7Cu1iSUAONkir7PB+2Mt/qRFCH6P+jMamtCgQ8AmifXgVmDUlun+4MnKg3KrPd6ZjOEKhVe9mCHtGozk65RDREShfDdQKBgQDi+x2MuRa9peEMOHnOyXTS+v+MFcfmG0InsO08rFNBKZChLB+c9UHBdIvexpfBHigSyERfuDye4z6lxi8ZnierWMYJP30nxmrnxwTGTk1MQquhfs1A0kpmDnPsjlOS/drEIEIssNx2WbfJ7YtMxLWBtp+BJzGpQmr0LKC+NHRSrwKBgQCXiy2kJESIUkIs2ihV55hhT6/bZo1B1O5DPA2nkjOBXqXF6fvijzMDX82JjLd07lQZlI0n1Q/Hw0p4iYi9YVd2bLkLXF5UIb2qOeHj76enVFOrPHUSkC9Y2g/0Xs+60Ths2xRd8RrrfQU3kl5iVpBywkCIrb2M5+wRnNTk1W3TtwKBgQCvplyrteAfSurpJhs9JzE8w/hWU9SqAZYkWQp91W1oE95Um2yrbjBAoQxMjaqKS+f/APPIjy56Vqj4aHGyhW11b/Fw3qzfxvCcBKtxOs8eoMlo5FO6QgJJEA4tlcafDcvp0nzjUMqK28safLU7503+33B35fjMXxWdd5u9FaKfCQKBgC4W6j6tuRosymuRvgrCcRnHfpify/5loEFallyMnpWOD6Tt0OnK25z/GifnYDRz96gAAh5HMpFy18dpLOlMHamqz2yhHx8/U8vd5tHIJZlCkF/X91M5/uxrBccwvsT2tM6Got8fYSyVzWxlW8dUxIHiinYHQUsFjkqdBDLEpq5pAoGASoTw5RBEWFM0GuAZdXsyNyxU+4S+grkTS7WdW/Ymkukh+bJZbnvF9a6MkSehqXnknthmufonds2AFNS//63gixENsoOhzT5+2cdfc6tJECvJ9xXVXkf85AoQ6T/RrXF0W4m9yQyCngNJUrKUOIH3oDIfdZITlYzOC3u1ojj7VuQ=', + pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCGXYU+uc2nn1zuJhfdFOl34upztnrD1gpHu58ousgHdGlGgYgbqLBAvIAauXdEL0+e30HofjA634SQxE+9nV+0FQBam1DDzHQlXsuwHV+2SKvSDkk4bVllMFpu2SJtts6VH+OXC/2ANJOm+eTALykQPYXgLIBxrhp/eD+Jz5r6wW2nq3k6OmYyK/4pgGzFjo5UyX+fa/171AJ68UPboFpDy6BZCcUjS0ondxPvD7cv5jMNqqMKIB/7rpi8n+Q3oeccRqVL56wH+FE3/QLjwYHwY6ILNRyvNXRqHjwBEXB2R5moXN0AFUWTw9rt3KhFiEjR1U81BTw5/xS7W2Iu0FgZAgMBAAE=' +}, { + id: 'QmScLDqRg7H6ipCYxm9fVk152UWavQFKscTdoT4YNHxgqp', + privKey: 'CAASpwkwggSjAgEAAoIBAQCWEHaTZ6LBLFP5OPrUqjDM/cF4b2zrfh1Zm3kd02ZtgQB3iYtZqRPJT5ctT3A7WdVF/7dCxPGOCkJlLekTx4Y4gD8JtjA+EfN9fR/2RBKbti2N3CD4vkGp9ss4hbBFcXIhl8zuD/ELHutbV6b8b4QXJGnxfp/B+1kNPnyd7SJznS0QyvI8OLI1nAkVKdYLDRW8kPKeHyx1xhdNDuTQVTFyAjRGQ4e3UYFB7bYIHW3E6kCtCoJDlj+JPC02Yt1LHzIzZVLvPvNFnYY2mag6OiGFuh/oMBIqvnPc1zRZ3eLUqeGZjQVaoR0kdgZUKz7Q2TBeNldxK/s6XO0DnkQTlelNAgMBAAECggEAdmt1dyswR2p4tdIeNpY7Pnj9JNIhTNDPznefI0dArCdBvBMhkVaYk6MoNIxcj6l7YOrDroAF8sXr0TZimMY6B/pERKCt/z1hPWTxRQBBAvnHhwvwRPq2jK6BfhAZoyM8IoBNKowP9mum5QUNdGV4Al8s73KyFX0IsCfgZSvNpRdlt+DzPh+hu/CyoZaMpRchJc1UmK8Fyk3KfO+m0DZNfHP5P08lXNfM6MZLgTJVVgERHyG+vBOzTd2RElMe19nVCzHwb3dPPRZSQ7Fnz3rA+GeLqsM2Zi4HNhfbD1OcD9C4wDj5tYL6hWTkdz4IlfVcjCeUHxgIOhdDV2K+OwbuAQKBgQD0FjUZ09UW2FQ/fitbvIB5f1SkXWPxTF9l6mAeuXhoGv2EtQUO4vq/PK6N08RjrZdWQy6UsqHgffi7lVQ8o3hvCKdbtf4sP+cM92OrY0WZV89os79ndj4tyvmnP8WojwRjt/2XEfgdoWcgWxW9DiYINTOQVimZX+X/3on4s8hEgQKBgQCdY3kOMbyQeLTRkqHXjVTY4ddO+v4S4wOUa1l4rTqAbq1W3JYWwoDQgFuIu3limIHmjnSJpCD4EioXFsM7p6csenoc20sHxsaHnJ6Mn5Te41UYmY9EW0otkQ0C3KbXM0hwQkjyplnEmZawGKmjEHW8DJ3vRYTv9TUCgYKxDHgOzQKBgB4A/NYH7BG61eBYKgxEx6YnuMfbkwV+Vdu5S8d7FQn3B2LgvZZu4FPRqcNVXLbEB+5ao8czjiKCWaj1Wj15+rvrXGcxn+Tglg5J+r5+nXeUC7LbJZQaPNp0MOwWMr3dlrSLUWjYlJ9Pz9VyXOG4c4Rexc/gR4zK9QLW4C7qKpwBAoGAZzyUb0cYlPtYQA+asTU3bnvVKy1f8yuNcZFowst+EDiI4u0WVh+HNzy6zdmLKa03p+/RaWeLaK0hhrubnEnAUmCUMNF3ScaM+u804LDcicc8TkKLwx7ObU0z56isl4RAA8K27tNHFrpYKXJD834cfBkaj5ReOrfw6Y/iFhhDuBECgYEA8gbC76uz7LSHhW30DSRTcqOzTyoe2oYKQaxuxYNp7vSSOkcdRen+mrdflDvud2q/zN2QdL4pgqdldHlR35M/lJ0f0B6zp74jlzbO9700wzsOqreezGc5eWiroDL100U9uIZ50BKb8CKtixIHpinUSPIUcVDkSAZ2y7mbfCxQwqQ=', + pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCWEHaTZ6LBLFP5OPrUqjDM/cF4b2zrfh1Zm3kd02ZtgQB3iYtZqRPJT5ctT3A7WdVF/7dCxPGOCkJlLekTx4Y4gD8JtjA+EfN9fR/2RBKbti2N3CD4vkGp9ss4hbBFcXIhl8zuD/ELHutbV6b8b4QXJGnxfp/B+1kNPnyd7SJznS0QyvI8OLI1nAkVKdYLDRW8kPKeHyx1xhdNDuTQVTFyAjRGQ4e3UYFB7bYIHW3E6kCtCoJDlj+JPC02Yt1LHzIzZVLvPvNFnYY2mag6OiGFuh/oMBIqvnPc1zRZ3eLUqeGZjQVaoR0kdgZUKz7Q2TBeNldxK/s6XO0DnkQTlelNAgMBAAE=' +}, { + id: 'QmckxVrJw1Yo8LqvmDJNUmdAsKtSbiKWmrXJFyKmUraBoN', + privKey: 'CAASpwkwggSjAgEAAoIBAQC1/GFud/7xutux7qRfMj1sIdMRh99/chR6HqVj6LQqrgk4jil0mdN/LCk/tqPqmDtObHdmEhCoybzuhLbCKgUqryKDwO6yBJHSKWY9QqrKZtLJ37SgKwGjE3+NUD4r1dJHhtQrICFdOdSCBzs/v8gi+J+KZLHo7+Nms4z09ysy7qZh94Pd7cW4gmSMergqUeANLD9C0ERw1NXolswOW7Bi7UGr7yuBxejICLO3nkxe0OtpQBrYrqdCD9vs3t/HQZbPWVoiRj4VO7fxkAPKLl30HzcIfxj/ayg8NHcH59d08D+N2v5Sdh28gsiYKIPE9CXvuw//HUY2WVRY5fDC5JglAgMBAAECggEBAKb5aN/1w3pBqz/HqRMbQpYLNuD33M3PexBNPAy+P0iFpDo63bh5Rz+A4lvuFNmzUX70MFz7qENlzi6+n/zolxMB29YtWBUH8k904rTEjXXl//NviQgITZk106tx+4k2x5gPEm57LYGfBOdFAUzNhzDnE2LkXwRNzkS161f7zKwOEsaGWRscj6UvhO4MIFxjb32CVwt5eK4yOVqtyMs9u30K4Og+AZYTlhtm+bHg6ndCCBO6CQurCQ3jD6YOkT+L3MotKqt1kORpvzIB0ujZRf49Um8wlcjC5G9aexBeGriXaVdPF62zm7GA7RMsbQM/6aRbA1fEQXvJhHUNF9UFeaECgYEA8wCjKqQA7UQnHjRwTsktdwG6szfxd7z+5MTqHHTWhWzgcQLgdh5/dO/zanEoOThadMk5C1Bqjq96gH2xim8dg5XQofSVtV3Ui0dDa+XRB3E3fyY4D3RF5hHv85O0GcvQc6DIb+Ja1oOhvHowFB1C+CT3yEgwzX/EK9xpe+KtYAkCgYEAv7hCnj/DcZFU3fAfS+unBLuVoVJT/drxv66P686s7J8UM6tW+39yDBZ1IcwY9vHFepBvxY2fFfEeLI02QFM+lZXVhNGzFkP90agNHK01psGgrmIufl9zAo8WOKgkLgbYbSHzkkDeqyjEPU+B0QSsZOCE+qLCHSdsnTmo/TjQhj0CgYAz1+j3yfGgrS+jVBC53lXi0+2fGspbf2jqKdDArXSvFqFzuudki/EpY6AND4NDYfB6hguzjD6PnoSGMUrVfAtR7X6LbwEZpqEX7eZGeMt1yQPMDr1bHrVi9mS5FMQR1NfuM1lP9Xzn00GIUpE7WVrWUhzDEBPJY/7YVLf0hFH08QKBgDWBRQZJIVBmkNrHktRrVddaSq4U/d/Q5LrsCrpymYwH8WliHgpeTQPWmKXwAd+ZJdXIzYjCt202N4eTeVqGYOb6Q/anV2WVYBbM4avpIxoA28kPGY6nML+8EyWIt2ApBOmgGgvtEreNzwaVU9NzjHEyv6n7FlVwlT1jxCe3XWq5AoGASYPKQoPeDlW+NmRG7z9EJXJRPVtmLL40fmGgtju9QIjLnjuK8XaczjAWT+ySI93Whu+Eujf2Uj7Q+NfUjvAEzJgwzuOd3jlQvoALq11kuaxlNQTn7rx0A1QhBgUJE8AkvShPC9FEnA4j/CLJU0re9H/8VvyN6qE0Mho0+YbjpP8=', + pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC1/GFud/7xutux7qRfMj1sIdMRh99/chR6HqVj6LQqrgk4jil0mdN/LCk/tqPqmDtObHdmEhCoybzuhLbCKgUqryKDwO6yBJHSKWY9QqrKZtLJ37SgKwGjE3+NUD4r1dJHhtQrICFdOdSCBzs/v8gi+J+KZLHo7+Nms4z09ysy7qZh94Pd7cW4gmSMergqUeANLD9C0ERw1NXolswOW7Bi7UGr7yuBxejICLO3nkxe0OtpQBrYrqdCD9vs3t/HQZbPWVoiRj4VO7fxkAPKLl30HzcIfxj/ayg8NHcH59d08D+N2v5Sdh28gsiYKIPE9CXvuw//HUY2WVRY5fDC5JglAgMBAAE=' +}] diff --git a/test/fixtures/test-data/test-id.json b/test/fixtures/test-data/test-id.json deleted file mode 100644 index 0b37a6cc9c..0000000000 --- a/test/fixtures/test-data/test-id.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "QmaG17D4kfTB2RNUCr16bSfVvUVt2Xn3rPYeqQDvnVcXFr", - "privKey": "CAASqAkwggSkAgEAAoIBAQDBpXRrSLoVhP8C4YI0nm+YTb7UIe+xT9dwaMzKcGsH2zzz1lfxl54e1XNO+6Ut+If5jswpydgHhn9nGPod53sUIR2m+BiHOAH/Blgfa1nUKUkspts1MH3z5ZaO6Xo336Y0Uaw7UqfeIzKliTM6bpev2XIHyu0v/VJ2mylzfbDLMWqZs/shE3xwCJCD/PxoVpTlr/SzzF7MdgDMxkyiC3iLZ5Zkm+baPbi3mpKM0ue25Thedcc0KFjhQrjBfy5FPamrsMn5fnnoHwnQl9u7UWigzeC+7X+38EML1sVrV37ExxHPtM6882Ivjc7VN6zFHOHD2c9eVfbShkIf8YkVQUcFAgMBAAECggEAVE1mgGo58LJknml0WNn8tS5rfEiF5AhhPyOwvBTy04nDYFgZEykxgjTkrSbqgzfmYmOjSDICJUyNXGHISYqDz4CXOyBY9U0RuWeWp58BjVan75N4bRB+VNbHk9HbDkYEQlSoCW9ze0aRfvVa4v5QdRLSDMhwN+stokrsYcX/WIWYTM2e2jW+qQOzS8SJl7wYsgtd3WikrxwXkRL3sCMHEcgcPhoKacoD5Yr9cB0IC5vzhu4t/WMa+N2UEndcKGAbXsh8kA7BPFM6lqnEpOHpWEVEAYasAwFGUvUN9GwhtqpaNNS2sG6Nrz95cC99Nqx58uIXcTAJm3Fh/WfKJ6I1xQKBgQD+g7A5OSWw+i/zhTKVPJg93/eohViL0dGZT9Tf0/VslsFl00FwnZmBKA6BJ6ZL3hD00OcqIL3m6EzZ4q38U97XZxf2OUsPPJtl+Avqtlk16AHRHB9I17LGXJ30xZRkxL665oLms0D2T4NIZZX/uVMoS18lRvCZj1aEYQFCrZYgowKBgQDCxtA695S0vl6E3Q4G6MrDZK+2JqjaGL0XTnpHWiAjnk2lnV2CCZnWpEHT+ebF2fWx5nYQo5sugc6bS+4k9jRNUgxf2sQieZYCBjbnjCEVrPTm/dPTkaw1CQ/ox5/R1/Elbw8vteF9uUAvR0FL8Ss1Dqw6B2SxdTowxMy6qQ7sNwKBgG2N3eMj2DeP2egm45kdliK8L2yYyX6V+HTXyjf2kuQFGIZuIvMIw7S2u1eY65ooon/fFEIsCdJFGB+J1X6R05BAzi2sh8StP+7qkKadi1UK4w1R352JS2jbIRrlmXSuw7LL2njXnBTqMQaOw7xp14O2vePb32EaNBGTd+ltsvulAoGBALGIc4370oA4MIDb2Ag2MXKNmJbnf+piuB/BOTVGEZtFlDKLUArR43W/+/xRgKX/97FyhVS/OxfV21Kzj9oCy0NasMrB5RojRraLoYnFsPZH0mWlIGlsEtG4c9bR9XtYX4WmR+pN1r04mCc/xGWK6b4PpK2zxXT2i9ad2pmctGxbAoGBAIcp0UML5QCqvLmcob2/6PCRaYAxJBb9lDqOHredMgQih2hGnHFCyKk9eBAbFf/KN0guJTBDaAJRclcxsLLn7rV6grMNt+0EUepm7tWT0z5j8gNGbGGhuGDdqcmfJTc2EMdQrfhzYDN3rL1v3l+Ujwla2khL2ozE7SQ/KVeA1saY", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDBpXRrSLoVhP8C4YI0nm+YTb7UIe+xT9dwaMzKcGsH2zzz1lfxl54e1XNO+6Ut+If5jswpydgHhn9nGPod53sUIR2m+BiHOAH/Blgfa1nUKUkspts1MH3z5ZaO6Xo336Y0Uaw7UqfeIzKliTM6bpev2XIHyu0v/VJ2mylzfbDLMWqZs/shE3xwCJCD/PxoVpTlr/SzzF7MdgDMxkyiC3iLZ5Zkm+baPbi3mpKM0ue25Thedcc0KFjhQrjBfy5FPamrsMn5fnnoHwnQl9u7UWigzeC+7X+38EML1sVrV37ExxHPtM6882Ivjc7VN6zFHOHD2c9eVfbShkIf8YkVQUcFAgMBAAE=" -} diff --git a/test/fixtures/test-peer.json b/test/fixtures/test-peer.json deleted file mode 100644 index ba789ca32d..0000000000 --- a/test/fixtures/test-peer.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "Qmex1SSsueWFsUfjdkugJ5zhcnjddAt8TxcnDLUXKD9Sx7", - "privKey": "CAASqAkwggSkAgEAAoIBAQCXzV127CvVHOGMzvsn/U+/32JM58KA6k0FSCCeNFzNowiDS/vV5eezGN5AFoxsF6icWLoaczz7l9RdVD+I/t6PEt9X7XUdrDCtSS8WmAcCgvZWSSf7yAd3jT4GSZDUIgIEeRZsERDt/yVqTLwsZ1G9dMIeh8sbf2zwjTXZIWaRM6o4lq3DYFfzLvJUXlJodxPogU7l7nLkITPUv+yQAMcVHizbNwJvwiETKYeUj73/m/wEPAlnFESexDstxNiIwE/FH8Ao50QPZRO6E6Jb0hhYSI/4CLRdrzDFm/Vzplei3Wr2DokSROaNyeG37VAueyA+pDqn84um+L9uXLwbv5FbAgMBAAECggEAdBUzV/GaQ0nmoQrWvOnUxmFIho7kCjkh1NwnNVPNc+Msa1r7pcI9wJNPwap8j1w4L/cZuYhOJgcg+o2mWFiuULKZ4F9Ro/M89gZ038457g2/2pPu43c/Xoi/2YcAHXg0Gr+OCe2zCIyITBWKAFqyAzL6DubAxrJW2Ezj1LrZ+EZgMyzbh/go/eEGSJaaGkINeAkY144DqDWWWvzyhKhryipsGkZGEkVy9xJgMEI3ipVvuPez2XAvoyyeuinBBLe+Z2vY5G50XXzbIMhIQGLncHf9MwTv6wt1ilyOSLOXK0BoQbB76J3R3is5dSULXXP9r8VocjLBEkmBuf4FXAKzoQKBgQDNNS4F1XE1gxD8LPkL+aB/hi6eVHVPhr+w0I/9ATikcLGeUfBM2Gd6cZRPFtNVrv1p6ZF1D1UyGDknGbDBSQd9wLUgb0fDoo3jKYMGWq6G+VvaP5rzWQeBV8YV2EhSmUk1i6kiYe2ZE8WyrPie7iwpQIY60e2A8Ly0GKZiBZUcHQKBgQC9YDAVsGnEHFVFkTDpvw5HwEzCgTb2A3NgkGY3rTYZ7L6AFjqCYmUwFB8Fmbyc4kdFWNh8wfmq5Qrvl49NtaeukiqWKUUlB8uPdztB1P0IahA2ks0owStZlRifmwfgYyMd4xE17lhaOgQQJZZPxmP0F6mdOvb3YJafNURCdMS51wKBgEvvIM+h0tmFXXSjQ6kNvzlRMtD92ccKysYn9xAdMpOO6/r0wSH+dhQWEVZO0PcE4NsfReb2PIVj90ojtIdhebcr5xpQc1LORQjJJKXmSmzBux6AqNrhl+hhzXfp56FA/Zkly/lgGWaqrV5XqUxOP+Mn8EO1yNgMvRc7g94DyNB1AoGBAKLBuXHalXwDsdHBUB2Eo3xNLGt6bEcRfia+0+sEBdxQGQWylQScFkU09dh1YaIf44sZKa5HdBFJGpYCVxo9hmjFnK5Dt/Z0daHOonIY4INLzLVqg8KECoLKXkhGEIXsDjFQhukn+G1LMVTDSSU055DQiWjlVX4UWD9qo0jOXIkvAoGBAMP50p2X6PsWWZUuuR7i1JOJHRyQZPWdHh9p8SSLnCtEpHYZfJr4INXNmhnSiB/3TUnHix2vVKjosjMTCk/CjfzXV2H41WPOLZ2/Pi3SxCicWIRj4kCcWhkEuIF2jGkg1+jmNiCl/zNMaBOAIP3QbDPtqOWbYlPd2YIzdj6WQ6R4", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCXzV127CvVHOGMzvsn/U+/32JM58KA6k0FSCCeNFzNowiDS/vV5eezGN5AFoxsF6icWLoaczz7l9RdVD+I/t6PEt9X7XUdrDCtSS8WmAcCgvZWSSf7yAd3jT4GSZDUIgIEeRZsERDt/yVqTLwsZ1G9dMIeh8sbf2zwjTXZIWaRM6o4lq3DYFfzLvJUXlJodxPogU7l7nLkITPUv+yQAMcVHizbNwJvwiETKYeUj73/m/wEPAlnFESexDstxNiIwE/FH8Ao50QPZRO6E6Jb0hhYSI/4CLRdrzDFm/Vzplei3Wr2DokSROaNyeG37VAueyA+pDqn84um+L9uXLwbv5FbAgMBAAE=" -} diff --git a/test/fsm.spec.js b/test/fsm.spec.js deleted file mode 100644 index 63328509a8..0000000000 --- a/test/fsm.spec.js +++ /dev/null @@ -1,168 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -chai.use(require('chai-checkmark')) -const expect = chai.expect -const sinon = require('sinon') -const series = require('async/series') -const createNode = require('./utils/create-node') - -describe('libp2p state machine (fsm)', () => { - describe('starting and stopping', () => { - let node - beforeEach((done) => { - createNode([], { - config: { - dht: { - enabled: false - } - } - }, (err, _node) => { - node = _node - done(err) - }) - }) - afterEach(() => { - node.removeAllListeners() - sinon.restore() - }) - after((done) => { - node.stop(done) - node = null - }) - - it('should be able to start and stop several times', (done) => { - node.on('start', (err) => { - expect(err).to.not.exist().mark() - }) - node.on('stop', (err) => { - expect(err).to.not.exist().mark() - }) - - expect(4).checks(done) - - series([ - (cb) => node.start(cb), - (cb) => node.stop(cb), - (cb) => node.start(cb), - (cb) => node.stop(cb) - ], () => {}) - }) - - it('should noop when stopping a stopped node', (done) => { - node.once('start', node.stop) - node.once('stop', () => { - node.state.on('STOPPING', () => { - throw new Error('should not stop a stopped node') - }) - node.once('stop', done) - - // stop the stopped node - node.stop(() => {}) - }) - node.start(() => {}) - }) - - it('should callback with an error when it occurs on stop', (done) => { - const error = new Error('some error starting') - node.once('start', () => { - node.once('error', (err) => { - expect(err).to.eql(error).mark() - }) - node.stop((err) => { - expect(err).to.eql(error).mark() - }) - }) - - expect(2).checks(done) - - sinon.stub(node._switch, 'stop').callsArgWith(0, error) - node.start(() => {}) - }) - - it('should noop when starting a started node', (done) => { - node.once('start', () => { - node.state.on('STARTING', () => { - throw new Error('should not start a started node') - }) - node.once('start', () => { - node.once('stop', done) - node.stop(() => {}) - }) - - // start the started node - node.start(() => {}) - }) - node.start(() => {}) - }) - - it('should error on start with no transports', (done) => { - const transports = node._modules.transport - node._modules.transport = null - - node.on('stop', () => { - node._modules.transport = transports - expect(node._modules.transport).to.exist().mark() - }) - node.on('error', (err) => { - expect(err).to.exist().mark() - }) - node.on('start', () => { - throw new Error('should not start') - }) - - expect(2).checks(done) - - node.start(() => {}) - }) - - it('should not start if the switch fails to start', (done) => { - const error = new Error('switch didnt start') - const stub = sinon.stub(node._switch, 'start') - .callsArgWith(0, error) - - node.on('stop', () => { - expect(stub.calledOnce).to.eql(true).mark() - stub.restore() - }) - node.on('error', (err) => { - expect(err).to.eql(error).mark() - }) - node.on('start', () => { - throw new Error('should not start') - }) - - expect(3).checks(done) - - node.start((err) => { - expect(err).to.eql(error).mark() - }) - }) - - it('should not dial when the node is stopped', (done) => { - node.on('stop', () => { - node.dial(null, (err) => { - expect(err).to.exist() - expect(err.code).to.eql('ERR_NODE_NOT_STARTED') - done() - }) - }) - - node.stop(() => {}) - }) - - it('should not dial (fsm) when the node is stopped', (done) => { - node.on('stop', () => { - node.dialFSM(null, null, (err) => { - expect(err).to.exist() - expect(err.code).to.eql('ERR_NODE_NOT_STARTED') - done() - }) - }) - - node.stop(() => {}) - }) - }) -}) diff --git a/test/get-peer-info.spec.js b/test/get-peer-info.spec.js deleted file mode 100644 index e550cb4661..0000000000 --- a/test/get-peer-info.spec.js +++ /dev/null @@ -1,134 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -const expect = chai.expect -const PeerBook = require('peer-book') -const PeerInfo = require('peer-info') -const PeerId = require('peer-id') -const MultiAddr = require('multiaddr') -const TestPeerInfos = require('./switch/test-data/ids.json').infos - -const { getPeerInfo, getPeerInfoRemote } = require('../src/get-peer-info') - -describe('Get Peer Info', () => { - describe('getPeerInfo', () => { - let peerBook - let peerInfoA - let multiaddrA - let peerIdA - - before((done) => { - peerBook = new PeerBook() - PeerId.createFromJSON(TestPeerInfos[0].id, (err, id) => { - peerIdA = id - peerInfoA = new PeerInfo(peerIdA) - multiaddrA = MultiAddr('/ipfs/QmdWYwTywvXBeLKWthrVNjkq9SafEDn1PbAZdz4xZW7Jd9') - peerInfoA.multiaddrs.add(multiaddrA) - peerBook.put(peerInfoA) - done(err) - }) - }) - - it('should be able get peer info from multiaddr', () => { - const _peerInfo = getPeerInfo(multiaddrA, peerBook) - expect(peerBook.has(_peerInfo)).to.equal(true) - expect(peerInfoA).to.deep.equal(_peerInfo) - }) - - it('should return a new PeerInfo with a multiAddr not in the PeerBook', () => { - const wrongMultiAddr = MultiAddr('/ipfs/QmckZzdVd72h9QUFuJJpQqhsZqGLwjhh81qSvZ9BhB2FQi') - const _peerInfo = getPeerInfo(wrongMultiAddr, peerBook) - expect(PeerInfo.isPeerInfo(_peerInfo)).to.equal(true) - }) - - it('should be able get peer info from peer id', () => { - const _peerInfo = getPeerInfo(multiaddrA, peerBook) - expect(peerBook.has(_peerInfo)).to.equal(true) - expect(peerInfoA).to.deep.equal(_peerInfo) - }) - - it('should add a peerInfo to the book', (done) => { - PeerId.createFromJSON(TestPeerInfos[1].id, (err, id) => { - const peerInfo = new PeerInfo(id) - expect(peerBook.has(peerInfo.id.toB58String())).to.eql(false) - - expect(getPeerInfo(peerInfo, peerBook)).to.exist() - expect(peerBook.has(peerInfo.id.toB58String())).to.eql(true) - done(err) - }) - }) - - it('should return the most up to date version of the peer', (done) => { - const ma1 = MultiAddr('/ip4/0.0.0.0/tcp/8080') - const ma2 = MultiAddr('/ip6/::/tcp/8080') - PeerId.createFromJSON(TestPeerInfos[1].id, (err, id) => { - const peerInfo = new PeerInfo(id) - peerInfo.multiaddrs.add(ma1) - expect(getPeerInfo(peerInfo, peerBook)).to.exist() - - const peerInfo2 = new PeerInfo(id) - peerInfo2.multiaddrs.add(ma2) - const returnedPeerInfo = getPeerInfo(peerInfo2, peerBook) - expect(returnedPeerInfo.multiaddrs.toArray()).to.contain.members([ - ma1, ma2 - ]) - done(err) - }) - }) - - it('an invalid peer type should throw an error', () => { - let error - try { - getPeerInfo('/ip4/127.0.0.1/tcp/1234', peerBook) - } catch (err) { - error = err - } - - expect(error.code).to.eql('ERR_INVALID_MULTIADDR') - }) - }) - - describe('getPeerInfoRemote', () => { - it('should callback with error for invalid string multiaddr', async () => { - let error - try { - await getPeerInfoRemote('INVALID MULTIADDR') - } catch (err) { - error = err - } - - expect(error.code).to.eql('ERR_INVALID_PEER_TYPE') - }) - - it('should callback with error for invalid non-peer multiaddr', async () => { - let error - try { - await getPeerInfoRemote('/ip4/8.8.8.8/tcp/1080') - } catch (err) { - error = err - } - - expect(error.code).to.eql('ERR_INVALID_PEER_TYPE') - }) - - it('should callback with error for invalid non-peer multiaddr', async () => { - let error - try { - await getPeerInfoRemote(undefined) - } catch (err) { - error = err - } - - expect(error.code).to.eql('ERR_INVALID_PEER_TYPE') - }) - - it('should callback with error for invalid non-peer multiaddr (promise)', () => { - return getPeerInfoRemote(undefined) - .then(expect.fail, (err) => { - expect(err.code).to.eql('ERR_INVALID_PEER_TYPE') - }) - }) - }) -}) diff --git a/test/identify/basic.spec.js b/test/identify/basic.spec.js deleted file mode 100644 index 6fb95dbf11..0000000000 --- a/test/identify/basic.spec.js +++ /dev/null @@ -1,15 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) - -const identify = require('../../src/identify') - -describe('basic', () => { - it('multicodec', () => { - expect(identify.multicodec).to.eql('/ipfs/id/1.0.0') - }) -}) diff --git a/test/identify/dialer.spec.js b/test/identify/dialer.spec.js deleted file mode 100644 index 31bd036ed4..0000000000 --- a/test/identify/dialer.spec.js +++ /dev/null @@ -1,192 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const pull = require('pull-stream/pull') -const values = require('pull-stream/sources/values') -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) -const pair = require('pull-pair/duplex') -const PeerInfo = require('peer-info') -const lp = require('pull-length-prefixed') -const multiaddr = require('multiaddr') - -const identify = require('../../src/identify') -const msg = identify.message - -describe('identify.dialer', () => { - let original - - before(function (done) { - this.timeout(20 * 1000) - - PeerInfo.create((err, info) => { - if (err) { - return done(err) - } - - original = info - done() - }) - }) - - afterEach(() => { - original.multiaddrs.clear() - original.protocols.clear() - }) - - it('works', (done) => { - const p = pair() - original.multiaddrs.add(multiaddr('/ip4/127.0.0.1/tcp/5002')) - original.protocols.add('/echo/1.0.0') - original.protocols.add('/ping/1.0.0') - - const input = msg.encode({ - protocolVersion: 'ipfs/0.1.0', - agentVersion: 'na', - publicKey: original.id.pubKey.bytes, - listenAddrs: [multiaddr('/ip4/127.0.0.1/tcp/5002').buffer], - observedAddr: multiaddr('/ip4/127.0.0.1/tcp/5001').buffer, - protocols: Array.from(original.protocols) - }) - - pull( - values([input]), - lp.encode(), - p[0] - ) - - identify.dialer(p[1], (err, info, observedAddrs) => { - expect(err).to.not.exist() - expect(info.id.pubKey.bytes) - .to.eql(original.id.pubKey.bytes) - - expect(info.multiaddrs.has(original.multiaddrs.toArray()[0])) - .to.eql(true) - - expect(multiaddr('/ip4/127.0.0.1/tcp/5001').equals(observedAddrs[0])) - .to.eql(true) - - expect(info.protocols).to.eql(original.protocols) - - done() - }) - }) - - it('should handle missing protocols', (done) => { - const p = pair() - original.multiaddrs.add(multiaddr('/ip4/127.0.0.1/tcp/5002')) - - const input = msg.encode({ - protocolVersion: 'ipfs/0.1.0', - agentVersion: 'na', - publicKey: original.id.pubKey.bytes, - listenAddrs: [multiaddr('/ip4/127.0.0.1/tcp/5002').buffer], - observedAddr: multiaddr('/ip4/127.0.0.1/tcp/5001').buffer, - protocols: Array.from(original.protocols) - }) - - pull( - values([input]), - lp.encode(), - p[0] - ) - - identify.dialer(p[1], (err, info, observedAddrs) => { - expect(err).to.not.exist() - expect(info.id.pubKey.bytes) - .to.eql(original.id.pubKey.bytes) - - expect(info.multiaddrs.has(original.multiaddrs.toArray()[0])) - .to.eql(true) - - expect(multiaddr('/ip4/127.0.0.1/tcp/5001').equals(observedAddrs[0])) - .to.eql(true) - - expect(Array.from(info.protocols)).to.eql([]) - - done() - }) - }) - - it('does not crash with invalid listen addresses', (done) => { - const p = pair() - original.multiaddrs.add(multiaddr('/ip4/127.0.0.1/tcp/5002')) - const input = msg.encode({ - protocolVersion: 'ipfs/0.1.0', - agentVersion: 'na', - publicKey: original.id.pubKey.bytes, - listenAddrs: [Buffer.from('ffac010203')], - observedAddr: Buffer.from('ffac010203') - }) - - pull( - values([input]), - lp.encode(), - p[0] - ) - - identify.dialer(p[1], (err, info, observedAddrs) => { - expect(err).to.exist() - - done() - }) - }) - - it('does not crash with invalid observed address', (done) => { - const p = pair() - original.multiaddrs.add(multiaddr('/ip4/127.0.0.1/tcp/5002')) - const input = msg.encode({ - protocolVersion: 'ipfs/0.1.0', - agentVersion: 'na', - publicKey: original.id.pubKey.bytes, - listenAddrs: [multiaddr('/ip4/127.0.0.1/tcp/5002').buffer], - observedAddr: Buffer.from('ffac010203') - }) - - pull( - values([input]), - lp.encode(), - p[0] - ) - - identify.dialer(p[1], (err, info, observedAddrs) => { - expect(err).to.exist() - - done() - }) - }) - - it('should return an error with mismatched peerInfo data', function (done) { - this.timeout(10e3) - - const p = pair() - original.multiaddrs.add(multiaddr('/ip4/127.0.0.1/tcp/5002')) - const input = msg.encode({ - protocolVersion: 'ipfs/0.1.0', - agentVersion: 'na', - publicKey: original.id.pubKey.bytes, - listenAddrs: [multiaddr('/ip4/127.0.0.1/tcp/5002').buffer], - observedAddr: multiaddr('/ip4/127.0.0.1/tcp/5001').buffer - }) - - PeerInfo.create((err, info) => { - if (err) { - return done(err) - } - - pull( - values([input]), - lp.encode(), - p[0] - ) - - identify.dialer(p[1], info, (err, peerInfo) => { - expect(err).to.exist() - expect(peerInfo).to.not.exist() - done() - }) - }) - }) -}) diff --git a/test/identify/listener.spec.js b/test/identify/listener.spec.js deleted file mode 100644 index ac4b3abc4c..0000000000 --- a/test/identify/listener.spec.js +++ /dev/null @@ -1,70 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const pull = require('pull-stream/pull') -const collect = require('pull-stream/sinks/collect') -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) -const pair = require('pull-pair/duplex') -const PeerInfo = require('peer-info') -const lp = require('pull-length-prefixed') -const multiaddr = require('multiaddr') - -const identify = require('../../src/identify') -const msg = identify.message - -describe('identify.listener', () => { - let info - - beforeEach(function (done) { - this.timeout(20 * 1000) - - PeerInfo.create((err, _info) => { - if (err) { - return done(err) - } - - _info.protocols.add('/echo/1.0.0') - _info.protocols.add('/chat/1.0.0') - - info = _info - done() - }) - }) - - it('works', (done) => { - const p = pair() - - info.multiaddrs.add(multiaddr('/ip4/127.0.0.1/tcp/5002')) - - pull( - p[1], - lp.decode(), - collect((err, result) => { - expect(err).to.not.exist() - - const input = msg.decode(result[0]) - expect( - input - ).to.be.eql({ - protocolVersion: 'ipfs/0.1.0', - agentVersion: 'na', - publicKey: info.id.pubKey.bytes, - listenAddrs: [multiaddr('/ip4/127.0.0.1/tcp/5002').buffer], - observedAddr: multiaddr('/ip4/127.0.0.1/tcp/5001').buffer, - protocols: ['/echo/1.0.0', '/chat/1.0.0'] - }) - done() - }) - ) - - const conn = p[0] - conn.getObservedAddrs = (cb) => { - cb(null, [multiaddr('/ip4/127.0.0.1/tcp/5001')]) - } - - identify.listener(conn, info) - }) -}) diff --git a/test/multiaddr-trim.node.js b/test/multiaddr-trim.node.js deleted file mode 100644 index 4367f3cb47..0000000000 --- a/test/multiaddr-trim.node.js +++ /dev/null @@ -1,41 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -const expect = chai.expect -const series = require('async/series') - -const createNode = require('./utils/create-node') - -describe('multiaddr trim', () => { - it('non used multiaddrs get trimmed', (done) => { - let node - - series([ - (cb) => createNode([ - '/ip4/0.0.0.0/tcp/999/wss/p2p-webrtc-direct', - '/ip4/127.0.0.1/tcp/55555/ws', - '/ip4/0.0.0.0/tcp/0/' - ], (err, _node) => { - expect(err).to.not.exist() - node = _node - const multiaddrs = node.peerInfo.multiaddrs.toArray() - expect(multiaddrs).to.have.length(3) - cb() - }), - (cb) => node.start(cb) - ], (err) => { - expect(err).to.not.exist() - - const multiaddrs = node.peerInfo.multiaddrs.toArray() - expect(multiaddrs.length).to.be.at.least(2) - // ensure the p2p-webrtc-direct address has been trimmed - multiaddrs.forEach((addr) => { - expect(() => addr.decapsulate('/ip4/0.0.0.0/tcp/999/wss/p2p-webrtc-direct')).to.throw() - }) - - node.stop(done) - }) - }) -}) diff --git a/test/node.js b/test/node.js index ced3b40123..fa27625f7c 100644 --- a/test/node.js +++ b/test/node.js @@ -1,18 +1,3 @@ 'use strict' -require('./pnet.node') -require('./transports.node') -require('./stream-muxing.node') -require('./peer-discovery.node') -require('./peer-routing.node') -require('./ping.node') -require('./promisify.node') -require('./pubsub.node') -require('./content-routing.node') -require('./circuit-relay.node') -require('./multiaddr-trim.node') -require('./stats') -require('./dht.node') - -require('./ping/node') -require('./switch/node') +require('./transports/transport-manager.node') diff --git a/test/peer-discovery.node.js b/test/peer-discovery.node.js deleted file mode 100644 index cc3075e340..0000000000 --- a/test/peer-discovery.node.js +++ /dev/null @@ -1,494 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -const expect = chai.expect -const sinon = require('sinon') -const parallel = require('async/parallel') -const crypto = require('crypto') - -const createNode = require('./utils/create-node') -const echo = require('./utils/echo') -const { WRTC_RENDEZVOUS_MULTIADDR } = require('./utils/constants') - -describe('peer discovery', () => { - let nodeA - let nodeB - let nodeC - - function setup (options) { - before((done) => { - parallel([ - (cb) => createNode([ - '/ip4/0.0.0.0/tcp/0', - `${WRTC_RENDEZVOUS_MULTIADDR.toString()}/p2p-webrtc-star` - ], options, (err, node) => { - expect(err).to.not.exist() - nodeA = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }), - (cb) => createNode([ - '/ip4/0.0.0.0/tcp/0', - `${WRTC_RENDEZVOUS_MULTIADDR.toString()}/p2p-webrtc-star` - ], options, (err, node) => { - expect(err).to.not.exist() - nodeB = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }), - (cb) => createNode([ - '/ip4/0.0.0.0/tcp/0', - `${WRTC_RENDEZVOUS_MULTIADDR.toString()}/p2p-webrtc-star` - ], options, (err, node) => { - expect(err).to.not.exist() - nodeC = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }) - ], done) - }) - - after((done) => { - parallel([ - (cb) => nodeA.stop(cb), - (cb) => nodeB.stop(cb), - (cb) => nodeC.stop(cb) - ], done) - }) - - afterEach(() => { - sinon.restore() - }) - } - - describe('module registration', () => { - it('should enable by default a module passed as an object', (done) => { - const mockDiscovery = { - on: sinon.stub(), - removeListener: sinon.stub(), - start: sinon.stub().callsArg(0), - stop: sinon.stub().callsArg(0) - } - - const options = { modules: { peerDiscovery: [mockDiscovery] } } - - createNode(['/ip4/0.0.0.0/tcp/0'], options, (err, node) => { - expect(err).to.not.exist() - - node.start((err) => { - expect(err).to.not.exist() - expect(mockDiscovery.start.called).to.be.true() - node.stop(done) - }) - }) - }) - - it('should enable by default a module passed as a function', (done) => { - const mockDiscovery = { - on: sinon.stub(), - removeListener: sinon.stub(), - start: sinon.stub().callsArg(0), - stop: sinon.stub().callsArg(0) - } - - const MockDiscovery = sinon.stub().returns(mockDiscovery) - - const options = { modules: { peerDiscovery: [MockDiscovery] } } - - createNode(['/ip4/0.0.0.0/tcp/0'], options, (err, node) => { - expect(err).to.not.exist() - - node.start((err) => { - expect(err).to.not.exist() - expect(mockDiscovery.start.called).to.be.true() - node.stop(done) - }) - }) - }) - - it('should enable module by configutation', (done) => { - const mockDiscovery = { - on: sinon.stub(), - removeListener: sinon.stub(), - start: sinon.stub().callsArg(0), - stop: sinon.stub().callsArg(0), - tag: 'mockDiscovery' - } - - const enabled = sinon.stub().returns(true) - - const options = { - modules: { peerDiscovery: [mockDiscovery] }, - config: { - peerDiscovery: { - mockDiscovery: { - get enabled () { - return enabled() - } - } - } - } - } - - createNode(['/ip4/0.0.0.0/tcp/0'], options, (err, node) => { - expect(err).to.not.exist() - - node.start((err) => { - expect(err).to.not.exist() - expect(mockDiscovery.start.called).to.be.true() - expect(enabled.called).to.be.true() - node.stop(done) - }) - }) - }) - - it('should disable module by configutation', (done) => { - const mockDiscovery = { - on: sinon.stub(), - removeListener: sinon.stub(), - start: sinon.stub().callsArg(0), - stop: sinon.stub().callsArg(0), - tag: 'mockDiscovery' - } - - const disabled = sinon.stub().returns(false) - - const options = { - modules: { peerDiscovery: [mockDiscovery] }, - config: { - peerDiscovery: { - mockDiscovery: { - get enabled () { - return disabled() - } - } - } - } - } - - createNode(['/ip4/0.0.0.0/tcp/0'], options, (err, node) => { - expect(err).to.not.exist() - - node.start((err) => { - expect(err).to.not.exist() - expect(mockDiscovery.start.called).to.be.false() - expect(disabled.called).to.be.true() - node.stop(done) - }) - }) - }) - - it('should register module passed as function', (done) => { - const mockDiscovery = { - on: sinon.stub(), - removeListener: sinon.stub(), - start: sinon.stub().callsArg(0), - stop: sinon.stub().callsArg(0) - } - - const MockDiscovery = sinon.stub().returns(mockDiscovery) - MockDiscovery.tag = 'mockDiscovery' - - const options = { - modules: { peerDiscovery: [MockDiscovery] }, - config: { - peerDiscovery: { - mockDiscovery: { - enabled: true, - time: Date.now() - } - } - } - } - - createNode(['/ip4/0.0.0.0/tcp/0'], options, (err, node) => { - expect(err).to.not.exist() - - node.start((err) => { - expect(err).to.not.exist() - expect(mockDiscovery.start.called).to.be.true() - expect(MockDiscovery.called).to.be.true() - // Ensure configuration was passed - expect(MockDiscovery.firstCall.args[0]) - .to.deep.include(options.config.peerDiscovery.mockDiscovery) - node.stop(done) - }) - }) - }) - - it('should register module passed as object', (done) => { - const mockDiscovery = { - on: sinon.stub(), - removeListener: sinon.stub(), - start: sinon.stub().callsArg(0), - stop: sinon.stub().callsArg(0), - tag: 'mockDiscovery' - } - - const options = { - modules: { peerDiscovery: [mockDiscovery] }, - config: { - peerDiscovery: { - mockDiscovery: { enabled: true } - } - } - } - - createNode(['/ip4/0.0.0.0/tcp/0'], options, (err, node) => { - expect(err).to.not.exist() - - node.start((err) => { - expect(err).to.not.exist() - expect(mockDiscovery.start.called).to.be.true() - node.stop(done) - }) - }) - }) - }) - - describe('discovery scenarios', () => { - setup({ - config: { - dht: { - enabled: false - }, - peerDiscovery: { - autoDial: false, - bootstrap: { - enabled: true, - list: [] - } - } - } - }) - - it('should ignore self on discovery', function () { - const discoverySpy = sinon.spy() - nodeA.on('peer:discovery', discoverySpy) - nodeA._discovery[0].emit('peer', nodeA.peerInfo) - - expect(discoverySpy.called).to.eql(false) - expect(nodeA.peerBook.getAllArray()).to.have.length(0) - expect() - }) - }) - - describe('MulticastDNS', () => { - setup({ - config: { - dht: { - enabled: false - }, - peerDiscovery: { - autoDial: true, - mdns: { - enabled: true, - interval: 200, // discover quickly - // use a random tag to prevent CI collision - serviceTag: crypto.randomBytes(10).toString('hex') - } - } - } - }) - - it('find peers', function (done) { - const expectedPeers = new Set([ - nodeB.peerInfo.id.toB58String(), - nodeC.peerInfo.id.toB58String() - ]) - - function finish () { - nodeA.removeAllListeners('peer:discovery') - expect(expectedPeers.size).to.eql(0) - done() - } - - nodeA.on('peer:discovery', (peerInfo) => { - expectedPeers.delete(peerInfo.id.toB58String()) - if (expectedPeers.size === 0) { - finish() - } - }) - }) - }) - - // TODO needs a delay (this test is already long) - describe.skip('WebRTCStar', () => { - setup({ - config: { - dht: { - enabled: false - }, - peerDiscovery: { - autoDial: true, - webRTCStar: { - enabled: true - } - } - } - }) - - it('find peers', function (done) { - this.timeout(20e3) - const expectedPeers = new Set([ - nodeB.peerInfo.id.toB58String(), - nodeC.peerInfo.id.toB58String() - ]) - - function finish () { - nodeA.removeAllListeners('peer:discovery') - expect(expectedPeers.size).to.eql(0) - done() - } - - nodeA.on('peer:discovery', (peerInfo) => { - expectedPeers.delete(peerInfo.id.toB58String()) - if (expectedPeers.size === 0) { - finish() - } - }) - }) - }) - - describe('MulticastDNS + WebRTCStar', () => { - setup({ - config: { - dht: { - enabled: false - }, - peerDiscovery: { - autoDial: true, - mdns: { - enabled: true, - interval: 200, // discovery quickly - // use a random tag to prevent CI collision - serviceTag: crypto.randomBytes(10).toString('hex') - }, - webRTCStar: { - enabled: true - } - } - } - }) - - it('find peers', function (done) { - const expectedPeers = new Set([ - nodeB.peerInfo.id.toB58String(), - nodeC.peerInfo.id.toB58String() - ]) - - function finish () { - nodeA.removeAllListeners('peer:discovery') - expect(expectedPeers.size).to.eql(0) - done() - } - - nodeA.on('peer:discovery', (peerInfo) => { - expectedPeers.delete(peerInfo.id.toB58String()) - if (expectedPeers.size === 0) { - finish() - } - }) - }) - }) - - describe('dht', () => { - setup({ - config: { - peerDiscovery: { - autoDial: true, - mdns: { - enabled: false - }, - webRTCStar: { - enabled: false - } - }, - dht: { - enabled: true, - kBucketSize: 20, - randomWalk: { - enabled: true, - queriesPerPeriod: 1, - delay: 100, - interval: 200, // start the query sooner - timeout: 3000 - } - } - } - }) - - it('find peers through the dht', function (done) { - const expectedPeers = new Set([ - nodeB.peerInfo.id.toB58String(), - nodeC.peerInfo.id.toB58String() - ]) - - function finish () { - nodeA.removeAllListeners('peer:discovery') - expect(expectedPeers.size).to.eql(0) - done() - } - - nodeA.on('peer:discovery', (peerInfo) => { - expectedPeers.delete(peerInfo.id.toB58String()) - if (expectedPeers.size === 0) { - finish() - } - }) - - // Topology: - // A -> B - // C -> B - nodeA.dial(nodeB.peerInfo, (err) => { - expect(err).to.not.exist() - }) - nodeC.dial(nodeB.peerInfo, (err) => { - expect(err).to.not.exist() - }) - }) - }) - - describe('auto dial', () => { - setup({ - connectionManager: { - minPeers: 1 - }, - config: { - peerDiscovery: { - autoDial: true, - mdns: { - enabled: false - }, - webRTCStar: { - enabled: false - }, - bootstrap: { - enabled: true, - list: [] - } - }, - dht: { - enabled: false - } - } - }) - - it('should only dial when the peer count is below the low watermark', (done) => { - const bootstrap = nodeA._discovery[0] - sinon.stub(nodeA._switch.dialer, 'connect').callsFake((peerInfo) => { - nodeA._switch.connection.connections[peerInfo.id.toB58String()] = [] - }) - - bootstrap.emit('peer', nodeB.peerInfo) - bootstrap.emit('peer', nodeC.peerInfo) - - // Only nodeB should get dialed - expect(nodeA._switch.dialer.connect.callCount).to.eql(1) - expect(nodeA._switch.dialer.connect.getCall(0).args[0]).to.eql(nodeB.peerInfo) - done() - }) - }) -}) diff --git a/test/peer-routing.node.js b/test/peer-routing.node.js deleted file mode 100644 index 16e08b8fa9..0000000000 --- a/test/peer-routing.node.js +++ /dev/null @@ -1,294 +0,0 @@ -/* eslint-env mocha */ -/* eslint max-nested-callbacks: ["error", 8] */ - -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -const expect = chai.expect -const parallel = require('async/parallel') -const _times = require('lodash.times') -const DelegatedPeerRouter = require('libp2p-delegated-peer-routing') -const sinon = require('sinon') -const nock = require('nock') - -const createNode = require('./utils/create-node') - -describe('.peerRouting', () => { - describe('via the dht', () => { - let nodeA - let nodeB - let nodeC - let nodeD - let nodeE - - before('create the outer ring of connections', (done) => { - const tasks = _times(5, () => (cb) => { - createNode('/ip4/0.0.0.0/tcp/0', (err, node) => { - expect(err).to.not.exist() - node.start((err) => cb(err, node)) - }) - }) - - parallel(tasks, (err, nodes) => { - expect(err).to.not.exist() - nodeA = nodes[0] - nodeB = nodes[1] - nodeC = nodes[2] - nodeD = nodes[3] - nodeE = nodes[4] - - parallel([ - (cb) => nodeA.dial(nodeB.peerInfo, cb), - (cb) => nodeB.dial(nodeC.peerInfo, cb), - (cb) => nodeC.dial(nodeD.peerInfo, cb), - (cb) => nodeD.dial(nodeE.peerInfo, cb), - (cb) => nodeE.dial(nodeA.peerInfo, cb) - ], (err) => { - expect(err).to.not.exist() - // Give the kbucket time to fill in the dht - setTimeout(done, 250) - }) - }) - }) - - after((done) => { - parallel([ - (cb) => nodeA.stop(cb), - (cb) => nodeB.stop(cb), - (cb) => nodeC.stop(cb), - (cb) => nodeD.stop(cb), - (cb) => nodeE.stop(cb) - ], done) - }) - - it('should use the nodes dht', (done) => { - const stub = sinon.stub(nodeA._dht, 'findPeer').callsFake(() => { - stub.restore() - done() - }) - - nodeA.peerRouting.findPeer() - }) - - describe('connected in an el ring', () => { - it('should be able to find a peer we are not directly connected to', (done) => { - parallel([ - (cb) => nodeA.dial(nodeC.peerInfo.id, cb), - (cb) => nodeB.dial(nodeD.peerInfo.id, cb), - (cb) => nodeC.dial(nodeE.peerInfo.id, cb) - ], (err) => { - if (err) throw err - expect(err).to.not.exist() - nodeB.peerRouting.findPeer(nodeE.peerInfo.id, (err, peerInfo) => { - expect(err).to.not.exist() - expect(nodeE.peerInfo.id.toB58String()).to.equal(peerInfo.id.toB58String()) - done() - }) - }) - }) - }) - }) - - describe('via a delegate', () => { - let nodeA - let delegate - - before((done) => { - parallel([ - // Create the node using the delegate - (cb) => { - delegate = new DelegatedPeerRouter({ - host: 'ipfs.io', - protocol: 'https', - port: '443' - }) - createNode('/ip4/0.0.0.0/tcp/0', { - modules: { - peerRouting: [delegate] - }, - config: { - dht: { - enabled: false - } - } - }, (err, node) => { - expect(err).to.not.exist() - nodeA = node - nodeA.start(cb) - }) - } - ], done) - }) - - after((done) => nodeA.stop(done)) - afterEach(() => nock.cleanAll()) - - it('should use the delegate router to find peers', (done) => { - const stub = sinon.stub(delegate, 'findPeer').callsFake(() => { - stub.restore() - done() - }) - nodeA.peerRouting.findPeer() - }) - - it('should be able to find a peer', (done) => { - const peerKey = 'QmTp9VkYvnHyrqKQuFPiuZkiX9gPcqj6x5LJ1rmWuSySnL' - const mockApi = nock('https://ipfs.io') - .post('/api/v0/dht/findpeer') - .query({ - arg: peerKey, - timeout: '30000ms', - 'stream-channels': true - }) - .reply(200, `{"Extra":"","ID":"some other id","Responses":null,"Type":0}\n{"Extra":"","ID":"","Responses":[{"Addrs":["/ip4/127.0.0.1/tcp/4001"],"ID":"${peerKey}"}],"Type":2}\n`, [ - 'Content-Type', 'application/json', - 'X-Chunked-Output', '1' - ]) - - nodeA.peerRouting.findPeer(peerKey, (err, peerInfo) => { - expect(err).to.not.exist() - expect(peerInfo.id.toB58String()).to.equal(peerKey) - expect(mockApi.isDone()).to.equal(true) - done() - }) - }) - - it('should error when a peer cannot be found', (done) => { - const peerKey = 'key of a peer not on the network' - const mockApi = nock('https://ipfs.io') - .post('/api/v0/dht/findpeer') - .query({ - arg: peerKey, - timeout: '30000ms', - 'stream-channels': true - }) - .reply(200, '{"Extra":"","ID":"some other id","Responses":null,"Type":6}\n{"Extra":"","ID":"yet another id","Responses":null,"Type":0}\n{"Extra":"routing:not found","ID":"","Responses":null,"Type":3}\n', [ - 'Content-Type', 'application/json', - 'X-Chunked-Output', '1' - ]) - - nodeA.peerRouting.findPeer(peerKey, (err, peerInfo) => { - expect(err).to.exist() - expect(peerInfo).to.not.exist() - expect(mockApi.isDone()).to.equal(true) - done() - }) - }) - - it('should handle errors from the api', (done) => { - const peerKey = 'key of a peer not on the network' - const mockApi = nock('https://ipfs.io') - .post('/api/v0/dht/findpeer') - .query({ - arg: peerKey, - timeout: '30000ms', - 'stream-channels': true - }) - .reply(502) - - nodeA.peerRouting.findPeer(peerKey, (err, peerInfo) => { - expect(err).to.exist() - expect(peerInfo).to.not.exist() - expect(mockApi.isDone()).to.equal(true) - done() - }) - }) - }) - - describe('via the dht and a delegate', () => { - let nodeA - let delegate - - before((done) => { - parallel([ - // Create the node using the delegate - (cb) => { - delegate = new DelegatedPeerRouter({ - host: 'ipfs.io', - protocol: 'https', - port: '443' - }) - createNode('/ip4/0.0.0.0/tcp/0', { - modules: { - peerRouting: [delegate] - } - }, (err, node) => { - expect(err).to.not.exist() - nodeA = node - nodeA.start(cb) - }) - } - ], done) - }) - - after((done) => nodeA.stop(done)) - - describe('findPeer', () => { - it('should only use the dht if it finds the peer', (done) => { - const results = [true] - const dhtStub = sinon.stub(nodeA._dht, 'findPeer').callsArgWith(2, null, results) - const delegateStub = sinon.stub(delegate, 'findPeer').throws(() => { - return new Error('the delegate should not have been called') - }) - - nodeA.peerRouting.findPeer('a peer id', (err, results) => { - expect(err).to.not.exist() - expect(results).to.equal(results) - expect(dhtStub.calledOnce).to.equal(true) - expect(delegateStub.notCalled).to.equal(true) - delegateStub.restore() - dhtStub.restore() - done() - }) - }) - - it('should use the delegate if the dht fails to find the peer', (done) => { - const results = [true] - const dhtStub = sinon.stub(nodeA._dht, 'findPeer').callsArgWith(2, null, undefined) - const delegateStub = sinon.stub(delegate, 'findPeer').callsArgWith(2, null, results) - - nodeA.peerRouting.findPeer('a peer id', (err, results) => { - expect(err).to.not.exist() - expect(results).to.deep.equal(results) - expect(dhtStub.calledOnce).to.equal(true) - expect(delegateStub.calledOnce).to.equal(true) - delegateStub.restore() - dhtStub.restore() - done() - }) - }) - }) - }) - - describe('no routers', () => { - let nodeA - before((done) => { - createNode('/ip4/0.0.0.0/tcp/0', { - config: { - dht: { - enabled: false - } - } - }, (err, node) => { - expect(err).to.not.exist() - nodeA = node - done() - }) - }) - - it('.findPeer should return an error with no options', (done) => { - nodeA.peerRouting.findPeer('a cid', (err) => { - expect(err).to.exist() - done() - }) - }) - - it('.findPeer should return an error with options', (done) => { - nodeA.peerRouting.findPeer('a cid', { maxTimeout: 5000 }, (err) => { - expect(err).to.exist() - done() - }) - }) - }) -}) diff --git a/test/ping.node.js b/test/ping.node.js deleted file mode 100644 index b677ab6a43..0000000000 --- a/test/ping.node.js +++ /dev/null @@ -1,61 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -const expect = chai.expect -const parallel = require('async/parallel') - -const createNode = require('./utils/create-node.js') -const echo = require('./utils/echo') - -describe('ping', () => { - let nodeA - let nodeB - - before((done) => { - parallel([ - (cb) => createNode('/ip4/0.0.0.0/tcp/0', (err, node) => { - expect(err).to.not.exist() - nodeA = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }), - (cb) => createNode('/ip4/0.0.0.0/tcp/0', (err, node) => { - expect(err).to.not.exist() - nodeB = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }) - ], done) - }) - - after((done) => { - parallel([ - (cb) => nodeA.stop(cb), - (cb) => nodeB.stop(cb) - ], done) - }) - - it('should be able to ping another node', (done) => { - nodeA.ping(nodeB.peerInfo, (err, ping) => { - expect(err).to.not.exist() - ping.once('ping', (time) => { - expect(time).to.exist() - ping.stop() - done() - }) - - ping.start() - }) - }) - - it('should be not be able to ping when stopped', (done) => { - nodeA.stop(() => { - nodeA.ping(nodeB.peerInfo, (err) => { - expect(err).to.exist() - done() - }) - }) - }) -}) diff --git a/test/ping/node.js b/test/ping/node.js deleted file mode 100644 index 55396510c3..0000000000 --- a/test/ping/node.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict' - -require('./test-ping.js') diff --git a/test/ping/test-ping.js b/test/ping/test-ping.js deleted file mode 100644 index 0dc1f9e10f..0000000000 --- a/test/ping/test-ping.js +++ /dev/null @@ -1,118 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) -const PeerInfo = require('peer-info') -const PeerBook = require('peer-book') - -const Swarm = require('../../src/switch') -const TCP = require('libp2p-tcp') -const series = require('async/series') -const parallel = require('async/parallel') - -const Ping = require('../../src/ping') - -describe('libp2p ping', () => { - let swarmA - let swarmB - let peerA - let peerB - - before(function (done) { - this.timeout(20 * 1000) - series([ - (cb) => PeerInfo.create((err, peerInfo) => { - expect(err).to.not.exist() - peerA = peerInfo - peerA.multiaddrs.add('/ip4/127.0.0.1/tcp/0') - cb() - }), - (cb) => PeerInfo.create((err, peerInfo) => { - expect(err).to.not.exist() - peerB = peerInfo - peerB.multiaddrs.add('/ip4/127.0.0.1/tcp/0') - cb() - }), - (cb) => { - swarmA = new Swarm(peerA, new PeerBook()) - swarmB = new Swarm(peerB, new PeerBook()) - swarmA.transport.add('tcp', new TCP()) - swarmB.transport.add('tcp', new TCP()) - cb() - }, - (cb) => swarmA.start(cb), - (cb) => swarmB.start(cb), - (cb) => { - Ping.mount(swarmA) - Ping.mount(swarmB) - cb() - } - ], done) - }) - - after((done) => { - parallel([ - (cb) => swarmA.stop(cb), - (cb) => swarmB.stop(cb) - ], done) - }) - - it('ping once from peerA to peerB', (done) => { - const p = new Ping(swarmA, peerB) - - p.on('error', (err) => { - expect(err).to.not.exist() - }) - - p.on('ping', (time) => { - expect(time).to.be.a('Number') - p.stop() - done() - }) - - p.start() - }) - - it('ping 5 times from peerB to peerA', (done) => { - const p = new Ping(swarmB, peerA) - - p.on('error', (err) => { - expect(err).to.not.exist() - }) - - let counter = 0 - - p.on('ping', (time) => { - expect(time).to.be.a('Number') - if (++counter === 5) { - p.stop() - done() - } - }) - - p.start() - }) - - it('cannot ping itself', (done) => { - const p = new Ping(swarmA, peerA) - - p.on('error', (err) => { - expect(err).to.exist() - done() - }) - - p.on('ping', () => { - expect.fail('should not be called') - }) - - p.start() - }) - - it('unmount PING protocol', () => { - Ping.unmount(swarmA) - Ping.unmount(swarmB) - }) -}) diff --git a/test/pnet.node.js b/test/pnet.node.js deleted file mode 100644 index 450bbbb68e..0000000000 --- a/test/pnet.node.js +++ /dev/null @@ -1,92 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -const expect = chai.expect -const PeerInfo = require('peer-info') -const PeerId = require('peer-id') -const waterfall = require('async/waterfall') -const WS = require('libp2p-websockets') -const defaultsDeep = require('@nodeutils/defaults-deep') -const DHT = require('libp2p-kad-dht') - -const Libp2p = require('../src') - -describe('private network', () => { - let config - - before((done) => { - waterfall([ - (cb) => PeerId.create({ bits: 512 }, cb), - (peerId, cb) => PeerInfo.create(peerId, cb), - (peerInfo, cb) => { - config = { - peerInfo, - modules: { - transport: [WS], - dht: DHT - } - } - cb() - } - ], () => done()) - }) - - describe('enforced network protection', () => { - before(() => { - process.env.LIBP2P_FORCE_PNET = 1 - }) - - after(() => { - delete process.env.LIBP2P_FORCE_PNET - }) - - it('should throw an error without a provided protector', () => { - expect(() => { - return new Libp2p(config) - }).to.throw('Private network is enforced, but no protector was provided') - }) - - it('should create a libp2p node with a provided protector', () => { - let node - const protector = { - psk: '123', - tag: '/psk/1.0.0', - protect: () => { } - } - - expect(() => { - const options = defaultsDeep(config, { - modules: { - connProtector: protector - } - }) - - node = new Libp2p(options) - return node - }).to.not.throw() - expect(node._switch.protector).to.deep.equal(protector) - }) - - it('should throw an error if the protector does not have a protect method', () => { - expect(() => { - const options = defaultsDeep(config, { - modules: { - connProtector: { } - } - }) - - return new Libp2p(options) - }).to.throw() - }) - }) - - describe('network protection not enforced', () => { - it('should not throw an error with no provided protector', () => { - expect(() => { - return new Libp2p(config) - }).to.not.throw() - }) - }) -}) diff --git a/test/pnet/fixtures/peer-a.json b/test/pnet/fixtures/peer-a.json deleted file mode 100644 index 107c8944eb..0000000000 --- a/test/pnet/fixtures/peer-a.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "QmeS1ou3mrjCFGoFtRx3MwrGDzqKD6xbuYJU1CKtMrtFFu", - "privKey": "CAASqAkwggSkAgEAAoIBAQChwzYwCNIyUkzEK3sILqq9ChAKZ9eU+ribY+B/xwAwDKPfvuqHq0hjauJBMcjiQyKAWz9xEBR3WupOM7h9M8oU+/e0xJUTt/CDOrtJ0PCgUXeBLkqsApbBoXW3yomHEDHxYttKzrtoTimiP1bhrxurcpVNC4CUYD+q8gw3sRZlsrqpeYYAfU04kS0BM75W/sUT90znnHvOxFXrEdMMdenEFhZOsDyEK9ENzwhkKgOGb18MBY4kN5DoW4bVd4ItfZnNwdkQtpP/X99tMWJxO4yqpngbywZGnkfirLeuRwt/xRGFVbLOigjBpTVpcbBqe1t2Flhuf/bfWYX4FbyElA5FAgMBAAECggEAJnDTcbrG6LpyD7QdeqZMYLwBb9eZfYfPUu37LaJGwyRd1Q/zf+YOP8HonoGMMWiuzD3i56Vgl7R9NbRIxUgHX9E43jZRDuyJNUZBt5r1c8OoWIR9rj63QLBz3wc8g2Iv3CMX5cEW/ASHFE1lAiCwvJ9wJ2zyU1BEEQWQLbPhlKzw7SLhr4fee45/7pnrKZMllt5vwC9pM6lrpIkICO5gUu0OWu5wfzzlTvfmCgfTb11VqKESEPbDBMUtpJibRqegE4xvipLklJ8VV8jz7NFs9bhgCpNM74Ngt5vGHcddeqtj//86UsClEw5YgWAdRe29ZjMApWvKIkginLjZEO8eiQKBgQDoDWii0rmlgBl1/8fENUSWxYvknGmWO7eWjVqMjDvA+waWUVDpTE+eHT1QAaPofM+nFz5PG+SpB55o4rXdxDesq+DqnaRAI9WtSHdgRtjgETyqoBAiahQ0zGWmSEYHGDB+xGctTMr8GxdhZxqZjjfyptp6oXXqZkmxgcogrx+WTwKBgQCydNDmCDpeH0kSvhAPxaNx5c9WkFEFSA0OCZOx57Y+Mt0MVamRILFrUrcMz095w8BQZkjlHjSHfsRgKa/b2eOd+3BhoMLZVtxRqBdpdqq1KTAcRRG4yA2KA39rttpVzaTV5SPfdDf3tsVlBtV784W63gVpN9gNfajyyrpeffiBKwKBgDnDrLprbl8uZigjhdznza0ie9JqxTXqo6bMhS/bcLx3QIqGr3eD0YXwjWSvI9gpyZ80gAQ9U0xoYxyE4vTTdXB8UL7Wgx6cTQKXuW+z8yTD5bArrBiFA4apItyjvRrjAJ9t0KlMJnNfYxCSE+MJrg+vTU+dhbbVw552SpScQ2atAoGBAKMu3rb6XyUiRpe05MsHVuYX1vi5Dt1dfVKQv1W3JJbLvAZDbsMeuh4BjRFRoMMflQPwBEg+zpn3+WpVtFG9dL5J5gHgF0zWeLDSnFX8BS2TdELlhccKaBcEC8hbdFtxqIFO/vaeN2902hv/m8e0b1zpGNmWDyKG/a7GYpV1a3/xAoGBAJtgGANDVk6qqcWGEVk56FH1ZksvgF3SPXWaXpzbZ5KLCcV5ooRyhowylKUZBBPowMeZ46tem2xwJbraB5kDg6WiSjBsXcbN95ivb8AuoRa6gDqAszjokQUSdpY7FTgMaL046AuihrKsQSly1jrQqbQu8JBgmnnBzus3s77inL/j", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQChwzYwCNIyUkzEK3sILqq9ChAKZ9eU+ribY+B/xwAwDKPfvuqHq0hjauJBMcjiQyKAWz9xEBR3WupOM7h9M8oU+/e0xJUTt/CDOrtJ0PCgUXeBLkqsApbBoXW3yomHEDHxYttKzrtoTimiP1bhrxurcpVNC4CUYD+q8gw3sRZlsrqpeYYAfU04kS0BM75W/sUT90znnHvOxFXrEdMMdenEFhZOsDyEK9ENzwhkKgOGb18MBY4kN5DoW4bVd4ItfZnNwdkQtpP/X99tMWJxO4yqpngbywZGnkfirLeuRwt/xRGFVbLOigjBpTVpcbBqe1t2Flhuf/bfWYX4FbyElA5FAgMBAAE=" -} diff --git a/test/pnet/fixtures/peer-b.json b/test/pnet/fixtures/peer-b.json deleted file mode 100644 index 4c52c6eaac..0000000000 --- a/test/pnet/fixtures/peer-b.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "QmYWHGZ9y1Bzx59bBzn85JsJxwmpBy5bpXDWDfwMfsHsxz", - "privKey": "CAASqQkwggSlAgEAAoIBAQDLVaPqWFA8WgK6ixuPvhTHeQfBblmEFLEmraLlIDSWbMUPva6aJ1V/hi2I5QLXNeeiig5sco+nF+RKhGnzQ9NpgHRVZ7Ze+LWq3Q4YxONdzFeNUjTvJrDSKgkubA5EKC/LI6pU33WZbjyKkomGo+Gzuqvlj4Rx1dLVXRIOjxUYcIQw3vpLQgwPpiz52eWCeoCpzn06DcsF6aNPjhlp9uJRZCRxZ4yeiwh/A0xxiQtnB4fdZuUPmia1r62+oaxrDl4hUwR7kzHYl0YGfXxAW9GT17KGtjES2yO4kAUgquelNh0hgBKZRvny9imwsObG7ntw5ZG7H62sP7UySIUJqoNRAgMBAAECggEBAKLVU25BCQg7wQGokwra2wMfPoG+IDuw4mkqFlBNKS/prSo86c2TgFmel2qQk2TLS1OUIZbha38RmAXA4qQohe5wKzmV06tcmwdY/YgCbF5aXSbUVYXLQ0Ea3r1pVUdps1SHnElZpnCXoi4Kyc2kAgSPkkdFVnhfFvc9EE/Ob8NgMkdFhlosE5WVNqm4BKQ+mqONddSz4JDbDOApPs/rRpgYm7pJKc3vkrYwniPjyQGYb5EoSbSWuu31RzIcn3Bhte3wKtfMMlpn8MMpPiYo2WJ2eVG6hlUOxhHgS93Y6czCfAgsDtD3C2JpteewuBjg8N0d6WRArKxny83J34q0qy0CgYEA6YSo5UDEq1TF8sbtSVYg6MKSX92NO5MQI/8fTjU4tEwxn/yxpGsnqUu0WGYIc2qVaZuxtcnk2CQxEilxQTbWSIxKuTt7qofEcpSjLLQ4f4chk4DpPsba+S8zSUdWdjthPHZT9IYzobylGBLfbPxyXXiYn1VuqAJfFy8iV9XqmdcCgYEA3ukROQQZCJcgsNTc5uFAKUeQvzv1iae3fGawgJmIJW3Bl8+4dSm1diqG3ZXP1WU31no2aX50PqOZjoIpbl1ggT76cnBDuu3pItR3dNJFQyMEpQOWOjO+NBWF7sRswCvlqbyjofWkzsdd0BioL7vWMjPftiusyyAFA55HRoeStxcCgYEA0tP7rKdSKKFr6inhl+GT6rGod7bOSSgYXXd7qx9v55AXCauaMqiv8TAxTdIo9RMYfHWd91OlMeNTDmOuJcO9qVhIKn5iw266VPyPac/4ZmL5VHQBobTlhC4yLomirTIlMvJeEBmNygtIPrjjUUGGe49itA/szPD/Ky5Z4lV27pcCgYAWU3mqIELxnVFk5K0LYtwuRkC1Jqg9FVNHXnGnL7l3JjsRnXh4I6lNII1JfEvIr86b6LmybzvtWi1zHI5Rw4B68XfcJmpiOpnzJxyf0r+lLci1Tlqpka0nQlCbzYim5r6l9YLeIeBT5Zv7z7xoq4OUm6V4dX9lCNv3tM6mvcVwGQKBgQC9hhjD64/VKXL8wYKZyTAOVO5xYCcqylrpI39qdzl+sS8oqmLUbXnKsGY4If9U61XdULld41BJCRlv6CsKreynm6ZN41j9YRuWWLu8STJcniV9Ef9uVl1M1zo8kfnCHMCym9LkTfJY+Ow/kYhqPukJJL6ve1CVmIuA4rnZlshjbg==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDLVaPqWFA8WgK6ixuPvhTHeQfBblmEFLEmraLlIDSWbMUPva6aJ1V/hi2I5QLXNeeiig5sco+nF+RKhGnzQ9NpgHRVZ7Ze+LWq3Q4YxONdzFeNUjTvJrDSKgkubA5EKC/LI6pU33WZbjyKkomGo+Gzuqvlj4Rx1dLVXRIOjxUYcIQw3vpLQgwPpiz52eWCeoCpzn06DcsF6aNPjhlp9uJRZCRxZ4yeiwh/A0xxiQtnB4fdZuUPmia1r62+oaxrDl4hUwR7kzHYl0YGfXxAW9GT17KGtjES2yO4kAUgquelNh0hgBKZRvny9imwsObG7ntw5ZG7H62sP7UySIUJqoNRAgMBAAE=" -} diff --git a/test/pnet/pnet.spec.js b/test/pnet/pnet.spec.js deleted file mode 100644 index c7f254fa4e..0000000000 --- a/test/pnet/pnet.spec.js +++ /dev/null @@ -1,105 +0,0 @@ -/* eslint max-nested-callbacks: ["error", 8] */ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -chai.use(dirtyChai) -const expect = chai.expect -const parallel = require('async/parallel') -const PeerId = require('peer-id') -const Connection = require('interface-connection').Connection -const pair = require('pull-pair/duplex') -const pull = require('pull-stream') - -const Protector = require('../../src/pnet') -const Errors = Protector.errors -const generate = Protector.generate - -const swarmKeyBuffer = Buffer.alloc(95) -const wrongSwarmKeyBuffer = Buffer.alloc(95) - -// Write new psk files to the buffers -generate(swarmKeyBuffer) -generate(wrongSwarmKeyBuffer) - -describe('private network', () => { - before((done) => { - parallel([ - (cb) => PeerId.createFromJSON(require('./fixtures/peer-a'), cb), - (cb) => PeerId.createFromJSON(require('./fixtures/peer-b'), cb) - ], (err) => { - expect(err).to.not.exist() - done() - }) - }) - - it('should accept a valid psk buffer', () => { - const protector = new Protector(swarmKeyBuffer) - - expect(protector.tag).to.equal('/key/swarm/psk/1.0.0/') - expect(protector.psk.byteLength).to.equal(32) - }) - - it('should protect a simple connection', (done) => { - const p = pair() - const protector = new Protector(swarmKeyBuffer) - - const aToB = protector.protect(new Connection(p[0]), (err) => { - expect(err).to.not.exist() - }) - const bToA = protector.protect(new Connection(p[1]), (err) => { - expect(err).to.not.exist() - }) - - pull( - pull.values([Buffer.from('hello world'), Buffer.from('doo dah')]), - aToB - ) - - pull( - bToA, - pull.collect((err, chunks) => { - expect(err).to.not.exist() - expect(chunks).to.eql([Buffer.from('hello world'), Buffer.from('doo dah')]) - done() - }) - ) - }) - - it('should not connect to a peer with a different key', (done) => { - const p = pair() - const protector = new Protector(swarmKeyBuffer) - const protectorB = new Protector(wrongSwarmKeyBuffer) - - const aToB = protector.protect(new Connection(p[0]), () => { }) - const bToA = protectorB.protect(new Connection(p[1]), () => { }) - - pull( - pull.values([Buffer.from('hello world'), Buffer.from('doo dah')]), - aToB - ) - - pull( - bToA, - pull.collect((values) => { - expect(values).to.equal(null) - done() - }) - ) - }) - - describe('invalid psks', () => { - it('should not accept a bad psk', () => { - expect(() => { - return new Protector(Buffer.from('not-a-key')) - }).to.throw(Errors.INVALID_PSK) - }) - - it('should not accept a psk of incorrect length', () => { - expect(() => { - return new Protector(Buffer.from('/key/swarm/psk/1.0.0/\n/base16/\ndffb7e')) - }).to.throw(Errors.INVALID_PSK) - }) - }) -}) diff --git a/test/promisify.node.js b/test/promisify.node.js deleted file mode 100644 index 16868a4a58..0000000000 --- a/test/promisify.node.js +++ /dev/null @@ -1,87 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -/** - * This test suite is intended to validate compatability of - * the promisified api, until libp2p has been fully migrated to - * async/await. Once the migration is complete and all tests - * are using async/await, this file can be removed. - */ - -const chai = require('chai') -chai.use(require('dirty-chai')) -const expect = chai.expect -const promisify = require('promisify-es6') -const createNode = promisify(require('./utils/create-node')) -const { createPeerInfo } = require('./utils/create-node') -const Node = require('./utils/bundle-nodejs') -const pull = require('pull-stream') -const Ping = require('../src/ping') - -/** - * As libp2p is currently promisified, when extending libp2p, - * method arguments must be passed to `super` to ensure the - * promisify callbacks are properly resolved - */ -class AsyncLibp2p extends Node { - async start (...args) { - await super.start(...args) - } - - async stop (...args) { - await super.start(...args) - } -} - -async function createAsyncNode () { - const peerInfo = await promisify(createPeerInfo)() - peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') - return new AsyncLibp2p({ peerInfo }) -} - -describe('promisified libp2p', () => { - let libp2p - let otherNode - const ECHO_PROTO = '/echo/1.0.0' - - before('Create and Start', async () => { - [libp2p, otherNode] = await Promise.all([ - createNode('/ip4/0.0.0.0/tcp/0'), - createAsyncNode() - ]) - - return [libp2p, otherNode].map(node => { - node.handle(ECHO_PROTO, (_, conn) => pull(conn, conn)) - return node.start() - }) - }) - - after('Stop', () => { - return [libp2p, otherNode].map(node => node.stop()) - }) - - afterEach('Hang up', () => { - return libp2p.hangUp(otherNode.peerInfo) - }) - - it('dial', async () => { - const stream = await libp2p.dial(otherNode.peerInfo) - expect(stream).to.not.exist() - expect(libp2p._switch.connection.getAll()).to.have.length(1) - }) - - it('dialFSM', async () => { - const connectionFSM = await libp2p.dialFSM(otherNode.peerInfo, ECHO_PROTO) - expect(connectionFSM).to.exist() - }) - - it('dialProtocol', async () => { - const stream = await libp2p.dialProtocol(otherNode.peerInfo, ECHO_PROTO) - expect(stream).to.exist() - }) - - it('ping', async () => { - const ping = await libp2p.ping(otherNode.peerInfo) - expect(ping).to.be.an.instanceOf(Ping) - }) -}) diff --git a/test/pubsub.node.js b/test/pubsub.node.js deleted file mode 100644 index 2e380ae4b7..0000000000 --- a/test/pubsub.node.js +++ /dev/null @@ -1,467 +0,0 @@ -/* eslint-env mocha */ -/* eslint max-nested-callbacks: ["error", 8] */ - -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -chai.use(require('chai-checkmark')) -const expect = chai.expect -const parallel = require('async/parallel') -const series = require('async/series') -const _times = require('lodash.times') -const promisify = require('promisify-es6') -const delay = require('delay') -const Floodsub = require('libp2p-floodsub') -const mergeOptions = require('merge-options') - -const { codes } = require('../src/errors') -const createNode = require('./utils/create-node') - -function startTwo (options, callback) { - if (typeof options === 'function') { - callback = options - options = {} - } - - const tasks = _times(2, () => (cb) => { - createNode('/ip4/0.0.0.0/tcp/0', mergeOptions({ - config: { - peerDiscovery: { - mdns: { - enabled: false - } - }, - pubsub: { - enabled: true - } - } - }, options), (err, node) => { - expect(err).to.not.exist() - node.start((err) => cb(err, node)) - }) - }) - - parallel(tasks, (err, nodes) => { - expect(err).to.not.exist() - - nodes[0].dial(nodes[1].peerInfo, (err) => callback(err, nodes)) - }) -} - -function stopTwo (nodes, callback) { - parallel([ - (cb) => nodes[0].stop(cb), - (cb) => nodes[1].stop(cb) - ], callback) -} - -describe('.pubsub', () => { - describe('.pubsub on (default)', () => { - it('start two nodes and send one message, then unsubscribe', (done) => { - // Check the final series error, and the publish handler - expect(2).checks(done) - - let nodes - const data = 'test' - const handler = (msg) => { - // verify the data is correct and mark the expect - expect(msg.data.toString()).to.eql(data).mark() - } - - series([ - // Start the nodes - (cb) => startTwo((err, _nodes) => { - nodes = _nodes - cb(err) - }), - // subscribe on the first - (cb) => nodes[0].pubsub.subscribe('pubsub', handler, null, cb), - // Wait a moment before publishing - (cb) => setTimeout(cb, 500), - // publish on the second - (cb) => nodes[1].pubsub.publish('pubsub', data, cb), - // Wait a moment before unsubscribing - (cb) => setTimeout(cb, 500), - // unsubscribe on the first - (cb) => nodes[0].pubsub.unsubscribe('pubsub', handler, cb), - // Stop both nodes - (cb) => stopTwo(nodes, cb) - ], (err) => { - // Verify there was no error, and mark the expect - expect(err).to.not.exist().mark() - }) - }) - it('start two nodes and send one message, then unsubscribe without handler', (done) => { - // Check the final series error, and the publish handler - expect(3).checks(done) - - let nodes - const data = Buffer.from('test') - const handler = (msg) => { - // verify the data is correct and mark the expect - expect(msg.data).to.eql(data).mark() - } - - series([ - // Start the nodes - (cb) => startTwo((err, _nodes) => { - nodes = _nodes - cb(err) - }), - // subscribe on the first - (cb) => nodes[0].pubsub.subscribe('pubsub', handler, {}, cb), - // Wait a moment before publishing - (cb) => setTimeout(cb, 500), - // publish on the second - (cb) => nodes[1].pubsub.publish('pubsub', data, cb), - // ls subscripts - (cb) => nodes[1].pubsub.ls(cb), - // get subscribed peers - (cb) => nodes[1].pubsub.peers('pubsub', cb), - // Wait a moment before unsubscribing - (cb) => setTimeout(cb, 500), - // unsubscribe from all - (cb) => nodes[0].pubsub.unsubscribe('pubsub', null, cb), - // Verify unsubscribed - (cb) => { - nodes[0].pubsub.ls((err, topics) => { - expect(topics.length).to.eql(0).mark() - cb(err) - }) - }, - // Stop both nodes - (cb) => stopTwo(nodes, cb) - ], (err) => { - // Verify there was no error, and mark the expect - expect(err).to.not.exist().mark() - }) - }) - it('publish should fail if data is not a buffer nor a string', (done) => { - createNode('/ip4/0.0.0.0/tcp/0', { - config: { - peerDiscovery: { - mdns: { - enabled: false - } - }, - pubsub: { - enabled: true - } - } - }, (err, node) => { - expect(err).to.not.exist() - - node.start((err) => { - expect(err).to.not.exist() - - node.pubsub.publish('pubsub', 10, (err) => { - expect(err).to.exist() - expect(err.code).to.equal('ERR_DATA_IS_NOT_VALID') - - done() - }) - }) - }) - }) - }) - - describe('.pubsub on using floodsub', () => { - it('start two nodes and send one message, then unsubscribe', (done) => { - // Check the final series error, and the publish handler - expect(2).checks(done) - - let nodes - const data = Buffer.from('test') - const handler = (msg) => { - // verify the data is correct and mark the expect - expect(msg.data).to.eql(data).mark() - } - - series([ - // Start the nodes - (cb) => startTwo({ - modules: { - pubsub: Floodsub - } - }, (err, _nodes) => { - nodes = _nodes - cb(err) - }), - // subscribe on the first - (cb) => nodes[0].pubsub.subscribe('pubsub', handler, cb), - // Wait a moment before publishing - (cb) => setTimeout(cb, 500), - // publish on the second - (cb) => nodes[1].pubsub.publish('pubsub', data, cb), - // Wait a moment before unsubscribing - (cb) => setTimeout(cb, 500), - // unsubscribe on the first - (cb) => nodes[0].pubsub.unsubscribe('pubsub', handler, cb), - // Stop both nodes - (cb) => stopTwo(nodes, cb) - ], (err) => { - // Verify there was no error, and mark the expect - expect(err).to.not.exist().mark() - }) - }) - it('start two nodes and send one message, then unsubscribe (promises)', async () => { - let messageRecieved - const data = Buffer.from('test') - const handler = (msg) => { - expect(msg.data).to.eql(data) - messageRecieved = true - } - - // Start the nodes - const nodes = await promisify(startTwo)({ - modules: { - pubsub: Floodsub - } - }) - - // subscribe on the first - await nodes[0].pubsub.subscribe('pubsub', handler) - // Wait a moment before publishing - await delay(500) - // publish on the second - await nodes[1].pubsub.publish('pubsub', data) - // Wait a moment before unsubscribing - await delay(500) - // unsubscribe on the first - await nodes[0].pubsub.unsubscribe('pubsub', handler) - // Stop both nodes - await promisify(stopTwo)(nodes) - - expect(messageRecieved).to.be.true() - }) - it('start two nodes and send one message, then unsubscribe without handler', (done) => { - // Check the final series error, and the publish handler - expect(3).checks(done) - - let nodes - const data = Buffer.from('test') - const handler = (msg) => { - // verify the data is correct and mark the expect - expect(msg.data).to.eql(data).mark() - } - - series([ - // Start the nodes - (cb) => startTwo({ - modules: { - pubsub: Floodsub - } - }, (err, _nodes) => { - nodes = _nodes - cb(err) - }), - // subscribe on the first - (cb) => nodes[0].pubsub.subscribe('pubsub', handler, cb), - // Wait a moment before publishing - (cb) => setTimeout(cb, 500), - // publish on the second - (cb) => nodes[1].pubsub.publish('pubsub', data, cb), - // Wait a moment before unsubscribing - (cb) => setTimeout(cb, 500), - // unsubscribe from all - (cb) => nodes[0].pubsub.unsubscribe('pubsub', null, cb), - // Verify unsubscribed - (cb) => { - nodes[0].pubsub.ls((err, topics) => { - expect(topics.length).to.eql(0).mark() - cb(err) - }) - }, - // Stop both nodes - (cb) => stopTwo(nodes, cb) - ], (err) => { - // Verify there was no error, and mark the expect - expect(err).to.not.exist().mark() - }) - }) - it('publish should fail if data is not a buffer', (done) => { - createNode('/ip4/0.0.0.0/tcp/0', { - config: { - peerDiscovery: { - mdns: { - enabled: false - } - }, - pubsub: { - enabled: true - } - }, - modules: { - pubsub: Floodsub - } - }, (err, node) => { - expect(err).to.not.exist() - - node.start((err) => { - expect(err).to.not.exist() - - node.pubsub.publish('pubsub', 10, (err) => { - expect(err).to.exist() - expect(err.code).to.equal('ERR_DATA_IS_NOT_VALID') - - done() - }) - }) - }) - }) - }) - - describe('.pubsub off', () => { - it('fail to use pubsub if disabled', (done) => { - createNode('/ip4/0.0.0.0/tcp/0', { - config: { - peerDiscovery: { - mdns: { - enabled: false - } - } - } - }, (err, node) => { - expect(err).to.not.exist() - expect(node.pubsub).to.not.exist() - done() - }) - }) - }) - - describe('.pubsub on and node not started', () => { - let libp2pNode - - before(function (done) { - createNode('/ip4/0.0.0.0/tcp/0', { - config: { - peerDiscovery: { - mdns: { - enabled: false - } - }, - pubsub: { - enabled: true - } - } - }, (err, node) => { - expect(err).to.not.exist() - - libp2pNode = node - done() - }) - }) - - it('fail to subscribe if node not started yet', (done) => { - libp2pNode.pubsub.subscribe('pubsub', () => { }, (err) => { - expect(err).to.exist() - expect(err.code).to.equal(codes.PUBSUB_NOT_STARTED) - - done() - }) - }) - - it('fail to unsubscribe if node not started yet', (done) => { - libp2pNode.pubsub.unsubscribe('pubsub', () => { }, (err) => { - expect(err).to.exist() - expect(err.code).to.equal(codes.PUBSUB_NOT_STARTED) - - done() - }) - }) - - it('fail to publish if node not started yet', (done) => { - libp2pNode.pubsub.publish('pubsub', Buffer.from('data'), (err) => { - expect(err).to.exist() - expect(err.code).to.equal(codes.PUBSUB_NOT_STARTED) - - done() - }) - }) - - it('fail to ls if node not started yet', (done) => { - libp2pNode.pubsub.ls((err) => { - expect(err).to.exist() - expect(err.code).to.equal(codes.PUBSUB_NOT_STARTED) - - done() - }) - }) - - it('fail to get subscribed peers to a topic if node not started yet', (done) => { - libp2pNode.pubsub.peers('pubsub', (err) => { - expect(err).to.exist() - expect(err.code).to.equal(codes.PUBSUB_NOT_STARTED) - - done() - }) - }) - }) - - describe('.pubsub config', () => { - it('toggle all pubsub options off (except enabled)', done => { - expect(3).checks(done) - - class PubSubSpy { - constructor (node, config) { - expect(config).to.be.eql({ - enabled: true, - emitSelf: false, - signMessages: false, - strictSigning: false - }).mark() - } - } - - createNode('/ip4/0.0.0.0/tcp/0', { - modules: { - pubsub: PubSubSpy - }, - config: { - pubsub: { - enabled: true, - emitSelf: false, - signMessages: false, - strictSigning: false - } - } - }, (err, node) => { - expect(err).to.not.exist().mark() - expect(node).to.exist().mark() - }) - }) - - it('toggle all pubsub options on', done => { - expect(3).checks(done) - - class PubSubSpy { - constructor (node, config) { - expect(config).to.be.eql({ - enabled: true, - emitSelf: true, - signMessages: true, - strictSigning: true - }).mark() - } - } - - createNode('/ip4/0.0.0.0/tcp/0', { - modules: { - pubsub: PubSubSpy - }, - config: { - pubsub: { - enabled: true, - emitSelf: true, - signMessages: true, - strictSigning: true - } - } - }, (err, node) => { - expect(err).to.not.exist().mark() - expect(node).to.exist().mark() - }) - }) - }) -}) diff --git a/test/stats.js b/test/stats.js deleted file mode 100644 index 08e05b4fdc..0000000000 --- a/test/stats.js +++ /dev/null @@ -1,29 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -const expect = chai.expect - -const createNode = require('./utils/create-node') - -describe('libp2p', () => { - it('has stats', (done) => { - createNode('/ip4/127.0.0.1/tcp/0', { - config: { - peerDiscovery: { - mdns: { - enabled: false - } - } - } - }, (err, node) => { - expect(err).to.not.exist() - node.start((err) => { - expect(err).to.not.exist() - expect(node.stats).to.exist() - node.stop(done) - }) - }) - }) -}) diff --git a/test/stream-muxing.node.js b/test/stream-muxing.node.js deleted file mode 100644 index d559664ee6..0000000000 --- a/test/stream-muxing.node.js +++ /dev/null @@ -1,338 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -const expect = chai.expect -const parallel = require('async/parallel') -const series = require('async/series') -const pMplex = require('pull-mplex') -const Mplex = require('libp2p-mplex') -const SPDY = require('libp2p-spdy') -const createNode = require('./utils/create-node') -const tryEcho = require('./utils/try-echo') -const echo = require('./utils/echo') - -function test (nodeA, nodeB, callback) { - nodeA.dialProtocol(nodeB.peerInfo, '/echo/1.0.0', (err, conn) => { - expect(err).to.not.exist() - tryEcho(conn, callback) - }) -} - -function teardown (nodeA, nodeB, callback) { - parallel([ - (cb) => nodeA.stop(cb), - (cb) => nodeB.stop(cb) - ], callback) -} - -describe('stream muxing', () => { - it('spdy only', function (done) { - let nodeA - let nodeB - - function setup (callback) { - parallel([ - (cb) => createNode('/ip4/0.0.0.0/tcp/0', { - modules: { - streamMuxer: [SPDY] - } - }, (err, node) => { - expect(err).to.not.exist() - nodeA = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }), - (cb) => createNode('/ip4/0.0.0.0/tcp/0', { - modules: { - streamMuxer: [SPDY] - } - }, (err, node) => { - expect(err).to.not.exist() - nodeB = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }) - ], callback) - } - - series([ - (cb) => setup(cb), - (cb) => test(nodeA, nodeB, cb), - (cb) => teardown(nodeA, nodeB, cb) - ], done) - }) - - it('mplex only', (done) => { - let nodeA - let nodeB - - function setup (callback) { - parallel([ - (cb) => createNode('/ip4/0.0.0.0/tcp/0', { - modules: { - streamMuxer: [Mplex] - } - }, (err, node) => { - expect(err).to.not.exist() - nodeA = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }), - (cb) => createNode('/ip4/0.0.0.0/tcp/0', { - modules: { - streamMuxer: [Mplex] - } - }, (err, node) => { - expect(err).to.not.exist() - nodeB = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }) - ], callback) - } - - series([ - (cb) => setup(cb), - (cb) => test(nodeA, nodeB, cb), - (cb) => teardown(nodeA, nodeB, cb) - ], done) - }) - - it('pMplex only', (done) => { - let nodeA - let nodeB - - function setup (callback) { - parallel([ - (cb) => createNode('/ip4/0.0.0.0/tcp/0', { - modules: { - streamMuxer: [pMplex] - } - }, (err, node) => { - expect(err).to.not.exist() - nodeA = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }), - (cb) => createNode('/ip4/0.0.0.0/tcp/0', { - modules: { - streamMuxer: [pMplex] - } - }, (err, node) => { - expect(err).to.not.exist() - nodeB = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }) - ], callback) - } - - series([ - (cb) => setup(cb), - (cb) => test(nodeA, nodeB, cb), - (cb) => teardown(nodeA, nodeB, cb) - ], done) - }) - - it('spdy + mplex', function (done) { - this.timeout(5000) - - let nodeA - let nodeB - - function setup (callback) { - parallel([ - (cb) => createNode('/ip4/0.0.0.0/tcp/0', { - modules: { - streamMuxer: [Mplex] - } - }, (err, node) => { - expect(err).to.not.exist() - nodeA = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }), - (cb) => createNode('/ip4/0.0.0.0/tcp/0', { - modules: { - streamMuxer: [SPDY, Mplex] - } - }, (err, node) => { - expect(err).to.not.exist() - nodeB = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }) - ], callback) - } - - series([ - (cb) => setup(cb), - (cb) => test(nodeA, nodeB, cb), - (cb) => teardown(nodeA, nodeB, cb) - ], done) - }) - - it('mplex + pull-mplex', function (done) { - this.timeout(5000) - - let nodeA - let nodeB - - function setup (callback) { - parallel([ - (cb) => createNode('/ip4/0.0.0.0/tcp/0', { - modules: { - streamMuxer: [Mplex] - } - }, (err, node) => { - expect(err).to.not.exist() - nodeA = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }), - (cb) => createNode('/ip4/0.0.0.0/tcp/0', { - modules: { - streamMuxer: [pMplex] - } - }, (err, node) => { - expect(err).to.not.exist() - nodeB = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }) - ], callback) - } - - series([ - (cb) => setup(cb), - (cb) => test(nodeA, nodeB, cb), - (cb) => teardown(nodeA, nodeB, cb) - ], done) - }) - - it('spdy + mplex in reverse muxer order', function (done) { - this.timeout(5 * 1000) - - let nodeA - let nodeB - - function setup (callback) { - parallel([ - (cb) => createNode('/ip4/0.0.0.0/tcp/0', { - modules: { - streamMuxer: [SPDY, Mplex] - } - }, (err, node) => { - expect(err).to.not.exist() - nodeA = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }), - (cb) => createNode('/ip4/0.0.0.0/tcp/0', { - modules: { - streamMuxer: [Mplex, SPDY] - } - }, (err, node) => { - expect(err).to.not.exist() - nodeB = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }) - ], callback) - } - - series([ - (cb) => setup(cb), - (cb) => test(nodeA, nodeB, cb), - (cb) => teardown(nodeA, nodeB, cb) - ], done) - }) - - it('spdy + pull-mplex in reverse muxer order', function (done) { - this.timeout(5 * 1000) - - let nodeA - let nodeB - - function setup (callback) { - parallel([ - (cb) => createNode('/ip4/0.0.0.0/tcp/0', { - modules: { - streamMuxer: [SPDY, pMplex] - } - }, (err, node) => { - expect(err).to.not.exist() - nodeA = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }), - (cb) => createNode('/ip4/0.0.0.0/tcp/0', { - modules: { - streamMuxer: [pMplex, SPDY] - } - }, (err, node) => { - expect(err).to.not.exist() - nodeB = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }) - ], callback) - } - - series([ - (cb) => setup(cb), - (cb) => test(nodeA, nodeB, cb), - (cb) => teardown(nodeA, nodeB, cb) - ], done) - }) - - it('one without the other fails to establish a muxedConn', function (done) { - this.timeout(5 * 1000) - - let nodeA - let nodeB - - function setup (callback) { - parallel([ - (cb) => createNode('/ip4/0.0.0.0/tcp/0', { - modules: { - streamMuxer: [SPDY] - } - }, (err, node) => { - expect(err).to.not.exist() - nodeA = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }), - (cb) => createNode('/ip4/0.0.0.0/tcp/0', { - modules: { - streamMuxer: [Mplex] - } - }, (err, node) => { - expect(err).to.not.exist() - nodeB = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }) - ], callback) - } - - series([ - (cb) => setup(cb), - (cb) => { - // it will just 'warm up a conn' - expect(Object.keys(nodeA._switch.muxers)).to.have.length(1) - expect(Object.keys(nodeB._switch.muxers)).to.have.length(1) - - nodeA.dialFSM(nodeB.peerInfo, (err, connFSM) => { - expect(err).to.not.exist() - // The connection should fall back to 'unmuxed' - connFSM.once('unmuxed', () => cb()) - }) - }, - (cb) => teardown(nodeA, nodeB, cb) - ], done) - }) -}) diff --git a/test/switch/browser.js b/test/switch/browser.js deleted file mode 100644 index 5364fa17db..0000000000 --- a/test/switch/browser.js +++ /dev/null @@ -1,12 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const wrtcSupport = self.RTCPeerConnection && ('createDataChannel' in self.RTCPeerConnection.prototype) - -require('./transports.browser.js') -require('./swarm-muxing+websockets.browser') - -if (wrtcSupport) { - require('./t-webrtc-star.browser') - require('./swarm-muxing+webrtc-star.browser') -} diff --git a/test/switch/connection.node.js b/test/switch/connection.node.js deleted file mode 100644 index f8447c0cec..0000000000 --- a/test/switch/connection.node.js +++ /dev/null @@ -1,451 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -const expect = chai.expect -chai.use(require('dirty-chai')) -chai.use(require('chai-checkmark')) -const sinon = require('sinon') -const PeerBook = require('peer-book') -const WS = require('libp2p-websockets') -const parallel = require('async/parallel') -const secio = require('libp2p-secio') -const pull = require('pull-stream') -const multiplex = require('pull-mplex') -const spdy = require('libp2p-spdy') -const Protector = require('../../src/pnet') -const generatePSK = Protector.generate - -const psk = Buffer.alloc(95) -generatePSK(psk) - -const ConnectionFSM = require('../../src/switch/connection') -const Switch = require('../../src/switch') -const createInfos = require('./utils').createInfos - -describe('ConnectionFSM', () => { - let spdySwitch - let listenerSwitch - let dialerSwitch - - before((done) => { - createInfos(3, (err, infos) => { - if (err) { - return done(err) - } - - dialerSwitch = new Switch(infos.shift(), new PeerBook()) - dialerSwitch._peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/15451/ws') - dialerSwitch.connection.crypto(secio.tag, secio.encrypt) - dialerSwitch.connection.addStreamMuxer(multiplex) - dialerSwitch.transport.add('ws', new WS()) - - listenerSwitch = new Switch(infos.shift(), new PeerBook()) - listenerSwitch._peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/15452/ws') - listenerSwitch.connection.crypto(secio.tag, secio.encrypt) - listenerSwitch.connection.addStreamMuxer(multiplex) - listenerSwitch.transport.add('ws', new WS()) - - spdySwitch = new Switch(infos.shift(), new PeerBook()) - spdySwitch._peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/15453/ws') - spdySwitch.connection.crypto(secio.tag, secio.encrypt) - spdySwitch.connection.addStreamMuxer(spdy) - spdySwitch.transport.add('ws', new WS()) - - parallel([ - (cb) => dialerSwitch.start(cb), - (cb) => listenerSwitch.start(cb), - (cb) => spdySwitch.start(cb) - ], (err) => { - done(err) - }) - }) - }) - - after((done) => { - parallel([ - (cb) => dialerSwitch.stop(cb), - (cb) => listenerSwitch.stop(cb), - (cb) => spdySwitch.stop(cb) - ], () => { - done() - }) - }) - - it('should have a default state of disconnected', () => { - const connection = new ConnectionFSM({ - _switch: dialerSwitch, - peerInfo: listenerSwitch._peerInfo - }) - - expect(connection.getState()).to.equal('DISCONNECTED') - }) - - it('should emit an error with an invalid transition', (done) => { - const connection = new ConnectionFSM({ - _switch: dialerSwitch, - peerInfo: listenerSwitch._peerInfo - }) - - expect(connection.getState()).to.equal('DISCONNECTED') - - connection.once('error', (err) => { - expect(err).to.have.property('code', 'INVALID_STATE_TRANSITION') - done() - }) - connection.upgrade() - }) - - it('.dial should create a basic connection', (done) => { - const connection = new ConnectionFSM({ - _switch: dialerSwitch, - peerInfo: listenerSwitch._peerInfo - }) - - connection.once('connected', (conn) => { - expect(conn).to.exist() - done() - }) - - connection.dial() - }) - - it('should be able to close with an error and not throw', (done) => { - const connection = new ConnectionFSM({ - _switch: dialerSwitch, - peerInfo: listenerSwitch._peerInfo - }) - - connection.once('connected', (conn) => { - expect(conn).to.exist() - expect(() => connection.close(new Error('shutting down'))).to.not.throw() - done() - }) - - connection.dial() - }) - - it('should emit warning on dial failed attempt', (done) => { - const connection = new ConnectionFSM({ - _switch: dialerSwitch, - peerInfo: listenerSwitch._peerInfo - }) - - const stub = sinon.stub(dialerSwitch.transport, 'dial').callsArgWith(2, [ - new Error('address in use') - ]) - - connection.once('error:connection_attempt_failed', (errors) => { - expect(errors).to.have.length(1).mark() - stub.restore() - }) - - connection.once('error', (err) => { - expect(err).to.exist().mark() - }) - - expect(2).checks(done) - - connection.dial() - }) - - it('should ignore concurrent dials', () => { - const connection = new ConnectionFSM({ - _switch: dialerSwitch, - peerInfo: listenerSwitch._peerInfo - }) - - const stub = sinon.stub(connection, '_onDialing') - - connection.dial() - connection.dial() - - expect(stub.callCount).to.equal(1) - }) - - it('should be able to encrypt a basic connection', (done) => { - const connection = new ConnectionFSM({ - _switch: dialerSwitch, - peerInfo: listenerSwitch._peerInfo - }) - - connection.once('connected', (conn) => { - expect(conn).to.exist() - connection.encrypt() - }) - connection.once('encrypted', (conn) => { - expect(conn).to.exist() - done() - }) - - connection.dial() - }) - - it('should disconnect on encryption failure', (done) => { - const connection = new ConnectionFSM({ - _switch: dialerSwitch, - peerInfo: listenerSwitch._peerInfo - }) - - const stub = sinon.stub(dialerSwitch.crypto, 'encrypt') - .callsArgWith(3, new Error('fail encrypt')) - - connection.once('connected', (conn) => { - expect(conn).to.exist() - connection.encrypt() - }) - connection.once('close', () => { - stub.restore() - done() - }) - connection.once('encrypted', () => { - throw new Error('should not encrypt') - }) - - connection.dial() - }) - - it('should be able to upgrade an encrypted connection', (done) => { - const connection = new ConnectionFSM({ - _switch: dialerSwitch, - peerInfo: listenerSwitch._peerInfo - }) - - connection.once('connected', (conn) => { - expect(conn).to.exist() - connection.encrypt() - }) - connection.once('encrypted', (conn) => { - expect(conn).to.exist() - connection.upgrade() - }) - connection.once('muxed', (conn) => { - expect(conn.multicodec).to.equal(multiplex.multicodec) - done() - }) - - connection.dial() - }) - - it('should fail to upgrade a connection with incompatible muxers', (done) => { - const connection = new ConnectionFSM({ - _switch: dialerSwitch, - peerInfo: spdySwitch._peerInfo - }) - - connection.once('connected', (conn) => { - expect(conn).to.exist() - connection.encrypt() - }) - connection.once('encrypted', (conn) => { - expect(conn).to.exist() - connection.upgrade() - }) - connection.once('error:upgrade_failed', (err) => { - expect(err).to.exist() - done() - }) - - connection.dial() - }) - - it('should be able to handshake a protocol over a muxed connection', (done) => { - const connection = new ConnectionFSM({ - _switch: dialerSwitch, - peerInfo: listenerSwitch._peerInfo - }) - - listenerSwitch.handle('/muxed-conn-test/1.0.0', (_, conn) => { - return pull(conn, conn) - }) - - connection.once('connected', (conn) => { - expect(conn).to.exist() - connection.encrypt() - }) - connection.once('encrypted', (conn) => { - expect(conn).to.exist() - connection.upgrade() - }) - connection.once('muxed', (conn) => { - expect(conn.multicodec).to.equal(multiplex.multicodec) - - connection.shake('/muxed-conn-test/1.0.0', (err, protocolConn) => { - expect(err).to.not.exist() - expect(protocolConn).to.exist() - done() - }) - }) - - connection.dial() - }) - - it('should not return a connection when handshaking with no protocol', (done) => { - const connection = new ConnectionFSM({ - _switch: dialerSwitch, - peerInfo: listenerSwitch._peerInfo - }) - - listenerSwitch.handle('/muxed-conn-test/1.0.0', (_, conn) => { - return pull(conn, conn) - }) - - connection.once('connected', (conn) => { - expect(conn).to.exist() - connection.encrypt() - }) - connection.once('encrypted', (conn) => { - expect(conn).to.exist() - connection.upgrade() - }) - connection.once('muxed', (conn) => { - expect(conn.multicodec).to.equal(multiplex.multicodec) - - connection.shake(null, (err, protocolConn) => { - expect(err).to.not.exist() - expect(protocolConn).to.not.exist() - done() - }) - }) - - connection.dial() - }) - - describe('with no muxers', () => { - let oldMuxers - before(() => { - oldMuxers = dialerSwitch.muxers - dialerSwitch.muxers = {} - }) - - after(() => { - dialerSwitch.muxers = oldMuxers - }) - - it('should be able to handshake a protocol over a basic connection', (done) => { - const connection = new ConnectionFSM({ - _switch: dialerSwitch, - peerInfo: listenerSwitch._peerInfo - }) - - listenerSwitch.handle('/unmuxed-conn-test/1.0.0', (_, conn) => { - return pull(conn, conn) - }) - - connection.once('connected', (conn) => { - expect(conn).to.exist() - connection.encrypt() - }) - connection.once('encrypted', (conn) => { - expect(conn).to.exist() - connection.upgrade() - }) - connection.once('muxed', () => { - throw new Error('connection shouldnt be muxed') - }) - connection.once('unmuxed', (conn) => { - expect(conn).to.exist() - - connection.shake('/unmuxed-conn-test/1.0.0', (err, protocolConn) => { - expect(err).to.not.exist() - expect(protocolConn).to.exist() - done() - }) - }) - - connection.dial() - }) - }) - - describe('with a protector', () => { - // Restart the switches with protectors - before((done) => { - parallel([ - (cb) => dialerSwitch.stop(cb), - (cb) => listenerSwitch.stop(cb) - ], () => { - dialerSwitch.protector = new Protector(psk) - listenerSwitch.protector = new Protector(psk) - - parallel([ - (cb) => dialerSwitch.start(cb), - (cb) => listenerSwitch.start(cb) - ], done) - }) - }) - - afterEach(() => { - sinon.restore() - }) - - it('should be able to protect a basic connection', (done) => { - const connection = new ConnectionFSM({ - _switch: dialerSwitch, - peerInfo: listenerSwitch._peerInfo - }) - - connection.once('private', (conn) => { - expect(conn).to.exist() - done() - }) - - connection.once('connected', (conn) => { - expect(conn).to.exist() - connection.protect() - }) - - connection.dial() - }) - - it('should close on failed protection', (done) => { - const connection = new ConnectionFSM({ - _switch: dialerSwitch, - peerInfo: listenerSwitch._peerInfo - }) - - const error = new Error('invalid key') - const stub = sinon.stub(dialerSwitch.protector, 'protect').callsFake((_, cb) => { - cb(error) - }) - - expect(3).check(done) - - connection.once('close', () => { - expect(stub.callCount).to.eql(1).mark() - }) - - connection.once('error', (err) => { - expect(err).to.eql(error).mark() - }) - - connection.once('connected', (conn) => { - expect(conn).to.exist().mark() - connection.protect() - }) - - connection.dial() - }) - - it('should be able to encrypt a protected connection', (done) => { - const connection = new ConnectionFSM({ - _switch: dialerSwitch, - peerInfo: listenerSwitch._peerInfo - }) - - connection.once('connected', (conn) => { - expect(conn).to.exist() - connection.protect() - }) - connection.once('private', (conn) => { - expect(conn).to.exist() - connection.encrypt() - }) - connection.once('encrypted', (conn) => { - expect(conn).to.exist() - done() - }) - - connection.dial() - }) - }) -}) diff --git a/test/switch/constructor.spec.js b/test/switch/constructor.spec.js deleted file mode 100644 index 7d932aeb68..0000000000 --- a/test/switch/constructor.spec.js +++ /dev/null @@ -1,15 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) - -const Switch = require('../../src/switch') - -describe('create Switch instance', () => { - it('throws on missing peerInfo', () => { - expect(() => new Switch()).to.throw(/You must provide a `peerInfo`/) - }) -}) diff --git a/test/switch/dial-fsm.node.js b/test/switch/dial-fsm.node.js deleted file mode 100644 index 78faf6dc4d..0000000000 --- a/test/switch/dial-fsm.node.js +++ /dev/null @@ -1,405 +0,0 @@ -/* eslint-env mocha */ -/* eslint max-nested-callbacks: ["error", 5] */ -'use strict' - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(require('chai-checkmark')) -chai.use(dirtyChai) -const sinon = require('sinon') -const PeerBook = require('peer-book') -const parallel = require('async/parallel') -const series = require('async/series') -const WS = require('libp2p-websockets') -const TCP = require('libp2p-tcp') -const secio = require('libp2p-secio') -const multiplex = require('pull-mplex') -const pull = require('pull-stream') -const identify = require('../../src/identify') - -const utils = require('./utils') -const createInfos = utils.createInfos -const Switch = require('../../src/switch') - -describe('dialFSM', () => { - let switchA - let switchB - let switchC - let switchDialOnly - let peerAId - let peerBId - let protocol - - before((done) => createInfos(4, (err, infos) => { - expect(err).to.not.exist() - - const peerA = infos[0] - const peerB = infos[1] - const peerC = infos[2] - const peerDialOnly = infos[3] - - peerAId = peerA.id.toB58String() - peerBId = peerB.id.toB58String() - - peerA.multiaddrs.add('/ip4/0.0.0.0/tcp/0') - peerB.multiaddrs.add('/ip4/0.0.0.0/tcp/0') - peerC.multiaddrs.add('/ip4/0.0.0.0/tcp/0/ws') - // Give peer C a tcp address we wont actually support - peerC.multiaddrs.add('/ip4/0.0.0.0/tcp/0') - - switchA = new Switch(peerA, new PeerBook()) - switchB = new Switch(peerB, new PeerBook()) - switchC = new Switch(peerC, new PeerBook()) - switchDialOnly = new Switch(peerDialOnly, new PeerBook()) - - switchA.transport.add('tcp', new TCP()) - switchB.transport.add('tcp', new TCP()) - switchC.transport.add('ws', new WS()) - switchDialOnly.transport.add('ws', new WS()) - - switchA.connection.crypto(secio.tag, secio.encrypt) - switchB.connection.crypto(secio.tag, secio.encrypt) - switchC.connection.crypto(secio.tag, secio.encrypt) - switchDialOnly.connection.crypto(secio.tag, secio.encrypt) - - switchA.connection.addStreamMuxer(multiplex) - switchB.connection.addStreamMuxer(multiplex) - switchC.connection.addStreamMuxer(multiplex) - switchDialOnly.connection.addStreamMuxer(multiplex) - - switchA.connection.reuse() - switchB.connection.reuse() - switchC.connection.reuse() - switchDialOnly.connection.reuse() - - parallel([ - (cb) => switchA.start(cb), - (cb) => switchB.start(cb), - (cb) => switchC.start(cb) - ], done) - })) - - after((done) => { - parallel([ - (cb) => switchA.stop(cb), - (cb) => switchB.stop(cb), - (cb) => switchC.stop(cb) - ], done) - }) - - afterEach(() => { - switchA.unhandle(protocol) - switchB.unhandle(protocol) - switchC.unhandle(protocol) - protocol = null - }) - - it('should emit `error:connection_attempt_failed` when a transport fails to dial', (done) => { - protocol = '/warn/1.0.0' - switchC.handle(protocol, () => { }) - - switchA.dialFSM(switchC._peerInfo, protocol, (err, connFSM) => { - expect(err).to.not.exist() - connFSM.once('error:connection_attempt_failed', (errors) => { - expect(errors).to.be.an('array') - expect(errors).to.have.length(1) - done() - }) - }) - }) - - it('should emit an `error` event when a it cannot dial a peer', (done) => { - protocol = '/error/1.0.0' - switchC.handle(protocol, () => { }) - - switchA.dialer.clearDenylist(switchC._peerInfo) - switchA.dialFSM(switchC._peerInfo, protocol, (err, connFSM) => { - expect(err).to.not.exist() - connFSM.once('error', (err) => { - expect(err).to.be.exist() - expect(err).to.have.property('code', 'CONNECTION_FAILED') - done() - }) - }) - }) - - it('should error when the peer is denylisted', (done) => { - protocol = '/error/1.0.0' - switchC.handle(protocol, () => { }) - - switchA.dialer.clearDenylist(switchC._peerInfo) - switchA.dialFSM(switchC._peerInfo, protocol, (err, connFSM) => { - expect(err).to.not.exist() - connFSM.once('error', () => { - // dial with the denylist - switchA.dialFSM(switchC._peerInfo, protocol, (err) => { - expect(err).to.exist() - expect(err.code).to.eql('ERR_DENIED') - done() - }) - }) - }) - }) - - it('should not denylist a peer that was successfully connected', (done) => { - protocol = '/nodenylist/1.0.0' - switchB.handle(protocol, () => { }) - - switchA.dialer.clearDenylist(switchB._peerInfo) - switchA.dialFSM(switchB._peerInfo, protocol, (err, connFSM) => { - expect(err).to.not.exist() - connFSM.once('connection', () => { - connFSM.once('close', () => { - // peer should not be denylisted - switchA.dialFSM(switchB._peerInfo, protocol, (err, conn) => { - expect(err).to.not.exist() - conn.once('close', done) - conn.close() - }) - }) - connFSM.close(new Error('bad things')) - }) - }) - }) - - it('should clear the denylist for a peer that connected to us', (done) => { - series([ - // Attempt to dial the peer that's not listening - (cb) => switchC.dial(switchDialOnly._peerInfo, (err) => { - expect(err).to.exist() - cb() - }), - // Dial from the dial only peer - (cb) => switchDialOnly.dial(switchC._peerInfo, (err) => { - expect(err).to.not.exist() - // allow time for muxing to occur - setTimeout(cb, 100) - }), - // "Dial" to the dial only peer, this should reuse the existing connection - (cb) => switchC.dial(switchDialOnly._peerInfo, (err) => { - expect(err).to.not.exist() - cb() - }) - ], (err) => { - expect(err).to.not.exist() - done() - }) - }) - - it('should emit a `closed` event when closed', (done) => { - protocol = '/closed/1.0.0' - switchB.handle(protocol, () => { }) - - switchA.dialFSM(switchB._peerInfo, protocol, (err, connFSM) => { - expect(err).to.not.exist() - - connFSM.once('close', () => { - expect(switchA.connection.getAllById(peerBId)).to.have.length(0) - done() - }) - - connFSM.once('muxed', () => { - expect(switchA.connection.getAllById(peerBId)).to.have.length(1) - connFSM.close() - }) - }) - }) - - it('should have the peers protocols once connected', (done) => { - protocol = '/lscheck/1.0.0' - switchB.handle(protocol, () => { }) - - expect(4).checks(done) - - switchB.once('peer-mux-established', (peerInfo) => { - const peerB = switchA._peerBook.get(switchB._peerInfo.id.toB58String()) - const peerA = switchB._peerBook.get(switchA._peerInfo.id.toB58String()) - // Verify the dialer knows the receiver's protocols - expect(Array.from(peerB.protocols)).to.eql([ - multiplex.multicodec, - identify.multicodec, - protocol - ]).mark() - // Verify the receiver knows the dialer's protocols - expect(Array.from(peerA.protocols)).to.eql([ - multiplex.multicodec, - identify.multicodec - ]).mark() - - switchA.hangUp(switchB._peerInfo) - }) - - switchA.dialFSM(switchB._peerInfo, protocol, (err, connFSM) => { - expect(err).to.not.exist().mark() - - connFSM.once('close', () => { - // Just mark that close was called - expect(true).to.eql(true).mark() - }) - }) - }) - - it('should close when the receiver closes', (done) => { - protocol = '/closed/1.0.0' - switchB.handle(protocol, () => { }) - - // wait for the expects to happen - expect(2).checks(() => { - done() - }) - - switchB.on('peer-mux-established', (peerInfo) => { - if (peerInfo.id.toB58String() === peerAId) { - switchB.removeAllListeners('peer-mux-established') - expect(switchB.connection.getAllById(peerAId)).to.have.length(1).mark() - switchB.connection.getOne(peerAId).close() - } - }) - - switchA.dialFSM(switchB._peerInfo, protocol, (err, connFSM) => { - expect(err).to.not.exist() - - connFSM.once('close', () => { - expect(switchA.connection.getAllById(peerBId)).to.have.length(0).mark() - }) - }) - }) - - it('parallel dials to the same peer should not create new connections', (done) => { - switchB.handle('/parallel/2.0.0', (_, conn) => { pull(conn, conn) }) - - parallel([ - (cb) => switchA.dialFSM(switchB._peerInfo, '/parallel/2.0.0', cb), - (cb) => switchA.dialFSM(switchB._peerInfo, '/parallel/2.0.0', cb) - ], (err, results) => { - expect(err).to.not.exist() - expect(results).to.have.length(2) - expect(switchA.connection.getAllById(peerBId)).to.have.length(1) - - switchA.hangUp(switchB._peerInfo, () => { - expect(switchA.connection.getAllById(peerBId)).to.have.length(0) - done() - }) - }) - }) - - it('parallel dials to one another should disconnect on hangup', function (done) { - this.timeout(10e3) - protocol = '/parallel/1.0.0' - - switchA.handle(protocol, (_, conn) => { pull(conn, conn) }) - switchB.handle(protocol, (_, conn) => { pull(conn, conn) }) - - expect(switchA.connection.getAllById(peerBId)).to.have.length(0) - - // Expect 4 `peer-mux-established` events - expect(4).checks(() => { - // Expect 2 `peer-mux-closed`, plus 1 hangup - expect(3).checks(() => { - switchA.removeAllListeners('peer-mux-closed') - switchB.removeAllListeners('peer-mux-closed') - switchA.removeAllListeners('peer-mux-established') - switchB.removeAllListeners('peer-mux-established') - done() - }) - - switchA.hangUp(switchB._peerInfo, (err) => { - expect(err).to.not.exist().mark() - }) - }) - - switchA.on('peer-mux-established', (peerInfo) => { - expect(peerInfo.id.toB58String()).to.eql(peerBId).mark() - }) - switchB.on('peer-mux-established', (peerInfo) => { - expect(peerInfo.id.toB58String()).to.eql(peerAId).mark() - }) - - switchA.on('peer-mux-closed', (peerInfo) => { - expect(peerInfo.id.toB58String()).to.eql(peerBId).mark() - }) - switchB.on('peer-mux-closed', (peerInfo) => { - expect(peerInfo.id.toB58String()).to.eql(peerAId).mark() - }) - - switchA.dialFSM(switchB._peerInfo, protocol, (err, connFSM) => { - expect(err).to.not.exist() - // Hold the dial from A, until switch B is done dialing to ensure - // we have both incoming and outgoing connections - connFSM._state.on('DIALING:leave', (cb) => { - switchB.dialFSM(switchA._peerInfo, protocol, (err, connB) => { - expect(err).to.not.exist() - connB.on('muxed', cb) - }) - }) - }) - }) - - it('parallel dials to one another should disconnect on stop', (done) => { - protocol = '/parallel/1.0.0' - switchA.handle(protocol, (_, conn) => { pull(conn, conn) }) - switchB.handle(protocol, (_, conn) => { pull(conn, conn) }) - - // 2 close checks and 1 hangup check - expect(2).checks(() => { - switchA.removeAllListeners('peer-mux-closed') - switchB.removeAllListeners('peer-mux-closed') - // restart the node for subsequent tests - switchA.start(done) - }) - - switchA.on('peer-mux-closed', (peerInfo) => { - expect(peerInfo.id.toB58String()).to.eql(peerBId).mark() - }) - switchB.on('peer-mux-closed', (peerInfo) => { - expect(peerInfo.id.toB58String()).to.eql(peerAId).mark() - }) - - switchA.dialFSM(switchB._peerInfo, '/parallel/1.0.0', (err, connFSM) => { - expect(err).to.not.exist() - // Hold the dial from A, until switch B is done dialing to ensure - // we have both incoming and outgoing connections - connFSM._state.on('DIALING:leave', (cb) => { - switchB.dialFSM(switchA._peerInfo, '/parallel/1.0.0', (err, connB) => { - expect(err).to.not.exist() - connB.on('muxed', cb) - }) - }) - - connFSM.on('connection', () => { - // Hangup and verify the connections are closed - switchA.stop((err) => { - expect(err).to.not.exist().mark() - }) - }) - }) - }) - - it('queued dials should be aborted on node stop', (done) => { - switchB.handle('/abort-queue/1.0.0', (_, conn) => { pull(conn, conn) }) - - switchA.dialFSM(switchB._peerInfo, '/abort-queue/1.0.0', (err, connFSM) => { - expect(err).to.not.exist() - // 2 conn aborts, 1 close, and 1 stop - expect(4).checks(done) - - connFSM.once('close', (err) => { - expect(err).to.not.exist().mark() - }) - - sinon.stub(connFSM, '_onUpgrading').callsFake(() => { - switchA.dialFSM(switchB._peerInfo, '/abort-queue/1.0.0', (err) => { - expect(err.code).to.eql('DIAL_ABORTED').mark() - }) - switchA.dialFSM(switchB._peerInfo, '/abort-queue/1.0.0', (err) => { - expect(err.code).to.eql('DIAL_ABORTED').mark() - }) - - switchA.stop((err) => { - expect(err).to.not.exist().mark() - }) - }) - }) - }) -}) diff --git a/test/switch/dialSelf.spec.js b/test/switch/dialSelf.spec.js deleted file mode 100644 index c0d543c24e..0000000000 --- a/test/switch/dialSelf.spec.js +++ /dev/null @@ -1,88 +0,0 @@ -'use strict' - -/* eslint-env mocha */ - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) - -const { EventEmitter } = require('events') -const PeerBook = require('peer-book') -const Duplex = require('pull-pair/duplex') - -const utils = require('./utils') -const createInfos = utils.createInfos -const Swarm = require('../../src/switch') - -class MockTransport extends EventEmitter { - constructor () { - super() - this.conn = Duplex() - } - - dial (addr, cb) { - const c = this.conn[0] - this.emit('connection', this.conn[1]) - setImmediate(() => cb(null, c)) - return c - } - - listen (addr, cb) { - return cb() - } - - filter (mas) { - return Array.isArray(mas) ? mas : [mas] - } -} - -describe('dial self', () => { - let swarmA - let peerInfos - - before((done) => createInfos(2, (err, infos) => { - expect(err).to.not.exist() - - const peerA = infos.shift() - peerInfos = infos - - peerA.multiaddrs.add('/ip4/127.0.0.1/tcp/9001') - peerA.multiaddrs.add(`/ip4/127.0.0.1/tcp/9001/ipfs/${peerA.id.toB58String()}`) - peerA.multiaddrs.add(`/ip4/127.0.0.1/tcp/9001/p2p-circuit/ipfs/${peerA.id.toB58String()}`) - peerA.multiaddrs.add('/ip4/0.0.0.0/tcp/9001') - peerA.multiaddrs.add(`/ip4/0.0.0.0/tcp/9001/ipfs/${peerA.id.toB58String()}`) - peerA.multiaddrs.add(`/ip4/0.0.0.0/tcp/9001/p2p-circuit/ipfs/${peerA.id.toB58String()}`) - - swarmA = new Swarm(peerA, new PeerBook()) - - swarmA.transport.add('tcp', new MockTransport()) - - done() - })) - - after((done) => swarmA.stop(done)) - - it('node should not be able to dial itself', (done) => { - swarmA.dial(swarmA._peerInfo, (err, conn) => { - expect(err).to.exist() - expect(() => { throw err }).to.throw(/A node cannot dial itself/) - expect(conn).to.not.exist() - done() - }) - }) - - it('node should not be able to dial another peers address that matches its own', (done) => { - const peerB = peerInfos.shift() - peerB.multiaddrs.add('/ip4/127.0.0.1/tcp/9001') - peerB.multiaddrs.add('/ip4/0.0.0.0/tcp/9001') - peerB.multiaddrs.add(`/ip4/0.0.0.0/tcp/9001/ipfs/${peerB.id.toB58String()}`) - - swarmA.dial(peerB, (err, conn) => { - expect(err).to.exist() - expect(err.code).to.eql('CONNECTION_FAILED') - expect(conn).to.not.exist() - done() - }) - }) -}) diff --git a/test/switch/dialer.spec.js b/test/switch/dialer.spec.js deleted file mode 100644 index 95bba64ee4..0000000000 --- a/test/switch/dialer.spec.js +++ /dev/null @@ -1,230 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(require('chai-checkmark')) -chai.use(dirtyChai) -const sinon = require('sinon') - -const PeerBook = require('peer-book') -const Queue = require('../../src/switch/dialer/queue') -const QueueManager = require('../../src/switch/dialer/queueManager') -const Switch = require('../../src/switch') -const { PRIORITY_HIGH, PRIORITY_LOW } = require('../../src/switch/constants') - -const utils = require('./utils') -const createInfos = utils.createInfos - -describe('dialer', () => { - let switchA - let switchB - - before((done) => createInfos(2, (err, infos) => { - expect(err).to.not.exist() - - switchA = new Switch(infos[0], new PeerBook()) - switchB = new Switch(infos[1], new PeerBook()) - - done() - })) - - afterEach(() => { - sinon.restore() - }) - - describe('connect', () => { - afterEach(() => { - switchA.dialer.clearDenylist(switchB._peerInfo) - }) - - it('should use default options', (done) => { - switchA.dialer.connect(switchB._peerInfo, (err) => { - expect(err).to.exist() - done() - }) - }) - - it('should be able to use custom options', (done) => { - switchA.dialer.connect(switchB._peerInfo, { useFSM: true, priority: PRIORITY_HIGH }, (err) => { - expect(err).to.exist() - done() - }) - }) - }) - - describe('queue', () => { - it('should denylist forever after 5 denylists', () => { - const queue = new Queue('QM', switchA) - for (var i = 0; i < 4; i++) { - queue.denylist() - expect(queue.denylisted).to.be.a('number') - expect(queue.denylisted).to.not.eql(Infinity) - } - - queue.denylist() - expect(queue.denylisted).to.eql(Infinity) - }) - }) - - describe('queue manager', () => { - let queueManager - before(() => { - queueManager = new QueueManager(switchA) - }) - - it('should abort cold calls when the queue is full', (done) => { - sinon.stub(queueManager._coldCallQueue, 'size').value(switchA.dialer.MAX_COLD_CALLS) - const dialRequest = { - peerInfo: { - id: { toB58String: () => 'QmA' } - }, - protocol: null, - options: { useFSM: true, priority: PRIORITY_LOW }, - callback: (err) => { - expect(err.code).to.eql('DIAL_ABORTED') - done() - } - } - - queueManager.add(dialRequest) - }) - - it('should add a protocol dial to the normal queue', () => { - const dialRequest = { - peerInfo: { - id: { toB58String: () => 'QmA' }, - isConnected: () => null - }, - protocol: '/echo/1.0.0', - options: { useFSM: true, priority: PRIORITY_HIGH }, - callback: () => {} - } - - const runSpy = sinon.stub(queueManager, 'run') - const addSpy = sinon.stub(queueManager._queue, 'add') - const deleteSpy = sinon.stub(queueManager._coldCallQueue, 'delete') - - queueManager.add(dialRequest) - - expect(runSpy.called).to.eql(true) - expect(addSpy.called).to.eql(true) - expect(addSpy.getCall(0).args[0]).to.eql('QmA') - expect(deleteSpy.called).to.eql(true) - expect(deleteSpy.getCall(0).args[0]).to.eql('QmA') - }) - - it('should add a cold call to the cold call queue', () => { - const dialRequest = { - peerInfo: { - id: { toB58String: () => 'QmA' }, - isConnected: () => null - }, - protocol: null, - options: { useFSM: true, priority: PRIORITY_LOW }, - callback: () => {} - } - - const runSpy = sinon.stub(queueManager, 'run') - const addSpy = sinon.stub(queueManager._coldCallQueue, 'add') - - queueManager.add(dialRequest) - - expect(runSpy.called).to.eql(true) - expect(addSpy.called).to.eql(true) - expect(addSpy.getCall(0).args[0]).to.eql('QmA') - }) - - it('should abort a cold call if it\'s in the normal queue', (done) => { - const dialRequest = { - peerInfo: { - id: { toB58String: () => 'QmA' }, - isConnected: () => null - }, - protocol: null, - options: { useFSM: true, priority: PRIORITY_LOW }, - callback: (err) => { - expect(runSpy.called).to.eql(false) - expect(hasSpy.called).to.eql(true) - expect(hasSpy.getCall(0).args[0]).to.eql('QmA') - expect(err.code).to.eql('DIAL_ABORTED') - done() - } - } - - const runSpy = sinon.stub(queueManager, 'run') - const hasSpy = sinon.stub(queueManager._queue, 'has').returns(true) - - queueManager.add(dialRequest) - }) - - it('should remove a queue that has reached max denylist', () => { - const queue = new Queue('QmA', switchA) - queue.denylisted = Infinity - - const abortSpy = sinon.spy(queue, 'abort') - const queueManager = new QueueManager(switchA) - queueManager._queues[queue.id] = queue - - queueManager._clean() - - expect(abortSpy.called).to.eql(true) - expect(queueManager._queues).to.eql({}) - }) - - it('should not remove a queue that is denylisted below max', () => { - const queue = new Queue('QmA', switchA) - queue.denylisted = Date.now() + 10e3 - - const abortSpy = sinon.spy(queue, 'abort') - const queueManager = new QueueManager(switchA) - queueManager._queues[queue.id] = queue - - queueManager._clean() - - expect(abortSpy.called).to.eql(false) - expect(queueManager._queues).to.eql({ - QmA: queue - }) - }) - - it('should remove a queue that is not running and the peer is not connected', () => { - const disconnectedPeer = { - id: { toB58String: () => 'QmA' }, - isConnected: () => null - } - const queue = new Queue(disconnectedPeer.id.toB58String(), switchA) - - const abortSpy = sinon.spy(queue, 'abort') - const queueManager = new QueueManager(switchA) - queueManager._queues[queue.id] = queue - - queueManager._clean() - - expect(abortSpy.called).to.eql(true) - expect(queueManager._queues).to.eql({}) - }) - - it('should not remove a queue that is not running but the peer is connected', () => { - const connectedPeer = { - id: { toB58String: () => 'QmA' }, - isConnected: () => true - } - const queue = new Queue(connectedPeer.id.toB58String(), switchA) - - switchA._peerBook.put(connectedPeer) - - const abortSpy = sinon.spy(queue, 'abort') - const queueManager = new QueueManager(switchA) - queueManager._queues[queue.id] = queue - - queueManager._clean() - - expect(abortSpy.called).to.eql(false) - expect(queueManager._queues).to.eql({ - QmA: queue - }) - }) - }) -}) diff --git a/test/switch/identify.node.js b/test/switch/identify.node.js deleted file mode 100644 index 4585de55cf..0000000000 --- a/test/switch/identify.node.js +++ /dev/null @@ -1,173 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -chai.use(require('chai-checkmark')) -const expect = chai.expect -const parallel = require('async/parallel') -const TCP = require('libp2p-tcp') -const multiplex = require('libp2p-mplex') -const pull = require('pull-stream') -const secio = require('libp2p-secio') -const PeerInfo = require('peer-info') -const PeerBook = require('peer-book') -const identify = require('../../src/identify') -const lp = require('pull-length-prefixed') -const sinon = require('sinon') - -const utils = require('./utils') -const createInfos = utils.createInfos -const Switch = require('../../src/switch') - -describe('Identify', () => { - let switchA - let switchB - let switchC - - before((done) => createInfos(3, (err, infos) => { - expect(err).to.not.exist() - - const peerA = infos[0] - const peerB = infos[1] - const peerC = infos[2] - - peerA.multiaddrs.add('/ip4/127.0.0.1/tcp/9001') - peerB.multiaddrs.add('/ip4/127.0.0.1/tcp/9002') - peerC.multiaddrs.add('/ip4/127.0.0.1/tcp/9003') - - switchA = new Switch(peerA, new PeerBook()) - switchB = new Switch(peerB, new PeerBook()) - switchC = new Switch(peerC, new PeerBook()) - - switchA.transport.add('tcp', new TCP()) - switchB.transport.add('tcp', new TCP()) - switchC.transport.add('tcp', new TCP()) - - switchA.connection.crypto(secio.tag, secio.encrypt) - switchB.connection.crypto(secio.tag, secio.encrypt) - switchC.connection.crypto(secio.tag, secio.encrypt) - - switchA.connection.addStreamMuxer(multiplex) - switchB.connection.addStreamMuxer(multiplex) - switchC.connection.addStreamMuxer(multiplex) - - switchA.connection.reuse() - switchB.connection.reuse() - switchC.connection.reuse() - - parallel([ - (cb) => switchA.transport.listen('tcp', {}, null, cb), - (cb) => switchB.transport.listen('tcp', {}, null, cb), - (cb) => switchC.transport.listen('tcp', {}, null, cb) - ], done) - })) - - after((done) => { - parallel([ - (cb) => switchA.stop(cb), - (cb) => switchB.stop(cb), - (cb) => switchC.stop(cb) - ], done) - }) - - afterEach(function (done) { - sinon.restore() - // Hangup everything - parallel([ - (cb) => switchA.hangUp(switchB._peerInfo, cb), - (cb) => switchA.hangUp(switchC._peerInfo, cb), - (cb) => switchB.hangUp(switchA._peerInfo, cb), - (cb) => switchB.hangUp(switchC._peerInfo, cb), - (cb) => switchC.hangUp(switchA._peerInfo, cb), - (cb) => switchC.hangUp(switchB._peerInfo, cb) - ], done) - }) - - it('should identify a good peer', (done) => { - switchA.handle('/id-test/1.0.0', (protocol, conn) => pull(conn, conn)) - switchB.dial(switchA._peerInfo, '/id-test/1.0.0', (err, conn) => { - expect(err).to.not.exist() - - const data = Buffer.from('data that can be had') - pull( - pull.values([data]), - conn, - pull.collect((err, values) => { - expect(err).to.not.exist() - expect(values).to.deep.equal([data]) - done() - }) - ) - }) - }) - - it('should get protocols for one another', (done) => { - // We need to reset the PeerInfo objects we use, - // since we share memory we can receive a false positive if not - const peerA = new PeerInfo(switchA._peerInfo.id) - switchA._peerInfo.multiaddrs.toArray().forEach((m) => { - peerA.multiaddrs.add(m) - }) - switchB._peerBook.remove(switchA._peerInfo.id.toB58String()) - switchA._peerBook.remove(switchB._peerInfo.id.toB58String()) - - switchA.handle('/id-test/1.0.0', (protocol, conn) => pull(conn, conn)) - switchB.dial(peerA, '/id-test/1.0.0', (err) => { - expect(err).to.not.exist() - - // Give identify a moment to run - setTimeout(() => { - const peerB = switchA._peerBook.get(switchB._peerInfo.id.toB58String()) - const peerA = switchB._peerBook.get(switchA._peerInfo.id.toB58String()) - expect(Array.from(peerB.protocols)).to.eql([ - multiplex.multicodec, - identify.multicodec - ]) - expect(Array.from(peerA.protocols)).to.eql([ - multiplex.multicodec, - identify.multicodec, - '/id-test/1.0.0' - ]) - - done() - }, 500) - }) - }) - - it('should close connection when identify fails', (done) => { - const stub = sinon.stub(identify, 'listener').callsFake((conn) => { - conn.getObservedAddrs((err, observedAddrs) => { - if (err) { return } - observedAddrs = observedAddrs[0] - - // pretend to be another peer - const publicKey = switchC._peerInfo.id.pubKey.bytes - - const msgSend = identify.message.encode({ - protocolVersion: 'ipfs/0.1.0', - agentVersion: 'na', - publicKey: publicKey, - listenAddrs: switchC._peerInfo.multiaddrs.toArray().map((ma) => ma.buffer), - observedAddr: observedAddrs ? observedAddrs.buffer : Buffer.from('') - }) - - pull( - pull.values([msgSend]), - lp.encode(), - conn - ) - }) - }) - - expect(2).checks(done) - - switchA.handle('/id-test/1.0.0', (protocol, conn) => pull(conn, conn)) - switchB.dialFSM(switchA._peerInfo, '/id-test/1.0.0', (err, connFSM) => { - expect(err).to.not.exist().mark() - connFSM.once('close', () => { - expect(stub.called).to.eql(true).mark() - }) - }) - }) -}) diff --git a/test/switch/limit-dialer.node.js b/test/switch/limit-dialer.node.js deleted file mode 100644 index e6395400d9..0000000000 --- a/test/switch/limit-dialer.node.js +++ /dev/null @@ -1,93 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -chai.use(require('chai-checkmark')) -const expect = chai.expect -const multiaddr = require('multiaddr') -const pull = require('pull-stream') -const nextTick = require('async/nextTick') - -const LimitDialer = require('../../src/switch/limit-dialer') -const utils = require('./utils') - -describe('LimitDialer', () => { - let peers - - before((done) => { - utils.createInfos(5, (err, infos) => { - if (err) { - return done(err) - } - peers = infos - - peers.forEach((peer, i) => { - peer.multiaddrs.add(multiaddr(`/ip4/191.0.0.1/tcp/123${i}`)) - peer.multiaddrs.add(multiaddr(`/ip4/192.168.0.1/tcp/923${i}`)) - peer.multiaddrs.add(multiaddr(`/ip4/193.168.0.99/tcp/923${i}`)) - }) - done() - }) - }) - - it('all failing', (done) => { - const dialer = new LimitDialer(2, 10) - const error = new Error('fail') - // mock transport - const t1 = { - dial (addr, cb) { - nextTick(cb, error) - return {} - } - } - - dialer.dialMany(peers[0].id, t1, peers[0].multiaddrs.toArray(), (err, conn) => { - expect(err).to.exist() - expect(err).to.include.members([error, error, error]) - expect(conn).to.not.exist() - done() - }) - }) - - it('two success', (done) => { - const dialer = new LimitDialer(2, 10) - - // mock transport - const t1 = { - dial (addr, cb) { - const as = addr.toString() - if (as.match(/191/)) { - nextTick(cb, new Error('fail')) - return null - } else if (as.match(/192/)) { - nextTick(cb) - return { - source: pull.values([1]), - sink: pull.drain() - } - } else if (as.match(/193/)) { - nextTick(cb) - return { - source: pull.values([2]), - sink: pull.drain() - } - } - } - } - - dialer.dialMany(peers[0].id, t1, peers[0].multiaddrs.toArray(), (err, success) => { - const conn = success.conn - expect(success.multiaddr.toString()).to.equal('/ip4/192.168.0.1/tcp/9230') - expect(err).to.not.exist() - pull( - conn, - pull.collect((err, res) => { - expect(err).to.not.exist() - expect(res).to.be.eql([1]) - done() - }) - ) - }) - }) -}) diff --git a/test/switch/node.js b/test/switch/node.js deleted file mode 100644 index 7a2bfbd18d..0000000000 --- a/test/switch/node.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict' - -require('./connection.node') -require('./dial-fsm.node') -require('./pnet.node') -require('./transports.node') -require('./stream-muxers.node') -require('./secio.node') -require('./swarm-no-muxing.node') -require('./swarm-muxing.node') -require('./identify.node') -require('./limit-dialer.node') -require('./stats.node') diff --git a/test/switch/pnet.node.js b/test/switch/pnet.node.js deleted file mode 100644 index 2bc1c6450f..0000000000 --- a/test/switch/pnet.node.js +++ /dev/null @@ -1,152 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) -const parallel = require('async/parallel') -const TCP = require('libp2p-tcp') -const multiplex = require('pull-mplex') -const pull = require('pull-stream') -const PeerBook = require('peer-book') -const secio = require('libp2p-secio') -const Protector = require('../../src/pnet') - -const utils = require('./utils') -const createInfos = utils.createInfos -const tryEcho = utils.tryEcho -const Switch = require('../../src/switch') - -const generatePSK = Protector.generate - -const psk = Buffer.alloc(95) -const psk2 = Buffer.alloc(95) -generatePSK(psk) -generatePSK(psk2) - -describe('Private Network', function () { - let switchA - let switchB - let switchC - let switchD - - before((done) => createInfos(4, (err, infos) => { - expect(err).to.not.exist() - - const peerA = infos[0] - const peerB = infos[1] - const peerC = infos[2] - const peerD = infos[3] - - peerA.multiaddrs.add('/ip4/127.0.0.1/tcp/9001') - peerB.multiaddrs.add('/ip4/127.0.0.1/tcp/9002') - peerC.multiaddrs.add('/ip4/127.0.0.1/tcp/9003') - peerD.multiaddrs.add('/ip4/127.0.0.1/tcp/9004') - - switchA = new Switch(peerA, new PeerBook(), { - protector: new Protector(psk) - }) - switchB = new Switch(peerB, new PeerBook(), { - protector: new Protector(psk) - }) - // alternative way to add the protector - switchC = new Switch(peerC, new PeerBook()) - switchC.protector = new Protector(psk) - // Create a switch on a different private network - switchD = new Switch(peerD, new PeerBook(), { - protector: new Protector(psk2) - }) - - switchA.transport.add('tcp', new TCP()) - switchB.transport.add('tcp', new TCP()) - switchC.transport.add('tcp', new TCP()) - switchD.transport.add('tcp', new TCP()) - - switchA.connection.crypto(secio.tag, secio.encrypt) - switchB.connection.crypto(secio.tag, secio.encrypt) - switchC.connection.crypto(secio.tag, secio.encrypt) - switchD.connection.crypto(secio.tag, secio.encrypt) - - switchA.connection.addStreamMuxer(multiplex) - switchB.connection.addStreamMuxer(multiplex) - switchC.connection.addStreamMuxer(multiplex) - switchD.connection.addStreamMuxer(multiplex) - - parallel([ - (cb) => switchA.transport.listen('tcp', {}, null, cb), - (cb) => switchB.transport.listen('tcp', {}, null, cb), - (cb) => switchC.transport.listen('tcp', {}, null, cb), - (cb) => switchD.transport.listen('tcp', {}, null, cb) - ], done) - })) - - after(function (done) { - parallel([ - (cb) => switchA.stop(cb), - (cb) => switchB.stop(cb), - (cb) => switchC.stop(cb), - (cb) => switchD.stop(cb) - ], done) - }) - - it('should handle + dial on protocol', (done) => { - switchB.handle('/abacaxi/1.0.0', (protocol, conn) => pull(conn, conn)) - - switchA.dial(switchB._peerInfo, '/abacaxi/1.0.0', (err, conn) => { - expect(err).to.not.exist() - expect(switchA.connection.getAll()).to.have.length(1) - tryEcho(conn, done) - }) - }) - - it('should dial to warm conn', (done) => { - switchB.dial(switchA._peerInfo, (err) => { - expect(err).to.not.exist() - expect(Object.keys(switchB.conns).length).to.equal(0) - expect(switchB.connection.getAll()).to.have.length(1) - done() - }) - }) - - it('should dial on protocol, reuseing warmed conn', (done) => { - switchA.handle('/papaia/1.0.0', (protocol, conn) => pull(conn, conn)) - - switchB.dial(switchA._peerInfo, '/papaia/1.0.0', (err, conn) => { - expect(err).to.not.exist() - expect(Object.keys(switchB.conns).length).to.equal(0) - expect(switchB.connection.getAll()).to.have.length(1) - tryEcho(conn, done) - }) - }) - - it('should enable identify to reuse incomming muxed conn', (done) => { - switchA.connection.reuse() - switchC.connection.reuse() - - switchC.dial(switchA._peerInfo, (err) => { - expect(err).to.not.exist() - setTimeout(() => { - expect(switchC.connection.getAll()).to.have.length(1) - expect(switchA.connection.getAll()).to.have.length(2) - done() - }, 500) - }) - }) - - /** - * This test is being skipped until a related issue with pull-reader overreading can be resolved - * Currently this test will time out instead of returning an error properly. This is the same issue - * in ipfs/interop, https://github.com/ipfs/interop/pull/24/commits/179978996ecaef39e78384091aa9669dcdb94cc0 - */ - it('should fail to talk to a switch on a different private network', function (done) { - switchD.dial(switchA._peerInfo, (err) => { - expect(err).to.exist() - }) - - // A successful connection will return in well under 2 seconds - setTimeout(() => { - done() - }, 2000) - }) -}) diff --git a/test/switch/secio.node.js b/test/switch/secio.node.js deleted file mode 100644 index 3e9080d66e..0000000000 --- a/test/switch/secio.node.js +++ /dev/null @@ -1,116 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) -const parallel = require('async/parallel') -const TCP = require('libp2p-tcp') -const multiplex = require('pull-mplex') -const pull = require('pull-stream') -const secio = require('libp2p-secio') -const PeerBook = require('peer-book') - -const utils = require('./utils') -const createInfos = utils.createInfos -const tryEcho = utils.tryEcho -const Switch = require('../../src/switch') - -describe('SECIO', () => { - let switchA - let switchB - let switchC - - before((done) => createInfos(3, (err, infos) => { - expect(err).to.not.exist() - - const peerA = infos[0] - const peerB = infos[1] - const peerC = infos[2] - - peerA.multiaddrs.add('/ip4/127.0.0.1/tcp/9001') - peerB.multiaddrs.add('/ip4/127.0.0.1/tcp/9002') - peerC.multiaddrs.add('/ip4/127.0.0.1/tcp/9003') - - switchA = new Switch(peerA, new PeerBook()) - switchB = new Switch(peerB, new PeerBook()) - switchC = new Switch(peerC, new PeerBook()) - - switchA.transport.add('tcp', new TCP()) - switchB.transport.add('tcp', new TCP()) - switchC.transport.add('tcp', new TCP()) - - switchA.connection.crypto(secio.tag, secio.encrypt) - switchB.connection.crypto(secio.tag, secio.encrypt) - switchC.connection.crypto(secio.tag, secio.encrypt) - - switchA.connection.addStreamMuxer(multiplex) - switchB.connection.addStreamMuxer(multiplex) - switchC.connection.addStreamMuxer(multiplex) - - parallel([ - (cb) => switchA.transport.listen('tcp', {}, null, cb), - (cb) => switchB.transport.listen('tcp', {}, null, cb), - (cb) => switchC.transport.listen('tcp', {}, null, cb) - ], done) - })) - - after(function (done) { - this.timeout(3 * 1000) - parallel([ - (cb) => switchA.stop(cb), - (cb) => switchB.stop(cb), - (cb) => switchC.stop(cb) - ], done) - }) - - it('handle + dial on protocol', (done) => { - switchB.handle('/abacaxi/1.0.0', (protocol, conn) => pull(conn, conn)) - - switchA.dial(switchB._peerInfo, '/abacaxi/1.0.0', (err, conn) => { - expect(err).to.not.exist() - expect(switchA.connection.getAll()).to.have.length(1) - tryEcho(conn, done) - }) - }) - - it('dial to warm conn', (done) => { - switchB.dial(switchA._peerInfo, (err) => { - expect(err).to.not.exist() - expect(Object.keys(switchB.conns).length).to.equal(0) - expect(switchB.connection.getAll()).to.have.length(1) - done() - }) - }) - - it('dial on protocol, reuse warmed conn', (done) => { - switchA.handle('/papaia/1.0.0', (protocol, conn) => pull(conn, conn)) - - switchB.dial(switchA._peerInfo, '/papaia/1.0.0', (err, conn) => { - expect(err).to.not.exist() - expect(Object.keys(switchB.conns).length).to.equal(0) - expect(switchB.connection.getAll()).to.have.length(1) - tryEcho(conn, done) - }) - }) - - it('enable identify to reuse incomming muxed conn', (done) => { - switchA.connection.reuse() - switchC.connection.reuse() - - switchC.dial(switchA._peerInfo, (err) => { - expect(err).to.not.exist() - setTimeout(() => { - expect(switchC.connection.getAll()).to.have.length(1) - expect(switchA.connection.getAll()).to.have.length(2) - done() - }, 500) - }) - }) - - it('switch back to plaintext if no arguments passed in', () => { - switchA.connection.crypto() - expect(switchA.crypto.tag).to.eql('/plaintext/1.0.0') - }) -}) diff --git a/test/switch/stats.node.js b/test/switch/stats.node.js deleted file mode 100644 index e9852ebee1..0000000000 --- a/test/switch/stats.node.js +++ /dev/null @@ -1,280 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) -const parallel = require('async/parallel') -const each = require('async/each') -const map = require('async/map') -const series = require('async/series') -const TCP = require('libp2p-tcp') -const multiplex = require('libp2p-mplex') -const pull = require('pull-stream') -const secio = require('libp2p-secio') -const PeerBook = require('peer-book') - -const utils = require('./utils') -const createInfos = utils.createInfos -const tryEcho = utils.tryEcho -const Switch = require('../../src/switch') - -describe('Stats', () => { - const setup = (cb) => { - createInfos(2, (err, infos) => { - expect(err).to.not.exist() - - const options = { - stats: { - computeThrottleTimeout: 100 - } - } - - const peerA = infos[0] - const peerB = infos[1] - - peerA.multiaddrs.add('/ip4/127.0.0.1/tcp/0') - peerB.multiaddrs.add('/ip4/127.0.0.1/tcp/0') - - const switchA = new Switch(peerA, new PeerBook(), options) - const switchB = new Switch(peerB, new PeerBook(), options) - - switchA.transport.add('tcp', new TCP()) - switchB.transport.add('tcp', new TCP()) - - switchA.connection.crypto(secio.tag, secio.encrypt) - switchB.connection.crypto(secio.tag, secio.encrypt) - - switchA.connection.addStreamMuxer(multiplex) - switchB.connection.addStreamMuxer(multiplex) - - parallel([ - (cb) => switchA.start(cb), - (cb) => switchB.start(cb) - ], (err) => { - if (err) { - cb(err) - return - } - const echo = (protocol, conn) => pull(conn, conn) - switchB.handle('/echo/1.0.0', echo) - switchA.handle('/echo/1.0.0', echo) - - parallel([ - (cb) => { - switchA.dial(switchB._peerInfo, '/echo/1.0.0', (err, conn) => { - expect(err).to.not.exist() - tryEcho(conn, cb) - }) - }, - (cb) => { - switchB.dial(switchA._peerInfo, '/echo/1.0.0', (err, conn) => { - expect(err).to.not.exist() - tryEcho(conn, cb) - }) - } - ], (err) => { - if (err) { - cb(err) - return - } - - // wait until stats are processed - let pending = 12 - switchA.stats.on('update', waitForUpdate) - switchB.stats.on('update', waitForUpdate) - - function waitForUpdate () { - if (--pending === 0) { - switchA.stats.removeListener('update', waitForUpdate) - switchB.stats.removeListener('update', waitForUpdate) - cb(null, [switchA, switchB]) - } - } - }) - }) - }) - } - - const teardown = (switches, cb) => { - map(switches, (swtch, cb) => swtch.stop(cb), cb) - } - - it('both nodes have some global stats', (done) => { - setup((err, switches) => { - expect(err).to.not.exist() - - switches.forEach((swtch) => { - const snapshot = swtch.stats.global.snapshot - expect(snapshot.dataReceived.toFixed()).to.equal('2210') - expect(snapshot.dataSent.toFixed()).to.equal('2210') - }) - - teardown(switches, done) - }) - }) - - it('both nodes know the transports', (done) => { - setup((err, switches) => { - expect(err).to.not.exist() - const expectedTransports = [ - 'tcp' - ] - - switches.forEach( - (swtch) => expect(swtch.stats.transports().sort()).to.deep.equal(expectedTransports)) - teardown(switches, done) - }) - }) - - it('both nodes know the protocols', (done) => { - setup((err, switches) => { - expect(err).to.not.exist() - const expectedProtocols = [ - '/echo/1.0.0', - '/mplex/6.7.0', - '/secio/1.0.0' - ] - - switches.forEach((swtch) => { - expect(swtch.stats.protocols().sort()).to.deep.equal(expectedProtocols) - }) - - teardown(switches, done) - }) - }) - - it('both nodes know about each other', (done) => { - setup((err, switches) => { - expect(err).to.not.exist() - switches.forEach( - (swtch, index) => { - const otherSwitch = selectOther(switches, index) - expect(swtch.stats.peers().sort()).to.deep.equal([otherSwitch._peerInfo.id.toB58String()]) - }) - teardown(switches, done) - }) - }) - - it('both have transport-specific stats', (done) => { - setup((err, switches) => { - expect(err).to.not.exist() - switches.forEach((swtch) => { - const snapshot = swtch.stats.forTransport('tcp').snapshot - expect(snapshot.dataReceived.toFixed()).to.equal('2210') - expect(snapshot.dataSent.toFixed()).to.equal('2210') - }) - teardown(switches, done) - }) - }) - - it('both have protocol-specific stats', (done) => { - setup((err, switches) => { - expect(err).to.not.exist() - switches.forEach((swtch) => { - const snapshot = swtch.stats.forProtocol('/echo/1.0.0').snapshot - expect(snapshot.dataReceived.toFixed()).to.equal('8') - expect(snapshot.dataSent.toFixed()).to.equal('8') - }) - teardown(switches, done) - }) - }) - - it('both have peer-specific stats', (done) => { - setup((err, switches) => { - expect(err).to.not.exist() - switches.forEach((swtch, index) => { - const other = selectOther(switches, index) - const snapshot = swtch.stats.forPeer(other._peerInfo.id.toB58String()).snapshot - expect(snapshot.dataReceived.toFixed()).to.equal('2210') - expect(snapshot.dataSent.toFixed()).to.equal('2210') - }) - teardown(switches, done) - }) - }) - - it('both have moving average stats for peer', (done) => { - setup((err, switches) => { - expect(err).to.not.exist() - switches.forEach((swtch, index) => { - const other = selectOther(switches, index) - const ma = swtch.stats.forPeer(other._peerInfo.id.toB58String()).movingAverages - const intervals = [60000, 300000, 900000] - intervals.forEach((interval) => { - const average = ma.dataReceived[interval].movingAverage() - expect(average).to.be.above(0).below(100) - }) - }) - teardown(switches, done) - }) - }) - - it('retains peer after disconnect', (done) => { - setup((err, switches) => { - expect(err).to.not.exist() - let index = -1 - each(switches, (swtch, cb) => { - swtch.once('peer-mux-closed', () => cb()) - index++ - swtch.hangUp(selectOther(switches, index)._peerInfo, (err) => { - expect(err).to.not.exist() - }) - }, - (err) => { - expect(err).to.not.exist() - switches.forEach((swtch, index) => { - const other = selectOther(switches, index) - const snapshot = swtch.stats.forPeer(other._peerInfo.id.toB58String()).snapshot - expect(snapshot.dataReceived.toFixed()).to.equal('2210') - expect(snapshot.dataSent.toFixed()).to.equal('2210') - }) - teardown(switches, done) - }) - }) - }) - - it('retains peer after reconnect', (done) => { - setup((err, switches) => { - expect(err).to.not.exist() - series([ - (cb) => { - let index = -1 - each(switches, (swtch, cb) => { - swtch.once('peer-mux-closed', () => cb()) - index++ - swtch.hangUp(selectOther(switches, index)._peerInfo, (err) => { - expect(err).to.not.exist() - }) - }, cb) - }, - (cb) => { - let index = -1 - each(switches, (swtch, cb) => { - index++ - const other = selectOther(switches, index) - swtch.dial(other._peerInfo, '/echo/1.0.0', (err, conn) => { - expect(err).to.not.exist() - tryEcho(conn, cb) - }) - }, cb) - }, - (cb) => setTimeout(cb, 1000), - (cb) => { - switches.forEach((swtch, index) => { - const other = selectOther(switches, index) - const snapshot = swtch.stats.forPeer(other._peerInfo.id.toB58String()).snapshot - expect(snapshot.dataReceived.toFixed()).to.equal('4420') - expect(snapshot.dataSent.toFixed()).to.equal('4420') - }) - teardown(switches, cb) - } - ], done) - }) - }) -}) - -function selectOther (array, index) { - const useIndex = (index + 1) % array.length - return array[useIndex] -} diff --git a/test/switch/stream-muxers.node.js b/test/switch/stream-muxers.node.js deleted file mode 100644 index faf7409fc4..0000000000 --- a/test/switch/stream-muxers.node.js +++ /dev/null @@ -1,155 +0,0 @@ -/* eslint-env mocha */ -/* eslint max-nested-callbacks: ["error", 8] */ -'use strict' - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) - -const parallel = require('async/parallel') -const TCP = require('libp2p-tcp') -const multiplex = require('libp2p-mplex') -const pullMplex = require('pull-mplex') -const spdy = require('libp2p-spdy') -const pull = require('pull-stream') -const PeerBook = require('peer-book') -const utils = require('./utils') -const createInfos = utils.createInfos -const tryEcho = utils.tryEcho - -const Switch = require('../../src/switch') - -describe('Stream Multiplexing', () => { - [ - multiplex, - pullMplex, - spdy - ].forEach((sm) => describe(sm.multicodec, () => { - let switchA - let switchB - let switchC - - before((done) => createInfos(3, (err, peerInfos) => { - expect(err).to.not.exist() - function maGen (port) { return `/ip4/127.0.0.1/tcp/${port}` } - - const peerA = peerInfos[0] - const peerB = peerInfos[1] - const peerC = peerInfos[2] - - peerA.multiaddrs.add(maGen(9001)) - peerB.multiaddrs.add(maGen(9002)) - peerC.multiaddrs.add(maGen(9003)) - - switchA = new Switch(peerA, new PeerBook()) - switchB = new Switch(peerB, new PeerBook()) - switchC = new Switch(peerC, new PeerBook()) - - switchA.transport.add('tcp', new TCP()) - switchB.transport.add('tcp', new TCP()) - switchC.transport.add('tcp', new TCP()) - - parallel([ - (cb) => switchA.transport.listen('tcp', {}, null, cb), - (cb) => switchB.transport.listen('tcp', {}, null, cb), - (cb) => switchC.transport.listen('tcp', {}, null, cb) - ], done) - })) - - after((done) => parallel([ - (cb) => switchA.stop(cb), - (cb) => switchB.stop(cb) - ], done)) - - it('switch.connection.addStreamMuxer', (done) => { - switchA.connection.addStreamMuxer(sm) - switchB.connection.addStreamMuxer(sm) - switchC.connection.addStreamMuxer(sm) - done() - }) - - it('handle + dial on protocol', (done) => { - switchB.handle('/abacaxi/1.0.0', (protocol, conn) => pull(conn, conn)) - - switchA.dial(switchB._peerInfo, '/abacaxi/1.0.0', (err, conn) => { - expect(err).to.not.exist() - expect(switchA.connection.getAll()).to.have.length(1) - - tryEcho(conn, done) - }) - }) - - it('dial to warm conn', (done) => { - switchB.dial(switchA._peerInfo, (err) => { - expect(err).to.not.exist() - - expect(Object.keys(switchB.conns).length).to.equal(0) - expect(switchB.connection.getAll()).to.have.length(1) - done() - }) - }) - - it('dial on protocol, reuse warmed conn', (done) => { - switchA.handle('/papaia/1.0.0', (protocol, conn) => pull(conn, conn)) - - switchB.dial(switchA._peerInfo, '/papaia/1.0.0', (err, conn) => { - expect(err).to.not.exist() - expect(Object.keys(switchB.conns).length).to.equal(0) - expect(switchB.connection.getAll()).to.have.length(1) - - tryEcho(conn, done) - }) - }) - - it('enable identify to reuse incomming muxed conn', (done) => { - switchA.connection.reuse() - switchC.connection.reuse() - - switchC.dial(switchA._peerInfo, (err) => { - expect(err).to.not.exist() - setTimeout(() => { - expect(switchC.connection.getAll()).to.have.length(1) - expect(switchA.connection.getAll()).to.have.length(2) - done() - }, 500) - }) - }) - - it('with Identify enabled, do getPeerInfo', (done) => { - switchA.handle('/banana/1.0.0', (protocol, conn) => { - conn.getPeerInfo((err, pi) => { - expect(err).to.not.exist() - expect(switchC._peerInfo.id.toB58String()).to.equal(pi.id.toB58String()) - }) - - pull(conn, conn) - }) - - switchC.dial(switchA._peerInfo, '/banana/1.0.0', (err, conn) => { - expect(err).to.not.exist() - setTimeout(() => { - expect(switchC.connection.getAll()).to.have.length(1) - expect(switchA.connection.getAll()).to.have.length(2) - - conn.getPeerInfo((err, pi) => { - expect(err).to.not.exist() - expect(switchA._peerInfo.id.toB58String()).to.equal(pi.id.toB58String()) - tryEcho(conn, done) - }) - }, 500) - }) - }) - - it('closing one side cleans out in the other', (done) => { - switchC.stop((err) => { - expect(err).to.not.exist() - - setTimeout(() => { - expect(switchA.connection.getAll()).to.have.length(1) - done() - }, 500) - }) - }) - })) -}) diff --git a/test/switch/swarm-muxing+webrtc-star.browser.js b/test/switch/swarm-muxing+webrtc-star.browser.js deleted file mode 100644 index e5fe9e9390..0000000000 --- a/test/switch/swarm-muxing+webrtc-star.browser.js +++ /dev/null @@ -1,153 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) - -const peerId = require('peer-id') -const PeerInfo = require('peer-info') -const WebRTCStar = require('libp2p-webrtc-star') -const spdy = require('libp2p-spdy') -const parallel = require('async/parallel') -const series = require('async/series') -const pull = require('pull-stream') -const PeerBook = require('peer-book') -const tryEcho = require('./utils').tryEcho -const sinon = require('sinon') - -const Switch = require('../../src/switch') - -describe('Switch (webrtc-star)', () => { - let switch1 - let peer1 - let wstar1 - - let switch2 - let peer2 - let wstar2 - - before((done) => series([ - (cb) => peerId.create((err, id1) => { - expect(err).to.not.exist() - peer1 = new PeerInfo(id1) - const ma1 = '/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star/ipfs/' + - id1.toB58String() - peer1.multiaddrs.add(ma1) - cb() - }), - (cb) => peerId.create((err, id2) => { - expect(err).to.not.exist() - peer2 = new PeerInfo(id2) - const ma2 = '/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star/ipfs/' + - id2.toB58String() - peer2.multiaddrs.add(ma2) - cb() - }) - ], (err) => { - expect(err).to.not.exist() - - switch1 = new Switch(peer1, new PeerBook()) - switch2 = new Switch(peer2, new PeerBook()) - done() - })) - - afterEach(() => { - sinon.restore() - }) - - it('add WebRTCStar transport to switch 1', () => { - wstar1 = new WebRTCStar() - switch1.transport.add('wstar', wstar1) - expect(Object.keys(switch1.transports).length).to.equal(1) - }) - - it('add WebRTCStar transport to switch 2', () => { - wstar2 = new WebRTCStar() - switch2.transport.add('wstar', wstar2) - expect(Object.keys(switch2.transports).length).to.equal(1) - }) - - it('listen on switch 1', (done) => { - switch1.start(done) - }) - - it('listen on switch 2', (done) => { - switch2.start(done) - }) - - it('add spdy', () => { - switch1.connection.addStreamMuxer(spdy) - switch1.connection.reuse() - switch2.connection.addStreamMuxer(spdy) - switch2.connection.reuse() - }) - - it('handle proto', () => { - switch2.handle('/echo/1.0.0', (protocol, conn) => pull(conn, conn)) - }) - - it('dial on proto', (done) => { - switch1.dial(peer2, '/echo/1.0.0', (err, conn) => { - expect(err).to.not.exist() - expect(switch1.connection.getAll()).to.have.length(1) - - tryEcho(conn, () => { - expect(switch2.connection.getAll()).to.have.length(1) - done() - }) - }) - }) - - it('create a third node and check that discovery works', function (done) { - this.timeout(20 * 1000) - - let counter = 0 - - let switch3 - - function check () { - if (++counter === 4) { - const s1n = switch1.connection.getAll() - const s2n = switch2.connection.getAll() - const s3n = switch3.connection.getAll() - expect(s1n).to.have.length(2) - expect(s2n).to.have.length(2) - expect(s3n).to.have.length(2) - switch3.stop(done) - } - if (counter === 3) { - setTimeout(check, 2000) - } - } - - wstar1.discovery.on('peer', (peerInfo) => switch1.dial(peerInfo, check)) - wstar2.discovery.on('peer', (peerInfo) => switch2.dial(peerInfo, check)) - sinon.stub(wstar1.discovery, '_isStarted').value(true) - sinon.stub(wstar2.discovery, '_isStarted').value(true) - - peerId.create((err, id3) => { - expect(err).to.not.exist() - - const peer3 = new PeerInfo(id3) - const mh3 = '/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star/ipfs/' + id3.toB58String() - peer3.multiaddrs.add(mh3) - - switch3 = new Switch(peer3, new PeerBook()) - const wstar3 = new WebRTCStar() - sinon.stub(wstar3.discovery, '_isStarted').value(true) - switch3.transport.add('wstar', wstar3) - switch3.connection.addStreamMuxer(spdy) - switch3.connection.reuse() - switch3.start(check) - }) - }) - - it('stop', (done) => { - parallel([ - (cb) => switch1.stop(cb), - (cb) => switch2.stop(cb) - ], done) - }) -}) diff --git a/test/switch/swarm-muxing+websockets.browser.js b/test/switch/swarm-muxing+websockets.browser.js deleted file mode 100644 index fcf46f8705..0000000000 --- a/test/switch/swarm-muxing+websockets.browser.js +++ /dev/null @@ -1,74 +0,0 @@ -/* eslint-env mocha */ -/* eslint max-nested-callbacks: ["error", 5] */ -'use strict' - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) - -const PeerId = require('peer-id') -const PeerInfo = require('peer-info') -const WebSockets = require('libp2p-websockets') -const mplex = require('pull-mplex') -const spdy = require('libp2p-spdy') -const PeerBook = require('peer-book') -const tryEcho = require('./utils').tryEcho - -const Switch = require('../../src/switch') - -describe('Switch (WebSockets)', () => { - [ - mplex, - spdy - ].forEach((muxer) => { - describe(muxer.multicodec, () => { - let sw - let peerDst - - before((done) => { - PeerInfo.create((err, peerSrc) => { - expect(err).to.not.exist() - sw = new Switch(peerSrc, new PeerBook()) - done() - }) - }) - - after(done => { - sw.stop(done) - }) - - it(`add muxer (${muxer.multicodec})`, () => { - sw.connection.addStreamMuxer(muxer) - sw.connection.reuse() - }) - - it('add ws', () => { - sw.transport.add('ws', new WebSockets()) - expect(Object.keys(sw.transports).length).to.equal(1) - }) - - it('create Dst peer info', (done) => { - PeerId.createFromJSON(require('./test-data/id-2.json'), (err, id) => { - expect(err).to.not.exist() - - peerDst = new PeerInfo(id) - const ma = '/ip4/127.0.0.1/tcp/15347/ws' - peerDst.multiaddrs.add(ma) - done() - }) - }) - - it('dial to warm a conn', (done) => { - sw.dial(peerDst, done) - }) - - it('dial on protocol, use warmed conn', (done) => { - sw.dial(peerDst, '/echo/1.0.0', (err, conn) => { - expect(err).to.not.exist() - tryEcho(conn, done) - }) - }) - }) - }) -}) diff --git a/test/switch/swarm-muxing.node.js b/test/switch/swarm-muxing.node.js deleted file mode 100644 index 8ca774e5e8..0000000000 --- a/test/switch/swarm-muxing.node.js +++ /dev/null @@ -1,248 +0,0 @@ -/* eslint-env mocha */ -/* eslint max-nested-callbacks: ["error", 5] */ -'use strict' - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) - -const parallel = require('async/parallel') -const TCP = require('libp2p-tcp') -const WebSockets = require('libp2p-websockets') -const mplex = require('libp2p-mplex') -const pMplex = require('pull-mplex') -const spdy = require('libp2p-spdy') -const pull = require('pull-stream') -const PeerBook = require('peer-book') - -const utils = require('./utils') -const createInfos = utils.createInfos -const tryEcho = utils.tryEcho -const Switch = require('../../src/switch') - -describe('Switch (everything all together)', () => { - [pMplex, spdy, mplex].forEach(muxer => { - describe(muxer.multicodec, () => { - let switchA // tcp - let switchB // tcp+ws - let switchC // tcp+ws - let switchD // ws - let switchE // ws - - before((done) => createInfos(5, (err, infos) => { - expect(err).to.not.exist() - - const peerA = infos[0] - const peerB = infos[1] - const peerC = infos[2] - const peerD = infos[3] - const peerE = infos[4] - - switchA = new Switch(peerA, new PeerBook()) - switchB = new Switch(peerB, new PeerBook()) - switchC = new Switch(peerC, new PeerBook()) - switchD = new Switch(peerD, new PeerBook()) - switchE = new Switch(peerE, new PeerBook()) - - done() - })) - - after(function (done) { - parallel([ - (cb) => switchA.stop(cb), - (cb) => switchB.stop(cb), - (cb) => switchD.stop(cb), - (cb) => switchE.stop(cb) - ], done) - }) - - it('add tcp', (done) => { - switchA._peerInfo.multiaddrs.add('/ip4/127.0.0.1/tcp/10100') - switchB._peerInfo.multiaddrs.add('/ip4/127.0.0.1/tcp/10200') - switchC._peerInfo.multiaddrs.add('/ip4/127.0.0.1/tcp/10300') - - switchA.transport.add('tcp', new TCP()) - switchB.transport.add('tcp', new TCP()) - switchC.transport.add('tcp', new TCP()) - - parallel([ - (cb) => switchA.transport.listen('tcp', {}, null, cb), - (cb) => switchB.transport.listen('tcp', {}, null, cb) - ], done) - }) - - it('add websockets', (done) => { - switchB._peerInfo.multiaddrs.add('/ip4/127.0.0.1/tcp/9012/ws') - switchC._peerInfo.multiaddrs.add('/ip4/127.0.0.1/tcp/9022/ws') - switchD._peerInfo.multiaddrs.add('/ip4/127.0.0.1/tcp/9032/ws') - switchE._peerInfo.multiaddrs.add('/ip4/127.0.0.1/tcp/9042/ws') - - switchB.transport.add('ws', new WebSockets()) - switchC.transport.add('ws', new WebSockets()) - switchD.transport.add('ws', new WebSockets()) - switchE.transport.add('ws', new WebSockets()) - - parallel([ - (cb) => switchB.transport.listen('ws', {}, null, cb), - (cb) => switchD.transport.listen('ws', {}, null, cb), - (cb) => switchE.transport.listen('ws', {}, null, cb) - ], done) - }) - - it('listen automatically', (done) => { - switchC.start(done) - }) - - it('add spdy and enable identify', () => { - switchA.connection.addStreamMuxer(muxer) - switchB.connection.addStreamMuxer(muxer) - switchC.connection.addStreamMuxer(muxer) - switchD.connection.addStreamMuxer(muxer) - switchE.connection.addStreamMuxer(muxer) - - switchA.connection.reuse() - switchB.connection.reuse() - switchC.connection.reuse() - switchD.connection.reuse() - switchE.connection.reuse() - }) - - it('warm up from A to B on tcp to tcp+ws', function (done) { - this.timeout(10 * 1000) - parallel([ - (cb) => switchB.once('peer-mux-established', (pi) => { - expect(pi.id.toB58String()).to.equal(switchA._peerInfo.id.toB58String()) - cb() - }), - (cb) => switchA.once('peer-mux-established', (pi) => { - expect(pi.id.toB58String()).to.equal(switchB._peerInfo.id.toB58String()) - cb() - }), - (cb) => switchA.dial(switchB._peerInfo, (err) => { - expect(err).to.not.exist() - expect(switchA.connection.getAll()).to.have.length(1) - cb() - }) - ], done) - }) - - it('warm up a warmed up, from B to A', (done) => { - switchB.dial(switchA._peerInfo, (err) => { - expect(err).to.not.exist() - expect(switchA.connection.getAll()).to.have.length(1) - done() - }) - }) - - it('dial from tcp to tcp+ws, on protocol', (done) => { - switchB.handle('/anona/1.0.0', (protocol, conn) => pull(conn, conn)) - - switchA.dial(switchB._peerInfo, '/anona/1.0.0', (err, conn) => { - expect(err).to.not.exist() - expect(switchA.connection.getAll()).to.have.length(1) - tryEcho(conn, done) - }) - }) - - it('dial from ws to ws no proto', (done) => { - switchD.dial(switchE._peerInfo, (err) => { - expect(err).to.not.exist() - expect(switchD.connection.getAll()).to.have.length(1) - done() - }) - }) - - it('dial from ws to ws', (done) => { - switchE.handle('/abacaxi/1.0.0', (protocol, conn) => pull(conn, conn)) - - switchD.dial(switchE._peerInfo, '/abacaxi/1.0.0', (err, conn) => { - expect(err).to.not.exist() - expect(switchD.connection.getAll()).to.have.length(1) - - tryEcho(conn, () => setTimeout(() => { - expect(switchE.connection.getAll()).to.have.length(1) - done() - }, 1000)) - }) - }) - - it('dial from tcp to tcp+ws', (done) => { - switchB.handle('/grapes/1.0.0', (protocol, conn) => pull(conn, conn)) - - switchA.dial(switchB._peerInfo, '/grapes/1.0.0', (err, conn) => { - expect(err).to.not.exist() - expect(switchA.connection.getAll()).to.have.length(1) - - tryEcho(conn, done) - }) - }) - - it('dial from tcp+ws to tcp+ws', (done) => { - let i = 0 - - function check (err) { - expect(err).to.not.exist() - if (++i === 3) { done() } - } - - switchC.handle('/mamao/1.0.0', (protocol, conn) => { - conn.getPeerInfo((err, peerInfo) => { - expect(err).to.not.exist() - expect(peerInfo).to.exist() - check() - }) - - pull(conn, conn) - }) - - switchA.dial(switchC._peerInfo, '/mamao/1.0.0', (err, conn) => { - expect(err).to.not.exist() - - conn.getPeerInfo((err, peerInfo) => { - expect(err).to.not.exist() - expect(peerInfo).to.exist() - check() - }) - - expect(switchA.connection.getAll()).to.have.length(2) - expect(switchC._peerInfo.isConnected).to.exist() - expect(switchA._peerInfo.isConnected).to.exist() - - tryEcho(conn, check) - }) - }) - - it('hangUp', (done) => { - let count = 0 - const ready = () => ++count === 3 ? done() : null - - switchB.once('peer-mux-closed', (peerInfo) => { - expect(switchB.connection.getAll()).to.have.length(0) - expect(switchB._peerInfo.isConnected()).to.not.exist() - ready() - }) - - switchA.once('peer-mux-closed', (peerInfo) => { - expect(switchA.connection.getAll()).to.have.length(1) - expect(switchA._peerInfo.isConnected()).to.not.exist() - ready() - }) - - switchA.hangUp(switchB._peerInfo, (err) => { - expect(err).to.not.exist() - ready() - }) - }) - - it('close a muxer emits event', function (done) { - this.timeout(3 * 1000) - - parallel([ - (cb) => switchA.once('peer-mux-closed', (peerInfo) => cb()), - (cb) => switchC.stop(cb) - ], done) - }) - }) - }) -}) diff --git a/test/switch/swarm-no-muxing.node.js b/test/switch/swarm-no-muxing.node.js deleted file mode 100644 index ef9a2dc969..0000000000 --- a/test/switch/swarm-no-muxing.node.js +++ /dev/null @@ -1,90 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) - -const parallel = require('async/parallel') -const TCP = require('libp2p-tcp') -const pull = require('pull-stream') -const PeerBook = require('peer-book') - -const utils = require('./utils') -const createInfos = utils.createInfos -const tryEcho = utils.tryEcho -const Switch = require('../../src/switch') - -describe('Switch (no Stream Multiplexing)', () => { - let switchA - let switchB - - before((done) => createInfos(2, (err, infos) => { - expect(err).to.not.exist() - - const peerA = infos[0] - const peerB = infos[1] - - peerA.multiaddrs.add('/ip4/127.0.0.1/tcp/9001') - peerB.multiaddrs.add('/ip4/127.0.0.1/tcp/9002/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC') - - switchA = new Switch(peerA, new PeerBook()) - switchB = new Switch(peerB, new PeerBook()) - - switchA.transport.add('tcp', new TCP()) - switchB.transport.add('tcp', new TCP()) - - parallel([ - (cb) => switchA.transport.listen('tcp', {}, null, cb), - (cb) => switchB.transport.listen('tcp', {}, null, cb) - ], done) - })) - - after((done) => parallel([ - (cb) => switchA.stop(cb), - (cb) => switchB.stop(cb) - ], done)) - - it('handle a protocol', (done) => { - switchB.handle('/bananas/1.0.0', (protocol, conn) => pull(conn, conn)) - expect(switchB.protocols).to.have.all.keys('/bananas/1.0.0') - done() - }) - - it('dial on protocol', (done) => { - switchB.handle('/pineapple/1.0.0', (protocol, conn) => pull(conn, conn)) - - switchA.dial(switchB._peerInfo, '/pineapple/1.0.0', (err, conn) => { - expect(err).to.not.exist() - tryEcho(conn, done) - }) - }) - - it('dial on protocol (returned conn)', (done) => { - switchB.handle('/apples/1.0.0', (protocol, conn) => pull(conn, conn)) - - const conn = switchA.dial(switchB._peerInfo, '/apples/1.0.0', (err) => { - expect(err).to.not.exist() - }) - - tryEcho(conn, done) - }) - - it('dial to warm a conn', (done) => { - switchA.dial(switchB._peerInfo, done) - }) - - it('dial on protocol, reuse warmed conn', (done) => { - switchA.dial(switchB._peerInfo, '/bananas/1.0.0', (err, conn) => { - expect(err).to.not.exist() - tryEcho(conn, done) - }) - }) - - it('unhandle', () => { - const proto = '/bananas/1.0.0' - switchA.unhandle(proto) - expect(switchA.protocols[proto]).to.not.exist() - }) -}) diff --git a/test/switch/switch.spec.js b/test/switch/switch.spec.js deleted file mode 100644 index 142d40fca9..0000000000 --- a/test/switch/switch.spec.js +++ /dev/null @@ -1,37 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) - -const Switch = require('../../src/switch') - -describe('Switch', () => { - describe('.availableTransports', () => { - it('should always sort circuit last', () => { - const switchA = new Switch({}, {}) - const transport = { - filter: (addrs) => addrs - } - const mockPeerInfo = { - multiaddrs: { - toArray: () => ['a', 'b', 'c'] - } - } - - switchA.transports = { - Circuit: transport, - TCP: transport, - WebSocketStar: transport - } - - expect(switchA.availableTransports(mockPeerInfo)).to.eql([ - 'TCP', - 'WebSocketStar', - 'Circuit' - ]) - }) - }) -}) diff --git a/test/switch/t-webrtc-star.browser.js b/test/switch/t-webrtc-star.browser.js deleted file mode 100644 index 0bfa843127..0000000000 --- a/test/switch/t-webrtc-star.browser.js +++ /dev/null @@ -1,83 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) -const PeerId = require('peer-id') -const PeerInfo = require('peer-info') -const WebRTCStar = require('libp2p-webrtc-star') -const parallel = require('async/parallel') -const pull = require('pull-stream') -const PeerBook = require('peer-book') -const tryEcho = require('./utils').tryEcho - -const Switch = require('../../src/switch') - -describe('transport - webrtc-star', () => { - let switch1 - let switch2 - - before(() => { - const id1 = PeerId - .createFromB58String('QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSooooA') - const peer1 = new PeerInfo(id1) - - const ma1 = '/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSooooA' - peer1.multiaddrs.add(ma1) - - const id2 = PeerId - .createFromB58String('QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSooooB') - const peer2 = new PeerInfo(id2) - const ma2 = '/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSooooB' - peer2.multiaddrs.add(ma2) - - switch1 = new Switch(peer1, new PeerBook()) - switch2 = new Switch(peer2, new PeerBook()) - }) - - it('add WebRTCStar transport to switch 1', () => { - switch1.transport.add('wstar', new WebRTCStar()) - expect(Object.keys(switch1.transports).length).to.equal(1) - }) - - it('add WebRTCStar transport to switch 2', () => { - switch2.transport.add('wstar', new WebRTCStar()) - expect(Object.keys(switch2.transports).length).to.equal(1) - }) - - it('listen on switch 1', (done) => { - switch1.transport.listen('wstar', {}, (conn) => pull(conn, conn), done) - }) - - it('listen on switch 2', (done) => { - switch2.transport.listen('wstar', {}, (conn) => pull(conn, conn), done) - }) - - it('dial', (done) => { - switch1.transport.dial('wstar', switch2._peerInfo, (err, conn) => { - expect(err).to.not.exist() - - tryEcho(conn, done) - }) - }) - it('dial offline / non-existent node', (done) => { - const peer2 = switch2._peerInfo - peer2.multiaddrs.clear() - peer2.multiaddrs.add('/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star/ipfs/ABCD') - - switch1.transport.dial('wstar', peer2, (err, conn) => { - expect(err).to.exist() - expect(conn).to.not.exist() - done() - }) - }) - - it('close', (done) => { - parallel([ - (cb) => switch1.transport.close('wstar', cb), - (cb) => switch2.transport.close('wstar', cb) - ], done) - }) -}) diff --git a/test/switch/test-data/id-1.json b/test/switch/test-data/id-1.json deleted file mode 100644 index 504e71ac92..0000000000 --- a/test/switch/test-data/id-1.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "QmYmfUS4A3E64BzU8DsCmCWpPhcXWU2KTKNRGtdtN4oCgU", - "privKey": "CAASqAkwggSkAgEAAoIBAQCYtGLh+ow9WEJMn50voPGa6MsqSgJx8pNXGtk5kMSktWxfYHrejLZJjN0+br2CwpFMtf9JW6dAIpxb3qViBCFXjzEK8JuYaXM2sHC6sapyCxeZUbZJtGAXNWQW3qV7m8s8cJTOu2s1euT/G6uf/mIVFIzCkQDx+Ejh5Aie+BTAEf1WbLmcoDDxVESe22gpTxtMG8WTocMV34BxKn8d8vhcZZsi8LLkjg172QwQr3Q68jKgdja3K1YYm6fnso6H3+H06IHgPFAvVhycBbmlyR3bL/hFBl6+ElwBxeIrlM/oAY93KCs622SLYWFHb+J2q7WofSbUSscp3gWj7c8KJqHvAgMBAAECggEBAJZi4BcpBj/L0c9gSg8D86zZomvNY0cQ3GYmPNPibKbBPS9Y9uiBr2wT3DeGHADQ2QOxIO7/4mDZNR+Mz1cONj/i9yuM9c9N2nd7oClcmz2hCualgF5p01BH9oBHWLW5IpgtT3+hN939X9SVTZpNjg6wpEdhQosKN8yvJIZaTyUvh/ZMRIJvbnbLg13gIF7Lpyn1rtFovQg0dET0C8zhTCDPacJIOLp8BIBMknPfOl0SrvOMZjufzVZLvbt0YraXhLK8EWe87ffTMoBlIktWpEKdPBOCuFf4E4WRXJ78tcbvNtx3f5zGi+ZVbKcLA1axu+OqbjHCG6yrlywcVBoTuxECgYEA56yDBaM0VFD1CqsqwYIWmAyYBjV7dkM+ogMb+mfQn+ja6QSt+U/APXB3dP+EDvysh5AZR0wpUrmz14xC1yB1/XAKIfMLQZB8DdUkuj5UcsKjkzLJkIFYGOXIutU7IHTma7s/0fLxwp8SvkEL+6nHuZskf77yjDAvWLZeSD/CYWsCgYEAqL0mKeyyhBBFvNJyE3CyyhDfzgf+NrvrNJcx73nAzLDE44BPc/3lHYn2AJJhasNnjJfRiFzW90PNgCjZLLXqeHkX4xixoibvRtb31WHR2UyxXe/KQZwBy11mPzStnI4Y83C2A8OXsx4xAPq69nX9foSFD6cuLkWUGeb8f7Jxbo0CgYB25mfcJdW+jEom7pAj/kLgSF5hmWNC3+IuPhBG5K8C0vw+6ULsmEyee7EjX9wD4RQfAwqmN+VhaqNtNbQ8OpGzv6PDprwZKzEv3DtcRo8K0vAmpMMkIe334T6y/Kq6zqRPmCt58gi4DPIOqM2gnJM/o+sIkRRkdHpoOjiLNgXp/wKBgQCNrGpLjwl/am4zEHppKhljIPHX+cwORo8/06ZAi/g9pDlbThLnr4fb2kaqyjxyuGfLmnh5xoFSkCINdb6KFJ8t0XYl3UjffVMvJjRle0EG8qaE2Vz24zZ6egvsC52ssX3vf3XDCUjoQfQg/2NUpVJWFIvnzZUvkom7ib38tWUZzQKBgDe0+OqdJEIdajkwCMEYbmZDYqkbw4pgmwSqCwK7HeCi8dvACW5OCCutnN0L57eEltyWy0XP2XmRlfsD0atkKBq3KgNfSawx6/t/K3OtZa8VAtg2M0PbCZljW/8Bz6xlxiyPXFTRgr9zr4yM1homMmPA39hURmXNNedXUh3IMkH7", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYtGLh+ow9WEJMn50voPGa6MsqSgJx8pNXGtk5kMSktWxfYHrejLZJjN0+br2CwpFMtf9JW6dAIpxb3qViBCFXjzEK8JuYaXM2sHC6sapyCxeZUbZJtGAXNWQW3qV7m8s8cJTOu2s1euT/G6uf/mIVFIzCkQDx+Ejh5Aie+BTAEf1WbLmcoDDxVESe22gpTxtMG8WTocMV34BxKn8d8vhcZZsi8LLkjg172QwQr3Q68jKgdja3K1YYm6fnso6H3+H06IHgPFAvVhycBbmlyR3bL/hFBl6+ElwBxeIrlM/oAY93KCs622SLYWFHb+J2q7WofSbUSscp3gWj7c8KJqHvAgMBAAE=" -} diff --git a/test/switch/test-data/id-2.json b/test/switch/test-data/id-2.json deleted file mode 100644 index 167daf01a5..0000000000 --- a/test/switch/test-data/id-2.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "QmQAbW9j3wQ8JDFmg8JRid82EpZabuCngVDmhqzCmJwqt6", - "privKey": "CAASpgkwggSiAgEAAoIBAQCAQjiCzMF+PQaDUuNa7avUsj2xnNTQcUrs4yHz/L+JI/AY2ij0iXsBSE0chK1KtBu24gZzWs3/BDyNl28E0Sd41QpK6oTVMHjUfLovO+h7G78bqpI83vk5CEOKt29VihQs282fivbQb5ALYwzBIW2lsIoWwrQq1btsNA5NXJ43OAcPZ9SybBUg49f5gWf/kmh/J6e1rvwyVjQc7cmmpzcQUc+XNL7db6T3ArokXZMyBK6oQCOaJc1bqwgHwYSI3parjds9k8Z6fXA2ub3Va//1EgjQ50lRZH03PGYS42HR1QSSz1eLjMmdrbJrZZj7IbXgqAO6gT6wlGLr5xMQudabAgMBAAECggEAQ9NBESJ4fGqHJDFUG8St5pevelqGTAhtZ+IhFWamXz6K/Il5uP9u9dmnNZqQDX47XbYfVSdC4kX6Q6I+SlzUs9htTfrA7gBpFW00BEB5C4k7wcSs+tWrE9bj6NpiXOjdDG/cSC9zn/wvP2ZM22DzG/jEvY6POku2hlzs50pAPNB7bBaKysA/e52J0Tu/Wf/+sZyp2MiYQJmIkfbYeDF2rqm5y04S6Z31O3SMQIETNcBK8T+L2jwx+Q0msB8toam7hRf1KjxD0yZe+Vff9tPfwjgEoWF+O27g3+rjDq/QqUfzOPMgvAFgELBMpv6CCM8/3l9gUu+7itBxDq65sDCoCQKBgQC6FTLTQA3ux3WV0/7MKXJIHgYZ4b8lIbiiWuO/6t2ZnwvLfTbiU5br/8bcRPL5ygFuIdzkx8VHcbkOmld/VE7qaRZoJb94JVvC6N+5MQxr+pzbWQSNcE+cKJgy1RADea8nad698ifls/39kZGCc6Srt2TqxTBuoZ3c9jEMs3N2pwKBgQCwcxNSw7Wkq302lKc/7QdtfegrwlLjRClLYaW9ESQeErayRY8pxLgl/XKap1HPyc0aQ+78W6w+DAxvcToGBsLak0ujJjzP7b8G6fo+cexuIr8NiGL4LVzpZfQjkfQU4DDwsOdedeKzGelIdstMMtAZDFG9eNPe99XeJBnYfIDS7QKBgH8xFjiHQ/6+n4T2DueGPPNGcm0mfPzoe8ed0KbR5v6mU+2XfPheon5VqpvNFTff9/JLey11z0byWMe+f6gs/HQFuKcfhiydfIdRnfp7qD32Y1kbE52J8yCOLtowAG4fsrWCDBpRdyvvR+EWqxs76IbnKDfA6UX1em4aaZSA5J9pAoGAE8aB5ue6Rt9VZDWa3QZCq9nNmIHp6kCsZB9ohN0T8C7mvOog1myOuutB2eVgvOoAC66LbUsU7ctJ5X+KIjzFv9t8Qae6bw9VNoAopLD974YDZY/gj7H91Maxav8jnOdXdNJOy/5oTuxbgdyWgk67leMUkiiljjq2hHQFVYb2pS0CgYBam0ZJ5Trds1LijE2eoYPyiJdhWEsHYFDzoV17cyjhbSrmlWJBNKQfw6q6UtnxSNFMvsPOZv53d3B8iIDnZ/UHFvw1et+yQk/QrxTfXurqn8lJcMCfKzm3ORKibgJPMmtcPbLoxuEKXMXx18iwoCsMnapijJ0Qj5HofluiupSfxg==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCAQjiCzMF+PQaDUuNa7avUsj2xnNTQcUrs4yHz/L+JI/AY2ij0iXsBSE0chK1KtBu24gZzWs3/BDyNl28E0Sd41QpK6oTVMHjUfLovO+h7G78bqpI83vk5CEOKt29VihQs282fivbQb5ALYwzBIW2lsIoWwrQq1btsNA5NXJ43OAcPZ9SybBUg49f5gWf/kmh/J6e1rvwyVjQc7cmmpzcQUc+XNL7db6T3ArokXZMyBK6oQCOaJc1bqwgHwYSI3parjds9k8Z6fXA2ub3Va//1EgjQ50lRZH03PGYS42HR1QSSz1eLjMmdrbJrZZj7IbXgqAO6gT6wlGLr5xMQudabAgMBAAE=" -} diff --git a/test/switch/test-data/ids.json b/test/switch/test-data/ids.json deleted file mode 100644 index db62a09658..0000000000 --- a/test/switch/test-data/ids.json +++ /dev/null @@ -1,904 +0,0 @@ -{ - "infos": [ - { - "id": { - "id": "QmdWYwTywvXBeLKWthrVNjkq9SafEDn1PbAZdz4xZW7Jd9", - "privKey": "CAASpgkwggSiAgEAAoIBAQDiRcrWi2mFd9G3bRd3YvmL+NfuZ4/62WKbDkXndWfrUXGxLkQOAwCp4zJklWWXwGeqEx0GJbvhFK6BUgotcSMDjuWL45fCnNL+kttJI1oM5SP5nhub6SdJKsvYa9GpTjfVu4iI3JkCVxlG8QoPhC0d0k72HsCVf6goTgRuI0ZHTv20wOjbYevRIGgGr4MZEXLiasGl4yD8it7X8MXcvVWQBYkdJOyB4z4cRGxRM0TtjPdxBmQg2/tFc8veIBuoTQhLyu6ClNm72gDQTNRrGtZUCx3pKmfaJPvf5A36Wv/JZyO/KkexxRMlKOYpHUNI5yJWdXcSLjtuNpx8m0vpfu7XAgMBAAECggEALU0t1BBrWvZnPWMQ/K0LKzPx/2Aqml1leYe9BR8jZCCVM5UAuRFu05SSJUMn6N7zokBbYjyxxdl/KpMDSJ/LE85LNNunKaZ+M8uxLY5vW/+QWUyHWIqwe9yenUDQ5CWt1hPKvSP1WluXyvU9P2gGJF9TwcDca9H4F8Gu72IOkv3kE8yoA2z33hBeRlzUVIhlcnrQ0pLASoWTDG/XeZWIWR1zbJXKliPzn5p19MdfJLCjEQR24f/X+vxOFlrLK0l74j5/rhOZWTR8wqE0R04MCjC8BYqTHnfyEaqP8ZL67GFOjIfGdPOlfI0hrI5t47j6FKEmoCoJjTdVQqyXUuXjUQKBgQD+2SS9XtL+IBcRA+6rgQYX3Ly0kz+rncM6lx/k1zD4ciRXQbvf4MhX3ab1IZMmujkCvaNPMBKLxZ8E9N7MY4dNdNVPy5fjvQY/2Qt5dB+o7dbzRIlBRj9Y4oPtswfKWiuyOVFa5tIf0TOpApcuIyyD+O9zbNyqIOZUfKQniUzr7wKBgQDjS5ZR/8Snvrp4kw1tfNUOwdsEcQUmt51ey3sNmGe+U3DDdvwKJx0W1b5PiR1b1Wheap8KR1d5UQn2NjUp7G9GSbvFr5uOH82vxk26Fsv5ZghXZTBEs4/HcX2sQ+mCb60sOVbs5UlV2mnok+EXY12sZrVhmmsw0Q1ZqCuMQr/jmQKBgFdWm5y6rpyg6sbODjGAmlH7OEC6ZguumYWu3SNUDFhY5dNxl612H7LdJ6bCxudy0q75xsoQs4prQ8AzG1f4lBobfC9ImtlVopqnC6OoBGGkgRIF3vQb2wHfP09rF7RliqwdsJ/ykviMfaPiW2VYcJ0Z5xYrrMQxWj6CKM/T4iTJAoGAdvA4ytPiHj0p6qpYnnByNPSwHRTfMzFmAhLMY4La1rdnDIGYxd9N04MpwQjo+gMkSDPW4VQPrAYCBnq7OyLj34352ipYZfiyc0Z7qeL//ZOszb6/kVO86wqyTpCDAqRZpAilOfWJeImAXhnz8X8np21fgKGDcdoS+FWN5CmRrBECgYAwLH/G3e+QqZ9d8ihdKiPVBhrTVcYD3ElL2tI/S6ELTP7GBMz7HH64gLoZNqT7drcNXgrbPOjSC6b7fVyyIjEmT5vXUNNtZtlkSb9mNKwP4qFUSkS33liHn+dGa3gERX84AvL8lKTG8P+VXbB1XJHnPW6LPyI/eGelW6lEVwFkAQ==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDiRcrWi2mFd9G3bRd3YvmL+NfuZ4/62WKbDkXndWfrUXGxLkQOAwCp4zJklWWXwGeqEx0GJbvhFK6BUgotcSMDjuWL45fCnNL+kttJI1oM5SP5nhub6SdJKsvYa9GpTjfVu4iI3JkCVxlG8QoPhC0d0k72HsCVf6goTgRuI0ZHTv20wOjbYevRIGgGr4MZEXLiasGl4yD8it7X8MXcvVWQBYkdJOyB4z4cRGxRM0TtjPdxBmQg2/tFc8veIBuoTQhLyu6ClNm72gDQTNRrGtZUCx3pKmfaJPvf5A36Wv/JZyO/KkexxRMlKOYpHUNI5yJWdXcSLjtuNpx8m0vpfu7XAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "Qmf6uXDHoY6b2Ahbiw5yf18XvRP7WDBS867v2F2V4K4QES", - "privKey": "CAASpgkwggSiAgEAAoIBAQC89Fbt4LtEk6LkBedZCoa+nIIAiSQpFW1XnMU9xTLviUtgBLCaYNgCuwpO4kVsZJvQMjINQqJEI1BjQvgpEkkduUVDq04NFdtcUmM1XVu5E8sm6nIWa+0skEkKEmm5vtnmB5qt/zN+pmwJzxOQXglRQ9f1WsiFSvKSMxNeXIh2Ht0QMqSNRX03uN8xBbrW+ZtoyUGbaPFQ4ZBABZcZdjHZmhkWBqWIRpn4JaUIdAt6nU+4LKISLV7aM+sBDiZMZ/CH3yNNwH2xDzvuAO1K3rygvMICNOIMNg23aieQ/C9MCgJTspGiMHzn0TkOesv/Ay45ONcPk1Dgy3KQg1/KXPB9AgMBAAECggEAPxS3XKzU9/ztuYA7Dt/TwhjP0cv29XxAx6n/szJ9YbiNIF4Qc0l3c9nrhBBIKvqfhe7sBL9FGshLUwgNfvCq1jB+7itnYDj2xah/lFY5g90WykQkmFWplWIJ8EHbZ/ZOGlxZiFMVZue6U7/9AQpTw/yJQVDwdodh2esRQURVDlGGQa8uqH9DImVOtnifClJcrftnF4BO73Wp7tK7rjNTt+UN6P234cZ/S2YXZRP/fGw3ObSopEYOSxMn212u7n+ieI0Cl02A/lUAcLQfIAN+XoH8cSQowc9xUOmBomplucOXCV4Mu3karZjlS5GeL17XAXXHg+J1kAbkWmXNBcDBYQKBgQDq9wx9P0ThBsiqOfPVI9Mwn/0rcaX6j29xK+kDMUVuxotaQjQB+gdO/o26DV1SOj6T9EmaAS/vCfQXMvWqgmubH2lCgC079RjgpQliJmo7FDufO0HoFMgwZzf8oX5Xld9fi/5QusWoGpyk8txvl8vvDlcg/8iC+Q1Lx1ZnPVq2SQKBgQDN3tDZ7n2IAu0I5B9e2YYSCsbPBh3TmQNGoguVGeseyzTGaLIEzmXYjgwwuhAzIgWZ/HMQMvvutMtP+opT+yU9XGQyWnZjPDzDMNtexY4XTmqQ1SWST2qNPbj9Gs6oZ1jJK6+MdgoNh7hFeT+uftzXtMzxVGrOcBMAK4qzT6oYlQKBgBeLT9YRC+7chikAi51U7KmXrn+28KHN06Xsd3nZaxKxlG8j6SA1lJvmx/7Xrf06VuDufp2O9uWmAq58bb97OBsgJ6UBQQccBTUldG5AWS64VU0cW/tMcc7f2O1YpVdTbkGdvosKXBn/KKkiqNIJzOaUckidONNe72UjgVXxAPD5AoGAAQYot8zN5w1MrIyl80zVs+VF0+XN5C2QrJtFv3ofh0mve4UtzYRRUWBzgxKJ3hc/O+Lbl6sJQci4ci9m3MAVEVcSUIXOrPOxwa7OiIwnBsqnEQ1eYHnwp7802l11xbSt5mJHP0WfCy4vpnjR7kZHRvNpSZIH7fr0vT16NSYiTHkCgYBEhZ0CHQcekk1/cWjIfNNjbA7as+doecQ+hj4nwlZG9Ng6CreCS/pMN0oupYO2pw8fm0xPZF2BABIQ0sI5igppyvD+sYF/SkKNUDCBq3KSD/5cuOdv2bEefnIhkhLTR75f8gJ4MJeCl4lkP695lMgYkalYHrCkJLuKaGcGmZg4FQ==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC89Fbt4LtEk6LkBedZCoa+nIIAiSQpFW1XnMU9xTLviUtgBLCaYNgCuwpO4kVsZJvQMjINQqJEI1BjQvgpEkkduUVDq04NFdtcUmM1XVu5E8sm6nIWa+0skEkKEmm5vtnmB5qt/zN+pmwJzxOQXglRQ9f1WsiFSvKSMxNeXIh2Ht0QMqSNRX03uN8xBbrW+ZtoyUGbaPFQ4ZBABZcZdjHZmhkWBqWIRpn4JaUIdAt6nU+4LKISLV7aM+sBDiZMZ/CH3yNNwH2xDzvuAO1K3rygvMICNOIMNg23aieQ/C9MCgJTspGiMHzn0TkOesv/Ay45ONcPk1Dgy3KQg1/KXPB9AgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmdoLademTcdA2VM29NLb99GpqajKZWnVeQrKp99XpJn61", - "privKey": "CAASqQkwggSlAgEAAoIBAQDEzlaMbNq9kIAET1QHVsAXFENOtU9iWTiv+VyEoEl2P6ABkf1q6c3WjsP2CxgUDdHvstb5kyt38RhCIxa6CfQ0pg1c5/lJlDLmqx4eG1z6KTgYh6YePHjTvSFbKjrhCzFnXjuwitzkzAyYcaZMNeN0jkBU8uQ0FmgIzDZ31YgymkXqj8aFdxNx1Ry290oYJbTH+wsSEeIjSCGwvyZSF+IxKnwACfYG4rnZchEvd0mBnfdVIJW1xKfeVrihWnwbqn7Abq52gvFvojbCQlDzPvwku+BoH8yBEBuShXPHfHA6pFyULPa4f5TwI5/dyGm4ptLCCTCEYCuagrVqaCH6BwpdAgMBAAECggEACccWlbNyyqg7M/uc+SBeOsdO8MIhR4mXP2bsKcqs26sdj/Zo2L708wv0wGycraJiI76G369oIXVg9yg3INcNwu/dChicUgOC4+LshCJn5CXYG5/hqO7oMdzbo2PduQCNW81audKsVtGsboZ29KJYwpmuqInIvK3ATW+X5Sw+sATTxMG85Csr6s+kO3h6JVkbe+xyaUvyTNu/9ajkdpAIGlFqPtuhD3yxKaVxuEP1BIqr4RHzUA8IV1qB2M7PLstIFqAsUUOYymleeHZb6xDSjZxg9bO9nfSFTRCu/d6mSFj8ISE0YaE7w6nHf+P25VPFhIGzsQ1lc9Jo2n/DlxzZAQKBgQDy+7EXHewzOuDh+Fpsx5oBJVOixUnOryFgpFXe+o96x4Fjy/rHUUwSt01ikOOdmXqbtNpyJjWARPZWud1C5euBcePrzaVyL/fwUjCd0DFfka7ljB1v+mcb5dKkeVp0KFheQlqA7iOyPy4HC6xB3ZGY4uXjnEkB+nHLiQ0jxg0oBQKBgQDPWV2cswRDwQ124hhVn4sB3SfaUzNZ1m1zklHWy4sGXPYaEaG+gO4IIP6tiqG5E5Aj+wUCEPKBCwANW80bNOxdZIlFYImt82MhLuc2e/9U/hA8vXKwrX1Xm9/HPfbi/ZkB7WSVBGntoMx2uU6SDdJURHeR4RYFrUknw76GJ1ygeQKBgQDTQAPdB0Td3Wi6zYNAY+D+8gbe0wuySAyKyxVlQQ4RPva9XxBuzb2H4BnFghaCZHd2fCwXZiTJmitZh0pY6TBxYCU6U5ZtykqTg8GE0wa6Ahy+say+OEQAuzUBjggYSSNa//FTerdKNye7NGjU8t+svkgENVI8CBN7U3I7EetKSQKBgQCumNCjz3Yq21fMIGxfRR3XLvOM+vxFjLLTW4VAOlrRu9ubbfdlo8lL3QS2+wJdBuUb9xZre/vHv4yGsyON4k2aArs4SScF6+kwGv+kuFrzpY/kpZ36ucvOxrlzW3EWCHcb0Vsdw/6ykvE4k6degvb18EVC+GcD1rvAGSrIakKr+QKBgQDqTd13e9sjAtwabxykZif3j/eGSzHUrxrAAEqUJTA/yrEEHIRuFRG5q6Y3rqD+EZMoIHd9KeSKnv+WyeHLhT07r4V8RSdHKWJP5+avy25nJ8ykOUpawiwoKXeID1N/xLNMrF4suUgjjxEWjP6nW52eFzDhMqcOjgSWfb5zsLfBOQ==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEzlaMbNq9kIAET1QHVsAXFENOtU9iWTiv+VyEoEl2P6ABkf1q6c3WjsP2CxgUDdHvstb5kyt38RhCIxa6CfQ0pg1c5/lJlDLmqx4eG1z6KTgYh6YePHjTvSFbKjrhCzFnXjuwitzkzAyYcaZMNeN0jkBU8uQ0FmgIzDZ31YgymkXqj8aFdxNx1Ry290oYJbTH+wsSEeIjSCGwvyZSF+IxKnwACfYG4rnZchEvd0mBnfdVIJW1xKfeVrihWnwbqn7Abq52gvFvojbCQlDzPvwku+BoH8yBEBuShXPHfHA6pFyULPa4f5TwI5/dyGm4ptLCCTCEYCuagrVqaCH6BwpdAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmZR5a9AAXGqQF2ADqoDdGS8zvqv8n3Pag6TDDnTNMcFW6", - "privKey": "CAASpwkwggSjAgEAAoIBAQC+5OEQhdvRsGh2pBY/a6dGBccThaH0L8coYFYHPoQx9M7puC7smS95zIyvP92j2+RK9VIswgdAjuqWRyBpev+i2TfFE37kcuE67HMGVWdk+2CAF79e6Ndabcb2/TuAT/1S101mV60niVn7URYqfFStbfqaUcsBViBbEKCIQtpA4AfmcN4rI+C0q4BncRLQHgr0tqJ7sHPYWbhPHBP6zJfFi7Wl3o/6lp8Mn2TuMdGhFmYaGZjn97F7NEUct7VzXnRTxXdwtNHu2RyeluASctHDr6HwKUsilJiUiZcpOrBdV2zrLYqO1yV2uh6R6r+xfejcIbhmS9Izd5wvlRv/v0UbAgMBAAECggEADY/VLYdVBqCxyzv9GKRdTew7KHfl+aMrUwMFGZ6nZaUuzgv3yXdYmB6gIBM5e9qzbV/gZq2iNkPxBpwnAVdrsfYcsDOiYDiJJ9aElX6byeDSCkeloOiJ5DLIX+O9xm/oX2pMZWj1NEndyq0IFhyfJ3MYyr3k3kNwKQgVX5jgSJuCTCMB4PP8IJDo0dRJQucjyBCeE91S2FpF7+eyxPsfz7ssl4EZ1RzqSmoAB2p1B9e+ajzSkRl0JX7nFTL3P/lcwn9QyvoBtSmulfZDXvMm7g07wE5B7EqFTzI7nbU2ZjgD/1Nk8zCzUSfibdmI6EXBc3k77h/dKc1WE77nn99DgQKBgQD6fSIja07V1hpMczSrmLabuRQy8aBAiOUT6gNGqU07eLJYokpUlLEv4Gs9vy/tcfGbPrJ131v0bd9nh/QdktIyhVwA3g0CU5kBUNezYHWRRA3zXHbNYuiHmRgdaouzN+SmMMylOxL/2tOj8Bb5Nm9L8LuCp8y/E83dYRp3cCKtHQKBgQDDGBTG2QWWftcQnU1jTWAHeBf7K05KwDgJAPAwB7dmQpFtlNxLxWpOOEixOPzO8b2FP/QRjapnxsktaRrhnMZ/nVz3U5nq7HBJEairFsyLNYF5DIyKXValHxjol+4abaxGw6YotDmsGtg00Y3nz5WOViNiNJxdXm0NCuf7uJR9lwKBgQC4lZmgjCTuAvYiPAsmIEUAf+RYniG/LKHSiPGdEoltN8YE9qLbrS7c3v1n5QlGal7mTc9oeQ3kE0s7mb3URStMO2XO5dKkUkI/6/jnoD9Cqum02gBZ3XcI5VIV6zvC938w0GkdoWigzfqDphrnzqs5RM6Iu2pvrAJaDoJYXXPQKQKBgFfvrs3CXIZtPbs7a/pqkfJL62NHLc77vUYxqhG8KKprLunZw0JUBYqkS/+11B3jUK2TGgwfcsO8EknpqjgvVjmHULQadrIxSJtm3kPfzuqgf290fJSRZdCfp7aPZL981749ydNnCOfOYc3M9s2Z/6tcoC5P0Hs1aKoMVGxd0nCZAoGAVi+UF2A+mYuDcu1dU5vlBt6FQmcwvy8wodY/0RR4dpDwMsgdSwMzfVy/nR8U1HXOWnQP2rlej0PrOc3fWKC2vMpTYtQ2lUVGCZRj+DnjIsodF2dyQXD7zzpDzh9PLO0n9+ZWf8aZkmKhHQc7HxB4aJLSJ+r6ti9zfjHqzOnXC2Y=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC+5OEQhdvRsGh2pBY/a6dGBccThaH0L8coYFYHPoQx9M7puC7smS95zIyvP92j2+RK9VIswgdAjuqWRyBpev+i2TfFE37kcuE67HMGVWdk+2CAF79e6Ndabcb2/TuAT/1S101mV60niVn7URYqfFStbfqaUcsBViBbEKCIQtpA4AfmcN4rI+C0q4BncRLQHgr0tqJ7sHPYWbhPHBP6zJfFi7Wl3o/6lp8Mn2TuMdGhFmYaGZjn97F7NEUct7VzXnRTxXdwtNHu2RyeluASctHDr6HwKUsilJiUiZcpOrBdV2zrLYqO1yV2uh6R6r+xfejcIbhmS9Izd5wvlRv/v0UbAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "Qmeb34XKhbo7VtHMXmyep4MKdYWHKSVvnBVm1eijoJmy8U", - "privKey": "CAASpgkwggSiAgEAAoIBAQC/uQH9VPMIQ0Q5FIHRJj1rvYhXX1q/83Gv1EVBc4fOEPJMoRys47V78y9NXtPu1NivOi3qN+hFwXdNIX4azEQNiODHRJ4To8HC2uyoDtL0cjZQucEGDSJXXkMbtEwS1IFvKG+iKaaoFjw4LErYvaSYVSsV5xFpzi+ScL9u58bWEpwG5pAI5WJjoV4BX/mO5NFDfViTOsXAFkeiZQeru1GoJ+bWuulsLjnmggO++9yQG+PnXw7d+7S6XynCEnJv1F1KZrpCyfStpoayHHBg2zyDwE2EpwHvXQzTxXN414+8V0AZ7PPkQptFcxEKtbJWXXBnlIc3V70rtEKRAmlcKqZlAgMBAAECggEAPzcYUdh1ve64Cv4ZA8ZREDpRP0XgnVP+01PxdfBLAgYSbnPdCaCXUYRQv3kZ9jDWNYjAZO8ENiPhW1xEwT9C3ReZzfpxCNbA56fZylwA8LrL7/gfjgg8n4QkKnlbcAYDm4xAqr6DBf824eqwzyBQqi3C5BjpY/KpOubUKBRiOmkcUeqqVxXe0Dud8qz3rOaSOsQ7LR8VWliasCqp41uGbgBlSNnhFDFG+ONep71OqNMEV+/S341E6MbDj5243JVnLZGdyqUK/V46D9ALMdr3QKy+51fHXPz86GJvhzBq23xdmHARt1nmDbEjALw8lva1Yjss1vXPKJr167lkHYAWDQKBgQDejPUK/qqmYLFi3PhfF+Rw6YSSPPi2n+tWF5F7gm6xxzi8vLAHfr+U9vV5GtWXztDJ5DbdKF31+WlfBOOFWMYw0G2Z6ad2nxvzAMcuC2jP7cIf6x2sMq83AdCSdxr0eFr/eDA26CNPrIQyMkAnScSwsnJlce3j08dU8zOsvUdZRwKBgQDcieO6EdgxZr4adkSUaaKeS4NZSzOo0owZhDk0ipHHCc7j28LmyKrA6ouokLKB9GSYHIf3hlhmUORljMwR3wElG6aDd94awEcsuw7gYnoHUp6XIYr2H5Kr5r5rcUOmIV20SKD0k9OZvpeZDo+paRT+xPdN8lpdGZXrD60qH3vY8wKBgEL0e4CcT7EQpC2PN3Y8lPDXgJgSme0vvbjADHfxLOZ1fn9h8T/ABVmG1yFhTmOGyFAFRfBRhbtMF0SMDvt+Uto6ys6kekp44grA8CvNKPJtoJrDvMCi2w4ckKiQBt8IGrCDc1YBjyYYTAliDuUDD5btiPc2SJDjlTPcm25b38xfAoGAZat3/b7eQSARgdeGFDmCy6EaY58EqM6v4c+QI8XCINVHuMoGVyipd5hpXAOhF8IYYfu9PwKDXF/se1hmd9KsD3Ro1nD7Rq/f4CI4YH9lrFyNWjUPgBncHz2YCaZEvqDhNwzIjxhbU6SG9Pu+hSY5lJ4vOJMCz6rM73nhpeqvyLsCgYBk6nr0QbXLRDnYcK/91NN9mJQWoj2p3K4KLtkJh7zWHKDihf1a1cxRUj+kfNU875PH+TaZ0Md48VnnYlTwe07Hfc38rKZMX6vODBHbe654XGd5yhibhtTq6dhejLatGpwVj7cFM2TfA8lhn2yS2aSZLeOsUqEsRbpFc7c7dOcljg==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC/uQH9VPMIQ0Q5FIHRJj1rvYhXX1q/83Gv1EVBc4fOEPJMoRys47V78y9NXtPu1NivOi3qN+hFwXdNIX4azEQNiODHRJ4To8HC2uyoDtL0cjZQucEGDSJXXkMbtEwS1IFvKG+iKaaoFjw4LErYvaSYVSsV5xFpzi+ScL9u58bWEpwG5pAI5WJjoV4BX/mO5NFDfViTOsXAFkeiZQeru1GoJ+bWuulsLjnmggO++9yQG+PnXw7d+7S6XynCEnJv1F1KZrpCyfStpoayHHBg2zyDwE2EpwHvXQzTxXN414+8V0AZ7PPkQptFcxEKtbJWXXBnlIc3V70rtEKRAmlcKqZlAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmXcgkTakwu7rsVf8ZT7A3uD5XwNB6Pbz2AbbjaENrFe3N", - "privKey": "CAASqAkwggSkAgEAAoIBAQDW/ZsbRHAZf3Vh2PyUSlXbBRHl0K/PCX26vMe38j/eKSYn80Up3iRk1Fk4MUKgC0X3t1Ldf/Y6In5RhohbX7mpni4kHErjbhKtjLqQWCTiSNWMDVuFoZ7jiy//Qu5V+w4tdHogyNVgLhQhn1SouGJ5wgYP+XA67NQ3XoSzycAPU1N3LWuqcWwHWI/L8mY1qsmZ46ShporoSul+Pp4h0sDXqSPSJAZJsSV4/baywJMBXD91wRo1IhKvd+3OcSgVAnNnmBMvVodSzN0xNmmMGvHijslam+mYWW4B/603QAeFmYK3orEKcQ2lZVV/1lsvbg4GqrlMLkK4hM8NbTASa4LLAgMBAAECggEBAI0xl0lMJBcK12uAlzlIrKQf60Y0TRI62IDodH4BMiLUgYOhSB4cD2jM8R9vcqMrZDMxCdIAtRQvDSi7oxfngUa9ZO5ASoqdAtVJ5EjiKq8WSHEnYKEdqP0lr0sEiQSc0g3WPlMDsubsvDnsqyv3lG0EmPiqyCNa4HDQuXReHq2wxh8XMkX68CaqeNOianRZ/r/pgXjVigcvxckUA+wyTm+N1tvnbF9jFAZkJOYbL71qcf1VHTgYIZF+TZCRixSaysWSotmeGX3PeoJlYHF4/5G+9YJknNDRRB31+IAZJX68S2Nz69DxuttWCIRiSp70gyctG+DN0xmNAQhTEo/VRYkCgYEA+RcXbAgMMovnp4rPSklez8+C7tFxNeHMzYfu9UfHIfzrQXZncs3W8K1I+p3e7eeKZ08mruzAL/U8vrlr/ydtF1kNNaBDMUfibovUh8a4HlHzqvYlBNQq0NM87YM0P4ishQ5b6MhTIcLv/Xhjjr8tyLFmevt8qwJWh8uEl0pREQ0CgYEA3PRbb+qMECEepuYxjkh5LztzQNLcab/KxtmK8om5hmXahT8t8iBobxZlzjDxVhAL3OYCYLA9tomULMBXJxJLS44uH+S04M/LbKFl4sDZ5scgXbwOpR2+a7OQJeRwhxqc0ETdFP3qfGLtCeJaYHdeq2M39fp2ywb1HEPB+nID/TcCgYB7hVLtFJSP4EbxE2m16eplXP8N1LiyQpXf+h+qbHy4QwaagM/N43tKAHRnKzBog2Bj2KFTLz4iyhbkcWi3r+JuKI/fXujTIFWOAjNTXVziVDtkNQmoeln9EjNtiJm5Q9phZPx41BY9cMC3ziJ4oB9hHW+3Xsy0tMUaM/c9WvIWZQKBgQCYNs52/wGWavqOx64D8vFpFG+FjL3DLBkpe9w40aA5chlkCe5BCwpm3OstbJIVU+CYQOwKZ99bzNODMM3ZYMT2O/CSkB/7b6sYHuftmiWC0lL9v/vmy+LOl1kKgaDzseWtpIMZXwMWxZ++W20fX5ycPTHkBrOnkhdxbUxImBsfaQKBgHTr3FGm/7BhVdlG1GW5qlTIi1gRLr2w+0Txmikoq6i1ljfV5sQ+qHWNYX5SEzShcXvG3qghyjtkvHzP2h5GQ9ji4Q6R5XC885vSpe/4/B1F82otAMmEqPMppk1qZnYbsjfDi06n8KHk4EGZjTVfPe6mkXs8R+hK0VmFoHYMQzdy", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDW/ZsbRHAZf3Vh2PyUSlXbBRHl0K/PCX26vMe38j/eKSYn80Up3iRk1Fk4MUKgC0X3t1Ldf/Y6In5RhohbX7mpni4kHErjbhKtjLqQWCTiSNWMDVuFoZ7jiy//Qu5V+w4tdHogyNVgLhQhn1SouGJ5wgYP+XA67NQ3XoSzycAPU1N3LWuqcWwHWI/L8mY1qsmZ46ShporoSul+Pp4h0sDXqSPSJAZJsSV4/baywJMBXD91wRo1IhKvd+3OcSgVAnNnmBMvVodSzN0xNmmMGvHijslam+mYWW4B/603QAeFmYK3orEKcQ2lZVV/1lsvbg4GqrlMLkK4hM8NbTASa4LLAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmVc9FLuDob3aE5ToYqx3L9MandZL8TD23mzmamUqBtz62", - "privKey": "CAASpwkwggSjAgEAAoIBAQDR23+nHVwMhKiv06tm4hsMbvPUpMMBCnv4SctsJA/6ByOfCy6tw66RURfMGbk2xnLFv74/vayf38jckD9lMmz2RSePsRk05IsZxXXgBzxNEO0eL7MoK+60CvJaYflFB9TzHKOnZheZktEa7wZttpF1IxpXS0XqejvuSPH5FqO8iBBWgKqpiXi0mV/5M9UJ0exLynKFPiB9zaijJEY4SXmbPqHPQeARcivqGorjmpRBe6QXWjGOKsRaWdbB5WbI5iz4Qh0jTThy1LOkM9TBY3DvbgmtZ/+0G/BgLjrZov38TKSBb4nNbNysgV1iphUucQl53sUxbWdaLq20RHrOkyx7AgMBAAECggEBAKGeEel5yvI5GFCRC2foqjwhFtelLCkZEfBdpLRb8ZH0/ZH24rQgB8kSUul0xhdRLgLtcG9WfCOEDQUQckJVW2UuTRF0qpz5hccLM4SdDeusJXEh+y/s5aDy7UJ+QaLQLUgtvjulfHdhgnjjrGfCOrOjnR2tcuLp0E3rD69tqBwAp+744DBvk+GEnwduzr+Akk9F1UlxQ1C3FmnKEwOYrHcdGQvsFQf9mKsfbmDhqhqaH7cr1f9hlRXjFKN39xmVWVgqVDWZ1Fx133am8fNn/Gzd+5lSDkyAWsWcFRJZoJ+n03YOsf4yC2OzDhzVyZ7DKjouIwYeqI587gseox7BBoECgYEA+K79ITiGNYJx6rur3MojRTPajcX38bcvHBCDegttmAKbUgepYhQppueuC0xFjdqg6NBbtjbeURwI4G/qyD7kd2jI3ZXRiaBRbV21BctWt+fdCtp0Ri7FLWSKHMq2XpgHex7nxj9POqmOdPf0hHbJcm+WFK8S1VFKxt7jZLQ0siMCgYEA2AgVNl+wBGr1lgvqnxwZhfFjZJvbmn9lA/IabENcRDMGexxVvPwX3U0gH/2BMrsSG3qMoyF0hH3HHAYJQSnl28vqJtd3AbBqtJ8p/+KQHJ8RinPuxrc2/lt9WlKxxV191PR5hWgyR0r7GGEZAPiDTya5/pkmRgILpOVNX/de5ckCgYA11kxeoMoNU4wt8SsnxWsVVECAaNdgsPO1861DAq5bNlVB0P7OiObrh0SalYyJRUeIn3L7Y62FibgyPohpiZQUdc7micSvMtHuB1dlRbwkXEHyU5DQkNeHGDj+OrR4jhkwgmRS+unAHW0FzZhWBRFfgODQ4YYGQG8b1q0L5Cd0WQKBgA49eih7Zj7kTgv1/SE/2O7bWpHnNDKa8y2vZ857IjncozC6TWyHsYsE6nkxXLLbYfYtvdeC/Qs+v0E5pKKHAH/ckTK+QTn7Rw1g8IPNi3JXifB2c+blbNqXbUvm55D6+LBw7RG+LJJGfwa8X8mQmBc/lkMSFVPIDrxv4QnSZI8BAoGAQIoWEL49iWhre4EedAS+awGFxVs1twC+VteWZoKwEmISjavUL7ecRzFANPCi1X30b6VJhdI4lZp+c6OeZAQ1f2o8I/OERDYWeMuEFZVKQCzxsfXVUtv+bjKflKKdevhMK2wtG1KgSEKvhLWaNZCbUNgEyijyikx0Gn6f3mx7e/M=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDR23+nHVwMhKiv06tm4hsMbvPUpMMBCnv4SctsJA/6ByOfCy6tw66RURfMGbk2xnLFv74/vayf38jckD9lMmz2RSePsRk05IsZxXXgBzxNEO0eL7MoK+60CvJaYflFB9TzHKOnZheZktEa7wZttpF1IxpXS0XqejvuSPH5FqO8iBBWgKqpiXi0mV/5M9UJ0exLynKFPiB9zaijJEY4SXmbPqHPQeARcivqGorjmpRBe6QXWjGOKsRaWdbB5WbI5iz4Qh0jTThy1LOkM9TBY3DvbgmtZ/+0G/BgLjrZov38TKSBb4nNbNysgV1iphUucQl53sUxbWdaLq20RHrOkyx7AgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmYazJE7CkpYuDRQihzJdAqTNxHnPspEyoqTQ7TJ5pGQW5", - "privKey": "CAASqQkwggSlAgEAAoIBAQDR/98aOI26A/x1Uxu1/gm/belKQw3FnUi90eniBFANW8Fp3EVjYhuZELJelILmYex1ELxROoojxJ9g+eBNxM5/ZopzfbqBT2vUhH/94AdXXRksNR2WKHMVdy9M+tnIUpMELuey2ibkQSOMlYoqdQI1zNK3fg9QsACRUkFe1T2Z2gkIJZ/Lasb5pU4SqaTrhqtBs023ghdPaC0wV+LRwAhuso9mwWysonYfuxRi1++1RLizBHfFjfJ9sXRTaPjb6DEY/W9/984ypIZOoeif5+zq9UV+tt9LBrmuHKeE/K19Q0WIpoK+KlSGN00TwglSy1vI8q34FFguxr3d41FqOlIxAgMBAAECggEAeC7q/TOukO3lFzRYIKDh7Ue3AwQ7JoSsc85l/y8erXZ8y9v/bjBgwQoYOx7dh4I1dI3+aLKLCotl93cqUve2gp0p0Yz8JzNP8BFguufy66HhXTaM1zoRGxDZ5kGOUCJJ91Ps0KQfK/THppaSu1e5yxaM5ezkUPZZbNHZja+WkKx5gFU2s0VD3zpX8rrGbsJ7w6n0sW5dqHYyBcE5u4dhopLkRcLwtAti1PL2N6qsKXc7eEjlKsgFCfe0gBZZO6o9A2ABXob5skB9k8TatRCKc54aGJtGjHjff7Lwl/D6gGb+GrBeUCwohIC/OON+lg4dv1eT55wNm/RfAOD7NnlgXQKBgQDv6FMoy4zLpSjDtWU2QDzfX/t437hCXSPR8pG9XTF6lO9aQk5pOvMrDt1oeOp6L27L02d8RGxcCO5krj47zAxkGSSJS2Msw4Vg7/IpcV91D86wGEORxzzhITHccKkXK8eAIwffKDg8QgGA/dDpGWKURx+v+zErOORDuKtk+gKUwwKBgQDgFffcZqoilhj2sIu6r8lp/a1DZeLHLIQTMVtflP9GO2qprnbOIYwU9W3iVc28XpLDjI1x2+MduohqBYkjalpvOlx5XTI2t65fBa4xtmCOfOwiBNhBkLS/sf8cSCsVEDA7Ixwd9FnufZNlTDumKzipYi+vpQt94d2t3dMv3sA9+wKBgQDgKNDC1mYoxZowOyZlqWH3SSSbzVXKVGKqwZ6hNBmOMujuCfRf6J/bBJmmCwzzu6wnsNEJ0Jj66bFty00E7GRLhx6XViRFaC8Q40H+rRsHMwzphtJjvKjKpgyDr5SevN48gP7S6S6aRwZGs2Hm2zw71bTq5qcLfq3yBPPIdr3ApwKBgQDHekbe6HVjvIIUeCyqz3lY5P2sFbK+4x3fh/xzJcvo1VOqISiZbruonKJo7UDsArRbZ28ygC+5cyekWbEu2aoPgcB4OUJN+006QXBDyLpDnWkHD5EDLLH6Q5V5s7TGV1bYDfUlpTO5XggsEKS405jpEAKrNRz5vmr8L4+j+YLgqQKBgQCBBGCglLh4eUZEx3cDXzAonLEJGKHgQtvDXBHqYXImmeBHHxs1zcL3oL1n3dYTuXljz39YmoN3RRWVBzHsfnj6m8vBROZgR3SEipLPr7mVtJizdFiaia+YBEPPZdoOTHIOxmLYumvu/WArbpSwpk9RBJndbD0wLpbUIAUUoR4CIA==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDR/98aOI26A/x1Uxu1/gm/belKQw3FnUi90eniBFANW8Fp3EVjYhuZELJelILmYex1ELxROoojxJ9g+eBNxM5/ZopzfbqBT2vUhH/94AdXXRksNR2WKHMVdy9M+tnIUpMELuey2ibkQSOMlYoqdQI1zNK3fg9QsACRUkFe1T2Z2gkIJZ/Lasb5pU4SqaTrhqtBs023ghdPaC0wV+LRwAhuso9mwWysonYfuxRi1++1RLizBHfFjfJ9sXRTaPjb6DEY/W9/984ypIZOoeif5+zq9UV+tt9LBrmuHKeE/K19Q0WIpoK+KlSGN00TwglSy1vI8q34FFguxr3d41FqOlIxAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmTw5gC2FkobMiSJ9uWAUPXuLoZMRXkXrAVUQYTA972fo2", - "privKey": "CAASpwkwggSjAgEAAoIBAQCvs+za7Dg7qKqOg3e+Zc4y0fKucQ72ECtbYTlbXmPiVeLQ+FZAl4QDWno07aya0ej2PgNlWX9USO4gU7gjBWYJpfm35vFpSedV/dglS4G5SdysaE6mvh39GBqpP2mIsD+R7STrpCamh+b/K3GUb5bwfbrdmkMYpsfANH5Y3TCGVBNjFFZPlnOIlafzgpMHJRcTOhCGmO39KQt76ZFfgtXqFXnvZhiedeP5bdWTRMHc4zP12IRLLLAoyseysfpYBdDoSETytPe9YGEQfCHBcfzz+xPTrEQXFu5uTz8PUEyH3pR6lX/aFLrQLyJ/CZZHh3ouCEQlj2/EM8ppYNkgiZu/AgMBAAECggEAROVGgOmTe0E977f5Yj1FR4QvpttKRI4+kgxjk0JF5GBNGifmmllPOIln1g1EW0joEnZqmnknhoM6bI6na4QYaLweWVBDZUfHYF6zPJyI94DQ+QHFpXhzBeVHvwnQdfq2UqAslAG/7hjoKTJ9zPictRx4A6ETojzzophy2qGQ/3qdRxyc0ybcUAAcSvArnqTPp37LZgSyBXU4yo+oRJtmgCFBCdUimgn5YVx8YQ5Zub6mGutWixVVuGChaXYx181ifT/DqWI26WACvs1mTr+9GVD4SkZ3FUEH5X1KvggydQesvFQrjKp7842b/bsP+z+jOXU78+V2I5DJCsnfJr/+cQKBgQDoFrx3J06md18BfrL0PDz3eRFKR5vQ/8l7WUo0tN5UUFMoPq4xHlbBcaKEdhM0FjBb0KPqugugLIgscX5oz3AfEA4l4ljHoDABN7D7A4XNLyMgx7erH3aXj/1hz7++QH3VhW2rZ/uUw8voTkFdvYjXoeToUIlESYywMA/tOnYHewKBgQDBzgckMga8SGr8p+QOdP3HDt41/XqDRtHX0q8dBw1akyB8p6vYmxKb0BQuNlR+A/rzJXM5usVdjSRdRWOr2lHPyJ/NkH2oKJiYH4+q/n61DFRHA0f2/XuWqLaOdOk7PpH/7ctx2tEodXMmocyG+cVFEwRnMoMXXaeN/Ck75/9njQKBgEGe/BaslH5YzhH8ItkPlyVZo9vet122lN89dc/FO/+W3oxIfLQCogD8Ajl1sSRPCclMCqy5gcP+E1qNlHJKBKejwHxRrUx0LF6LwoyWiGRlaYdBMNs/gCaGXdwkA1Dlpy6SFVobgnSjj6nVRoIcru5ZJgHRk54tNYwzaq1mlCy1AoGAYCfcmzTG6rvzeQ/DsviQwSa7UYZGNsP4cWByybAqC/pbb/2w4XNvNCd1G8iQ+0T2SZUXKllkexoAJNa8sRNM7A7aWp+J+NjLfQ6LtYc3TpSja+hQ2FbD7ugeS2fuIBrXTWeqPP8YLz62t0AnvgBGxBK/aIRDTmCFNYka3EIrEjECgYEAtL8cYqCkxJQF3fvN+B/iSUabXV7kUKeB2eELaodzAEBJjcfKwwTEyAdvb+7YXND5TAF83+qoXsq+f+Dbimh1MKsYV3kBTIxDOHSeUwgb7IS4mMlyr/A3SftDQkYZ/nJ9MyFfLhfC549WSOZNMwXs6AVxV3YzfoeF+5Fv4dsNZa4=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvs+za7Dg7qKqOg3e+Zc4y0fKucQ72ECtbYTlbXmPiVeLQ+FZAl4QDWno07aya0ej2PgNlWX9USO4gU7gjBWYJpfm35vFpSedV/dglS4G5SdysaE6mvh39GBqpP2mIsD+R7STrpCamh+b/K3GUb5bwfbrdmkMYpsfANH5Y3TCGVBNjFFZPlnOIlafzgpMHJRcTOhCGmO39KQt76ZFfgtXqFXnvZhiedeP5bdWTRMHc4zP12IRLLLAoyseysfpYBdDoSETytPe9YGEQfCHBcfzz+xPTrEQXFu5uTz8PUEyH3pR6lX/aFLrQLyJ/CZZHh3ouCEQlj2/EM8ppYNkgiZu/AgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmRkAAboTpBsx8xB3BZpf9JcGfojpbUapi5M2tTgUuhaR7", - "privKey": "CAASqQkwggSlAgEAAoIBAQDNK8zVQEpDabjQnPpSAgU8O0yqtRHUiHPIFDB/MJ7aKlqXnOSIrM3ZmOBPl+WRRwOZnUJFV+Zkp1rYMF56oKRn2CJ0jrAYDdJ9At63ozjS+3ejg+F46zo3r6AEvcCKNIRFAf4IkI7iFLYL+FbiO0RVB3fmQePanSCTAz3WuNlmO93MgvEQB/HwAytMyuhX/aV0dYKUS2cHfJXUvw+6qnBnZ9nmW4acVUWf3+A+bVXrR2KX9YBG2p8XwljIkJexdilfiK+6PMae2OPvzgfd1J2fHCTLQBEyujNDu60egxnhVM5EjGwkwUqrFGWs6sXD7JO5lfRbjigZ8rxKInVMISL/AgMBAAECggEBAKAYtH4W65wM7CUEySOi5fjpANsX7bDdRRN0BZ/KDbqJYCV8TKwFw58u9qHFEmK5eiqtFqBLhcE3AeE+ZQrlPUS217QB/5DVgFECI05CdD3V8bZLW25ihwwa5A+vDYYKksfSVSrTulrZ9HAEua9QtfJvoHSxJ55YC6oL1n4twZ5OZAvCA5qQgx6SisBK3WoMwUtGPvpiktr06cXJTveHzn/8gQs+UgUi9hGQwT2ytwFr+15o7FMHxiEPXoln5Ltsw5PbIWD6kAHLRPaH3NEMdhR3CpeOxLg/Hk4qWLcDr8kvOccAf1gsr5mYqkdTNqX6zTgGi9krfR9L+u/9UBG19uECgYEA9uyvmVPtgVroJkTG2KxfDXX4zRcpa8+WQTZCv0tjdRC4A6nXk+zppzYibNdt8e6k+ofmHQ8sEZ0UlLy+5auNWGxiQ4DUyX/SlhB2BY+bpfQ2/yXyW5b1l8y/XSzojQ5coYG3CSvCm+iSCiJVXD5oFpjzi4GjwRfgSsrsbbVz5q8CgYEA1LZBd5RveQCGdL7H0N75j3VyOF3Xu033mPO2NFO1517xEcYJlfzNe//Zikwe7fxPH5HEp6Tud88pdIcNxXMbaA6GL4gJAXMWoT4lyZVbG4QAz0oyvasByEv54UZtgebX0k1aKb8AJ9FPfhuJ9CQHdXWOiBHhcLNh/fB8GQtUnLECgYEArnAClVT/IjTwb6iCuSr8c2v1+hz0vB8ITMViXfWKK3dGKABiNTRW1DOgGjgOia1Hi11aKQlA3qiTk4fLbEDHN8JJoNpweHD+edjjJ4aONKzT9Wf/UMjScwzH27EQECYnNkmG3sm1T6L7GIGsv9+udNhUpSdOYejWIMA+Sjq3yC0CgYBn3xQ7E6YXvZTq/5rNuYS+dEixk8ncMmedLi2kgdhLQsaPulhGAOxLCBYv/ZoA9vugW+tfPiAhK21/9M9Zwyr39le6cECNj6jWVmXXeXLDDgPjNcVvb0lwiQFd66lgDN0JWjKUPiwSRZj+6O3F5a4qwpw2gBzJjx9kBQJkrG7GEQKBgQCtuQoVnjFUVBzKTJP9N+SDbS+hg8uyzoawfNPhs6AKdo2I9eveJBut2Fdi+ZUq52OApnwn7+YvLSUl+9eS7Aoq5vxvfsE7ooavtlOXkrIMkWPUZiUp5xLVy276R0AjVhWWoZ3+7mPOmUx8F6w3+lk9+g82bjxZoI065f9hml/DTQ==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDNK8zVQEpDabjQnPpSAgU8O0yqtRHUiHPIFDB/MJ7aKlqXnOSIrM3ZmOBPl+WRRwOZnUJFV+Zkp1rYMF56oKRn2CJ0jrAYDdJ9At63ozjS+3ejg+F46zo3r6AEvcCKNIRFAf4IkI7iFLYL+FbiO0RVB3fmQePanSCTAz3WuNlmO93MgvEQB/HwAytMyuhX/aV0dYKUS2cHfJXUvw+6qnBnZ9nmW4acVUWf3+A+bVXrR2KX9YBG2p8XwljIkJexdilfiK+6PMae2OPvzgfd1J2fHCTLQBEyujNDu60egxnhVM5EjGwkwUqrFGWs6sXD7JO5lfRbjigZ8rxKInVMISL/AgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmcEhzB1bUTptZ8tMHGDscgAgRUok5gPZUiqHWrXy64dG4", - "privKey": "CAASpwkwggSjAgEAAoIBAQCtS3RmkSwMLNS7Kvn+8ztrn9olH3EzScK3r/5+PyJV4klYYLZ5RkrPWdbCB7gRmVkwc+GqwiMaHVDualDoQNdbguAX0r4nhxo3/46qDgUf7JpNAT0sjyyr69LzJj9/Qs+o0YGTX8W8rAX0UBikoFI7DjbFf9dD4a8cEKLiaqh5CGKS7zgYVCGv/ohp3MbZYXZGbV2FAVOvLQwY1edJP6s7kBHlQhinmkzW0bCdAgIUD5DZKiwsmPjkX5oBQw2sCUComKuAQGiacXcgJ0/ykqLkj4779KO10Qs2DwaqvEORx1W+sgtQ8KllbDqQHzCq2fXJY6359noSTAlgn+LI1b6XAgMBAAECggEBAJcBrUjDL/LcDfObG4WCRkEeZmT65RWgLMEL52Pzd+QG74rHm7pJ+l59Fpq1RzxuuD10fSzjRts2uJNIqX/5ILBpdwTLa0/edoZddt/Qn76V2k9HyRrPGEonkQa4SZSHj5S4G4Vka1ZhQD8InLC303AKjsfDAr3wJzr5dDaAYpYzvSe2KVZpQ06JPYxwiaS2iwxJX1f5vNyvVj3HZOShaw3evKsFilzRo5sZhmeMGa16F8koEckbbjtPmK/zQkrJ6Tayy/8FClffgMOEinrU9V/DKq57wJT+MlwrlIa9i+/N9Yx+X9qgSNfnMJHPLRxRnNELzl8TXGm0/Ey2UJfyDaECgYEA1zttzJUHIQX5bGNwvoFMs6arLs11OIZOJXAodfTSlZjJJqwFfpuxfqaUXzA7WCSKAMbUJsgD2BPU5yZMF8RxhWCCadweXZdELK5kXXip9ddhcw7Hi/eBC5DWk2in2QS1fwC1sC9+WHGpemUQJ8kQWVrSH6Vga9ez2VMjAI1gtrUCgYEAzh58/H/4zyPJWGQaD5m5tUC5AOW7h5hYsVhgHM2g/vlt4V8iAQNo/eY7xO5jNp1c+rpr2QbHxKMmgCYPSGVOO46JnKsafOxupW4rYsxkywLHGlK+dop6yORBygP5wkNdcpGklR1kvrBN7bC8Kal44YIFVrIbLVoqIvSbHAN3A5sCgYBNkjmsdjmviTuv+Nb1khxW00b3A02wJZecnqO2f5o2GG7G5VDFpM9/2gG3nOaGigTC6uYjZAseoWcmOANMvZw8eeAGzzKSgKYthFzf41E+LXYNxdHdfEKiLH1pe1qjOLNBJrxU14ktzylJ14rPDAQ8cCMzDKOHuqIzPWdsF4g30QKBgDTo4J6UXxMVFZ9J+uKcTG55kcPoNO5GriXAENPz+OrarlkW6YynCnF6g0c3BmLDnFWEOyD3u5n/Y2er3WpxDtb87Ng5l9APhQuULzDqVMlECkX4jYmyXHhrF3Q69wbl8fvx5PSeGflVGnv0TSjIpw4EKUiq6Y0Hwx87+QEE5q0XAoGAcDHscbuA/w4SCywZEbzrlnY/Za1VW9hwgiDbQFxQD85eSYLMt0D9u141l663JHAHEHAWcFv4LhdeCrdhvIv1+/6J1vndPjX2cJ8XCV76x3mnSSUvrwalMoMTOgERO5MLQ4B/qHn3mJIWEUMPSIZ4aJSqpS5nne2113TbW7ijQF0=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtS3RmkSwMLNS7Kvn+8ztrn9olH3EzScK3r/5+PyJV4klYYLZ5RkrPWdbCB7gRmVkwc+GqwiMaHVDualDoQNdbguAX0r4nhxo3/46qDgUf7JpNAT0sjyyr69LzJj9/Qs+o0YGTX8W8rAX0UBikoFI7DjbFf9dD4a8cEKLiaqh5CGKS7zgYVCGv/ohp3MbZYXZGbV2FAVOvLQwY1edJP6s7kBHlQhinmkzW0bCdAgIUD5DZKiwsmPjkX5oBQw2sCUComKuAQGiacXcgJ0/ykqLkj4779KO10Qs2DwaqvEORx1W+sgtQ8KllbDqQHzCq2fXJY6359noSTAlgn+LI1b6XAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmXDuRHJvjK8WDmqwjnd9PSRi1aUTC1p9BryZ922wdhdBY", - "privKey": "CAASpgkwggSiAgEAAoIBAQDXo5iqDYnxNP3h40X+0fAw8b3bCEfYdtzrYCV6huKegNzxPkySFTh+9y6fcfi8Y9HYH4RkovmHqavlWlE999IreLQOzvsRVJ2pzOaSLURid2KjLO5hSi9CZTRPOMF70OGh/k2Egh7Y1buYVbDKGrgB+aHm76xxI03MQpvzhINtxrVlgzU4ErYEOZDFNXaccSH1UOfATb3FnAtD/3rhqldMZn9sAyULThoDYi1x7WxgX8j+DLx29cwOyoBBFtwS7/AmI0p3rYN3Y6otBNv0ycGnfg6dcnMdMDS+OcxBzqZph3o3ICKXtfI2+cP7zWl1QRBkblQdbHOYo8qevdw3wr4XAgMBAAECggEAB5FaPj2TZb+yWUccocDEaTNSsmkr/FDPmAMbzZ0GPwHOvzisf0P3Y51RKY9aZ2IpbyhMASwnDbfKrJXq2/3ihlwKFar17LnHfroOLXshN0NxVsCw7QEpf28F0vHu+GVwRbsjBU97vahimQoI1k7xvkAAipZGuwG+LTj5OCaiZivOkFmFwiZb8FSwGkDkGnWEObh2fWXbh/NNZU3JJ9CqU9RcHaguSonk0z5gOrQrcrbQrY3oNpgPssTVtwdPFxtGzf85D6tnhhmQsjS9E/dDyGYoNK+Vue7E0KoFGkJCXj9z/I8hPt4eDLh6HMA6Zqjxj1EcM9AhIqe8B2wYtAhCIQKBgQD9JFWEbvJ5PuNznOPmVG8ScBpQNbFulVnxOorg9S7dBSeS/TU3hFJZi0ojjtCKzIfDGumc+0Vf60M8FvXxbkM51R6DBaHtHwbIaX1luH6s05e5tpat9B/mc4xwQPaoFbDO/EvrdflBZRpUjXfM2Rgxa5rnzxgOKyN6Mck5aFZAowKBgQDaEt3DE3bSgnvovVsYlxOSDdGBEs718ZzGtrxz5u/i2WxXgXhzriRWYXoLjEBCUF017frbTyMIilBzX3DR4y/WrPP3NP+n/IbdyERQUEo4CY4cfOuHjS64UQo2kvIxBpz8tAbE7w2eQAFm1c9AIOhKPYmSC/SRo7iO2ZMekPx//QKBgGoxpOJyvKuac0ab6YtFnnboqlE9xRpz8xBck8g9cxRrRifGq12H2BgSc96o2dlwZf+2OYyOaJMNmd4Kb9CBhhgrzKoAYeacnnbSsjVLCXEtLrhM3bdJ81v021R4HEF1IAAlHSBBFHiXlk0kL76y0BBjaM+YNCo1dKOdYSIBIDXrAoGATqUtKtQTLxn1u9rGRpj9atfm7WiuEM6A3r06O4ZWjvYgd3Ju0TFFU4216QI8jm3TH8biiEMC/Gp9Vw5dbqRDNWWMWmPXq2qL7OHzmQ9LpOf1Q1rdyjXlWn2HdGUMSRf8d7opEs6vl5m3p7GGG7eCbnvA6FW9buSfg4z93LEnDrUCgYBNxymg6Mgnrw4XMDhPS0RR8NE1j1AWm3bhaMNrcgRnOCtmNcT5GGdUX626DJOk5xx7whfg6Ee4qb7Zj4hcVOSunOyjIOv5RRsWFC1Gw34n/WiyMdJF0afXZJKSRV31BZ4OP0R4EfVVLcfzKuScZKuxtuKjn6/2UVRfwjWAbz5WWA==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDXo5iqDYnxNP3h40X+0fAw8b3bCEfYdtzrYCV6huKegNzxPkySFTh+9y6fcfi8Y9HYH4RkovmHqavlWlE999IreLQOzvsRVJ2pzOaSLURid2KjLO5hSi9CZTRPOMF70OGh/k2Egh7Y1buYVbDKGrgB+aHm76xxI03MQpvzhINtxrVlgzU4ErYEOZDFNXaccSH1UOfATb3FnAtD/3rhqldMZn9sAyULThoDYi1x7WxgX8j+DLx29cwOyoBBFtwS7/AmI0p3rYN3Y6otBNv0ycGnfg6dcnMdMDS+OcxBzqZph3o3ICKXtfI2+cP7zWl1QRBkblQdbHOYo8qevdw3wr4XAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmeHPhtUcxVo6eVciRKbDpS4x2KAGgsiyxA9Cim3k9gFzn", - "privKey": "CAASqQkwggSlAgEAAoIBAQCnch4Vp835CUp+5RNhMUbtgg5N37Ejekcd15fbmZkjSeCUjoE0dY6Lu4WN1z/z+F6V8TYd3ql4Nvx1qp9eB1urcJ0WXKQ/o/4dZ+iZqbonPAxyBl2OEzqQ2csbAiSkrio0ciSylMNAIF2Wv18gRwHw9YZWa+sOM55HNPiLK/qav2HEvaSUfypjWrwdRhrYOho0+pRrcHoU/neI8mFUkQQDMvArnWpeaN/dgPDsyO6RRaXyyi3A0ULK6FFjMyiwZEqQBQlRdrc61lC2o7b4ySpzfgzk0j1zwROSjZaUfH5IJn1FZVgAl9Z+S8nLgTvV/xrBjABee1gYsD0DVFleEgUrAgMBAAECggEBAIsRTkcyDPFOdB6b5tKL+Jp9r5+hrx8GCVaRnj/2e6dBTlJTYJ/PGsqWvb8mDKl1mCj0IrwAF8QN9vNK9/1CIzJp3y2ZV5i7fOuzRw2IV2EKkFOLUdwTwEpZeERALWrQc6EHQ89FmjwCJXh0DG9kSgp0AFR6YMh0unntVpdPuV0XSfxI1cEKoBRjcXweewxXNmQc3a4BTI8n860sr83jF2f3aoMihVnOCPDTk7NPDF3OUI5lXV6xUCpbAtdN7u/aG1st3exS34cLl8lMk/a9+s7GMBwBseJiBzENkF4i6cEkz2c/oR2VMvFd7nnIDGcgqWHM93ghQMYa7NBaljr809ECgYEA0/5ju1fU7Rwn6+5Sov90I4zgIhS1b/CfYow055U2s12RX5iAzhW4etYIX8lNFj+lHQMj6zHSdWJXtGxxSrqBQXLkOmWrfc9ZvG1aonYhMAasydNRcsajr5WYkfWbS1FgnUo6vBeOBdGzFi7yUYtgLooNigdg0Ob7m4HBEUQRPnUCgYEAyjRlGGbyHoEixo2Zpg6zMaYCVquB65P1kIJXDVpQAjz6pI035/VUsiF/HIfPkKfQ/IokuR5qCIfFN++OwCSFDub1lttikUJFakgeCgGlLVuu8VCc3IeQbPT3pSxqF/BHngWcvykTq7aNzz1yJiY//TjAucLthdVOYu/eyRYpAR8CgYEApWac76Wmtt059KV8ijpfpgEbOtwHd/A4mw4jlPBhvm5ppzl4fdKKniRyYjHQWGSN8eXqV24G85koLthRSGndwW/fzARZWg62yAJWLd2XJT5///RFXxTGz48be/4yDQDQLcilrO1/3OBxJwS4AZGKGKWTzLbW/gbKFtmVBmCiR6UCgYANSqxqkjnQL4TtsFktRUIaPWNh9xwvNCasPSUjx5AC1adUMcQ/By1uGC2W3oaSZ7WhJCON16X4sZQRPToQ/1WPyTbTl9A+5DBT8DGpTrpg5On3CumExZSE1QWCYg0HTdAnXw8SscyNOQ7RVKSwRUtnhdeFXn7mkUL51fK7HS3M2QKBgQCD8OOaClhJNMSvkHInrcKImhl96p+VgCw8GMoLxPHV8MnUGwuNYNWWatmoLQps8OavZTMWgqnbjusvBJMFlKSZ93ULaUnMaLFekwePiaPlLnE7DCpmBjlKbryLzBgrdOLW0R7PwFW88fDJ2q1IqZZmD01U6LnyfF+tRhj5ysdY5w==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCnch4Vp835CUp+5RNhMUbtgg5N37Ejekcd15fbmZkjSeCUjoE0dY6Lu4WN1z/z+F6V8TYd3ql4Nvx1qp9eB1urcJ0WXKQ/o/4dZ+iZqbonPAxyBl2OEzqQ2csbAiSkrio0ciSylMNAIF2Wv18gRwHw9YZWa+sOM55HNPiLK/qav2HEvaSUfypjWrwdRhrYOho0+pRrcHoU/neI8mFUkQQDMvArnWpeaN/dgPDsyO6RRaXyyi3A0ULK6FFjMyiwZEqQBQlRdrc61lC2o7b4ySpzfgzk0j1zwROSjZaUfH5IJn1FZVgAl9Z+S8nLgTvV/xrBjABee1gYsD0DVFleEgUrAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmTd3oibLxr8XKqj7DDKcm28a6bqnMLJu4tc4xf8yAm5xJ", - "privKey": "CAASqAkwggSkAgEAAoIBAQDYsSTm+HDwoD4Th72iDePNdWl/RMPTFPLX4Uj1GmY/4l5B776+pSYGcY16Ak8rlBURz4NIntiqQcW31eTMdRKusDCgBqNzY3as4qFtsTig7D8baqFYwoswSj8awe4y1Cq6N6R1/w54r9pEap1RzA1+MPof5+WjGA972h4BQaXoE5xdidm2PJxyk1ZTK9hKqZI33JII1JQ2Zdr3VQzMeH2Mh50U+OsJU0rtGNUqSOkvfCy67a0pNJHssGeKUWI77go1t7gWy5F9s85zGEJKd5J6lh1bpam3cDX0hX+jfxKtwHut/kCtEEGxBSxO6n7EHHhdLJylP26skd+YhAk1VfHdAgMBAAECggEBALsZgV55B7OM+OyOGPvy+E4v4e6E5ny8qs4h9IfFyqHAiFhwdIdSO5n2tAy0L73V97dQMPAkT7n6XojUA+FR+NaixOl3sevw5shySqZXDilMs1St5jCokdwZT5F//3cd4OK3JqbHmqw0UsceM0YsZT4fdejUp2ACZ2QuOhgloeXWaPKb+CcukkQywFa/mtCkbpD8yjUMmaTXg6Roq6tBcr2qk90ICgHxp9qG3udkWRFd0FWoWxXNKn8AuxeJix14S7LBSgfzDck311M41wNOb/wMzy2Z4tWerxlhd9YZ564fC7+xudAxN6KcNlN7D0Sy7jckfjRSI2015a0uPZ62lqECgYEA/zozaRWhlk6Q51Dr0hHcm9EJpktAqhGcbd8BvztoMKIJJhcPa0bmWaKDvOV00F8n/Ps9c7AsoD8iSyfl9Hhznjn1QXEiuwXJ4wyYmby8a2wWCs0JggMrSgdoZiOtqGhFem1Dkmy9mLvy5pZK6w7CzeiRu22Xecc9aVH7pEQPYAUCgYEA2VkUJpdfkUS8cLZK0hBANxWB/poTAkp1H6T1ECPYNRHGRskpoZcPWKVfFdMgIBSkU32nIL1D95zY0LzAjK47SwVzOfhUsmt+FTTZ+YFseWeqNUNuA0Uid9ARJW0weFPPK4yJLmO9v6C9c7fZFllob+ajcS+MKYpE7VVeDO2Z6fkCgYBslWxN5uAKPH61it3pT6QVvodmclmegUOWEuyBWVroZeeShvkOYOmbdOKrOMvL4s/2d0UbtPYnbvS+GMliiuRVir7nCqUGAF519GPv9DYNVbzC95x17bc7FY+69K7rGQGGJno7D3xSQJQEuihBfNQwGiP2I5fwPW3JIxH2PuZzqQKBgAQ9383NAHl2TPMqK5Wj6Yzpp4rPePV/fH+smXfCK1MF0MfK3zwfFZaWS5/CagsWPArBFgTmjLAFaJnSRTO5psCVD6We+hAtVt2VFXfwFazc4A6ADWKU89JAxkTjt6FxiUaBTKASJD7cJTZf7SWpgwdECgaIdgTNhQDYvKgl7u4JAoGBAOKu34WNvGDTX9BB+NeMQjrmkjVCnaoqRsg7yurUhzUlTbMNA3S2o3ZoUrbXUh2quYZlTDAqQjKpeCOLMpZn63fIA7Z0Pp/wfTry+QFL8cPs+4q0d+SJXFI7s/DcgUgVIbndtGDKSeougb1VNx7v4FILW+/tqUqcN3oEblmnFjuI", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDYsSTm+HDwoD4Th72iDePNdWl/RMPTFPLX4Uj1GmY/4l5B776+pSYGcY16Ak8rlBURz4NIntiqQcW31eTMdRKusDCgBqNzY3as4qFtsTig7D8baqFYwoswSj8awe4y1Cq6N6R1/w54r9pEap1RzA1+MPof5+WjGA972h4BQaXoE5xdidm2PJxyk1ZTK9hKqZI33JII1JQ2Zdr3VQzMeH2Mh50U+OsJU0rtGNUqSOkvfCy67a0pNJHssGeKUWI77go1t7gWy5F9s85zGEJKd5J6lh1bpam3cDX0hX+jfxKtwHut/kCtEEGxBSxO6n7EHHhdLJylP26skd+YhAk1VfHdAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmNNQLdERMKBZNs2kPd7ejWVVYLpBwb8UMqAYUNSQVE6p7", - "privKey": "CAASpgkwggSiAgEAAoIBAQDF0/EXsnhPXtfFZVyibJ0t0g11Z/0D8DXgQYriyYOhYWlBho562/Ga0ETEcwoyuY8mtsJ/8//3gAURAcsyfYgx5R0AFC+HV+ar0Ft+fNhxO2QStYF6lB2fYc9bFnxJ+fT/4ZS9BKao5ioGIRNA/zVCk9alc/dhQIA2Lj5rwYBw151ucKwuMNpLg/mh4qcsLLCEaJ5BITxTaKlpHguWn1b93nhmp4Th52mZUv6yHKD/G4gf9W4Kb/GJEYYb1a6ctw7h4Z0HTE/unesgWCROd8crzDx6X1HRKnR7wbudulfBNJXUtBKJUlabe30i+LNXEPwAGJQ2e8cAkKbDWslZZtdFAgMBAAECggEAEcOnaaZYEWCF5a7lc5xnPN8Y4EsXOExQujOIgjbwQAScTAsGLlgjyPAczLs71jQ9e497xbumZ5YyXkWX9o+5NCnLwd8OKYwmJZWPMbuKQBjCMr/jwZsdUduZoCdTv9zXOEcMcTDCunX4nhZIQVTpdnIKG09fjncZTEQ4zLpSi09YqjfVBfxACUrMhmcpERSAGam1UB7bBEze0Ddv+l9HZyRXW0+1elX5YFpRXW4VaaXjtDI4jZx3iGUsimvcIlWRiOxOzPCE4Plp/thQQ0EAo+8DcU3RKCoHoX9UnbEwr7mRxE24en2JQGX/R6z9nialaQHAmIXtI4Nkqb/SZL/FgQKBgQD6IZ7qOqrHGdwwDYHngxU8hPotcFD5nbmS84t7VIuIJ/lmbvFreFtEjV0DZMgEMlDlctivurRVKltwLrOsV9Fpt1+VVwpvleMhjwHNLldP64PBsKfhB6wgTQLxK5dAgNbMM4NOUUan/IzIro9xio6vXewNRVVczd5tpH8uQe9/5QKBgQDKeCrsSP17kkbrgEtemcE6ONw8n2PDuaW67jtWMX4W2pqeiFjuNuSuO5ttERAbLi0EoqzqKszUql8LSUr2TE4Ya7jPII/AARtnM7X+OrzPmm5aJhmk/QiJ4ff9KpZZuUlSLtmlBRsi0DUlZWRJA4aJuSKVMrnaPGrJqo9jCczD4QKBgCS6QRJVkPPxOSKZKSTsW3bqc62uW0V7wl7wgd+XF3HjpLxEuBA2uPgE5c50wuXS2YwHZAfRm18R/CEpyloY/vfN5CwSfsbJtHMeA360Oj/S7iLHpK7nKIAJrs/ovanMAT40pigeyQgrjiR9dTSPysm3OcztDE63L9zblY0eQ2N9AoGAOfwaRttMhSRKXU27yBb+qL76C/6V4sr7NMLfiXrZIpBusbJYzbg429FEXQMC+tXJnMc+AD5LtSgp2iCecFVAFGxdXCx2HsXyZCcCGxIVWtteeUDqHT8+P8bQb9fPgVi4L+os+L6ym9DHN7OG+gYhdLXpupLxeRfOeXz4XaPD2eECgYBvLlpviAeCe0ZpcNEu/7LHogxfE7q0ijHY6gJUXFcFVlHmRUFdqpM63XzVEzzfTBJ3CWIHVwybwPQJF5CIaloOXP6Ac0MTY2x7mJ+rIqnqF2qgY8vf8I2beM3a+A7zJGiERtiIYXQV1jzcgnT/FI8rcq8/v2jSB3U3kzd7P6JLSA==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDF0/EXsnhPXtfFZVyibJ0t0g11Z/0D8DXgQYriyYOhYWlBho562/Ga0ETEcwoyuY8mtsJ/8//3gAURAcsyfYgx5R0AFC+HV+ar0Ft+fNhxO2QStYF6lB2fYc9bFnxJ+fT/4ZS9BKao5ioGIRNA/zVCk9alc/dhQIA2Lj5rwYBw151ucKwuMNpLg/mh4qcsLLCEaJ5BITxTaKlpHguWn1b93nhmp4Th52mZUv6yHKD/G4gf9W4Kb/GJEYYb1a6ctw7h4Z0HTE/unesgWCROd8crzDx6X1HRKnR7wbudulfBNJXUtBKJUlabe30i+LNXEPwAGJQ2e8cAkKbDWslZZtdFAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmbhNX2z766ycmLDP8awr5BLB2FbqX8FyvUt6fBLRRWsjH", - "privKey": "CAASpwkwggSjAgEAAoIBAQCgRIZTcbtfOQI4aEpjVgyVx6+hR5z0TurjppSOv3p6e/Q5uBcXHBx0LjP7bLW93oAcgwMAPZwXBHOHIXq//XtBdzIAYDzUUztfCk76AXTWr5MR3jafXqf5lwyu5XLKSa9PiZHDwSEQDY05N4WeCS3AMyprcu1Z4ey5W3nGMy/ClPicyLPhCgyWONhk+ig4bVNfByP8AUcTHAzN2hgfhY9t6f5aqvXodybvG440GMt9kMH5sSYdZrqPL3Ixpmz7mOxkfbc6gaXh0M/pTD+q/f5JTrYZ7NbF0jdrLWcx/wAgJA9iLyrHG93ObDvn1L41k5H5l+QBqTNQzfz9mnv1Sx8xAgMBAAECggEAORiwkkHOcxooRFhDSCh7y1CcrWSJ8i+7Vucdvc1RoRlP5NBEyaLmMC3VrxkHlmESWxYBl7BbT4fycI3o4UU5CBWi5qdihHIykKVnhYHHUkSyrIbyBsz+ItlBV32+63pczoVAPPEtCj8JtPymyaqTdgnEbws+q+rlHxQLyiSqOzOu/eJJKFBaW9/C6Trh7EOxEwZYfIoHQuihb/zdrZtlsU36yotKxckaZSv44IXTv5cGT3vO3U3s/t0rPH8MJdz1wONr7iowqEdUp5GZivFg1VwUJoHvutBjIj/eiJGebQ/jlKBOg4HltjbHf3YAniK7Dty435eTRaQvIfS0zgZ7kQKBgQDSyytoU8ssoIP/CyIwbRGpQfnqHPeUcBXXntS3P/gbXV77iDdvQz3YCI0wmbDPzMVxynWbQuquPZVxhKInj58SF6DW9qHV7AuDScVY2fK37YbrEg9UdIX0WJAYbtiLeA3ejZ8BqOUeh3zNPrvx3z+QLiDIEFXU3iIigsIxFIUZYwKBgQDCo2r03ArJMeaCgD5i0U4ZAD5Dy6AAeYke2mS1L3NHGjIFlFNGmcc7LvChGGzCv0fktFzZTbD1reY0f3eF+JKz3DjhprUQRdsWKdSxnFagJmUjujal7BJvhBYg1oiqRAVXmGQXtVgylDU8VKdK3dAoBYkufcEoiG5P3VogTkBTWwKBgGm1CvqRcsTZZfgjPCzutTmc5Vfa2OkuYDW159RRlvkaFMSspaf9H2lTuIITwJAkjysmLV4D664fIe9AZRTTuCCZisXh/nxJl+hpuTZ6bXaA/fSqJNfkazyCoRgvlhYyyTm+6WsqqGNr7FD80cFUhAqopzXMw04xawrFad60/J4jAoGAIpQbvVKWS/YkiIy2CKI8qK5lYW/8hfkRhjywZYv/g+NAfcNDJCjPv1DwiP4o3FRVNmlgkW5/ALabTjpTBqcJkRCPvm76feCbMo3N7pviu+L2VumPKd0NzWf+8miKsQ0SkeRN6/RYreuspYI4klFj2KhbHbpTpZrPVjrx9wlP3j8CgYEAnz/M0t8/PcYvjwRS5Naned4gEMLFIPkwJdkQnqAVw6W0s2ajeUTEcCRYKT6fVUqyAj3unH9R1k7YT0kzjFLNypYuQjVbQJtVOSHnssbulKKlWcs9dwW3NUTxvf6XdOP+6ywZodwGLSZpQJqMpwEvS/qN7uHlkAQY0gPGLtOC0MI=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCgRIZTcbtfOQI4aEpjVgyVx6+hR5z0TurjppSOv3p6e/Q5uBcXHBx0LjP7bLW93oAcgwMAPZwXBHOHIXq//XtBdzIAYDzUUztfCk76AXTWr5MR3jafXqf5lwyu5XLKSa9PiZHDwSEQDY05N4WeCS3AMyprcu1Z4ey5W3nGMy/ClPicyLPhCgyWONhk+ig4bVNfByP8AUcTHAzN2hgfhY9t6f5aqvXodybvG440GMt9kMH5sSYdZrqPL3Ixpmz7mOxkfbc6gaXh0M/pTD+q/f5JTrYZ7NbF0jdrLWcx/wAgJA9iLyrHG93ObDvn1L41k5H5l+QBqTNQzfz9mnv1Sx8xAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmUQUxKeaJrVqEp89fpG38FKJKh8sFpyehCKosevfnE9bJ", - "privKey": "CAASqAkwggSkAgEAAoIBAQCdIzV6DUcwSmKGhCT1a2vs2zlhDs/0I0JR0OGTaNgDYeFHGM6Hw1a3QBoxvP+odOncCOkCbGLIEMfj4Yca9RkDvuF64wZWqx/tVQYvXJ5MYx25gfB2XQq5FMXxqaASP66FZN15kc8i8Gkzxt9Ff+My4vk7ve0QzXokSOcn46LAjyGU1D+abAS5RnAyBwsTEoXiWl2yTR7iunKEwbtJBIuCKtCYThRnbvTmr3wX4Ywn8twbJEjg+esR+MTrGerv7dqPtbB6/q8QPh0CCT26yRTpzOjielqqYs8ZOTBwe/+a2Wjnjr+PG6zPWZM1xl4DtxmTImsLUqRe3+48NFhLv3h/AgMBAAECggEAJ3V0804sRzMWpKLASSSNeG/ga7/1dl/4QmVKj+KvA8JreJgBHNRvjRq6uSy1ok6hfxB5upMPByA3ocC7VYignHEtW9dwewkDvmwwXmpKkfH9v9yiToa0r59IyZOHz61QHM0kVGfJ9QMb19WjsWcY3Wljnp3lzudaOYxZB4pBD0s9LzrSfiJ2HI1skgxIeMLtU1vvVhz3KUYar/xCjfuqqeVP2RKcDNbnlECokndpzVhqyQCHvcdNitpkkGmG7cwTHReVlZnrFcZeoH4pPKRdebZAxBZXg7jtHNBwZK+8X4hVxZgVRE9b5q1RQvFl75q43aB+CgiXg1cvmaG1tZCxAQKBgQDJk3YH3+G5PnAxXO0tejc+hY3mobFyNv5LYtx+fnY1Osu5rmy4iz2XVaA0xlegQwhxjPHDTd/ePVDAOaAANLa+iFT4Sv+dwlBNICfIXtM6eNhDjECY4kfZ6SDS6CsLEGsTV9d33ybq1vSlrYYWeGbShURIzpGKbTSKDynjgbPrgQKBgQDHkD84Mi4DwJiIuuxPesf9BHQw+F4dycDZ7ZkZed19aH1pWk9J+sqlDuLOvpGeCq52av1rK/t+Zn3vBvgJgs9upHq7zx+NgX0uvjr4V1i1Nxu23N++DBuhFRD9HcnyS+rOXjRVmoK3NId2YxwBZ0629NVBssP4fBqxdZ5uo/Vj/wKBgQCR9DjpWL0bIU+hHnUJkc3AcnmdvgQ6/ADC2xFmcfDrd+gdSWOleASfuDspG1hFTWQmu/QuAwwO4fy/QrpMi96qNRK5Oay+MP1t6tODbM2rL+b/eeUoDegSq4+9xqer+jZdqiP0wtpt/jjkYbGOQZ3J3v7jbNbLEWmScYpWFgsNgQKBgQCMGL/I+7FCARsUIeVzhoaPIWlQV4v67X/tfddVAzByscAZDcVL8jwA1Ap1iWNAx87iYwm1CxNrERinjQTj6GknC2D+J9HGzXjML8/GN8uWrDFQlo6cJHPhCaD7kMYMyy7z4T5sOiQ56S6P9dPbSGMCHa74iD77WmSC4Edw9Ll4kQKBgDGjPA5lIztQGNDZwdadsf34AQtN1k+JYziBj++alVdk57CT26Il7w0X/qPLtEzqigZgjWW6EfFV7f/qKpSjl/jcpzJFeUC1/mwOfTYa9HoX10gV7knHZP/vKL6+Q8woHE8rjUPswirX9jxU1N5iYlaIBkGbfDbrIBO2GbGQW91H", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdIzV6DUcwSmKGhCT1a2vs2zlhDs/0I0JR0OGTaNgDYeFHGM6Hw1a3QBoxvP+odOncCOkCbGLIEMfj4Yca9RkDvuF64wZWqx/tVQYvXJ5MYx25gfB2XQq5FMXxqaASP66FZN15kc8i8Gkzxt9Ff+My4vk7ve0QzXokSOcn46LAjyGU1D+abAS5RnAyBwsTEoXiWl2yTR7iunKEwbtJBIuCKtCYThRnbvTmr3wX4Ywn8twbJEjg+esR+MTrGerv7dqPtbB6/q8QPh0CCT26yRTpzOjielqqYs8ZOTBwe/+a2Wjnjr+PG6zPWZM1xl4DtxmTImsLUqRe3+48NFhLv3h/AgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmT3LdBHJFQUcNfCD4u4ecknh28k31EkrCBAnaSRg7ngXc", - "privKey": "CAASpwkwggSjAgEAAoIBAQC9YSjJQC8rMGegKO9Wn2sZnJt/BVDAv/druvl2uB6mgKc7GBLQNyTOw2DFsktbovXDMSMhYx9FvRa2rAShGzzeWmO9HnyXqu5mzL7r6oVhtb5FTHG9j6PetN1HJEiUuI7xSEjh54DI9GWfsnwhML3PXT1+TsDaz2BTBoNWW5NLKFZCqitIxMHWk8R1ooBz0gicqvRv50isfrIDAL9teiBNcfbUNSiXJ3F8ueq3NPuUKCVaCPoFrCsNlK0h4qGEV0jb50f0CkK4sKzeDcg/UD/S+jGrkNDAaA3f7cuks7JeVuL/gJpCaRQlBBjG4lt/FK0bmrPSm2AOL5wOUdGGUgbZAgMBAAECggEBAKJ9wykq0U4VclSRywpgLt0C6sjKHsfD7t+YxoN+542lxdeGiF3vcr2WFmqK2O3/nS+l8aasDiEgZWTHpBE39bozhHC4v97C41uBQi/aQifccS20scMchFaKiXKJR12UHdIZW6+5m17RlIC5/Jfd4n8SWbkOiZs1ZEjYxchLOs64i02bumZuj8NT8OMFEyfR7fl4TG08HSToCUvRNyGAz8T7p/fy64/Z2lw+xP24XCW/YHe1gD5Bu/QmOys1mrkT14w8SUhIDW1z/Lr1+mnfe78prPRxe0Z7BR2aidHVAu7h65LS0XuFvi9uOl3qrzTrrLTwXyTmUhJuqN5YnJVej6ECgYEA5mmujbZ+n9qKuOq6GVkSf00LqmUd8oVbEcI/d3lT/u/omapmKPrfPzz+VFrh0S7I8rCYlLUmoDnkBNGzF68llt0g3cYTIO8MnW3iySbiCv+mTCYCfzlCxPX4N+zW6Az1ofCds1SDMAZL3qZEowwy2iresS8KmEqj3gUYKgiFNcsCgYEA0mj1K0YdPZ59klb7PZAScIeScT3rH7xnRGH+2tNMQcDW/W8H6GXhqZJBrqH5Fs9y6Ndyi3dWQiQiEvuBIT0uaiMa9UkFDhhyz05uIajBqBpJY4JJAu67whUL3ng9uFhHF875wSsygxvJMNuTf9pgThyibKvq2l8O6gJfFMx0QWsCgYBKIxD+Gg0uJCRkkWolw8o22bR6NCTpps0Brs27BHfpXIor/271mpsAfwCaZc+o/fO8WuQNXSg7f8UFY+/LHBjtLONpWFVJUIFvmi7RaEhtH4sDj2tYQjVgqIAghn0zlw/l9kTXscawSiZZUohdKgymtAqJWkh/bezCAEOhKrKp9wKBgEYgy04QAWDvOSUULoq3QR4WYX2yyHH8ZmLJUpr2f90Oe9leL0GK62qMH64nuBCdNcxbOoc3UB2dU2oGP2SnspeXeb21B6VKCsIDfvti9qCjmkA7RUBf915Zi2oro06UxaUuy9lRH3XJRgYtuPyM+TovmwcjSZRcyGjAP5Z8CmdfAoGAUBDcDoxjm4qn21HCt0Kb83fxhyd0KAeCZx1DkA8ENu+sGaq0Var+iXdJwIzH84HfPzNVRylSfis6p3tkKtuhiVLxE5aNauPPD5oKwf5/nr06CgiMG8ENz7vdih0LilThK/cSGm9M57oITBkrw6fTEMSqt/18dRuvH7hD2Arr0l4=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9YSjJQC8rMGegKO9Wn2sZnJt/BVDAv/druvl2uB6mgKc7GBLQNyTOw2DFsktbovXDMSMhYx9FvRa2rAShGzzeWmO9HnyXqu5mzL7r6oVhtb5FTHG9j6PetN1HJEiUuI7xSEjh54DI9GWfsnwhML3PXT1+TsDaz2BTBoNWW5NLKFZCqitIxMHWk8R1ooBz0gicqvRv50isfrIDAL9teiBNcfbUNSiXJ3F8ueq3NPuUKCVaCPoFrCsNlK0h4qGEV0jb50f0CkK4sKzeDcg/UD/S+jGrkNDAaA3f7cuks7JeVuL/gJpCaRQlBBjG4lt/FK0bmrPSm2AOL5wOUdGGUgbZAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmSeTfyubWsRZRcVczDxzBxhDJBgj7MhgHYbwv7zU3ib4c", - "privKey": "CAASqAkwggSkAgEAAoIBAQCwZ/HZRcMVjQ88dKCc5YnKARHUGBLt4sj1vCVofk6fotl5qojz/pNN+lCKtxi8nmVt4GQDVQC/xl/uB3iHfFkc6qho1XzaaRxI8XaBSJV9z3BOEGmrXBkISluDnJAXhfaPquZJZUhWoXjO9H+54G0Sd5sXeLDlzXSUGt9tMGE8tWggthN3TWR0G3jKw8oacoQZKOzJneuksyf+RZGeIY85THzaT8tBBCF3s/TPK0SkLbHyy/Yet8Y+8FSyz2sQPLLpeCRKnIZ+opxvtE7M7/mwD6NXVJ155eNLrFLBQOWOYU2bI451YQXCErJP98JwYeKqaYCETIw+jWY0ekc47edLAgMBAAECggEAYHnYmN1AXg7xYDzggi4+900ydO5dm+BFy68EPmulkES9735GvDpkUWcumU6dprpx+m+YAwKAEGHroQBQ+LgW/GuRgxQO3lxR7cqw5u/NYisK3oa3Y9JQlmokNoxveY34VIZAv682qrpQmc658+w7ergTB/knteZxdXZk7xBgfZRINv65p9ArT0rvsEOk3Ff6uq8g++zXN711b9xFMBmxJT1haHGT7fX7JuyUExCSqTElXjSMFykk40zOxjMzDQpzjxcSihffhTRyhScIjpru372BrCmL1WCeDrvN8pEV5DIJoYhgvjGLI1RuPGaGwqIGiR+OwXjX2kcyRgkXnVPPkQKBgQDbjljw1em8741Mkq8N+W/voAJZ59O7bUALSaxjrU8Hpljgd6uu3MI9YVTGp4gjI63XIHo5mKlONXDyer047E/58KNNjl8v/eyLLDUi/9lERzD6TPBsRspwgs172Yljm3UpmiWDfveiu+7/qVbrSK6b+4V4aAnRH7kSyUU29qSgrQKBgQDNsAM4qw+48wEscGWbqM+PlUOwVHhpzyo42gM8y1xtYhRC9oaYeUVpUPwXzS419apreart6KC/AFt/2mBh75pEBk/Vp9Z5l9a13yJV4xdTnDxRAUh+Zywqb6mCTqWFBnqeoPDFKZBcYBDqkgYkCuHLMjYZcebP9EC66yIcJBeO1wKBgA9kzKGeLfQ8S4jp4/Iz4gBIFMIe+f5zK4FfGgInHZpotGSQn230Nn49O8dt6aKlFsQ1l7xAEubT4mZt6qR6FSVuFNUUPWJNCG+9msAodiBOaYWzLUw6LmlzElszpmlgdfeDwkuU9GHpkVlFkz2N7Agtu270xHNwKPbDO+Idqu9FAoGBAIgZ0Hfd0QB7YyppkQJH2FfU175ElozE9NY7g+rlUVpbjLamc3dOv1wppzWEofA4hzSohC76P+tCrEjUUfRb3ALo/kiMz0ET9JHRfOHB6zx64/ph0/s3/6Rw0IQV0DZOjDKMoeSEVS6arnbYetG8lZ2jsuJxWN3/bBmC3sYqJ6BvAoGBAJT5hMfOTN+y2+DbT0o3E+bCbGGVJaQA7BBD53xoyvO414/weVqJ6JexW6hYAiTJ/VeA4Pbs7nvxZB9Ych9jj98Gmm07LRs59YcuICxsy7IidFwvUyTV9i8dFxmc02kXTqJZXihwNbPz/bKCO8DpMRPjk7Q5cT1uocsfZyEwyguY", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCwZ/HZRcMVjQ88dKCc5YnKARHUGBLt4sj1vCVofk6fotl5qojz/pNN+lCKtxi8nmVt4GQDVQC/xl/uB3iHfFkc6qho1XzaaRxI8XaBSJV9z3BOEGmrXBkISluDnJAXhfaPquZJZUhWoXjO9H+54G0Sd5sXeLDlzXSUGt9tMGE8tWggthN3TWR0G3jKw8oacoQZKOzJneuksyf+RZGeIY85THzaT8tBBCF3s/TPK0SkLbHyy/Yet8Y+8FSyz2sQPLLpeCRKnIZ+opxvtE7M7/mwD6NXVJ155eNLrFLBQOWOYU2bI451YQXCErJP98JwYeKqaYCETIw+jWY0ekc47edLAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmV3Gf2G79yWWGLnKQHuDuRTQZbvbnhb6NSKg6yGTKdibp", - "privKey": "CAASqAkwggSkAgEAAoIBAQC2qI/XfXskB0F8LzOEQS9Yx25lkj5thj+ddMMsOsKV5IDfFNKyd/t8T6LhC90Jhxsfm6BpN3cSSuR7i9pHBVHdoPB2n1LSTyHYi95ZzAm3SRy6SA6G3n+kXtQHaTgqZy38qEQozAeLUOoDFU4SGW0ai1cv14TojXnsPNphim3DwHRceouOmWkaIA7zRCdgNkB6Wfx9D5AVw5RravHrSsdn+jaBAXLjCqa0zoLJWPuvKlRD948JHyTG4ozvC260arxvqOtSlBDixnbaKAnycna7j6Xme2gJ7/t/cDSbWtBJGOYczst5u2W0zxfpqLeT+//BPoz5qhjyTrz1EHQTPm2RAgMBAAECggEBAIhjxUR7BgAZCuTXuff/VINOJzjgwoy1ubqw/SuBlNqoDTKGMe3heX+RV2YDncEHiVFIu7bVG6wlEAbQnuR5LG/5RJTO0uEHBZbUmesjV/3sMe9G7tH2QglSZbBC+RVwhf4rBvoPn3J/sL0so2cQZU90zF2E6FFdkrS7m7VJ0Dxhq4wAl8Qveda8hwvi2FAiTLL+8l2sf457pt2XF+jXH7qb+3uC7YuFCGKohPXK6jmsNPlvMZC/LIvyUNV4unla13t9VrUl5BPwUaPd6UI0MwiSDVwwPRK0m/pQVw46CKQNiX2lGoWFKyJjqa/OFtkKQ8g7CD/jPpbTiUl5JzCwYCECgYEA8dRJEmvrHZZiUGxwCxWc64BXVvDqOmakFxNUjxITGb5X4X5tWEYmGODEEoxmujfVbM9+xE43I2AaeowKBZssm8ebi0+rIP/SZJcMHXLUfDOj31BF2TJ7r6mAQBQ3nBPY0NpW0aOja6oQiGznxe2tPcPW4jKkKXvE2WSUfHXTIAMCgYEAwVyloXRV1UvLpnXegmQn7vWDZJI4WW9eJ2BicRP7E2RviRMo4JMAqDzUdANxu4UxQScxoVcWuZHPVC1NlDGdH76VoQi+tBKObc8vcPR6K0MM2Tgu+xf6pLthp/LlpcztJeZHeGD0nAxB3cWIc+SowM4UAVnbzMIRW9EWuisrWdsCgYBcwTDZ2PzQV2sUL9N13O9YQNy/Ix6kEdRkaWyoh6U93Y01l1l3X0ijiCqMdr+8M0gwORIFV368mdLuKCJ77f3ZLmGRuJgJyzW2kVz7Op0XmnMDZ3WzDjL0uI3Rhi+iNNaXnPdp51r6I7u9qA/qEfS92Qzlq8jdhHSHcZWme0bkYwKBgHFNWniK9Kixazm1I5cAHS42irFpxL8TNPaZ0dU0whCQ75JAudkuClqKmmsIgaJB36Sv1LMXludR+0z15tmJYOpzALaFq0lU/kR1/PSRLO0gsuytsUnMuT/B1O1WtR48QFHO594v4eV2gTn0P4q5V/DyUGKiRttqdEV69XhNR2+1AoGBAMgpdNgV3hogNUHJqPvT/MuGo5O74NK8xp6j0FrBlZUlE+8JSB8pxI6IREBifOkDZ+cy7Lebi1REPwZ5QVVtzRpKppgKJO0ybd3XFI2iEBMebDQMMZP/7Lq8KO/FHrNgiQGQfhnf4vBZWwj4ZHcI8+UeXcctTSwRYFqsFl3Vbyif", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2qI/XfXskB0F8LzOEQS9Yx25lkj5thj+ddMMsOsKV5IDfFNKyd/t8T6LhC90Jhxsfm6BpN3cSSuR7i9pHBVHdoPB2n1LSTyHYi95ZzAm3SRy6SA6G3n+kXtQHaTgqZy38qEQozAeLUOoDFU4SGW0ai1cv14TojXnsPNphim3DwHRceouOmWkaIA7zRCdgNkB6Wfx9D5AVw5RravHrSsdn+jaBAXLjCqa0zoLJWPuvKlRD948JHyTG4ozvC260arxvqOtSlBDixnbaKAnycna7j6Xme2gJ7/t/cDSbWtBJGOYczst5u2W0zxfpqLeT+//BPoz5qhjyTrz1EHQTPm2RAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmeecBL9V5NybZhaKKMcSxQCaH553JCup6uf1oqhQziL1F", - "privKey": "CAASqgkwggSmAgEAAoIBAQDVHmqxlszl0nTg0jJTgt+7ubcx8TSwn1UGdvgkcb6Wi05UkcW1qqUBOaeQR1P3xe0rCLRHEdWBmG/Ap2GgOXh2xqZYFfb/LUU5+B3BKVIFpUk4WyK1T7BYWr10zDeWxbi6VGAOk4n5bZ+DGSjA8utJpIAVeQyePSJkKfrs33pes2cR6keshYbDuKk1cXROVaUacEoHTL9iSly4LBJ7bWaG14g4tPGBOATiRtkB6ABkdgO12aq5CwuzgUlfdhfQqzaIdwn8/tw0WAg4A1/MyFq8XAl6S4LwWcM0FL3qJZeZggpQ7CMJYjXyt1mAjGG99d4jDNl2npayWPorr1eGeIhDAgMBAAECggEBAKtOWsbLB4JIq+g3LXrRPRQBkQ7U6tx6Bnc+0/E/eMo7ycfSsNB5DU8xz836d7U3ZI9t3LMv06XrKRD7uk53Q6x9uyIc7cBp3DZfiVNF6oddN8DUCM8i8gXjUlx69sf7wKQNxHSTBZn4EvrnE0odOSGl18rq1UiwrV9EG02hyRQquktPlmABrp+WRP/eAcvq9ZKTyVlpoZnmL17MhdGarA1O3abGIsbbXpGC3n0jAzleG3fkaXO4nAWwQxBScfIV/wf3JAbyG+4sSYBhSDvCIjGjecBdFvBUjjvWoedO8enL69V09DI/PPfeC7RZMmGhySqIyG1hRexALYsE262n5ykCgYEA+313sml/E7mrCEjvSb1okAGb8F7rEVSh0HjsMuw9C1hmDmYba20a6Yew3j65GRuAYgyfGaG9ftPHwXqPuf0dzCnQtf6Pk1DwuoPLaciAqFl9f+jOCaMmzpOgMWC/QWlZ5cnpx2x/VkF9ssCEgPNP3jEPkR3k+BHKEvE7xobWxoUCgYEA2PDLqVREUGxByfex/c8yz4xf/eb6exy6p0p7CUJWIbYtgSmVRhUORfG0EMHONbZrvZMgt325WQA5eKbKm2eozIUKNnnjS1EViYDUQ0yQrckGZiH7ZlUZL+cL+d0pMlrBzkMsvsNBdJ/lThRdeM+ksEykGVkwLahqbYwcTbvqQicCgYEAmf9rg3msUiTYgXs/4/SzCbOijJ9i7DrZ13GkmU4l10OrQtftpGusFiJ8AKuB5sj7ZY77AdQT2IzQfj6Rsj83tuRIJJmby4a90kiQD9eySOR7wA6L1ETup4KojnQCyYg8f0ST/gUHOIdj9EiFGv1jA9khAii/I9So286SXvAEpo0CgYEA0PJMFpF1IsjCLNcHdmBkngakRhZ8Vqt7E7nm+yoLb3jaJzd38QJCtxdvyVwBUzaaWwMkVdcf+BsBP7XWGwwiRqo1Bfcr9tToG4Ib754FE301TpWYYB3CnqK4pDZhgYBsfk+w/yNtHfkLkMKIrN3Bz5Rh0ZBXmQJHT6/Nawl9Pa0CgYEAsyT3mt0dbXyOGK/qdHj/BQBt4FpPN2g8EVtT2sWWjZhWNnsNN3ITHeMG7GC+2ewzW365WXQnfmBOaWLL3yUnA4weRnaQWiwHVhcy1TH2ezR/lQ7ihwMbxwmlS/6O6Bmlmvh4DsjntJ+vZjxbGTC9pd3e4t7b0fhGJVR5lL0wLZE=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVHmqxlszl0nTg0jJTgt+7ubcx8TSwn1UGdvgkcb6Wi05UkcW1qqUBOaeQR1P3xe0rCLRHEdWBmG/Ap2GgOXh2xqZYFfb/LUU5+B3BKVIFpUk4WyK1T7BYWr10zDeWxbi6VGAOk4n5bZ+DGSjA8utJpIAVeQyePSJkKfrs33pes2cR6keshYbDuKk1cXROVaUacEoHTL9iSly4LBJ7bWaG14g4tPGBOATiRtkB6ABkdgO12aq5CwuzgUlfdhfQqzaIdwn8/tw0WAg4A1/MyFq8XAl6S4LwWcM0FL3qJZeZggpQ7CMJYjXyt1mAjGG99d4jDNl2npayWPorr1eGeIhDAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmbcRvn1ZcdqsPsrGqM6sgJRcskAKratagpVLXF96hP9Dt", - "privKey": "CAASpwkwggSjAgEAAoIBAQCtymwBl1Fp6mEyF5tcv6qvHoCLgDDREbFB1IrkYBDk88FMfpYIJ5WA02pN+hZ2S6BoA+uNBAgae3m+zfsSav1QRzyJJUoAYcZ9XMiGkFKAJyGV0FFsKP98I6ic31yaoSd78iROxei+lhkXy7dvfR4z1maVW2HjhW6mMF6Qz9i/zdc3txq8fOkJvb66l+H2RJz846Mz6h7OrNmdRP1ZkH31+Rllc8/itwqM0nsoG0iKq3CrMHwflr66Byt9cLfdEBvElGmHFt+eshq8ShtRjVRIEW/yGORra/h1y/G8WhUwLoL8UpgNkIqE7B0Rw7Zy+KmbfaNEcFQhZeLm8lvkW6gHAgMBAAECggEAckzisj0yV4XGPSrXjK2mdZyLELTT5n1LZq+CVed01RAYPtY2mNBn/J2Pmg90fIMK0b5aSpmvNrOlA7/3dEqXphfkEZNL02p7IHJIlHARQqX56c1j784bEitltx8UicKZ9GPySzjQ9aBEiqj6UUIp/g/x0iOTAw/8ESNY3sdEmAiUdc/rBjbG1qRC8gi8QOsMqu81zh0qh5hp5chQucgWFzeMfYyNmjg8Pohx1GhG7os2XMUxzaBnI0Y4G9oVAObWPqYOwUHZTOQYd5faj+k8h/QcuTVtBLXvLjjmwwoSaqBbc9diorx55Jo2lg3tyoAU8i5FX6FmUjjC2xAyvMUhYQKBgQDiKiDnjnC6Gkq7UeiqiRzC1OeeBTqXBDcV2kBcDOEy9f4gApZJz/FjZ57WALWRAE6VM5Rd2ktai8s3fyZ99yIwqHfeWa0KdVTmKk8KyijOfxB6KPfajS0cEA8LRtrxsHo82a+Xc5hytl3MiLMH0DF1ALwcQCgMFXVGJ8E608Gy8QKBgQDEt49pjZNrhnRUDnBy3NXzxDGWhquJEbg42+6ZLI91k6HvOr7Pl0KZsDZGKYdlgKUuJFB3agQGzKx7ZWyRXaLZ6CWodMIXvH68ffmylsbMBAxOVpDU+Oa3jJ8rGvWi2CcsqR7SPSTLSPIWTigQ4vJaAKMDnflWSQld3nGpEXUadwKBgQDhvJTtKkIfrsBqqX2mQYagfKrWEXgCZaWpvRbCCeT43YkRYCOrds8Dndhu13RiT0EgMMRkzM6riJ6EPPgpgHLyyCQknbNWnffoZ9BO/6qtOSw0EhIZZRHiUbECW22LEM9hTxGxBCLkVFvZG5Q+NzI2C062j96o+P390Q5P7i4GsQKBgDu9v1j//PhXsfZhGDdZ58QLHkAnj+qlrfvelvx/suWzOyeLAK3MsxY3lJQEQrFJu2Bi+Oj7ElP6Tpt+9tTCyhVBUkZxhwxsW1TlMTLSZXdJ927HDV8QZAj0NNaDbnvRBzyh89FHbmgqNBMgEzzln1JEBT2w+SsCLU0LpBsDSTwLAoGAfteotaR33Qchy3Z5vgIMzY0txZ7vU3sGErxjiSNdmv0Yp/dxHWeGv/jvHm57BV+9iyANDicjrzNH2XtOX13lmYlctgAHRkCLVEdHxw+f0xXMto+unep4fniX78CDNUQAfvmBl9etJMmGXYNsISANxZPgHeMgi+EkMjAj9tdRUic=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtymwBl1Fp6mEyF5tcv6qvHoCLgDDREbFB1IrkYBDk88FMfpYIJ5WA02pN+hZ2S6BoA+uNBAgae3m+zfsSav1QRzyJJUoAYcZ9XMiGkFKAJyGV0FFsKP98I6ic31yaoSd78iROxei+lhkXy7dvfR4z1maVW2HjhW6mMF6Qz9i/zdc3txq8fOkJvb66l+H2RJz846Mz6h7OrNmdRP1ZkH31+Rllc8/itwqM0nsoG0iKq3CrMHwflr66Byt9cLfdEBvElGmHFt+eshq8ShtRjVRIEW/yGORra/h1y/G8WhUwLoL8UpgNkIqE7B0Rw7Zy+KmbfaNEcFQhZeLm8lvkW6gHAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmfV6NS1ah83o84zMPfo9hCHgZ52X12QJsfixvfc9gw6v1", - "privKey": "CAASpwkwggSjAgEAAoIBAQCrRsmO5XAh6ObYK8t6BTU07TxuJvwttZJ+3OgTI0Ek/dyLztxEKY+/HXd3WLEb2uzvLHVDnvY3y2GFdtE5hFygF8vGtj+bz+fU8ax0kIMLNhyOlb8rv7twgp9syK/4PI10DS2LOAPNyUI5EHH68E+rlZwG/AftCVBp5lLCq7wxad6h0VizeoX/RzPXNxWpf4NiNUlbwAA/EH7rOUgZO3YWkTVHTehAmnJjQW0ZOfHFBQ2WTdhKQkLJCzFKt9466N01OdphW1F7JhVCqFK2SQZysHR3dohrsIGEu1TMOUFMenxeq8OMqzwqO+u8iFmxATygdqM5pC4nKOTevFYjWhvxAgMBAAECggEAHvC/soeyFP4czYpDzLwqG3CLzR5PyfYWC8LeTa69svAFKmBpHAsiA5VQIogsHmsTCDXQzTFnKzcbW9/V9fz6OpVx42jC3uPU7nvl+nysn5bb28ojacTOGIoQQLeUSlSt/PvwcUjiLwefZe2ZmYpV6hoxwHVA/UoEc8z+wFoDui0pEVdGYTvsKSKGfAkD/ZAIl4qDbSgTJhkh3OxEONlecBdocdwRdbqJJijpSN1snInKKFbXwvPfGAdRhezNe7wtVdWcUCHdvMsZuHyiHwrqd84J+Q1f+cLMloDVEyz5RwxlWhsCcFDs7AA15VyMErpCv2O/vaSphmt3z2Dumr+xEQKBgQDfcdTNBP5o0mvqkMUR9ODWEfC4JgZSRvG3cuHkPiUkJAVa1SH81d9mlqS5fzF2e2fUB8C+Qa9ZlqzBAA/S8Oia3D7T1SvAG9Iu+N1o6nrgY+BwfFGQfkJs5jp3bBa8qjVntcA0SeOipO/q9F5iaFK+2WvZa7vC2XOe3R36Z45KpQKBgQDEOyiOLcwRphATycwIRveAtqyZLKd81kYBRLOneEbhUpRRbxyPXstq7WngeaXCn8ZhfhiBgZCjs37Gf8GuQNyUNrVlF0OShplzegbiMRErmXkHe2g9CHbUm3IEwGxJmfOub1nIwCUCAbyp19/lcEnd/N+woEuaLnDUjSkHpZqmXQKBgQClyL578zWTrnQVUJ53KTpcemkhKE1OZIbZdqp1f0ptWzCB6VrTThf39NN5Mg8P+pXZsnrmbrPcg7ffZt1WxBnBNKKE50gTvFChO1KDkl3i+RfAPe0CiTtdsyA0FQV1q8/+B9L4uM3lkfzUVcVlvEOQiJ7FbXKdKlvnxeWFMapYZQKBgF01pIv0oQx5DwX3Qt1jqEkRfGa92UjpFxOfKJ8R+Mkqypzr5GsNoh5Ga5Ze8ifCcR76IHXTr3qy1jM/mCZHVP9qBTvhkw1Utist+XsTx44oNl8hdWAYVymiNMShCk7ju+ZNqh47dti/LniWvBll/xBc/3wMiBzSlnHAI48oUI9ZAoGAKRycRjjgIZrrqeTA8fJ7iR0MB8faqDrP81LD0ykEdu4spF1cw0fKb7rLWAMxYzrbIQkbaYMGQLDIVzGlga4xS2YNvDwcu6YKg89DtpAsW1dHs10rIgkb71ryaD380qtg/ZOCkzR6HVgtRumndDZkZb4b92t3DxlqBGa595DyVjk=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrRsmO5XAh6ObYK8t6BTU07TxuJvwttZJ+3OgTI0Ek/dyLztxEKY+/HXd3WLEb2uzvLHVDnvY3y2GFdtE5hFygF8vGtj+bz+fU8ax0kIMLNhyOlb8rv7twgp9syK/4PI10DS2LOAPNyUI5EHH68E+rlZwG/AftCVBp5lLCq7wxad6h0VizeoX/RzPXNxWpf4NiNUlbwAA/EH7rOUgZO3YWkTVHTehAmnJjQW0ZOfHFBQ2WTdhKQkLJCzFKt9466N01OdphW1F7JhVCqFK2SQZysHR3dohrsIGEu1TMOUFMenxeq8OMqzwqO+u8iFmxATygdqM5pC4nKOTevFYjWhvxAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmVjmfRFrzBbhAUNQ45RcTik47MJe26933z8EHosgVEsu4", - "privKey": "CAASpgkwggSiAgEAAoIBAQDCIO8WRwEqMJeuKKCtZrpj9UPtmeLVLo5V0u83sklprjxHD7hG35/YzeFJzECApslKNMExJVb0dw2n8TZYHaCiBB+MJQjjyite3OebAlDVmqRGKIFtbz6AAwsQY8f5TViDKzSIjsOKPYPQz9prAAmZ2Ah/vhlI/azfSyi2Mxu0zhbxtw3txtpUXy3VPkSa4pLeGgpBr+oeitQxeBmrWVDkVoKeH9Po86QDuUTIVRiRNYraCbxawomnlYgNEEqWIhspw4LAicvGjLd3fIP6XM5WtRHGfTWzf2A+DCAzMQ9E5E3wXWS4OHISx2Dz37FLdYdn2W1A28/u9NJAu5281xFxAgMBAAECggEAIOsGv8dQij/tKIoZHO5DgvmvCBZFIZMgbas0B0TDMBlsfTxMKjB3YYMfxazN70LY9S1W6SeExDV/6k97wJtdhrueQdxx0naQvihFWcKdxGrRmlf6An2PopNhh+jzmvGjpbJo2RMkU0e1F253ghdiiWTZpBevH/JsIv0SrTqjYxgXo7EQAspYQ6eMwbk9G/cRjoSoKqOxQKBfNPLj4gE7ZbD07S30th2udB2qOiB1r0j7hRR/UuLkbrcRqgLFN+yeCU0nw08BVpThoFDWRF4xiQMqOWjvEIkomPp2WO0b2z6QFfrYqvYbE7kMYU1U7/TFPxYAY1evUcdFBg4bAI6DQQKBgQDmwrfLSWvPS9rwUSYaZP+PbGWikDf1BdNU0xRK9Z3P2rWzxx/jxmrkvRKJYaYoQui7pQKPc5OKcmTqvOOBDnvF2PyTnv/0QLC8n5ifDAW4/bVmS8MLHODsSp6ek9HzIumnvsHoRb47gZTc71YUrwTDNXbjSZVqV4VP2qomqzFmCQKBgQDXXIX1iytD1xvDHnWUfonaDs5ODBiFeriPn/LF9WbKhkSsJPTiGrFepO9tD2gnNOwEKmhyjirMB9fk0Lq1Ds5k1/6WejuSyvCADnO32DzqqDQj/a8HbKpEcoQ2YLPvCwChhyo5zudD3On+6DubJzdyrOh7m0Rmhn2Q4YP/y75qKQKBgCNAxAtOYCX/FKd5/jQyEci7apt3JNVN2ocu5/67nyxN4Uxhs0F84n+nUtmiDVxBPITOJKH9qiCQcVJbIPZqXAZRq+RxefC6oUVvrEU/9O/Z8oh6MoXUF5iBndHkC0L1pnR18/GkFffJSBCoj6IBStz3of3/E9B3JmqYoT3fEWDhAoGAGhagN62DMTWmrE1NSw7FHkA656N5ePnzz5o9q5Ndv1zihsP3UkiPgfqS8nAyWsWDbcHBY1crggnVMmfCplpD0F2F/q6R9udUmP6nL/cm8fosTsvVXx3fxmjk8T1nrqZzjh20lMomo8boJbP2PIZUpjSh+Q9HCvBx15IqDludFnkCgYBWDyb6zJmWYA6f96e3+qAX8XO9MPUkQXCIGvZZllg3hladYgcy/GEi17hWenXPIIB014ldAuYbttclh1dAQYBWhxxLofVnGXnGlHuzkB1YxPucSvfzJeRusyPMbYiIY7V2OQBN2abtbnau+FJUnLKKUFGQdoSZsOxNYAPbCh+B3Q==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCIO8WRwEqMJeuKKCtZrpj9UPtmeLVLo5V0u83sklprjxHD7hG35/YzeFJzECApslKNMExJVb0dw2n8TZYHaCiBB+MJQjjyite3OebAlDVmqRGKIFtbz6AAwsQY8f5TViDKzSIjsOKPYPQz9prAAmZ2Ah/vhlI/azfSyi2Mxu0zhbxtw3txtpUXy3VPkSa4pLeGgpBr+oeitQxeBmrWVDkVoKeH9Po86QDuUTIVRiRNYraCbxawomnlYgNEEqWIhspw4LAicvGjLd3fIP6XM5WtRHGfTWzf2A+DCAzMQ9E5E3wXWS4OHISx2Dz37FLdYdn2W1A28/u9NJAu5281xFxAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmWbAeXxuHTEppG9BRP1mw9gYj8ZCHT7RMowEUeQfhYks6", - "privKey": "CAASpwkwggSjAgEAAoIBAQDRAiJ40gZZKj4v6a2lvxnVrmNT7DSjC++V5C+lHnAhlGg6jmH/FBHOfrdjXitqjgkoSj6mcugO1onNOm/yBf4f97J8iSRkNFkuuaTr6b/80FTWfhX73zjM5w+TezQfPzIVxZdi4KIzsJ925BjamZvtksqGurY2Ng2fjzMhCvTPTsqYWn0gyhSNvJYGy5LdGHQm7e72RN5cD5pCpjyrpA07UWo4D7go9OycHqyVxFmz9zQlZOUrx9CsWPCnH2LWdX4H3FOOsi9F46uW4fAN1g75BXIiWkRS21NEkmNBhdOmCY5YThUjT3WcSJoainCa2W07rriezft9dIJo91F+AyEFAgMBAAECggEAfU/XVTMvJTSjllx3hWGfXrMw0HdVU9BrNCZcvpYSSr/NAhauAJ6K0pC86THju/4u1V42U9ue8I6GjmqUBbq8E3SSKgKbtAyCz/X0QJGkTzKlOvjbu2ipiIicmSMMLBPatp0CWAEwnuctpL27fQ0OJRGWpdK6PqSH5HuZ/xyvjL6uVZLYoQBpvFK3vQS/x7iWkGRLJGAEN7OsiicvdPa7OavQF724M9m8yZ/iLoytmI+pBopyHwSYyNQoz8uXr1rGD/gTLIcbi7BBsbOAT7C1UNtaZlhaZkM19aM9ngz6UxgZo7GYe8erfhR28UT55vWbWvXlAvDZU0FL2zLcb92U/QKBgQDtoVFT0z3smS2onuPAEQORHLptS7owGv9FJcAvCYIpjefvk4NBUA8G8uE9X95q/a5y4tnjcjQqgWPsBEM9oEso7moqY2vjVcMNYljH4mkcP8tDAPjvYq1gfEfzh0rFbzO70MY9IfX+5ESBZbuQcXKeTBgd+f2U45HQowSNBMuLlwKBgQDhKmTBr9djZrvDpudoY3whdIXfx54p5ONZqwj6HpVqNMpKD+nEpcR6CpkcRRD9fKLtZ8LRWMvD8iAyRm7tvezYn10zXIBTnuXSnrLumA15mmZUQXZiSKNm8tcGetCWw86yUDV8CpmY33WDHrtmlTRdr45DzetJf0sKWZ276hQ7wwKBgQDXbNiCys2nsZI//JNyKrp2Eno73VwUglULRdb9jXwv2dL7UVq7mi2VWhiyADht/D7rLhbj6EO8iQKiE5c1xhx9Je6fMPS86qHif1cHFo29q2PFAZurwWR2RRUhhHRXmqFm0jT1dNVDV4N3X1fz8bU8JrXybxDhqpEleLQGd+NjTwKBgCmbOc8IfRZjD2MR3kTNzUwpSeuV6UX4g4I4NopxSE69vnt9AUdTEkEy4CP3JzKP61NPDxK8A7sLbKOdnDXWGIPWvtQUzaml/PW0WX/5HNRRkYMULZnvrjIBwXXzD8QsHm+YnqlzE/rJn99AuIQ2Id0F6ZXh4Q5NtUIOWTU2BdMdAoGAZUfXHiQW13yWkYQ1HMoy5clhqmfvTjf2xENJddudBGwU1+1X6Q2rJZpgfXVJ3yJepSetuVPghDfNIXT8Pcgn7LyMhPxZhb8RD5naafImDS+WjM6oxSw1vAouzKa5LaOzEkjylg+Abi3wC/kE6b+ncbzmGP7k0pfZMAME9lQHfvU=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDRAiJ40gZZKj4v6a2lvxnVrmNT7DSjC++V5C+lHnAhlGg6jmH/FBHOfrdjXitqjgkoSj6mcugO1onNOm/yBf4f97J8iSRkNFkuuaTr6b/80FTWfhX73zjM5w+TezQfPzIVxZdi4KIzsJ925BjamZvtksqGurY2Ng2fjzMhCvTPTsqYWn0gyhSNvJYGy5LdGHQm7e72RN5cD5pCpjyrpA07UWo4D7go9OycHqyVxFmz9zQlZOUrx9CsWPCnH2LWdX4H3FOOsi9F46uW4fAN1g75BXIiWkRS21NEkmNBhdOmCY5YThUjT3WcSJoainCa2W07rriezft9dIJo91F+AyEFAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmQT3xZJ34uwg2aqbSp5nGbo22g9dk6FurgmKV1kFqyjht", - "privKey": "CAASqQkwggSlAgEAAoIBAQDXWSE7zuoPE9GKt/qJazpxZZC9R9qYRpDQvQI8nLNbcmnGnpryPo7jYxoxe6rB0Vpgj6FTvpxDBLAdEq9uuqJq7QWhRdbzi4KZfRTqMdT06QlYNNbkVH6vy9hFaUwJQSa37FYaXNtrc1eL6AS9MU+rU+W0fDo7GFzRsz8m2616RHR9z19CWayYc6yv0+zECyP9GDSASvL6lnQvU36sqpZZiMgbIjDPwBJmpLTDZTz8Tp6CJzZjqq4RkVEWU/E98hJ506FylvwftHT5ufZxLHZRhr0MsLpn5QGeGMnbEEvMCzpUBBMzW29Bz5lbzNwdAh5B5pIXSRS/YUWCY678M0VDAgMBAAECggEBAJkveteLka3WADm4M8zq7PDbOcGbSmEF2V/TA7NQGLnVQm8aRchKPeR8i5ZljQtAPBTyNuVWctutiwWzU/3lX0HGhzm4b3ZhaC587pLFjeIFnzMSq0ZS4Kd2zspZY9A1ezBcOseYBDGEI+OO0Ugvuqd6D616rQV6iBRXeHXQ0K9mkb/1+JGg6p7i0uw0BC3t4+At8SCvZCNiqiq2t/kYfGVJ3fzt5ZY6jWiF2q6C5petc944BYHxCGrUUHwR4d58Z+2fN+mX4fHSquJlyqRUhyH+1auS5/gLzne60YH8BBTAMWSzn0GqGzLKWZdwt/ERax8LUZcl50Dw8rO2NxaT8fkCgYEA/BX3ZzMP7ICvB60a/uUxk9QamhHt+zpK9oOH5yklc2SxmgcyRMoLEIwNiclggc74iIkVKd/68HcPTi3uZiinASJOBW2ID5iLll9Sm9aJrVj50ilX63bqcP/RBVGvGDnZiJlXoM28DmUOJ9PtLq2RhHSeddB7d6ZNBUfQYmrlZe8CgYEA2rEh4oDDfHOpuibuuFdlHuVM0ytkxDSQUlTbdqeZ3RSfdUCFmgOhbVtqXliQnG75S8MipSmTjqR7SGMs1bEXb6C0N4TCmNpRH194/ejrVBAw10KE9KO3ks0GwpgC3ZsOPGnDrk5EF2OUwiwHl4EPPRQOkwpUe92GS/GUZgEpie0CgYEA2kKOpezBIc09PpEzqXR523uu2K0jdvy+wPebKJsokOOjHjCS5ppkwBvy8NTJ2TqBV34RM+N42tDLEK6WFh+mkUXJdcujHZW/bh/0X3d+VveNvdgMBpQ8YkAsEsXpqzkTTsEt7M2UwIXgnr1QQ7UGJD/wnyM2c58qWqMWGtBg9EMCgYAdvD3+PUHXVya5z/dfi0qNk+IJSHowD3GcMDuS+6D5JYe0+qvv0BSP+QESiPpIuvIcshCw4mFU4Np+cjWzbJviKri2X8/R1sV2/ZVG+Peee4EYk8veM7CPPl9v8BlbpmyeHEdmGPA7OegNKs1xdTPsOyDsL1hjazCKfPOPlxLd1QKBgQCUeBVAqy52rJQJqBSeBo+ULjdxWbSvIatU9A/RTcs5s/Zk+T7wOohFfShrthmw13uLVIok3XkNB8QLkxgJ8YBYm1d4tS2pKL7X3qa1QQfJWykiGZY0Dd5jk9biPCBUfzohYCNmkLIM0FdDjwTDS0inzxslRowOTrYMJHAi+PmfGg==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDXWSE7zuoPE9GKt/qJazpxZZC9R9qYRpDQvQI8nLNbcmnGnpryPo7jYxoxe6rB0Vpgj6FTvpxDBLAdEq9uuqJq7QWhRdbzi4KZfRTqMdT06QlYNNbkVH6vy9hFaUwJQSa37FYaXNtrc1eL6AS9MU+rU+W0fDo7GFzRsz8m2616RHR9z19CWayYc6yv0+zECyP9GDSASvL6lnQvU36sqpZZiMgbIjDPwBJmpLTDZTz8Tp6CJzZjqq4RkVEWU/E98hJ506FylvwftHT5ufZxLHZRhr0MsLpn5QGeGMnbEEvMCzpUBBMzW29Bz5lbzNwdAh5B5pIXSRS/YUWCY678M0VDAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmdwBFu8NAZDKRvr5N5CJNYbQkAqt83MM2CkpHCuRk5ZW4", - "privKey": "CAASpwkwggSjAgEAAoIBAQDF3whIIfICn+U5ciRAqEsU1dFlv5HRVIQlrQHbNh6ev1YtV9l2we6zYcY5HafE1CBISgyKQgI7SYww5mDH/468m1pFPgMDfjCYpFQETVx4V6uB/CqAuQbxXP+QSgSJPEim2aL3UBwoAB48nKhPFRTcePfmli4TLsA3HxmEqdlu+tE/GZNOcQzjaersffCCP8OVLdKecBPxq67tEjw1UZVaJzlhGx8OE4K0aYwgGsL+1l+/EwoUqSrwsaTBpIUtWsXKUVB2kATJmxvQG0oVMtRGMll7C/zFNDHbA5P80ieL+owfOkKVeWuAl25cqx8tshMG4ead/P4OcyO1clivPkPPAgMBAAECggEAXfYgR6ie9LIbNuFF59JC/Rzf99I1m1LoAcAbHo6fkcDIWnXaFXPYNySZ7atwbJ5SyiEnvUvFJYQyZ1Iu6SopDNU006az5ae5yfJW10gpPhhboDkvsbqrWlhQH6OWbdjLoze8FHbdN/1+XkgCALPBGUT0a3IrZP6RVluVUZMaZoEsLSpWLZE3LCs+eKnMw96EmA2WIlgXj3na62pTgKC29OG5yXKV30FI4TAuh5JsSZEASu5D/rrvvZwjKQYkLl5BOexMVtvM2qkQv6E9T8xk49z4ffmL+GBEKvqRVlLZXuoLJL82sr4pbXKRUKreliZiSN6Kg1eCUFyEwrIsYqUnyQKBgQDxf1jyykVjXjeKQdlFE2GA0UY6A3G8p9GFz06OpoT73aTFOva4AEYP95Vzvj/bHX8wBU1821HJ/4gBnIATx7GRr1YACZKp4PMdm2pSOuev6w1LNvaor/jGijs221YCsIR7WHUk+gRhlPL12PcInu1aNmgOxjBwX1ApHz97LcNbZQKBgQDRwP+ikLp1bi1YpaO+rSLpK/HxXTTdB5a0WtT0AVojh4sPj4Fi2g0/YyYLZ1m1xqlS4kvfuN5v0XxJDZbGGpQ9KSR5hXXzM0yZaE8rld9X6gn7LqNPFQp8msCtmzHVTDo4ogeo9I/YgqeVBhEhPB1BG6/zSSTxtnvZ4OhJvpHhIwKBgB1rljp9ydZBNCLzwrRXmBlJZXTL1p9VEoFqr/dQ8gJ9DgW5GTVxUxe+4cYn9z+KaGRBQR9k2KHzL26C0leWjFtjMObwQ53Oec+xj1JVOsSDrirrl0EVrwkA7hXQwrmxJ3KfZCYND1uT+cVZmT7DncbPuf2Sx3PpKKrZ07H98T7BAoGBAKqs35YZLA/HshBS39WUrjaLcphSnmRH+4IP8v4FZ6JHdYkY3VBhW6w7ckaPNzkpSLhPuSt3E1BrZjVPYGMcV4kYxDw5s8tL78VYUiuGDTFNGAgSYAJGfbz8c1IQWVFVcH6Koa8CKVYkolYplKC1eJx0+gv9dZlVQpv8XSc8cRl/AoGAZXbLSJ3ulzrJqQYZSlBJA75VikpJG20qEgvipc2wYjLs9MKLyad9vkLVhsPYrYDPzbIG2JquZziF7qjtWOFsW1epgb/DsSRG0sC0yrwjGH6cWmCkL79coYLO0PE0yrIlm+H/BA6dinIOLp18wJd+qyYcjesRCLB5Z4C9nnvRfAg=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDF3whIIfICn+U5ciRAqEsU1dFlv5HRVIQlrQHbNh6ev1YtV9l2we6zYcY5HafE1CBISgyKQgI7SYww5mDH/468m1pFPgMDfjCYpFQETVx4V6uB/CqAuQbxXP+QSgSJPEim2aL3UBwoAB48nKhPFRTcePfmli4TLsA3HxmEqdlu+tE/GZNOcQzjaersffCCP8OVLdKecBPxq67tEjw1UZVaJzlhGx8OE4K0aYwgGsL+1l+/EwoUqSrwsaTBpIUtWsXKUVB2kATJmxvQG0oVMtRGMll7C/zFNDHbA5P80ieL+owfOkKVeWuAl25cqx8tshMG4ead/P4OcyO1clivPkPPAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmQdkfy5Cz8AHXfRr22NNo9s6xUtzD5CfZMeP3H5WSaw8d", - "privKey": "CAASpgkwggSiAgEAAoIBAQDNsZvWO6HtI285SOv//ynnZfnVNEluC/to5NffooIqcxiIxaeDR2gXVPxQpG/nkfK7YddqCqfWYM33i+Ng92zZAw0XjtBDcevpyqO6u4i9wF4avI3QTDuf+VRqjvOATQQeivCYnHFAdTbWLbgZ+dpanjQMBoW5r5+XfTZV+WGFwOtys0ATjr20RDiYS4qUrUWs6ougnIzqnohcoCkIDolkmsjZOfjR5OhrTUlSWopt1w6I1mhjXwLa0Rc4fBAVzzZbfDDMCcw7x9aj0lh4hpOXYo0vRiDbdNOOMfAHSlnc4/ikan+yvXJuHIXR8Ru5ObWN2lx1ijTb1E0WRz1ekENjAgMBAAECggEAaLmvxRBRbiInY7wb5Beu5xCFdaaMaEoTc6Fnw4XCzggRirlPg0hc19w+JnTCQN2O/xZeja/lKgHZe9quJtVyhr7F8KOWp3AeE8dHOzB1+14wy14Kue3GQbm44BPuJ/mOSlqlCp5EDvReugdG/3q1UIPRrfm4JgUjtQZcHsO8glL/82ev1TSft/eiEGN+zRzEPJwBlYiymeeI8y/u7BrwxH5nK0F0C1R5Ef4goGG68R0zfcAI/1WJ7JlhljaT0I5G68f+NBqahfb0yDctio3o26Fvfii7LlbzdpsxJxpy94VPsfZmY+JS4IEwdkgU/OvFNMKq4MBoRSnersjecG9nMQKBgQDuwYVNpSwh/oXV4Rpqk74ijW7dsNxfwTEKpj9r2z8/YcyHsE+RrTMP4oY+bXUYyuEErEtbnQ9M43Vx+ixakEplaawdRKqAeNC6uZLlPnzjUPO7LU4+IGi+RkcaB0lUCpl7DhSA2PwDD59yJAXywEj6+8A39mrDYzcM7m/CgOHeCQKBgQDcjMj8a9mPNRHWIQygbL93gdbqltW2HVPYBkz1hQ6YBOIwi1p/9vjHyD2okgClr7IoUCMN8hfBTgV6HPV1zXATDlDc4OybN/59333doKaXMrSNb6wO5jXUUNHdVxBnslOE54vEgjWxygHKrpT3yR0HA1CYZebmCfi+TjDp5XwxCwKBgGgDaNaNua9Jmfa2bXK20KNu6DiuXyNcH8ha6tBLIL+1FIycc92sDc3CyucRem0FnYgSo3XS86J0iWrRKVd++to5ciECFCGKAK0IQYWbdn71emk18JtCNT+HkFw3hmuVfo3McYQ8g3W17amlJe4+dMzatj/rG1HpvEbm7UtYKI45AoGASKm5rjB6RUxezAWne1NY4a7NeAyp7I5NCWdKA7oKzNsPCp9e+boMzQWUCu3PeMciE1YTtoyEdxOVil3wIRfGTQDyc1NHoPwZxK7VcSd0u2vhQJgCQAZoxcK64gnFReTiz27aBaxAtIqxfG14dwqznZPiAdPQ9wliApEQXH9XI3ECgYA0kRZ26i/Qq935wFtGRmoADDH8q2zcGcl16p08SUPzf3dEH1eT/BXkDWMd7IXZrWIk5Bq/vMKf2yIMlHpek6Nz/pZ9llyByAyxGggC+7ZO5PDq4QHWc+WcqDFpyGnHzvPlEMr3nxpfu617fI3GNFUCueyEAJzXOalRkpduODSAJQ==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDNsZvWO6HtI285SOv//ynnZfnVNEluC/to5NffooIqcxiIxaeDR2gXVPxQpG/nkfK7YddqCqfWYM33i+Ng92zZAw0XjtBDcevpyqO6u4i9wF4avI3QTDuf+VRqjvOATQQeivCYnHFAdTbWLbgZ+dpanjQMBoW5r5+XfTZV+WGFwOtys0ATjr20RDiYS4qUrUWs6ougnIzqnohcoCkIDolkmsjZOfjR5OhrTUlSWopt1w6I1mhjXwLa0Rc4fBAVzzZbfDDMCcw7x9aj0lh4hpOXYo0vRiDbdNOOMfAHSlnc4/ikan+yvXJuHIXR8Ru5ObWN2lx1ijTb1E0WRz1ekENjAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmQ8D7hnLCMEXe9adEwjugWgKeZbDzjX3oUUzQYKbQ6CpC", - "privKey": "CAASpwkwggSjAgEAAoIBAQDSiEBozfLjRVvHlCP8OasiVQ3OLiyGhnm0jkgntuEgaErDvkMtKOSe5WS9PzUhN0n42WB/TGtYLsk6fsfmhL+sfD/GV4boOJTdieuXmL0cwdLUoFXkdbTUXeorhVmuTzhV+7TRKgZBOzf3qNL4L4he/dqFrdfsgZ4jXX0QjTcGpUcFoDm65lfYvoWAnPu5EWcqcwRFLXaHF+//AQOtnxMYv/BU255pdit2eaOOrKVLzAQYWRmCYRNTEOcU00Lr1QgGyJJ7f+Hyv7Y9WbTInj0SOxm4bz/cDZa05Z9Sl4MT7mkD3ixpnFAyQVRkdc4mZbbWbuH3vjdWuoVd9iFNbxhXAgMBAAECggEAEH7qhQu1/0a89TtPQoEGPq9pYIFPrc61lIcdcjcrFo31ZbbvrocouqaAqS9dq1eYrS3jGLZVJtirnbC3WwGFvy8RFCphgKqGR4F5+yvVjX5GVbCmajsqywT8xyIwr663XE1Xkpf3W38XWIla1mVrCv5a8+R2KarSSDUYCob2C8gdBiE+dj4O5t/SkfUYrpetcOSa3ifXM3Ctig9X6mKDxT28MoOGAHMeqNtXiAJQFJqLzp8FAW+k9ATCMqlkSkY60FPMr2BsqmBktHr7rO3AvL1WG1dTLRY+eVuBYTMGvqFkLYSqM71id6QWxTE2RYX/7xvgoOFpWJ84sElJQN4V4QKBgQD0NH6p6tZB4XhKlZfmy+VpviGjfgawt2nOvD+iwMbjn2EYelR8TwxWqKSpL033nryE2OGiLn1tGpEAtqTUV0is9FBWnCZeITfNNs1gVhtGXNXBbDb8+3P7fidV52YOKCi/hWbAuQdu+wbHAxdPri0unrV7Jt3oq5BXotd00dZgsQKBgQDcs2egfVRHX+Gz0ygMHikMAHtEbEhBu/Gu2+mcqetFklJ1IWJdewY3tyX4qPZHoRB5QdGFXSEzEyxiO+b+YCz7E1YiFtTmvRZ9DFjF61YP3Dp6afYHh9bMigzepXPD6CBBmle6PUo88hp5xhMxAoCMWaJh17RGZCT6dK1ttYKLhwKBgQCS5wFLNfmtp/S06Uh3jjBza+zQbP+ZTrxXoOanAVCjnTzLfMtV/Ddv6gMjw1EjpFnDkLQq28yX1WNlCnodQmR1poKtl0F9Xn4y9MSXLzU5Hp93u6FYjes3XqxLAOhjm8TncVhelu/h0yBAl5tuU1jasp55dugHDy3FijASFijgAQKBgHK7uYWPYf7w847emRUjoMcigPKjMDUsFYqHvLy7ARpb5Q4LWu2qBSN1zQGmJNI8AypmcxvXvGim8Q3ogj9/lCK6fK6gG/IQHt7HSmcp3sXEAYqeB08G6T3QDry4WqRfylUQfcbOEgf4/JaNyHBUEqvj9SzUTF3Dtg2WForQL5uFAoGAZAg1h6sim7bNBOLV09JTucuTHndWAe7N31yUqVIGWqxiyZWcSx4AL6fcy9PhwGXv5Zo9ylZ+HaOYWrPAtFt/6F32tBvylpgjU7LX8DEg+95AgSA5rPGvxwm8yVnWnb8r9FWE208ZhUKrorDTun9LKOCRZ5fngguan9wapxTGiDI=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDSiEBozfLjRVvHlCP8OasiVQ3OLiyGhnm0jkgntuEgaErDvkMtKOSe5WS9PzUhN0n42WB/TGtYLsk6fsfmhL+sfD/GV4boOJTdieuXmL0cwdLUoFXkdbTUXeorhVmuTzhV+7TRKgZBOzf3qNL4L4he/dqFrdfsgZ4jXX0QjTcGpUcFoDm65lfYvoWAnPu5EWcqcwRFLXaHF+//AQOtnxMYv/BU255pdit2eaOOrKVLzAQYWRmCYRNTEOcU00Lr1QgGyJJ7f+Hyv7Y9WbTInj0SOxm4bz/cDZa05Z9Sl4MT7mkD3ixpnFAyQVRkdc4mZbbWbuH3vjdWuoVd9iFNbxhXAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmYKfrXc9w7FKvcQSL1YsBejuuN5PBZKaYuSe3wYge1KUq", - "privKey": "CAASpgkwggSiAgEAAoIBAQCrUJc02dXVwzKc1zc0xuS/yEHTb8hz40N9aRydtRc6Iu5/7UTq3TFQwseg96PUS7NLCkPcCjAWu+wK0Snd1jx1vspRdgI9ChkzCIaG5hh8IBkyV6DMdoWs398uTs2IXbkXNI1bxo3WwxH+IKwM0ZI0UFlhnm2SqAYB0Ei2HVQsH7Dul6bk7f3aMnOTFiIKHwatxzQs+EITa4blDxaecI3vtjeOx6PY2hXD5RtPtIFd9pzo4pEbs2aDakE8Lh9gO8DcjY3WJpLQxYOWjCUmE+AwQ7x3kTVlyOxqlDmpcIMTMLwC4Cii9DO9EWVsKNwF7XqkbxOo6wMeao22iOqDt2WbAgMBAAECggEAJP+wyF9LiXEw2yK375QND0ZmwQ1hU3X/u3QaFA1qSMoGjGZn/flrjy+iAae6ID2BKXG8GiexHxfS8LsfuaNtR1i/RTyhWyF1M8phk3zaSOR9zJuURNRMJnvrLYsjZJIpSVO2O930AC/9EM9pmRMh6l54D1cx/vx+36FmMr6+0RBkoLtKpUxaBX91wcvo+JC6ka0cPW3hwhRBAqETb3buXOpCZkkNAcKDvOYWn0/VcwpNS/DJ7KEl9VydhR0gIu1QqAj+Pi2mYi6dZMrS545EwoXuLZVH/uEta6DtQTVeA5PjozkSOqfF1vkm19aIYXcxCSd0moDMQRPo+cmJPWfagQKBgQDWQrlUEBXNMIBUzTyf1PEAU9yQe5u6DgFN69Vdcx1NERyf56oi1ILDVuQjEEfIpOIAQEThqxkoW/2xRVNoAN+TFSNuOIToHR4iM00oqsnpI6iqGK3+xEo/BFz5suGaa+Y/3976EJZv472QAEl8Ft253S80FET9wB41sA5gONRgoQKBgQDMsCNtHnCHciSDrJtGutirw/JrEk3lUDi3rGQwmrunPk3I+86x1if63D2Rq6XKJI55dRpzQfSX9rAeWSBKokp/XKw+YNNlLeb7LAqTiQzA9Jw3dRJa6PPWg5UaqnAaX/6kqsmFUh8/jINedwZ5b6qHzHxcIHhR8y959ie4btLQuwKBgA3aIHsz0wUCBrn0zt+Sd8ZKpa7dnvLHZwQvpAq3n4RU/+HCq3g2/wE8A+HUcp+hMU9M2GcylZzLXbpxPfQyYkHzEuhUVRtgjostf+aKLCWbfZMJp24aKKasVIp8KyO9qBQnGBZYrjErqxy9OAMCw3D5wMyAJvm0yv8zk6pa4jghAoGAOfYOshGSj+g0iszP04GJZWpBNSyjvjGvPeOlI1ZNmRg9cpJLf3RDMfg3vw46Djm31pDggo7Eslt6l71pNXkrW1FkvO0yL06GP83C2PBQGjuqGNIf9npMwgvUpw5oXC+ergZmtkgA7T/e21sdDDogsf+nn3baW2pfoUuhB8rqC40CgYBSI7a6mtBD5mHivfib+TyzaSAp6Lxj3zUJOq9l79BDb4HYA2X2Akqh+ZAe9sbOK24Fig3iZvGfKRDrCUBU5Z4tRzPBYXeP/UUNXZQ98Ay1ZJAQZdeDd4pKKFN07rO5gpDiuZKlYZvTsiaK/nT2xdJozCCXnAqYd0ODL7QOyQ9oDA==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrUJc02dXVwzKc1zc0xuS/yEHTb8hz40N9aRydtRc6Iu5/7UTq3TFQwseg96PUS7NLCkPcCjAWu+wK0Snd1jx1vspRdgI9ChkzCIaG5hh8IBkyV6DMdoWs398uTs2IXbkXNI1bxo3WwxH+IKwM0ZI0UFlhnm2SqAYB0Ei2HVQsH7Dul6bk7f3aMnOTFiIKHwatxzQs+EITa4blDxaecI3vtjeOx6PY2hXD5RtPtIFd9pzo4pEbs2aDakE8Lh9gO8DcjY3WJpLQxYOWjCUmE+AwQ7x3kTVlyOxqlDmpcIMTMLwC4Cii9DO9EWVsKNwF7XqkbxOo6wMeao22iOqDt2WbAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmQpLk8DTM5Wqu19B1uo2GRjMJyLQyZh7Vz3ejwfnETNHL", - "privKey": "CAASqAkwggSkAgEAAoIBAQDeJD/uc++aiPTDzGhaobaZwQGxUcBTOD65utvylkQx7DJAGKb51QjlH2O/5AC3wqcMn7D9MkxSX8IUUlDjbJjE0ohCqNnAYh20zWqT4fNTzPnTmcanuQm3aFVxEf9itD6GRiiCcZJPGnxdetkaN3vRq8ik6pg+yb5acLSeEWpw9FwhUbWYQeIeaPwlbmtaD51FTS5jbpIwl/oDQf3HRS1u99mf3XrKVWNDfEEr98ciEn2NJgHQxC3hf702xCD52F8ZIJbd7Lb30vdaxzNWoZbDuzVI6D/RuYbxx/oxtHxnyoH21fbtK2JXJVNzUNEdmgbglQv4w8U30PlZN2UDrjyBAgMBAAECggEBAMsb9eB+3Ks9Yh7MfPWxOpYmpPeOOf1dRezn70dFIaFLxz5XzARORs3H/5pqTEW4kqi2Mkuve50ttPSDtzXaC2ya2r+oR0Dh9StlTndcdvE+T4ar6bldNIcfvE+gFxQWnbyD1XI/iXkOTHvkYTDZXjr9iH1RilaOe5+RwXNtlRckgaFfZSTSODpVVxcJLmx9HUQxAcJGbeD0iOrLZ/5BNRO4tnwfdbgIbAvnCIGmy3SsalYjOJeQYxnUZydV8IP5FD1b8WwiAIbhvWfhFOKIOePljOac5aAhTRCNeXVzQFAH0jNK6paDvMNNIhU3JKJApSUppRwdCv2TsDnucIitPskCgYEA+pe+YLnwZCJYvcW2w4/W8Oyvr9yleIz0RmCcRkp4/qDRcDROO1L7z2wtsMOE9lGain/pQeV3/ho8B3grbe/9S/Ezfz+htV6MNONjH9wNVB10FZau26YGcVdPT/rjM+fl1IcaaVHc0RE8OubKTAvNW1yzBmU/zY+fCrMg+12PS9cCgYEA4u9YDnhIiHdewJ+uD/Tk8FH1OpEsvqkfi521Dyen2J9AITLvj8B/jqUG235eM6ZwYh5YXLkuJmvZgcLhkSbQieM8kb5jbGaFcu+z/13qlUG0HzD9iVWqd2u476aeVW4uGVBV6LON/MZZ3AkkEkcNMQ6baMuZvhLIt4EsN1Nv72cCgYBJho5wWP4kk0NQYxuN471gMUIXKnlOlqTxpVUU9rLrmwn4jxBJLb7+jDIXxDZWA3mBm6g4EnkTkGT+mA6+EgVS6/F9K5Fp4tTmi7VA2tL6VC4ES5MAlYUcak62G9ngF/GCWyWvszpECXePnLnMeEYHwXoxrTF8QeCbRhWuSzRJPwKBgQCkVvfJ4smEKg3wKLMA0zRH5NJWS3O/zvINRXQtOWaPtSPX5u8dhyXYwyGoKmdFuC6Cn78VxvTo1gl5swtu9lDmyiy+zsVpZwUVKwmK0RRkamRqgivZHLSKLvSKeHsJGvU/V7IfBoi4mVvRwLzij5m6AP4Ccg8wWqIIYf8HQeE52QKBgBQCEVLGzF1F6CNhu2LUso3+72usPip3yNP6EUWCZaiv+/r9V9p2KRxYrSNFZgh9O5kOc16XCuFqvEVBCSK93OYuaurVj/lw0+GWL4V3cjcfkEJP/wJbz5ZG1pVsT3ZMYLD5MerY/I7MzpDK+Qt0VgnT78/2B2gxrWL6uSsuTsFW", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDeJD/uc++aiPTDzGhaobaZwQGxUcBTOD65utvylkQx7DJAGKb51QjlH2O/5AC3wqcMn7D9MkxSX8IUUlDjbJjE0ohCqNnAYh20zWqT4fNTzPnTmcanuQm3aFVxEf9itD6GRiiCcZJPGnxdetkaN3vRq8ik6pg+yb5acLSeEWpw9FwhUbWYQeIeaPwlbmtaD51FTS5jbpIwl/oDQf3HRS1u99mf3XrKVWNDfEEr98ciEn2NJgHQxC3hf702xCD52F8ZIJbd7Lb30vdaxzNWoZbDuzVI6D/RuYbxx/oxtHxnyoH21fbtK2JXJVNzUNEdmgbglQv4w8U30PlZN2UDrjyBAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmdhiMT6xxQoJhz7AEN7LbeFaxYMXghAZ8SQVofNbdg773", - "privKey": "CAASpwkwggSjAgEAAoIBAQDkiM7cicZwosubnFMqdW2aWX4pkHbUV21ihnfY2ZWGlsc9/tRyGzjKVORdedDLyIHduVif14/AZTOML/DnlZ18WhfG5XBSu9ksJRKpdQgj40Hvuel7yZGs/HJ7s0p3nR3+CWK/ACo1rvlSIb2UZIG2TdVvDYpBM2VZLU1mR0HlG5aNz12T7DPgN/r9DjMNtxIZCvk5rJwJnyeaSzfkOVwADIlxO7qI7jO4SkYyzDIIyDemsSr6hd0ehSPWp3E4IuZs9wVJ4HvqzlHedG8RtzZtqC3gL9K1IWxY1ioy7WrgMwZzGPGXSRs3z+5JPIUuscMd5Xwb+7reqQg/41SaBhq1AgMBAAECggEAJmLUXDbIHiM6D+kyDu+qeUKO7mxViVUmCmaLuuDRPMoWrVMgXAo2f8XClfDgIVqMdbGsMS0D+E0HW4Sx8jQvP7PiSoY/V6Y11DRl7hC6TUzexmVz0lcJIQVGNYDoAS9i2ki5TVu5u0qoliMUtNgs8XIhZ4XesxTu8Quq9IMDjnfCcXtOOFF/9YS9HsqaTd5y5UJFJrmjiSz3MR0r6xEpXjwNAkgBfT7iopdVkShxu1k7Hd072+yc+3MsGWtTpPr/JxlSOUCqTvsuOEMdYOtCS6kVFsK2nwf2EULTFkawohc3wdqTKrd8oLJez4ZZ+BRghWyzumqjGExgbABo5fZgAQKBgQD/eG0Fphq4Ne6PaDj2WKNJP0/BjvJhFfWmonr96hlMXYEVkQ7gx/yi4f4I5Vm7oR6yj4Afy/XZodKOQT3GnDla4YxGGh/yO69GjBp96CPYufODtrturCy0t8tCM9f27uDNw4gGhpFOQmMLY3yFX/9MulMz4MH4SSCUdMYkRXXf3QKBgQDlAhZxhOUC8Z6Rc+i/HW6Ot8AvRYvaJGUHtdn4TA25sgvvcwkKv3JQDv8Gx4o1kpHte9WnnrzUanYhMqKqOtPeGbtGMkohIleq3plwUwyX9ax2EpNV2tlvmcQbgXYJUeH+DmVEPpwYTaJ6qtbZJW7p4caIz1tObD4z+H1yailkuQKBgA4nburkNBDGtCvv21ASwyE4x8Nylw03+T89O1E8GiC4AYHfYpKjoeSoXrnBc0JI//lmp/ObCkj/hTnqdXC+kRLu8iWkJub11ZU0B/e319yXGN3QTvwnv+ZXVISbeLiurXfZAH1UEVLjrLch0PFWyz9GB3wVVMnby1lOSvgRfSFlAoGAceTx6I9hnm8wn8J31OT8YTp9+ISsI1fKb2U//L9GbD5itToPGytP3QU4TNTcpfw5W1UlU3IdE7/G9IfMYsFTMbi2bRkBySzdUPvYcAa90q26khZ29FIdpeVhpRRj8gqpTMM4FhLVazjhQATLSb/WQ7eoF86Y6I3o+cvyB/9IivECgYEAzqTpNA5ZZXLdzKAjf5kysgvlmdsk4psGu8a2bq8qDAbh41IDWqsNko5Eefv3RIHLqg1gGB7f1qnXth0SUzNOx01o+5LA7jVggzWhq2jgMnSc1yds3Wqj/EMWVaGP9kNyigbGHfuTRhFhgegCiPz4xf9p6tifDoRChc5tIy0mocA=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkiM7cicZwosubnFMqdW2aWX4pkHbUV21ihnfY2ZWGlsc9/tRyGzjKVORdedDLyIHduVif14/AZTOML/DnlZ18WhfG5XBSu9ksJRKpdQgj40Hvuel7yZGs/HJ7s0p3nR3+CWK/ACo1rvlSIb2UZIG2TdVvDYpBM2VZLU1mR0HlG5aNz12T7DPgN/r9DjMNtxIZCvk5rJwJnyeaSzfkOVwADIlxO7qI7jO4SkYyzDIIyDemsSr6hd0ehSPWp3E4IuZs9wVJ4HvqzlHedG8RtzZtqC3gL9K1IWxY1ioy7WrgMwZzGPGXSRs3z+5JPIUuscMd5Xwb+7reqQg/41SaBhq1AgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmQedrgwE7CLDyJa5Bv9hTjf7Paan526sQd5gVFCmMmsHt", - "privKey": "CAASqAkwggSkAgEAAoIBAQDoGW857wBAmW222eVe/8OtzHX+7QKwOCtdH3fJ1xEbKUrSezIYr5xwUwlm2LC3AvMPVvFFEFauOqzOJwuIitLHx9S++J9N2ML6rjigp6OLS/j2jtWViQPMkojjMMGONirttAvQGKKoyAXZ1ExnAIXBHZ1P3U5jLrQSCrdKKmrI5TXuI7vxeWfEABT2oHTfZZrdBb3qCY7aiQJmkKQDIQUS05jazhwt4wqaN4zByBagiHf/NU0r+tHKsIqWDi2uKkzc6BELb2wYPyODIiqwDC1714TyKqCV7HqT7OlJ5N4fcZ5M1BBk5Oa1ZMAGlXGn4VqHz2agu2Jqxq3tC2FvSemHAgMBAAECggEAU5qt1QmBZsOdoKr2k3S/0MAAlPZc7IsfG6k1JhCBSe5i1FSqI/hF+rP+g/x0E0hNs23W9NDA2HusOYoY/nM7H9mcibnW8FyvR0swfLZGE+wm3vFugDHdm3gBNQ0f+5EJf5xGUQw+s1txuBhf+Q5YH8sCGn2WOeXd2U3g3idPVdODL/IZXiqR47rpFVlUIheJu5pkXCaqMrSgh/JfZVl4aBK9BoXu3hXCgsXE+lBa0w0yl6BDwI9U6vi7USy9VnehW278ZagI/i57BT1Zzgzjj1WExQj1J3t262DbIHhnM9P4uLocXs6PMKfpozmGkKucL4E4MyUGi0YahV+7TPO1mQKBgQD1Hm+ofJIavaGTDtWYoUFQp3mYHodeeOfDbZu+8JqQl32xA2sZSyQlFlh/OPdlKl49eGJjkcibMINrvOabqgdgG9Cw6TwltYawd5HrvzBASjQIVGLWi7N4wVlRwvw5AO02IsGVHXPob7xQWx03/9ZnsxVBNR6j2IJfQRSU7a24pQKBgQDyZwrkP7EGXE0VMGluMNmsLKzuICXIC372+3DlHmavHFakWA3bqXLh/u2/4L9fmtcGTX3u1MacjnfgqFVLg/f7yJ/UmAHnqKklXvOfO73svGwgNOBOVbw6ms6ZzP1GN//RubeCFxOq8U9x96CAxVTHGEsPm8//Z+QvqbHjIXyVuwKBgQC/aAz5HI1apEnPc/4HOaSvPpgM2YoLk44nZSgBahDIaAOWfnzbO3n2HATvE6TcMsF0btUlu2lTBgcZ0mChnZw0yIOmIfr910pd8oDX/mvHSCppdrvXnS+AVDtTRVd/i+GwLGPN9TnVf6sldIDUgcsDHyyxxrEucJsdlsxjn1XQoQKBgQDQoh2+vI8KEXGK9kMYQ1VmmoEw51x9ZF+gBmRx34uz1ilAhEVRNfQaTcel6bPtfqDp3NKyOFLFtt248EmRmIFdJZ1jZn3lPMZw0tvOxqW+V6KcycXxxlse+dUujT/FKze09Crc/i3AaLffOKndi3pfbipUwd/xTSMaXu0rt8u6NwKBgCXXYCSUjEfi2qyeYiwt+iTM1FBNWE4roswyaM+HZBO6mXhFgg0JMbiovCJckfnVa0pNEo2RxM/LMVDCN9UmqOVwhNc6tMEUCerNhei+OTYugKcBrVIhcOt8YkXAAzdYlAXjiV2oU9cHQQ8jWHIMUNiIUsKuZhLP6I8y6NmQ3/D4", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDoGW857wBAmW222eVe/8OtzHX+7QKwOCtdH3fJ1xEbKUrSezIYr5xwUwlm2LC3AvMPVvFFEFauOqzOJwuIitLHx9S++J9N2ML6rjigp6OLS/j2jtWViQPMkojjMMGONirttAvQGKKoyAXZ1ExnAIXBHZ1P3U5jLrQSCrdKKmrI5TXuI7vxeWfEABT2oHTfZZrdBb3qCY7aiQJmkKQDIQUS05jazhwt4wqaN4zByBagiHf/NU0r+tHKsIqWDi2uKkzc6BELb2wYPyODIiqwDC1714TyKqCV7HqT7OlJ5N4fcZ5M1BBk5Oa1ZMAGlXGn4VqHz2agu2Jqxq3tC2FvSemHAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmPxSGSsvpFYHvsNYPnDRe7D8YFp5sFc1DRJyJjA6Z2pZw", - "privKey": "CAASpwkwggSjAgEAAoIBAQC9UYzpcTpZ5pmfeKJ1+RKlXbVBloV6Y2xX4pmxDcKmlOYjLdlPTd5o0/xR1Jnz7yABwNmbqeoEywvQ6DxIdSBqI/fY+Eiq5voSAb8b75o74KjJKIpzpX1IfLjyGpPHF8dNt/2tOe2qWNYfYu/9byt6I3Yj28x9UJl7jQCq4a+mxZnQRfb+7StUEeTOumbYnx+YkP+eyt6a5TfrDqRbqcEVgS64Gbne9vJcU9Atr+y/QV600JHSXVr0l0KzWgN1ihCUSBhAiq+n87NgM1//hFlm771/DwtREvaFBMDIBoGgUP6mhAMMlP9IuigLMaL1Pp/DW12TLcE8vWM6XT12XF3BAgMBAAECggEBAJThFuFVy797GwBPy+Ledo1Y/fuQNXOj0EXky1xzJ8n8embb3XMCF490dY6clF1ChXcbg4Vov8H5M1eb6hxJD66ojnYv+mV7stiKSxHbAP1plRJsMUT0tWtVudOalvAQgQlbUcDyNzapGeog0f4JeLVaQcO9TDiYM7r3jbjUNl/81tBiQgHWP+Bih+nXWk3531gOhqx8abJCWt19W2AU62SnafstJ37WCMrKtM4ZZgwBEJnOOEdmDAOBU7o5oUXicUuAKheJg2p0CNKLpyf5/PFItqLKASmjgmS8i1C/NtcWsza1w90A1j6ddiYfHA7h2zrZGh4kYLffnUdXhmu6LkkCgYEA3YE0wF3jNY9L9tKuRHKRKe/W8Eifu8Be4fu6pxHvAAXWDQtXdnaKZGr2JohCAG/PyWrY3qtH73bXI7uOv0tWQbAZ5wVG1SKuODfFMuPy0iw8qcTinGXGSJc4YHtQeunKg0O4ueea9kKdobDrz6+PJj51iDEGyxMpv9wrU7DtFE8CgYEA2s0rXmfvQft636W4kLlDqx8bIB7oamr2A0gC8zegpiu9OA3ctJSxsym574BDgyhRAVTSYr7SAgKB4rpVPOpGm7CLRReF4gf7NH3VjiOUccO/G6hJ1uJ0XgPl0/CWKJ4eZcgPiEYawMYJMEr3riW3h+uoYmVyrWveD6NvEhQxGO8CgYAbjynbDVNppIyVBx17kq2RBDA/8Sk+mO61Oza79rU/0XoSYWjeal1JpS0/GhDsMP0vWEXnXnQyzRxza7CVCHCQ97IhVjy74/a9M+MrM8VQdQSPMtnnD5qeCYKQLoeS42e48UIYj0JuhVdLeNG+I1+yKG9DJKZtudKl9mTFouu8bQKBgGsZSGQyfbOPdAqq5Je6h3vogu+LEXqdloPuqLsCfJk6Cam5Z1HhAsZO41tvLhyyDEyZh02cV9FyBr/DM1vY1Oz6UoFkTT1haL295l1n3w58oTvZeSM8v3cRc1r1hZqmIvzxG2E553h6tx6zY18TyS031bksLSDkDtMazZBM3+dzAoGAXBmm63xkkEVJlzCfBtlumcE3OsIbQchVaz56rF46O1whVw7tJnALDx8IIl3xT3W6arp01mgJSzqWN/93ZGl5VvbCALcEwX2w9I81SCrgsyXXpA0REo6d2ex/EVavRK0uZQUFD5ZwhWPTPpqT2bXCW8aF1MSh+U4Do/3MkLNgxuw=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9UYzpcTpZ5pmfeKJ1+RKlXbVBloV6Y2xX4pmxDcKmlOYjLdlPTd5o0/xR1Jnz7yABwNmbqeoEywvQ6DxIdSBqI/fY+Eiq5voSAb8b75o74KjJKIpzpX1IfLjyGpPHF8dNt/2tOe2qWNYfYu/9byt6I3Yj28x9UJl7jQCq4a+mxZnQRfb+7StUEeTOumbYnx+YkP+eyt6a5TfrDqRbqcEVgS64Gbne9vJcU9Atr+y/QV600JHSXVr0l0KzWgN1ihCUSBhAiq+n87NgM1//hFlm771/DwtREvaFBMDIBoGgUP6mhAMMlP9IuigLMaL1Pp/DW12TLcE8vWM6XT12XF3BAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "Qme9BHBNLCqt8kagc5K64JwHHbVYrB1o3AjCZvpSUghUME", - "privKey": "CAASqgkwggSmAgEAAoIBAQDEEH3wzUPnAbHJ/p3hwFbGbu+bGLxdGQrq42iprklRW8mK2IKD7vOv9nMc3i72nXBDeJWtmrCjSuQj0KI70+X0luyPWG4jCUDq39kOxgc2w40nWJwjD63mfVNiswkFBvmQ/ll2nxJRTW4b1qz1EcUNJwXNWSi2Lb+PcuzB8bEZoLpCPsRbiHkDuUPb3DLDm/NUtv3MLqUfPmLGvy1iN3UfmCdItRtapC5ZKeGiUakzjTHBzN39UnNRFHHTfMFcXt1HWx1bLXqru/kAZLgVVZ2yQJ+oQ0CZoiLgJcXP52LGWh95m7hvUA5EjNxSVGcwESXkJc2z8pda+UOJ6gZCcwJxAgMBAAECggEBAJAXHrdt401ObX7p5NYYKK3EscrmLuiskt11K2IoeDGWp1OnMqQLZIQZNxgsIY+UvQCZCkd/u/kF/QxlNBWL8SAEGu5uKuMM1ezHfhnhZ0PUC1SzRmxuBXuy9yk+Mo7DRX5NryoCVc/ye81xw8KHwK2d1CHKOKVKkdG2wFD4cxNFRLU14QJ9ier2b9EWlTpqY0ug5y0J6GfISNTGLUsccY0od9TdCxCy4HbbbCNZfVegx5vX+qaYM3Bd1xiF0MyBpwLMT9+hVOgUkp7BM1UG8DgvnuW8gKhQVqtpwvUvbEI72uHiK4OMruQx31rxc9I0b1g1GavsVYAwsJ7iH2tlRFECgYEA+tuRKfxYdUJ/sntLKnvGIVKJdNID60OO1mopOgryjYZ6pVzQG5ZBoOWq0m/f3gnAjAMDzzIyLUUxaDkEJ0118sCHhUiV04eIZ1R0woKTLRvCcwaekiVVHuPBqD21eRzK7MiCsuuMADbg3glk3C7LRJMzUw+0SH855Kg/sDdblAsCgYEAyBVif5pHW1Mteh7btXcDxuDh8DrmYu95zEl01i+GEK+/Rlz5uoh2RrqXuiO5NGBitZ+pc33e7b9tNq9NPoTEHc4Ykh60X4yVZXlm0RP9wCD28KBkYFwEDeULZz62fJuBZNYmR+Gm6i6UT7ARKadx93H2ceBxaULp/ndusAgh9PMCgYEAoRf7YsEAdVzc8Fso7AFMPP3p87EifySFR8Ao9XMuTCA+Bo9RvUWCo7aZOkZJtycAFWmiOp57hoLWtZ1Xw32E7v0gikEQpiR1PhYIXRjJNsCK4J8xmZyLyyhrpoTqUvpgfipNdGS7JTAYu73AnX0XX9Q/s2l0VtIM9X/uVlVWY/0CgYEAiTEolcgqj3MsJqVMD1Ro8ZA3O+qXGFWOFUZ053w0l/J52/xae82gFAVTjh16m3BPnqu4m+k915U/hJSVCX4tnyY28NI+6ZlSwv6IQmpLvtabnAjOasgNO53GwOdeZ3iVM5gnLXiLY93GchGO4xneakXpLtIv0XZBTeuEqQ0ag4MCgYEA4xlnp+GerKONdky5ThXKXzrxAPqvP8DbKETXc7nhccHkDrzHFjfbcdAeSs9dksbyo2VMw8nfnW3vbWSC/ZRyaDx38uEg8xNmbkr5rET81R5DSlf0zUlkpld5wD20ftI+aoP5QqlSMdcaEFzHzvWSowbrfnEbdKol02VfniAA6qQ=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEEH3wzUPnAbHJ/p3hwFbGbu+bGLxdGQrq42iprklRW8mK2IKD7vOv9nMc3i72nXBDeJWtmrCjSuQj0KI70+X0luyPWG4jCUDq39kOxgc2w40nWJwjD63mfVNiswkFBvmQ/ll2nxJRTW4b1qz1EcUNJwXNWSi2Lb+PcuzB8bEZoLpCPsRbiHkDuUPb3DLDm/NUtv3MLqUfPmLGvy1iN3UfmCdItRtapC5ZKeGiUakzjTHBzN39UnNRFHHTfMFcXt1HWx1bLXqru/kAZLgVVZ2yQJ+oQ0CZoiLgJcXP52LGWh95m7hvUA5EjNxSVGcwESXkJc2z8pda+UOJ6gZCcwJxAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmUEi89sQq9Bm3iNEEy77DDi7ZDb4HUJBfdF21wMYcGqoc", - "privKey": "CAASqAkwggSkAgEAAoIBAQC8vTEEHjOlrz0+wkTdN1MGC2LYlOly6bql01OBqf+gS1jC2UBd91ltJwPKLzDNKpGH0dayR0eaHKcfI9nj0tHlf5gPQxlDIULemrYS7SkJExdJ1RHKNLuCUjMDHQgOLtMm48p968YwRHgF3LBUb9pnWHh/tGTkpOs207qSP/yhr5yOwiyeoeOYlaUW5hXHfePpdQ3gzx1TTMKN620MQnaTqRCTI1dIbetKxre5rIXacnMH5RXE9jXI/CJk/+0DR7ZLsAjxjxE3S+OFENWNv/EidxkVguqOm3fukfe3n56j3hPLJsDwFSd4IP89VdtEGSsE2g0q+Enjpz6NCBwknO3dAgMBAAECggEAc6xoLCPud28tVBdwaTwNEDlOPXsWkK0bDaK1HVT5LF7BaboIrw53qmQs+G9vs26RfvJmaSEyiwtgib9JPU3qAoPux/vRscji2NdtG7BqY/tlXITPwGQNP9PtG81hMIAWPVGCuyYTc2WjQcR99WIQMyKPx4TiCRfiaNnfEN9SkCypWEHKMQ8wRXZxnV/BGb8ydDTY3qYs1tnUAR5Xnf33lemDUhkQAVkcXqfkN+Gb0Nam+KUqB5lf7cwYniMuspZtePiwpPidLWkhA+2FqGTJG1FN0dGg+XaTqTKDCjiox+qbFl3SACSt2IqL4SOoBq1H01Vve7LTeJemItNBozkCqQKBgQDk5AQJ+X5LGe2VDTC9eZQApHhai54eVA9uE0bo0yZj637pE3GI/d/4ngr/92HxroZ0SlFrlZHGPWJiiz36zuE3OCi4M2+SX/20KOeP4WEVsJ8iL4pF/4T+G7IQfbSyNoC6iUIHhuPjOqaovt4VBUUgt2mjzQppu9m8Zj13UelfGwKBgQDTF8YwSrPLjjQR5nDw4+tqiGKu5mZSCbAct5wciTpTDlaiQX+/T46WTEl8SE18sEWiRw2s/BBFBrmDNg+FBvmsjIxqrCQ/yzGXLatLk2Vpkv8G7VEt2do2kkv1eQsQui69JnS2kYuhfXqDfkeVOw8/UFMdEXudq5nDZqIHVF+eZwKBgQDgYNnIwWhZzNgHFoAiLe21V4WYFWfyiSr7GDCaCmuG5hNp/qJ8zYrimGNmGydLmW+6ziPU2DGn6QLqYV9n36gNzqK0N8/26Ny24KZneGQItDS7eWkOR3ci9xluaxxY228D7Yvp/wSk+xjnMPxaFOl4MfSAG39KuViwBHXa41Rn4wKBgCuPeGJ2x+t1iOE4wI21OttdEaAuA2digGksqpZo6xRAnTgWdBoyfKYfT/rJoNPePEBkkTnlOiZEYPvmqAU3j0ZAKqnIpCJV+AHOds69t+u1XdM8HchscE9amToqpFHrWcHGsccK+dl1X1bLNFJjQZ47ISuac/vxcWWVRFJm4uR5AoGBAL6P8mC1K8XwmKxSf43QWWi5Ib56y7rQEqihRhNRaZHHpfLDj1oRbb/OFYwOvqNK3IbCBH6LjqO025dRZoAKMmkVt0T+WqZp7Nq3zS6hmitZ/3hZhagaS5iBxDshdB17mtBPD8/WygDf48UNNeM8JbGY22WJ9lPs09G79TK8RQUu", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC8vTEEHjOlrz0+wkTdN1MGC2LYlOly6bql01OBqf+gS1jC2UBd91ltJwPKLzDNKpGH0dayR0eaHKcfI9nj0tHlf5gPQxlDIULemrYS7SkJExdJ1RHKNLuCUjMDHQgOLtMm48p968YwRHgF3LBUb9pnWHh/tGTkpOs207qSP/yhr5yOwiyeoeOYlaUW5hXHfePpdQ3gzx1TTMKN620MQnaTqRCTI1dIbetKxre5rIXacnMH5RXE9jXI/CJk/+0DR7ZLsAjxjxE3S+OFENWNv/EidxkVguqOm3fukfe3n56j3hPLJsDwFSd4IP89VdtEGSsE2g0q+Enjpz6NCBwknO3dAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmfFpRsArhLdQdXHsB8ft9Him69qZedcK5cEbfqvEN2atd", - "privKey": "CAASpwkwggSjAgEAAoIBAQCzE0FP8bilVeNMzjBZpdBGADuaAJlD+PgRA6xchdXIMh6dyJtHbQHFu4o+u0U96m1e5aieNKyNhaDuW88DcbYwkVicT0Z91LQTGhVVStZwmRGx0XNekcybU7RyK9+Zbhnd94U1wycSBj+5MelWEpZfxCKHUWB5eKkurYR4Jp40TqK4EqXpLxwR7LuT0iocOPQDJHRvkz++XSKKi+9AL2RkM3f0a1ldwV99k8Wya7UTkz5i6fz0xjffcbuu5dWW92FZQm5Nl0iUjZwoSq02u2Z+pTU7nQ86IroikE6Xuukkhr24EXUVw6gC5FYpc39segik76zGLfrTGMSh0/729c4xAgMBAAECggEBAIYARaJd/k7ya0mhDTs4Qhbvu6ntAsODfZW1yvfdSnEpWBG3+MJFBsuBH9z7Y8AGOVuGvVvNjMXGFfvnhYxNPgkv6j/lbplgXnPg08/kVX0ifcQzOIKu1Y3x4BiDTinQ4thfjTYC16y8MlkRyUqYVCBLc48QzQF40hjUzUjflQkMAlEkVbcE51OpKoHzaQ+ow7xInCni7PN9OoKpHnWYl3HfsY2cxNhFaLi3MYAPRJfxzdn8Ouav+I8na1Ggd/YoLtG7mFFNPFWAAkhXRVE5edrmhDsDbzUSf+VdAP8eaaaa7Lh0tIgznanN+KflLBR87QBZD7WFM0+SVFjfyxPQGqUCgYEA3IPpcHKsoOVryZE/zMNQseeRRLCjxErEflFRTPJy1ydvGxQP2imiE3b4qjrv5lQBkJmK5mPqC7AgubSaShLVQP5PlJJI0hUD3I5992PXkcjxmnTR5m48KMJTEheFwpyvLgxbLjRflFapIAwJSV/VcwtaeYT7VFYqM2JE0Ga/C68CgYEAz+Q6IMS9jNykySwj1sRX73poN6Qr4+zGdWD2GTAuECPTF30rDpb2xJrQ550rPKF6a0czuaXrLzOPfXhLVpy2z63A1hVVxeBypomCjhM/+zYgBUeoxfS/6Ga7yoTQmgqZDcQaeih+UbDbtPRBT8VMqh5p6NFjdu8TtCjN1toH3B8CgYBoNn8QAWHL+CBkdhxsrLFqIkHo8IG0tpD+EXgWoU3cmGpNpcGIHLzX7hW+fXP6qiDDMY0PLJDjTS1qFgwEjbnyqTz6vddkUUIt7bliPPEXmJt1n1fDSr1rlcqkdjFks5+mZ3h/8YhqFjp/RrDs2DmL0QXFAC+2v7HZ7ssOokAPSQKBgHTZLuLkMjZOfkCkkrBQQ6zS/Gjp2dGOcC3hhfG6ZumjeS6mp+DXcXQoIGtOp9K4YHqT1rruSzaIoIpBZvcTtp0caFrsOv2xnj+E4uDAaSHl1jGhiXdajdMuizbVV/p9InHeW5N11ypLYfJfp6YSm3izB4xYxLNAxa5pkOjGO8y5AoGAcPv7LKtI/y9dABCgxoqQx+sMAazwaeFL1uMbp7FEy3GRx7QyWI7zOzgIAZIUKw9GCEVsxFISeYDUSyYyMO5frZmI0mEN7h8/A13bVS8wuCPK78muCHLiG+mr2FJypNYMXQdGDjZRXb6L3wK0rFsOA6cSGV9dSQLRf+HqjX7LrRs=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzE0FP8bilVeNMzjBZpdBGADuaAJlD+PgRA6xchdXIMh6dyJtHbQHFu4o+u0U96m1e5aieNKyNhaDuW88DcbYwkVicT0Z91LQTGhVVStZwmRGx0XNekcybU7RyK9+Zbhnd94U1wycSBj+5MelWEpZfxCKHUWB5eKkurYR4Jp40TqK4EqXpLxwR7LuT0iocOPQDJHRvkz++XSKKi+9AL2RkM3f0a1ldwV99k8Wya7UTkz5i6fz0xjffcbuu5dWW92FZQm5Nl0iUjZwoSq02u2Z+pTU7nQ86IroikE6Xuukkhr24EXUVw6gC5FYpc39segik76zGLfrTGMSh0/729c4xAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "Qmc3Y2dKQEQzv754NMz2pkXCL8ReHCssg9Z14UBCcPQiRN", - "privKey": "CAASqgkwggSmAgEAAoIBAQDNJti1FsTPq4FmeUbyEoWkS+YHypKjCQBt/jMKLyyTXF+9mFi7sO2iIrmUmEO+mhfcAF0VkZbHr1MZwmIG4J8UUpBVbIX4y4aPrK4tUfr7/HTSuxP435C06z3TUxK6e8FK4zuj7e6Z189H5qt9ZlO59DyRG9u8PfVjx+qg8FYqlxGGfGQaZFstPMj6OWeGCHeLGlVA5SVPS+xjtkjrv56eFJ3JhfBr6Txv2CmCklTeQctplAwdq701XHqihDVLEWFrj1EhXGuNfq0EiH3TVE9VBsMODF36cY8vjKPSYCSq9Z31I4MC2BXID9ksYB2SE9Hq4bDyYxfSXtfisE2YPdjDAgMBAAECggEBAITWlpQLvjzKTOvRs8Kjg62zB6wb239+IK0YYGxDx5VTxxq5PxupoPXPjmNNhPAyTyjBg4Sn1P5P5HtVhqv1XoyGObdWohlLkEIQCmiGIQJxoiOhx3jrKoQ7nrjrncDqyWp4YPHw6wLq3ukrz/dO/v/1yhIb+9iUNgT6Ok8j0GeaaKQzoym/rcVq+JVVD+f7zGtMHl2QmplRRQUpNlBWBxacTPOO4PSHmp+xLMohwpZxLxYfX4GCKZtWG61SkIFGtnyvA80BnTi2FJcm6gtIIBlQoSeYtMGDfxWP3pKG9iZFseuOtOgbKjS7WGXcgKD87ZvVPN+DPoWJjWEWA2WFK0ECgYEA+AVOXdmoW0R6MifotlF8OA7eWFiiAJw16WtlEhe7JhMJvifEya2po8Sw8+i/QgWqI+N+pSITE4mYeOz32xgyR2i5Mk6qXe6UkTW0OjaAGaD73Lxg0/cxqxR21ePFLcmnHBlzfcchBvkWwNwd1P5iswmubHQk06oYF0S8IUogmKsCgYEA08B457ICfNNyzUINrKmVE8pdkUZ5SoZGyg5DMpJnmi1Y/Ip+Z0WFN8PQ0ccBZlYSeBySBriZoiIaZk/+Z+4OX+WCQ58xTcUFS7k8Qlg2hV+U4wkx8ZE1IBBH8lrF3T8BlAVVPSaTjEmYOn/EL9WPAjVHGTkDImTkQihSTZV28EkCgYEA5ULiYdZkzZjK67oAXyeLj7YOydOETNQY8Z+YWdUd5eALTX8tZM/m079pYs1unfTmhS4xTyvkPlceXgmOQzRmpaOkLWCSEyoKov/ljTn7x7ULm8t2JfmGLAJKpwRYrC6PDmZoX4fGe8+cvMG7wbs0ORNl7FKgCBhfFIMw9AS1hOkCgYEAvs4apDzE/RHTyp0QkVsl1/VrprJoLP0d4IhFiNZfwI/INZfeGtSMHBm4mq7F1h8M+WpVMvU4it5MB5FhXuklzseSP7i8xqUYBondgLLYPgpIsOPiOxhrVH8XNY0R6jESDP1ZN4cBQVI3d88VSz0WZhj3/gRfjKh4/hwzPXHHAPECgYEA3PEpJbRe3ezreODAIyUpCGQ5E+vvGJsPW6S7XHHlfK05l1ALIx1sTKFXRvf1FiRpWBqbA76d8CPVFcadws9ImyXzppm23pe3cBoDvEGUt55AgdGdd+hztSfRuI1pYkyKYCvbKLKIdqRHuYgooDWj0zhs+9jQZDrz8vSxwLFGLT8=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDNJti1FsTPq4FmeUbyEoWkS+YHypKjCQBt/jMKLyyTXF+9mFi7sO2iIrmUmEO+mhfcAF0VkZbHr1MZwmIG4J8UUpBVbIX4y4aPrK4tUfr7/HTSuxP435C06z3TUxK6e8FK4zuj7e6Z189H5qt9ZlO59DyRG9u8PfVjx+qg8FYqlxGGfGQaZFstPMj6OWeGCHeLGlVA5SVPS+xjtkjrv56eFJ3JhfBr6Txv2CmCklTeQctplAwdq701XHqihDVLEWFrj1EhXGuNfq0EiH3TVE9VBsMODF36cY8vjKPSYCSq9Z31I4MC2BXID9ksYB2SE9Hq4bDyYxfSXtfisE2YPdjDAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmW8gRFnwbho8VpMsenJrRzur2oMDnRE9ZgEgXG1gNdVTp", - "privKey": "CAASqAkwggSkAgEAAoIBAQCwwjGfo5auOUnxe3jNKGS/L8McRkwCETpQXohTsYz7pXxBoej5tMJc6b8URDO4KIcmDxXUqBuO26GRheZjvehcRY9Bof25biMyPs1WzHImF8rHh2A/B1vmuTzqCknD4d1KHnGrvIA9DCg94S8cuYYCjDOBiiuKFFU/CzIWOv22a2xlfjTDvXKu/o+06U2TZHYkwkzQ/SwXlRJ005xjnWZat5+iO9L9ND5CSXuqJb56qmBMgux0XtAFPR3k9nDj6H2Wv9y0Mr+RKDAHnIErS/msCNaL0HchDAKc9utkARJsrgYwbKtWvJ+bqcpULwAf6oR5VHdbSuoKZ1Rc8hbDbOVTAgMBAAECggEAC6z4LCxZIq8EFGBsjViytvJHuBFoqeZLbM2hwa2Du4el2owAYKYxBIQoLAWJSQvcSYZLkd183IXjeUJYApSjyZyKpvI1WU9OId6GH8qna568tUta7y7kQixQOsFtN/Quctvp9EciTWYFLnk2bHZQxNBQAqmG0LshGmX56///jFIWFMTIidl7XTkIfnvs7RzscPKLm02bNxBTrGS33O1tv4+5/jdsSdShKMyzhr+ypYo01Iv1j/bWDpdzaixNqGQhrslhR/YaU+JfCXhY/22klzO0oTXMXlAgAHpezo/8EoNtRsEBH9x8L1/HXPqAU+ZpW0/eLpyfDiMtxN8+x+9fqQKBgQDgu7yIZHOUBlPFqnZC2smCd9uh29oTkiLhPd1YI1bpMClKTGAnWc7jiZsEuFo5+RM22Z4RF8JAwp2EOl4xky8BGNGlGOmoJsgvSx95Zhe39jsh9avKREVhBLmjPB9PngRO9KorQ+MkFAMXWRiof/tvFiMp3LK3vJ2e6jPS/gniXwKBgQDJWcDi6tkz81UeKSzh2xcp+Ezhm7X1iECm2lTto0B56hHRs66ye6lNY/KhFPe6XXePtavIvQn1xuF6Om4nB0o/O1bxc3bYftqYua+uqmCqr5OkzVndDT0AxKDoxfcDLU9ta8L7BoNgsMC8CwC9UKXboL1tC3feZ3BrOr2TW4YpjQKBgQDcfdWgTE5JsVuH2JNnTJng9A/9YmM4SG0IaVY+H44qBCK+zuiYMzkVbfE2VFnR/1qmuiSnyJPCTi+ViF7abPn1LZCjVyoI3OQT4rTiuxQSXffufccrEIixg51PVrGxv+uiO9Kp2FWHFEtkIPpceBUNDL87V1nRg7FyNX7bSHwSKQKBgDaKMU1F//+qcevxi07CYcvkji6uVuNjPN/1U/vqtJRRavI6kZ+XD4z+/cHURCYfGzu6IgYF7qS8cmcBXMUFnH70O+C7Pf32no+v/H57eCPD22JQnX7bDyMeH9fth7M8mr8w6WfFo+CVAB/vewvMxKBxMd5PtPBxZGonRyKbMAQhAoGBANBSWbIFkqachu1H5ewATo0BjvesEpqcZCgpKEA/lgW48E1oOn1bIq2UoJbZpAC7S3sSkF4ZtAX9sVg7wLFhWOfVeBosg6nDJE77ws+f2g47OunqsuNd9NB+gl0o4/jYvgERQLluVlhFevnHNkRYoMLI3D/uN4YRlAO+RAXjzp8/", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCwwjGfo5auOUnxe3jNKGS/L8McRkwCETpQXohTsYz7pXxBoej5tMJc6b8URDO4KIcmDxXUqBuO26GRheZjvehcRY9Bof25biMyPs1WzHImF8rHh2A/B1vmuTzqCknD4d1KHnGrvIA9DCg94S8cuYYCjDOBiiuKFFU/CzIWOv22a2xlfjTDvXKu/o+06U2TZHYkwkzQ/SwXlRJ005xjnWZat5+iO9L9ND5CSXuqJb56qmBMgux0XtAFPR3k9nDj6H2Wv9y0Mr+RKDAHnIErS/msCNaL0HchDAKc9utkARJsrgYwbKtWvJ+bqcpULwAf6oR5VHdbSuoKZ1Rc8hbDbOVTAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "Qma9mvZgzDdsTf1TLsNvDTHr8Z8mFdTDYNPFP7rt9DMPvR", - "privKey": "CAASpwkwggSjAgEAAoIBAQCzH9tpjgcWCV2Hy/ig1irSoJ5rsYxMeDTTQs4v32N9r1uVvGNDGhqeDOApP6CoB9f+ScQguxO+Y0PPum6GEtltkwGllsJRLsLZpeCIH/e+CxbNf7rGNkAGFk0k6kGQSllVlQKxxcCkeNhQll50/hDLTRNdnUydaIVdiYdABejBkVN7SXW/7zo+8lGXOU8xG1K/V67QXekFB4EhcLS738kVCM6qj7lCa1lH/u7H8UIhdoqBW/FToHeYpTFL8XGc6qnhVo4WF/V4eH27KxZSI6MTwC+2by0/4YN+x61dfM6eCUHF1dBVfAs420fkFpia4wZeC2wffLJMeIcvcs/IFIJnAgMBAAECggEAKQiCNdMAUo8AqwwRv55wHuIGiHsavaXHzCGApDzTSMZz/4AxaPzA3jXq3+gggH2TgEAburfAVRveO+bkTLhisJQ9i1ZW20wP/NXf8q8IDLPznE3HVoK09fAD6hHzxP8TKeTBwkGf2M2KGCPqLXjKFhho+EgBdgmsi3nmzsbLxBOI9E9D+A98HDqJJBI0h7JeQ+BndPyyugBYb5r156yzobTRnbwapGKfUbz7fh4EpJk1WgbFI+/W25JDyqhVW9CHr477t7caokibVFlJyVq4juNR+Cu9+m3gih0ssvmTeNoHuRaqO1AIVZU2ciXnlaWM6AndGnK465UvTyIsiAs1WQKBgQDrZzhsKJoF2VG4Ae8DoOs5/yzkk4vwurvw7ebuWXBHpdU1pIBUzBykcREJ9ilY5i7/QSC6HtbFgpLKKwl+h+rNSjOkt8RKg2ua1BpVs+Gp7L0gmxPVORtEgblq0vUkqalbwxQh/mJQPrfhKLvENVr6WMEZ1qxoC4WppNN82UNcXQKBgQDCzA18ZVzxfYGKyfDHkPT6erhvMN06oI83vKmnPKfBKtmTSOiGVf1W/zNgV3WA3Q2i91C7dzSQNtOT1wShawLWVLDYyDjbNRw0nbKbTDZhJDeu/nnQIxU/e7AaQxPs0yYiSYPp4sSdBgOdt9Gvltfbf3zyMRSTmxDuAhjBlDDNkwKBgQCxDmEc0OkQTyWs3h91PjrO04RjpCqUdQ9ZJscULUdLTIryHvm7Tg6ZDMYBFRqCWBevO8Au3XUy94QK9ZXdisNrh00SrnnAhdqQiMoJ/hNUqNCTzrB7JsnAnEXm+CcUXVwZvb/N1bUCoDnT67xW1r7IH6uWEKZ6V3hAYc4EULHerQKBgDBLVqyYlMpqS0uVdVSE47eV5VPr0W1PkTJIW+dSamTBst+JG9zyRLTk4F/qTv97zn2wwxs3GpkGfr4QeN1sIm/w30dfnHj8WdnRnw5RfsnmqMeB38FycToj+C0KpE36q2GkyEecKRKlAxB/GkVmKG4K1XdWI7vUngXkDy8vBkpxAoGAHflRwYwJNkdAvn94rzTCP1EkjkO45Q7n1SAeC0i5PSYxckpJEJlZ6NOCa0zLWY6HCmI/t6v3/i2v9nhJ1IcqlpRWycv8h04tjI+hHZIFw17/3WsuFdoqmYEupyqcgq5JptN8q8hWNA/EHJfP4eThrhcPlUWIm3+kTuvb7sty4pU=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzH9tpjgcWCV2Hy/ig1irSoJ5rsYxMeDTTQs4v32N9r1uVvGNDGhqeDOApP6CoB9f+ScQguxO+Y0PPum6GEtltkwGllsJRLsLZpeCIH/e+CxbNf7rGNkAGFk0k6kGQSllVlQKxxcCkeNhQll50/hDLTRNdnUydaIVdiYdABejBkVN7SXW/7zo+8lGXOU8xG1K/V67QXekFB4EhcLS738kVCM6qj7lCa1lH/u7H8UIhdoqBW/FToHeYpTFL8XGc6qnhVo4WF/V4eH27KxZSI6MTwC+2by0/4YN+x61dfM6eCUHF1dBVfAs420fkFpia4wZeC2wffLJMeIcvcs/IFIJnAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmaccPcfpmfL4LLtcRMotJ67Hc4iSCSAtEUpmvPbjdQUhn", - "privKey": "CAASpwkwggSjAgEAAoIBAQDSAHBObXQBN/HhtO4Xw2RWXgX3L1LQ58ZXReesjYIZfw2os1tK3zM0ubxJoPq7ICjRtx5jfrBUPwPNn81GxZlHp8Zoj6LaQlJL8peTsMQ+3rkJ+7cVTutcXT612b89sPXjl0NlNJMvThzi46vNp3dWSJ2PUc0/xGETTqw+s6xcj6PXlFM8qcDUCA/1YuQX0uSW2zHFrJ0tP/5O6dICr/eAtMXAtOLwkVUi8liPq3JDdZrNmceYdIcmzyYqArRCt1LKi3lmG3AogR7K/DcHo+RGFpZU8wS/ydhgaiiFZfOo0dEUgNFRFKe2lpUh5zvS65IB2M44GgXqhgmruJDjQa9dAgMBAAECggEAPOUbq+JZTTEn1sdcc0+ZfOHu4Oq8HQ/Yl94RfBvcqgAJue2of2GRu6xQSRmBG1oL/CQZj8hg4U0UkT/RisAp3nlsM03Tb27j5loGUjFj9scm6Row0OD9pt7zHFB0ADOcWc63IFXKiGEiRzi1zQDOvhp4deLGncMYUzzw/Y2kYYJPB3QnMl0kjq6m11ylqoy1ZZ+X6fdpFqLlZ1ZbNUbVDcs4roTU1jg/z7Uq8sx/MvHNO/Zd6CGOEJ1JRP970BQ+DaaMSzFUAdu2QRyvqiWCdt+kNINyzV304LQ5Y6yMiH7IYlUkNCrCG2mIIeriTKmBtsNQyMTa2jiSIAk0Y2ZKkQKBgQDrqakDy0aTZUgZCCbcKt75zsUGwidSxWiOo68lwijS6m4/hq9JG02YO1gUs6hsw8S18nhCCTSJcGkibLfTi87Q3CdVd6FIkqi9sUYwlplktHyNyNUigyowzfa6Yi7qCiM+lKy7BFuOoRXOucavdtw/CjcxtcgkVX/dHgU57RLrSwKBgQDkH93bj0nCUJX4B/exZlHxwUiwn6FE0/xYhdZckD9yVRKnt/saMjLmIEZVNdfHs7qXL63qqkC3QfcY4oQJgpYy6aQueCCceCjFlPTliNd2Z0ZdaV3aQEcKQCf3HEqzxQCSKTehI7tUmI1jzrv/uuWxsNCMxiMPxcveF1PoD72+9wKBgE8Kd4qzOjejp7vllQsRQfotVL4AjqnfVkNJOSyD46diQ5oA9XeitbLSbKd83oekXazc52LWrY1Pa6PFLR7B7Jr2zCaJWkn6DqiY9b7ENCynsILpkjriHVuDKTa4SZ3ryohp20lam87JzoOoobAmQJbQOVTt8HPnTVx/fidAkbDjAoGACr+1pHLL9uv1JQq7ERDRK6L/2dKrtqKGcWVdBF+HncuEZYK1wjY7T7yVk85FrJM7Z4RHnZcIFZp2GiYSMqCEk0GPCuF+J+FBio3KPEaGYH3dQumEEpSUxFbhizM6Ed5meHyYsm8MlJ/biahkE1irGgRKz1dGr6eSQ5S1z2lud2ECgYEAiYi26mKhBy4ASiNoCcmbDP7QJ1IA3A0P/zhRE9W3ANTgeMpYXgyablEGmbgIzeC+8sFhwp7WCJSoTQRf0ulEXUTfcbdcOKNl/6inhL7AMnZu5Z7XS8e5GQsGBrQIiLf1nwFCcib+JV7PUbenSDM/Tnnjd7dZ0RcWofON4ul1QJY=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDSAHBObXQBN/HhtO4Xw2RWXgX3L1LQ58ZXReesjYIZfw2os1tK3zM0ubxJoPq7ICjRtx5jfrBUPwPNn81GxZlHp8Zoj6LaQlJL8peTsMQ+3rkJ+7cVTutcXT612b89sPXjl0NlNJMvThzi46vNp3dWSJ2PUc0/xGETTqw+s6xcj6PXlFM8qcDUCA/1YuQX0uSW2zHFrJ0tP/5O6dICr/eAtMXAtOLwkVUi8liPq3JDdZrNmceYdIcmzyYqArRCt1LKi3lmG3AogR7K/DcHo+RGFpZU8wS/ydhgaiiFZfOo0dEUgNFRFKe2lpUh5zvS65IB2M44GgXqhgmruJDjQa9dAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmSvhA77qtKJ5nrVBXUPjbUkXmCTErkoz1rhS8gAeXzzpb", - "privKey": "CAASpwkwggSjAgEAAoIBAQDYDTAPWDfUR226p8Ok/MKft8zNsuFfBbG3vk88lDi2962fYgwrVw5LwFMEg/yxTLEBJVP0s1pljY7mnZmZdh2rgVyyVP7HEWoKuIuIteN84JZUovqkgw7Ea05cOnwdXofv2IKxRdEaQ5onSLYFMKUR1Impmy7FyOjUYQb5CBlK8tyqHUNruU+wK5bkF1kyjYrwsWzQK5Cjhzl/PexD5gX2qD2r1dFncY8ikzGobDFU+CZFLXG7BaCN4mv6BxXxuisxp45LUVUtfNCMANZLqwNZBmADfMQWplwGki2CFMG7YVmTskHjz8VZNnTOWtisEy6epDZROEwSOighGscHpYfdAgMBAAECggEAceOJyRz02ScKFdHn1SoUokMuZ+R63y9OPpDIjiOIPhMT6Ce0SIhslcv9Ny0oYIIP8I2v0xdUeKIFiVXcqUPVYhogNjWN1Hw+jQY5L8jJ8YMmW9lKDLy1ZR83wHBoCsdRG0LjqfUmxBSMx1aR9Oxup5aFNu4B2usMqR+4oD//rTye0oTpbrBZPyT0Kal6PdF2BXpZ5yQWGGgHKnS/Wl/cb/gGvyfef8sxA86YPnZ4dTiTfcZiFBZxi/M50UDwcAMcWIWf4g++VF83v40GqCiQccsL2xl0Aep5vrqYsWVjozCrcctX31H9vIQMvoVFjFsd/qdSq0+oCvCh2vAOc4hpIQKBgQDurGbzZPeMokdqIQe1klfqhBQ7anvV9S345mJODHhXSVvp9FdCxU+avLA1Gl/cAFwzqByFp3zZTXD0SJUbUS7uVTAKtNkpG+mQpDW0Z8dQeudxHGi3mQX7ucI6jNKmBlS4t2j6BW8m8HzY/zHOpAnJA3vua/uycui4yXGiJFF82QKBgQDnvF7722EU44h8eezVjbN12mh7Plpr2uuIYivZgSxRgoQCY3cxqP1Gj+9cdvAZ35ZNo0T1KGATq48tPyymyd3SW3DVnRE4KyxhYuxG0jyaDFtX/jIiJeLc6nB+xg3/3sgBWF8dF5PlnCGSXUmoicjHA8l1JRK48r6Wr7OvmFaQpQKBgG65xLk+KiowTvlJgY4W6np98/Tsna7RJBbIquqSlnHIMsAC/0iWySt8RjMcnUQvVpcQcsr+vMkDSFfMJICb1S30j2koJWcQ7/aOd+vOCYWovx6Wk245q7DwqM8I7eDgJwXa8PSs+LgT8ZeqLK01JOUAnMorhoVvEdBIhFM4jiVhAoGARTHxJsEl5ufeDFUXy9iI+qrhwdMniscOx2WQ9Fxm0FvpcREkOTbdkeFOtsxo+0DRD5Ot9oo8zgLPONKBUbg7PSHCunYw+xWhJd808By8rb7803R6ocmwSQjT2HbpHTr3e7dYh0ZQCiKpv5uNb/7cbdiKoikUwxbwo+wI+mjBiGUCgYEAnq0mC5Ixt2ya5GGhum4KjEdaC3wT6gpwBzZ7PAtn0ytdVpQrp/2QfcfmjfANFuU/WpOfz4swxgntY5Zf10EQXY915llg62uSdp816X/l/QUwll8BtgmDGfSc+ohWhMZ6n4B0lBZk8ZokJ7QZ8DY4PofgCASoYEaNmoST7n0bRWs=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDYDTAPWDfUR226p8Ok/MKft8zNsuFfBbG3vk88lDi2962fYgwrVw5LwFMEg/yxTLEBJVP0s1pljY7mnZmZdh2rgVyyVP7HEWoKuIuIteN84JZUovqkgw7Ea05cOnwdXofv2IKxRdEaQ5onSLYFMKUR1Impmy7FyOjUYQb5CBlK8tyqHUNruU+wK5bkF1kyjYrwsWzQK5Cjhzl/PexD5gX2qD2r1dFncY8ikzGobDFU+CZFLXG7BaCN4mv6BxXxuisxp45LUVUtfNCMANZLqwNZBmADfMQWplwGki2CFMG7YVmTskHjz8VZNnTOWtisEy6epDZROEwSOighGscHpYfdAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmbjRBWZxJSBsFhKsNeHhTSfL7rRcuZ2Qd3NR4Z1aiKePS", - "privKey": "CAASqAkwggSkAgEAAoIBAQDILimheFvXtSG32jx0B9DLy5ow+VjWqs9DBylaXNb6UTn3SJR7k3TZJVx983zgOWPXit1Q8k496DiNzrbvDcqknYb32KL8v+uq2LBOWQ+KsRRC3uk5ZGkO92CFBrmbGMi4H9fBeiPNbvMmWInLxDhLqlLtLQeB/JbqzbSRAdAc0JaNHiEsa5CtE2JTC2Eh8v1LZs1XugBN/wKe10nScDJqRDwPJdxSYBC18jxAlRLq5ZHkY5DZb7a41XUkNA5tjuh2WOikcbFzonLgesmFxhOOzARiRzeTAJ43EOrOrWwm0vCvHrgeEnuDHuKPIeEXAtBeYvIveCiWPW1UoAEIcbffAgMBAAECggEAYcLZpffnspLNIsK731apy71lUiGUF1JX4j4vHehVPO5KRs/1Y9yBpkKuxvwQsliUwAEbUJrlRyqP5AFeKaUsn/QmpAfyoUkBSPCGOd0Yz/znDjla4SJ+hEafppfAMVSLQhCbB+wkbAGRUdrPgOoVLC7ETPw+vGalNYq8ckzWXBtL45Qy0DEoEH0xCHON/unJORpLacPmGffPuaqrY7D7bGFQ7XwuwsaFfyV6jyXeQaM2XheaMwcNCoRSX0W48dJIwG36OkBOgvGDwwnR9hrC6qrnTwwnB0moJeXNea28Gpu4V61i30uDV0p8/e4pVB/VRJGOPPk0PxIPAM0xbCktgQKBgQD8NpHbCdHhl53wNkBecA7Ed2OK2RuFsbNwxREqCoXDwM7did4/1Tfkp6er/IsgzCH5QIuuaCsyhoIQFRsyd+uRrTG48qhElqoQF3zRFORYdNr+6Gs0vA/yg+nuPZ8gXZV3Ip0EAbRdTXXXB2K78JS+hBnYd/K9U0g63DE4OjAK4QKBgQDLL5g0YBfRdtaluL45sFduaTh6RpIxvskRky0Ob/H9bJ2v7EaJmtIzuJZoXT8oFP5/RjqlzwxNaQpksbzkBpGsedmgqdkYoQNm9LcYg/n1SXhS/3Rl3ZLJ8A2i6FeAA3h66dXX6Bo0pZ/sqy/RYzWSvL+do9E8KggzdJNuhhbavwKBgQDFfCUxEbtZnVJ56MD2MWAezi0PZ3h5cu9Cecw60wpygOJ57Z4s9VNSo0RTEugNwklH1haJdd99LH1jAmPNXMEDzE2Gt9qx+hcninydanJyIO3pcyuemzMRfeEKPw3+VcjXBC9WF8+WzzRaLtpMttCBbQafzSwwuqlwDUIs+MLtgQKBgEBjNLhkOygFoL+ja6ScXRh//4XAF1PsQYtwODb7ApRsdwvos/GnPjVlqUQpSHpLLNroRm2Ez0E4qDKAoHsiGceuVWi0ajeDzrAxnFQIfo1cWuTyTtB5Bqs3hxq4xgGrF+LbdwiUZLmKQsOc++o+pht59L7fys5mA3NK3e2IUHXBAoGBAPlDAIzBX8HsKVjMik5EuJKX9V30xWQPFx8LugzvHtkDj2n2eZQduy2wyQWkTY2BmG7xzEX2r6Nlnzp6iJBEF+Sa4Bhh5wxVjYs4/n5TmJfL2FiX+wOW1ULWwSPDsnWZTtVLk+ezezmgWV3Defirhr8VSct4qh/nk5c7z8nvkSTH", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDILimheFvXtSG32jx0B9DLy5ow+VjWqs9DBylaXNb6UTn3SJR7k3TZJVx983zgOWPXit1Q8k496DiNzrbvDcqknYb32KL8v+uq2LBOWQ+KsRRC3uk5ZGkO92CFBrmbGMi4H9fBeiPNbvMmWInLxDhLqlLtLQeB/JbqzbSRAdAc0JaNHiEsa5CtE2JTC2Eh8v1LZs1XugBN/wKe10nScDJqRDwPJdxSYBC18jxAlRLq5ZHkY5DZb7a41XUkNA5tjuh2WOikcbFzonLgesmFxhOOzARiRzeTAJ43EOrOrWwm0vCvHrgeEnuDHuKPIeEXAtBeYvIveCiWPW1UoAEIcbffAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmcaPaHp7MhjvWdTSCyuvJZ3H2WsudgBbFLDS9J31eZwSj", - "privKey": "CAASqQkwggSlAgEAAoIBAQCzd4cl9ugYvYGTbxgMF7Nj6GUKPvBafUzuyrfKGwnlwGtVirfmd6jpCLX+OknCS0TaxiL2CLtKK8z+RL0Ecq4iUG7h/D83StOnRlcp6x3fVODGz7jwRjdFivZw7Y8WZ29sadRmoLb2gFjmwsuV3xPKkx1mk/1Em2wblf2TcdAQSdr9tobSCvb2oZE9tk/k+llZ+X2WZKvQdTpiGNbFSM3+fuB+HO4qthxD2+a1nTgzn8qKvisW/opFpB7L7nHhJla/exgikzWThN6l6i6/spqcAs2N5rzFVcfhdzhUwTjIevt3070phf9ck2YugSfbE3p3CHAFNNqs4z+gLkDi1RglAgMBAAECggEAS+GYFSdGj19hMDNi2YoT4YRbZG+kNL6SDs1L1HqGPsyTFYInq5ygoJd8S9fdY/drT41DLwAWIJBQhpoNyZmrovqbR5XeLMTIpQuKw0CUSt+agrVFnuIxcIgHF0x6maB2bkJ4+kOt2J//9uIaLm458gcuATdFeQK2PRu4MeWHcbryZfl+gBNgKTttaHDYZVEKwCzkcKB3ePfsfTWdSybB34o3HLzeDcAnmn5dX9vL8l5WoTGYfuFYTzkHmPizVMFXT6xSmN5jyrlCj87LmI20LikE4F0uzJY6JD4tl+SuzMZd4/Z/EjDZSdnFE1jKHb7FzO/pX9Ibq/vT7hpp98dlaQKBgQDsngFwg6L4+k0SzhVMpry1ftEJnFsd+4Ypf+Mxfox6X3JWDORff129JHbyexC8PWAbnUbH4YRoJJ4x5U3w6aQ3y5oHHIFffeVsyO1N1RW77nZRCj1ZelAHcGuNRY5X3T91ad0j6/JXmZDfRVz8YQ1bFA/48ChvJIDoBELWcVCDuwKBgQDCKwx1/A4UmJN58oSt+EqvK9nKV9GRSs4D9SqEi9uixjzx2YY1jHMYbF7D8i6C9pQmvMI4+/8QoD51K1vlQ/XS5EA9TWHRRSeXLVUFEEtErDUUh+3om3sOVi1MSizPy65tLmyHPM0t5iACA2FHTFchXcf6eak4pWEa5N0rV6flnwKBgQCxKvHq/DWX9VqmXPZnyWT9BLKiXpd/EKj5A8/qbFXk/viOY+LPen+Gsvn5P5pdSBthMdcgrMRGcjydIZPFcjvKp0FyV66rAIo7dQryPz2h1MB0l5UuHT41A8EUK2OUeI4ebSDu16lCXDK0aqxgMI8ehhwbij7MUWnPz/j3tirSJwKBgQC34/VlOFZNg0MI13p5GRICXNFjJVDA/cunS+X8qkhVHNJTauQEiwPmOZx2j0MlnUoqddKsDV0/7cO5TFs4Aukp1ipQ5JyjiY85SiGfLhNa8o1C6ImVJsughFVaT1WpZwnHNZRrcFYSBkSCI5lZ4R8T5rGist5lW5tf0Sj2B4pnmQKBgQCqf+CCjTD1/QdMsf39ZRuu3BclKLPbDaxO6MZQAJU3m1wGjMl9UxB8T3+ZXhr5NTtSbYIOTyIrSpvMN4ZSA9sunkiU6Fxp3ac4WlWTJ6tIeGj8IuMJ+RG+SHl9YXS1yMUhddOBpl8CRF8lDsxbMEYpKqJuoww0xqHR3Dt9p95iKw==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzd4cl9ugYvYGTbxgMF7Nj6GUKPvBafUzuyrfKGwnlwGtVirfmd6jpCLX+OknCS0TaxiL2CLtKK8z+RL0Ecq4iUG7h/D83StOnRlcp6x3fVODGz7jwRjdFivZw7Y8WZ29sadRmoLb2gFjmwsuV3xPKkx1mk/1Em2wblf2TcdAQSdr9tobSCvb2oZE9tk/k+llZ+X2WZKvQdTpiGNbFSM3+fuB+HO4qthxD2+a1nTgzn8qKvisW/opFpB7L7nHhJla/exgikzWThN6l6i6/spqcAs2N5rzFVcfhdzhUwTjIevt3070phf9ck2YugSfbE3p3CHAFNNqs4z+gLkDi1RglAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmP2nWtwRR2TvwCxC3CjhRPvkiyV7bFc9MfHRU1fdCeBeo", - "privKey": "CAASpgkwggSiAgEAAoIBAQC0OKmKroyraUdXs7KMa/zE3q1+KDXXecqYeZ7iqOEiXs1KcRpDr85XN8vJsR3NyhFArM9b1LpH5LV0+dEW+zxFy+PH9BdhdYOqpeW2gcLhM+uZyfqXE/C19Cf0CFtbUOsfPsTQ5ODJ+5zyZlxhq/wL/zaj3DPCTrL0VEc++BJqLSsQcnkp+7vKkc2NQ1YPg4D/Y4JvH0Hc3ajzIc4xnooEVdYa4Zbi7EgVG2DLZL7I3TPS9nbE32ysmp8eIpjgq0s6hONynAOYtzhRPDVMF6vs/D3WuI3wvWPoe8mAupscmH1n7S7EBuLrIJ8dsM797J9OXf6+PZHzwbb1Iy7sJR7rAgMBAAECggEADjV1cICoiI8pV8nMJvQQnrjrtsmWzSFGDtVv6HDmJx6QUvEt3+5Jd2jnwUQclG/9AjtdseDIuwhWIh3cFVLDgsE7eTVObpmkQt0HimcapUTBq4NYJXcmAEJ6r+vEwCNWFkWNoOaarnIPArF9URoNKij59ttSnVw1EbxfTaCjWwmIsx3nrp/pneYlbvq3TxXxlbdsS+j7gPWDOhSeL93C0BBJZhsnJ2XC7TKm74dXrOmgN5oywOk2H6ms7CfHr/APSYfXhM0sqk3Ca1D05W0d+fOjjXFoMpjtEruP2LJij3myQ2Dt0ewSQz083MNe0MkStzdP7ucIUZhNWq200BVx8QKBgQDi1NCb24EQza5OlwmZfIMJybF5kpEy/SUvfBeCSro+u5g6CyhAFBu9npACFrdEDYQ7Pj2xTyMJa/w3PBvSuDo/jRZhZennw85yQMOcSdEUWrSw00L+m9t77vDuMc930B0HZLmLH++Erye3/fErcqKmeZjzcDVc3dFB5tJ10ielkwKBgQDLZXfPgbqeaabvbFV3t1sARMG9Y0Ekueq0PunIFnUuHFfe1DFfzD4RuTQdP2Pxugtgd0V2SAHl3Bn99rOYcRZd5KsMvodT/gK/xLKndQWRn2DkmvhtMdkNy28VKgugdPKt/q7/NTbrUtZdY2pxJh+JVxJ51FnMHi688msbNh54SQKBgCpFSH7TABFWkxYYNXTB7FWFnaovMxnSbPyVXngsXtrT8MFYVO7kEGtcwi9xdkObVToJFkwVmEzoL79HV1QEeu5e533NFTLYnX9TLGDSrMDjSmrtY72448ULuSBabfRA9zfqgF053VPXpEo4a5oSKddmL6emEHu25okmb6//Mt47AoGAdQsp2+ZKTrh7kNFliWOg4VGvr107cnfuINUHUNXjjqpOwnKXCwqMOUS7QY1l5QdrXpKkDUG4nd5/so5RoQqKlXNuHwJQ+7tzN4loSUbk8nyllEe9Z5DE19RWUvaEBEzoDco+R6wGs3pS0yDPctc+VJkfj63sErLXsHFLwzfsZskCgYBtZJeHpKStBoSlk8jviGe4stgiGpgycytEpRX/l5l5431lfQwByqrUwoxmSh8JIR3q1H24a2VM+Nmy4r7IGvFMudjGeN56hbcrlN5IZyc0GOYS5C8c61yGWESm362xIQZ74yNSQuqDYvV6w9FdV8rhVTvonjd1koJHtNNNKJG7hg==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC0OKmKroyraUdXs7KMa/zE3q1+KDXXecqYeZ7iqOEiXs1KcRpDr85XN8vJsR3NyhFArM9b1LpH5LV0+dEW+zxFy+PH9BdhdYOqpeW2gcLhM+uZyfqXE/C19Cf0CFtbUOsfPsTQ5ODJ+5zyZlxhq/wL/zaj3DPCTrL0VEc++BJqLSsQcnkp+7vKkc2NQ1YPg4D/Y4JvH0Hc3ajzIc4xnooEVdYa4Zbi7EgVG2DLZL7I3TPS9nbE32ysmp8eIpjgq0s6hONynAOYtzhRPDVMF6vs/D3WuI3wvWPoe8mAupscmH1n7S7EBuLrIJ8dsM797J9OXf6+PZHzwbb1Iy7sJR7rAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmThR7tbPk579XSHjkYY2nFRdHZgnnBCF2vUDb5jB6w1bx", - "privKey": "CAASpgkwggSiAgEAAoIBAQDQLavVsy5kL7DhbHHYobrvPRRq5CV2iARuh2AFYxRWDEUcN1lX8AXXX8/mTOP7578lIo8azZnFjzLXhw+NQ4GJgoHecMHSrGSpK+4ZpCNptSy296taAPwLgV0/GUCHy+iZ9fOkcZN4UaDIxJbnJP7HBnidpVd4aUkcoofA9cbGtUzqL23hdj6a9NGlMR9bc49YZvqDyEncWkAuq/b+iF8kyMJoR4LMT2xBNTnBWtpqCqvRQKeVqQ9KWuOFUykBzyfP91iGBU7NWd8uhf1R/w6nS+ZSuHRAd2IJxVrA8NSQVQl9Dj4v2fDrTVqKmAlXR0x9OdczSeGHwPDXOX+UU0KXAgMBAAECggEANzHkvWQkiKucWihGhwlaZtPq9exHgoXNpwB9lPAQFEBskm6aYZZh9hiRJp58U+294DqpdpHMk3TEJiDJHssnLS5NAI0k1paembvsBSBfw0cl89z2sYZRTTufXXt0gIyvvyJW1uLGFsCNwK1e1SoZ4ur0T9fmuSYxHEZ7d82yRjyQ3D9LOtcjTdCTZGzsrOjAp1EAL5e4wPDo3T89DdoI3WnVftUFDnlo/inEegBriotYpfFVGSQq4GNUwXZnHk9xAGkO7VNr2rh3u9PP+f3w52wU3k0Td2U092btFmmVwcyE82NRP9r8MqZ+fMaBX/IHbYfDx5/DNxgQZ0At0M+aAQKBgQD2P3+ZyT+x/vzrCO0o3Dm0ULFr3bqhl0VkStW3jGFWXcuiI+kKQe1m+oY4t/nekJAVoHPuJO2iKPLjzSMS0m2LeKf4Aj/8as2ev3JXW9dGGaJWXXPgzdFB19ibTi4XI87qCN9ZBmncntgSD4fvFFQbBUa1xuvxHNf8IcKuuXV5/wKBgQDYbDd/UbIZB6N+HAZr+ucmspZUpqTfERQ+NypAMip4o1qqv0OKZ4SQAgvXfjo+KcKsGCMS3MtlhSU2Vm6p6rAeOqOKnaqMoxLQ3xznSlc9ZSlhmj+ZgslNgTCSOlLTaiITMod12g2admDCEWIpuXiBn1P+x8bp87OSL1OeKLHHaQKBgElPeDaZkov0ZOm4O5rZjZhgGaIKXgCzn2YPXXcKpQPoYrJ/zGZQYFQzK3iBVTNsiGjX3wu8FL8dP8qQDOwSl6hZIHCWguQsC9FCH9FgN0PYZ9sccV4xCCZ5EzSRXulmsLg+Mfg4D5Yt+BfQZeDIhY2R0Y5WjXG365lVl7ca4Z2TAoGAT0B5pi8Fd/L7JNAgbeRIRzx4nnETyPfZINtUpoN4WAsBxasakZFM0utc6MG5lE/4kMqZ9WtTNE7ojJhkF+bwLXGtt7H65VtGJaS+UdhAUCQ+XhZ9Gbrx+mbHoZSoBfFEnyEOx9JczuZwkkCJYNwhS95LhO4lYkCyzmJ0TWN7jpkCgYBHabtedfXEPrcUfclXB4o8gAgMbXYJXJtA7UqOigw/a0Sxv4aDuSyQCdagBicuGZ6waZrtzS9q9CHZi6ggkI2uBlpBHHFuYfFdQq8qFvG2YUZqRcTiDxFvOkSbspNRWi8CVMITZeVjx0PgUF3IPFDHM9hWI0LQTrmH5aR/r5oGBg==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDQLavVsy5kL7DhbHHYobrvPRRq5CV2iARuh2AFYxRWDEUcN1lX8AXXX8/mTOP7578lIo8azZnFjzLXhw+NQ4GJgoHecMHSrGSpK+4ZpCNptSy296taAPwLgV0/GUCHy+iZ9fOkcZN4UaDIxJbnJP7HBnidpVd4aUkcoofA9cbGtUzqL23hdj6a9NGlMR9bc49YZvqDyEncWkAuq/b+iF8kyMJoR4LMT2xBNTnBWtpqCqvRQKeVqQ9KWuOFUykBzyfP91iGBU7NWd8uhf1R/w6nS+ZSuHRAd2IJxVrA8NSQVQl9Dj4v2fDrTVqKmAlXR0x9OdczSeGHwPDXOX+UU0KXAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmPbHJAYhY9wvwU8uct7PZxWoUBTn1bjjTiSqGBLRLaX37", - "privKey": "CAASpwkwggSjAgEAAoIBAQClxhN3b1UZdp2r5ge2RiAU7a5j0CQ02V4yXn8gll6tyWNOrblu70pZNpskGLvBFPlM6knavV9BrJwWCucw5imwkZ86Lz9phzPERft/aJWHu+vraS1OqS1TVuq6Qhhmq+fd05+0MHofnnsNQ4HgUnvz3t8CEpsadc1/JaxZEMnUolHf+9u2eBxemMQUr3/6vu1GQiXnzveMa9BBXCfKEYZt3VLDRLBAJXoJ/fE7AGyF1mVKVc2tcX8eaxUannc41lGUVgEuqDGqvAqSNwVBilsjsXYcdCEWtIO4v1ofJzjsX2piUL1apxbHYWdLiLcjk7BXXKFpjv3o1hJ+oL7NkmrFAgMBAAECggEBAJMjnv+xx/0T9ZswT8QPtkYdOV7KznhCP4PBsGECVwM173lUZXT73CgXediuQ2h771O/2NHYqIYoaVp/TvluMa7Rcl04trY6FU6vNy29bIvP1vVao6ZgLyT7ztiH9hSbnPCd9/D93kfWaS46rzqmu/KX7aVvUlBII6ApljJv3lVmV/UcV2cE3FhuQAcsyGdn0zf4m6utsB7It7Yi83yzE5zsghsLwCf0Fml/TtgasGZxgng6CwjXzReDDbA/pHITkLDllH41pEqxpVQleLvAKOs0Yw4IUgBGSK5xQVFBemJDik6PWM5wCV0pAbjMkpmdHxa5icv0NXLeO8m6hJMHuoECgYEA1sIwdrMEFGi97p1BJukrv29DolYrL7zSLI6nB47NR8pOc0yT6qiZXBsUaljYzyZ0Wrhl0LwaBTMia96AJ7NSuozujNho/RkL5e/XMTgl9kCeDO0W3weUIKXiVQMYfWu5zVUB5cov2XCn4tZ1eR4bjrKq9apGB08mxl7i5f+ss9ECgYEAxZu6qz7qvlzpjqZeDsNceSPX5u5X0CupxVx0tQ/+mEI6NnilKDDfl2rXtee4MxGDknQG1ztjMRQY2nCdflAmE435NLfv9ezf9o7N70ybhEkpec9Lp4GBJk/Opc91/N/h8n2RR94F/yYcW4wm874Ef8431ZEVm4PCylnAuUc8yLUCgYAxHGlOy7NUI3vDtGxwxIO/nGcgGYp4uTpq/BhQTyS8lRQJo+pzkCi5+mtZwoWaIZYcJO0LpehhZgcqGdC+w3BYvt/Sj666ql6hL47Lb6amwLIkDJfdWvNR3/15KWMRU3BC93yemvUESZHq+tYUY4EzycH0ugKXq08XsB09MZHB8QKBgFAaLnMYUAPWmf5vRhVp7+RTOUOtPf9uk6UjM1PqJeQGhJ5sDVbbaOdyMfrU8YASC2mkitlYg37zjJePquf3CVhH5ssN/MGNwcOqY6QrQ6c+GQf9lcdS4c1r8HKaRFO7VVX8vJWLVJb3FeuuRmPrlNtR9qQl6cJeiOmJtGvmiqc5AoGAEl4NSQcTnoS1V3fSCbYODYANvqdK/vOCIHY83fCAuztL4PmI//yP/9E/5Q5JGfuhaGlhs4/ponWVKkPrwntV0J+pEo4O+wzeki4Y/kzTIRSYNSEDhctYG3mQYbI9UvOQ9scy2DShuY7FRKmtOXeijP+AynQfA8t24fXv9tiiLs0=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQClxhN3b1UZdp2r5ge2RiAU7a5j0CQ02V4yXn8gll6tyWNOrblu70pZNpskGLvBFPlM6knavV9BrJwWCucw5imwkZ86Lz9phzPERft/aJWHu+vraS1OqS1TVuq6Qhhmq+fd05+0MHofnnsNQ4HgUnvz3t8CEpsadc1/JaxZEMnUolHf+9u2eBxemMQUr3/6vu1GQiXnzveMa9BBXCfKEYZt3VLDRLBAJXoJ/fE7AGyF1mVKVc2tcX8eaxUannc41lGUVgEuqDGqvAqSNwVBilsjsXYcdCEWtIO4v1ofJzjsX2piUL1apxbHYWdLiLcjk7BXXKFpjv3o1hJ+oL7NkmrFAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmPx9N7M52T2GLn3xmNawQZWzFhy7HigYqNPCqU8F6LHE9", - "privKey": "CAASqQkwggSlAgEAAoIBAQDlZOKQHk8yuE1qNuLqTUblDKF6KUszKRyheVJ6Gmc35UXj/t8QwZlHF5VpOMTklWEYqYX1R0MD81esscEoiriaR2SN1hnsEVScm7kRqdhjHsZi68deiXkMfPA60NMLWxIf68/xASKq39XMYyr9MGMytQzVPqjtAThls0HwkMZAnD7uK4BsUVRbtdk3VJSZG4xJ/atbYPocDBpLuyGDSbbNqIN22E9dybOPyYo6kUvsRTyP7FrpcCRaSxIMoGLZT5feW8X8uqqx/VfavgKTxDbvuDVnZ0GdCBmZFbBB86hBprLuiNfoekWaafvPK/LJn1mtNtM4431dwz3B7iroMae1AgMBAAECggEBAK0AguYSFcS4vpnGPyhZk4gXGIlbLz2sWc1mBE/WLdY38Zfblju65nB5VtN+Xu/NwOaqoz6yudX25j516Kk8xbCE+08FE5O4FknuH4s0vt8yTIg6LagcodBLQZn599BupKKyY6btJkocec+lUryUi5uoc783fIsSCoiYwrg9V2dNgWkuEIJKFmX4Q22EHx/bQ+y560rKQGU96tkw9ncSBNge4tNrP1E3TFe8VeQ1gjyq/tYeOkJIsiB8xxwYVPTuBZzIUPm2vYUqV6YbGDH34mNZjlUIE6tEQwjGBMW5ancrW+rILr8LEs+vpkOoA7BiRZwcdeOjv0Zklc5aKOYeC0ECgYEA+k1ihyddwiHuPv7MdPB7lvtK6QbAx84IF77aWVV6XDUa2/UO3efQ5gwy6CRwW6nDjv8DLHFi3pVQpXdqXMdydqXRwKZP9I7gIU5blDs5UTC0Rd9GWPIJOa2yLjca9yYXbeeqbpvEAVhbBUZ6EdiXonUHyomiNKzv8nP0RkCq/r0CgYEA6p2ozO5rmLB6qNP64wct1cut8S4ntKykjVKxZus7DlfaWMxe6IN0vTwbvKIvM0/qO69HF/IttuyWBw+YHgNYiwsdmwtmJawR8t8I0ydDyBewIjcEHieKkCvQ0/jvVd+lAePS9PEhM9h3s8uHdliempCQz9BH+JhGg/E7n9lY+FkCgYEA0DTc15YMbLbyyl4CzudXtwCzkGE4rTuaCb6NPLBYxyi5few8AKSbZTESi338JJNzg5hnGGn9Fy/XVLyfsiuJ8F4Au6LccY8Dq1DV5tjY1cuQuWp/xu8Wc28j/0OBX8LEzHxfjgBuK7xGgn3cfsnPYKi+4WBZmD2enuyLboDOfHUCgYEAmmIWcoutB7ORc0jSPdQ6iAXYNu1NOWmlek1g6T1/BegviOEqzsu55NAJ3G3Iq3Y5xv6GxK4bANTbwFe1nIJNIGm3GJA+ril1QiEbmH6s7p0PzOPw9LrGRipe5y1WqGZbGUxGQ+HsHEakNg6G3AxiiYj5kZYX1fC17hquRnhqQDkCgYBb+u2ykhNqUaCyrzFzizBYFPFboUV8gz7V6reI2uxNvVXQXPAzLZS/jVeiuQS/HubGEwc/yj9PW+DWFjqYKFnVAaUul4Z7o2MeFsEz9jf9/jOihFg+S+Vs+hjTwhRRCfRdWq/8olp6DfuIw9usN/72tmmjUmltXepDP1VnxuSG9A==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDlZOKQHk8yuE1qNuLqTUblDKF6KUszKRyheVJ6Gmc35UXj/t8QwZlHF5VpOMTklWEYqYX1R0MD81esscEoiriaR2SN1hnsEVScm7kRqdhjHsZi68deiXkMfPA60NMLWxIf68/xASKq39XMYyr9MGMytQzVPqjtAThls0HwkMZAnD7uK4BsUVRbtdk3VJSZG4xJ/atbYPocDBpLuyGDSbbNqIN22E9dybOPyYo6kUvsRTyP7FrpcCRaSxIMoGLZT5feW8X8uqqx/VfavgKTxDbvuDVnZ0GdCBmZFbBB86hBprLuiNfoekWaafvPK/LJn1mtNtM4431dwz3B7iroMae1AgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmYRvGtLMb93Cbbaa5N39rGVaMUzpzkKK6Pqpqr2fMvyh2", - "privKey": "CAASqAkwggSkAgEAAoIBAQDHV1dmaS4iKs8SiZ+/Pt6QlGG5mnfeu0wriTdH4ofhYvYg+s7/Ci2T9ok5jkq5RWv9pl6vq1m8Lg8LRWZu3NN7zRxkPE1CvH9knGbUdSYh+cHWHSHq6u4/qANd93yAl6mhkmX779KwDkrHz88uMHagrKUM6Nto43I1fvPQ+U+tfFFHned/OKXvKiuLbOpKkHdW0vc1vXIZRRzHGsh7OjBYFLLrKMGwLkAfKBmYVC4W9Gi9Uk05aSTOynjedsIZlp9Jp82BByTKn2dycU3DVivr1dHXQZhCJgPk+qhRsRMv2JcHJfsgVkWYg1p1/MmkMviaWYfKjIE13T0yjdU8pWhjAgMBAAECggEAJeSafqM7287bciCrN0WSNVWfhhKw+qwL/LKmyYlsXxHay8YhlyWuKFRTHZfI6JMjxiHcGfSuqDDxNylIIYbkxMHmxb8YyLjgVpXMjlJ+nzLFABilm+xwwbUEftZO2nr6Cfa0YEHkgQcWfAkqzxLzWfO3pE6XdsbVrQmm+3CJDuce3ltwIvitgq6ZnhLsjqS0eviYjucm0poHpNhGTHwd+xtmNAZcBQXTiXTMrkd/ppKPFIHEMdk680sbOwFORaCOXlklnZ/aZzS8PcksE2sQ1HirBSFCyr0uTDPENiAkxG4F1PcHIg4mUJ215/YNWKgT1Q4kmKRu0CikxSd/EiUccQKBgQDvFjHZ1LsywGuAyoCMlyqazgHHeVAf+7lg/bo4ZIUDCINrCuTYL4EWlS3xbBu6g6bB77HGm8r7bEkqZgnZ8Ekq4E0cLWKFBkSdL4AdTA0y9J81WARqhKucLanfYkkg7KluOwDsQniP0U0JyJkKtBelL1ixHZ7SpV0zHqVlSVD4ZwKBgQDVcV1J/VO5uFbDz0FijyofcuPFwR5A8QTiusn/PlFXFRkANno+LWoztHc3MUWaswbYwbDzXoDClnOxOhGPiOEz0+57bmipUjOtEjY8qPL9fvh7QLkXTPO/GnFeAAsg5aD0YIhEUCIsAGuRSk2RLmHXEq8aBOX3ddayNtX6yYeCpQKBgQC1DH2bkvhfKk8+LBrEXASrTa0TPM5sKdbrl7fY1GXVMjEycgFxpCeAzl8IHvGwf9lbqwNYfslrM0kEjliPbOI7UbeSytt8GI8E6N9/UAP+vjeB0bEmaGj7z6h/vJHcGNsE2jGMt5lMbxaDfiBGdrIhKIVlOiT3Jro459AfrzFdqQKBgHPJa7IXmrPFLExMwkuVHmSxDp7YhHD2TpAwhCPSyo1TBJz48JeKS3KBE6r9L6UcOTqc2EEtouvschZSSfRzbLeQ4G5VFrHDxgS9PG7rt+WMW3+BPOdG93NUBOvZWjAeYZIwS7vDPMZh8/h9NlbrsmfZ2uNihN4ZLr6+wJWrfbeBAoGBALVhYcxFkxxEKeDe8cVBML/c0qPJExMGGomDOtf/9QSIsB27DfFhO9juGxuDH4ejxTz7ME84ZU2JvdPz5NCxGmn5wGnixKojrjcl+0DKTpT8TuLe5/LGyvKs6w0BoX7WeVUHZSNjIMOUjncxGqR5fpJ6rD/h38jvSvHThiNbhL0S", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHV1dmaS4iKs8SiZ+/Pt6QlGG5mnfeu0wriTdH4ofhYvYg+s7/Ci2T9ok5jkq5RWv9pl6vq1m8Lg8LRWZu3NN7zRxkPE1CvH9knGbUdSYh+cHWHSHq6u4/qANd93yAl6mhkmX779KwDkrHz88uMHagrKUM6Nto43I1fvPQ+U+tfFFHned/OKXvKiuLbOpKkHdW0vc1vXIZRRzHGsh7OjBYFLLrKMGwLkAfKBmYVC4W9Gi9Uk05aSTOynjedsIZlp9Jp82BByTKn2dycU3DVivr1dHXQZhCJgPk+qhRsRMv2JcHJfsgVkWYg1p1/MmkMviaWYfKjIE13T0yjdU8pWhjAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmbxaZzmNDdW3krQy9tQQhE1QFuuoRrvYshihX6QgNa4Xg", - "privKey": "CAASqAkwggSkAgEAAoIBAQDBE7PBwQ1+5VT8oPmS6d8bTPSBMinWrG8UJ28Y8d/Lf/pjz2iCu2F88FRtiOcqkqhGU1jyj8jAwRX5YeQF/OjrMYKFv3SGs/mmmWOwA9NPVIxAI+MYDsrGZSCyOq7bbEwXByEgcvqTWbF5mpTb+n8w81lhdTVQ0pw3m5EJHKcVsi6u76QvQ8CT5ZXyF5fgNLQSN7Yeby/w2AFkNnKbtN11ZOVs7k92AxxwH/z5iniJlydY65jsRQeixDbUlyJpfJnyx5SsYe/Lxc/rcnhqirgTXlDhcjAiGPiQqlTSquxCacQ0yWauqJFTLi+h/KcUBK11d0nge2wPufelqZ+e7BRhAgMBAAECggEAZ/SJtmqRL5+ekJ7DgXx2aaaXhvBRYopZDErnIFEqo9D2KcNEjA8DwFdNveQWQu/Ptn2tyHvuJQpRIIK6WRcA+ZEgq46X2OcSJcc0y1Jj9bSaBvbLkOp19zf/0LaT6wR2O3fVODlv/OIwEj9OotpOnTaJC1YmLKwY/D/AaV2KAL2M2gosgcZyyIZFot1AenVcoxM9WXLHeY939BNWajyOHqFnRqt5CW3NKJVukM1KKtn7doaYY/63t1RMhgMxyiqRZlcw9iCzz6Pd9Ppv8n9ZaboHwhovHPVT6jbajxff/E+Z2bD73kxbJQ26SJV+jsnyKWkIyJFQSA1vb+AHTZBUgQKBgQDy1Pb9AimUoK4go68Xl+sB/RgAdPLBKjqvQoaj1PyXdWNzlZEegc7et7ZlZYXa0L4JSWq2Q5g0K/fiMO7/io5REE1PFBNLwrZvuR7AY2g0Dx7h5dQK/rC188ibSnfFVOh6uD0YFzSaFypwISP8JA1ldqcujeIykni5fO4WeO2sOQKBgQDLjAfXTmVQn9HqgoWODfG+F/f+HrnYzEf2RaXOiF0Av+sdTjIOG95LSM/OhqBYTiA+QSu4MsX1nsmiAR4lw1DPj1h0P+K69W4sLA5L2Viq7LO8JWgwJKMt0sQtP+weJdmyAdht/xioiVUF/ZrgN07XmwTZZF9hrA06Z/En+lX5aQKBgQDwImwRLZdC9FbdziBzO3daIxgeM4hwPzuDX01YLGKRwLNVdP3qZkHV+2SzBt+E0NJsyp5tmZClXymmE+/04ub0ASQCZH7kd6wD9dQUOvmsKZvHlojHSrAjbu3dq5mfmeTAnvtDnIcXLnt4IT29tUVOJjUTk5mxmykpfQLRVErs+QKBgDZkeB/wAiD2ZFj/ggMA9O2waAPPYChwBnboC7PSOtAdeQ2+vJ+KkO+bSHTPAwA1+GXKco1pe/7z7LvPAqhitjCRBLkj7Um6ljNVnohkT051rF4FvP7Ie5aeMPBKmaVAxhjMZ3KVbZh0AnV0XLO38+inszcInHh0SqCl8AqX2eupAoGBANPWOjAv98XqSJYimaTfTSs/Fkf6/xfo3bUOsarittbjnAz3NHqPTXYeSrgSSQC8UhzkD2aL4CA59A0ZIBsw9k1j52LfZ7+PT8V2JTXJxzfGU1bBqyyxJluIqoEn7wrQTx/n42RTBeIDuP3CKNwsu89rZTWzsAZ/eDsly7dSIaLQ", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDBE7PBwQ1+5VT8oPmS6d8bTPSBMinWrG8UJ28Y8d/Lf/pjz2iCu2F88FRtiOcqkqhGU1jyj8jAwRX5YeQF/OjrMYKFv3SGs/mmmWOwA9NPVIxAI+MYDsrGZSCyOq7bbEwXByEgcvqTWbF5mpTb+n8w81lhdTVQ0pw3m5EJHKcVsi6u76QvQ8CT5ZXyF5fgNLQSN7Yeby/w2AFkNnKbtN11ZOVs7k92AxxwH/z5iniJlydY65jsRQeixDbUlyJpfJnyx5SsYe/Lxc/rcnhqirgTXlDhcjAiGPiQqlTSquxCacQ0yWauqJFTLi+h/KcUBK11d0nge2wPufelqZ+e7BRhAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmNvMtvYwxgzJwinp9br4AeNSJgtK2XD7AF5YECR72R9b8", - "privKey": "CAASpQkwggShAgEAAoIBAQCd+JwQZTdHX/3+YyD9DB6i25z1ObF8j7T/BkXF40eX0AmIf5sYjGBqDnj/Ybdnitq0qQY6NQM43fKwHTxjcJ15z8rr2T0jXDLftUx80Szs/WOFDeVjGkj4jMfEJTHpNstrtS4DvAu7zj9VOzuwQZ9dIgitmVDup5hDFlwXneJnMGQfgOBoQ5+N0pcpHmPhyGRiUUReaNP57uOm27cqUDRR58aT1vdmIW5J62UvktsGjPgA27kQbjj2yEi01fQux088pKHahmQsvbIYPlMYoZ8k36zmY38Qw0RVwaOcQC8mDLE4+Qp3JJg+uXL62b1a/XSwlNKzEai34RXp6aaRaHuVAgMBAAECggEAFidyg54eRYVB0rZGPxa/CSnxdja0HHru8EEJ8fmw5aqIW7tBngy5zMXg1Df5B61ihKmbtPgQTp5Z1bcT7AI0I4wvsinSOC5K+DKt2mdffJEArv1G6UIbb7gWn/xzZniHyMAtBtsNbjY7jZF0CoD5f48xVl9FCWM5qFbvbWR4Bu57BFrvXkdMrPpO/efcbmbznKLRuDK7DumOdWTz56l8hu98yGL3Ve/28bs5trNGBjX6dvkHIiEA2EsFLU/YkyVeFYjDvHJI8GYYkCb0yYbmbke1Rw9cdTNMBxaqd7//q0SMqPM0hMYqWa8b/nGMoHF8DjKVoipIXUoH7rf3a/IdlQKBgQDJLxx7mW/InrbaaWU3Myq7ugo0pb8w+RZkTDOhWu4DKsU19GhMq4NHcOTDfuEk1S7zQSV01sjBhDuvRmT3IkW4GEcHhqrMjwU6cAKVFvyn8qnm96X4QwplD/qcYuQlZ3osUn9J/pTHUasQvbJRc+5M8l1h0Tg0APYuXNi2cO0yVwKBgQDJA1VxudXYpzgmB099VohvJtTE70U/BQ/xcVh41uSfuAcU37mLXmo1ZUpi6S5c7/CQYQZXhe7OHmh14Z1AJdOVjqFcyy5OUJ8fN0YMtNyIwK0NsHFHDz1ymyDsXKL/JxmN2uuTSx0rF2iGrvm2U68hg0LWcUTCk5arenaD5CoF8wKBgCd315Wj51srT+IPVSz8G8ESYVgswBJie3MXw/U+unzikifgl+maqDmGu0pjBNZOAFT2jdubG21jfLYJEFuvXJAeKykd0ToqQLNTMB6BkPV91LkcEnJe7JYhCWBOwkVYRI6XbKNej19+9RlmranvHWv5DDrZabZCDgnQay93fgEnAoGBALvMto567dTtXeMBn31dVDhskgqv9QUMyLltiRfUxWKHf248G1CfVCEw0g+ZBazkqt9pFpC828CM3lGMCOt+q7AlwpI8bbXTUubKMFL8wrGtOcD5YMvf7CvfzSGm5s31jMVgjAlf+w9gXlK+tSRoCM4JoW9SAci8NN9emc1dZPmLAn9dvNnOAYPPl6I5byHgT6m3qOq77WshzfpqgRUCxQ+kSKKplwUZ84T4+cM8zsEnxePMlPdNaRzKvkHzxPDSSAWiwtPIl7hPjr5cnNoj4K0rokU/nkq+NiZ7pfEFroajOzBJNMtter1kcfGpWsjuCM7DXt8VUgN4JrwJsKxwYFDC", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCd+JwQZTdHX/3+YyD9DB6i25z1ObF8j7T/BkXF40eX0AmIf5sYjGBqDnj/Ybdnitq0qQY6NQM43fKwHTxjcJ15z8rr2T0jXDLftUx80Szs/WOFDeVjGkj4jMfEJTHpNstrtS4DvAu7zj9VOzuwQZ9dIgitmVDup5hDFlwXneJnMGQfgOBoQ5+N0pcpHmPhyGRiUUReaNP57uOm27cqUDRR58aT1vdmIW5J62UvktsGjPgA27kQbjj2yEi01fQux088pKHahmQsvbIYPlMYoZ8k36zmY38Qw0RVwaOcQC8mDLE4+Qp3JJg+uXL62b1a/XSwlNKzEai34RXp6aaRaHuVAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmXuRygXg4acMEZbuDcTE2DKXSJyMbsJ16Bg1oKiFDnCby", - "privKey": "CAASqAkwggSkAgEAAoIBAQDRMeNG+9qg0JUaJpJYnsWljTuIJ47SfYOhQ8SKsfBYlcjEAvWij2F9KKo9BpN2oI2dqLCDL5ArGJ+33Dm+e40WEhRetW6Oeev6hgh01CetNVgGS8LseeaTzpWN1HjslF1UWCNs5NdKVFp8WWhIELbyfnCjymxafpRkczckfUMcbhvAJ1xmrKwC3ItumEtb/GqxBln/4be0O4aM90qPnXBDIbQ9ddWL/31USL6nnbjy6t7AOie7pXL9Bb23tBXcsw3vYprGGCDcluAdMiFt792bpF3HSZlYhh5Gq58ac0YDoNWUL4A4b4FUb2YGlmX6qIqT/3y1x9RDZ5ws8cpVR463AgMBAAECggEATQRQ6JFQrGQegMIynu3VVl3ozPfDXTtYesa4VVetZO/AOmnchTzEZ4/RHSaOo934RVMVqTaZnUQziT1LBRX3m2iMl1G0oj/A4Tr3Ygu5j8tT3P2HhghbG4+y/8R5wJ/evG62nCCkInlr1twTyHRe5mgmkCa2PZrchx7j7ksvqgc1RxGd7T6IjcH/ed/5bMMUdUhR+IPDfpqJdATu2PzxgsGyfY+INPIsd4PfJEUKA1WIqZL59ArD69v4es+jjGQ4Yjw2Eo/wBhTje7FZHucZt9rrIrTeoqa6syAMLe3tcOzxelC+mwU3GLg/SLTByKBXvHWtPkmz5bDu5wwiu/s6AQKBgQDqL++xlpYWdCpPG6AmkxC8QOYaxqH61UruQ8OAnYvFbZdDGbTreESun0zqKVjZglu6mPsgQfRIvZkyw41mAOBENKGwHFyOUMbftQIfD/KAf2ysT4csDVd8DD16CPjHbPR8xfc5aPo0RYGSHnrOjeFFrrAOLZx1SIrktz7o3T7yiwKBgQDkrgW0VOEMQof+kyUYbUlgbciSvMDMdrovjv3GO57MHEwVefiI7RVx6h2Kuv/MVmu0Hs3uRrauwtIDlP+oxVUdLv3F1nxk0QXiWmK3f/CKRx4ygvay/FsjpssubTZMsOBEslr7pqzKXTIoGSA7yBZfwH/haxEI6tp2BFnScZq2BQKBgD35B4ZIYll4zkV2+w+aNYCL8Bi/3deiIB0jY5Yimv1Y/gFsyRrTDeHkGBeTb4bH33xmxXYI3httyR/M7htDOhXyk6MmLjwfFjHXFcOglbz5e4mx1gSLV05lctNbknI73As03DKeHDA/AIXpePg2RZoKG171JQVIeDEEaSp4ehL3AoGBAK/hHCgPJCuOvBPTTjOUUlwk85/QJqTbJ+XOH2aokkC//tCBx+JgHh9IBcKegoDBcwLMsmvx3S1aT7ZLkbpXU1gnvSy9A11y2gi2pbgmYXWorxQAYAdXSi2Iajrh6mJfo42Sc6GbFshpl1r5wC3afULVxkU0WJy4LJ+aRw8xKuGVAoGBAOUPMQG+tf3Veqay0/4caZqqNvS1Mve5S9pOlNXoBhWnHAFOuKAgKjzMySPqs58giVMCFDDi4ZsrmDECCzBQ2/LF5w6uLnnR5GRLw2wAGYYyGXViKOCunbRmVHSyM7yhGm4URobU2RJjBLULGtcZ3dDQzV25EcSLxaWWLlQLOlYC", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDRMeNG+9qg0JUaJpJYnsWljTuIJ47SfYOhQ8SKsfBYlcjEAvWij2F9KKo9BpN2oI2dqLCDL5ArGJ+33Dm+e40WEhRetW6Oeev6hgh01CetNVgGS8LseeaTzpWN1HjslF1UWCNs5NdKVFp8WWhIELbyfnCjymxafpRkczckfUMcbhvAJ1xmrKwC3ItumEtb/GqxBln/4be0O4aM90qPnXBDIbQ9ddWL/31USL6nnbjy6t7AOie7pXL9Bb23tBXcsw3vYprGGCDcluAdMiFt792bpF3HSZlYhh5Gq58ac0YDoNWUL4A4b4FUb2YGlmX6qIqT/3y1x9RDZ5ws8cpVR463AgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmUFeJFeHPBFxQmAuiXxajjywc7UgEgxMfqoDfL4Bt6jGX", - "privKey": "CAASqQkwggSlAgEAAoIBAQCdqL5vlmpQHlWrkpjZiz/hZq3HK6PKTCCy5/q06uWenHU9aPsq4nCfCR8LGVciqucq7wker+0WLmioE3evR8qYyUHnkycxXiEaTFSymSk9DhCtb3PGORDzmdNRlCndLmDLitkbbQzmD/qia1v0uxRayMDZn7qFsmI3kSgLmyv+1GidBulu5Y9DURgrWCMqpZBiTUSNy0SWj0u4BC0GHDPDmYZNHBhhilJqAyYNCE+74N8UeJqkpQalx3zQuXYg9Tm/OfyrRXWJPuETECU2rORtKNXqEK7iTFllRbh3nq1hLcFTVcgq7qNXUogMu3c34d7EPeGTbNOBSG4pE4x3mBYdAgMBAAECggEBAJwS5qM09n3l6c11zJbfgRe0PChFjVnAz0YM3GWpfDLulCl8+dhUXkUyFGc6aMZLBZm9FPwqELy6qKRq0TrWCTwDUJjdVhlLI94S3m4HrYlhmST4hlYfPCbLiyThVig9t1kIVTEPXYuLGgUb3uaBJP9SaYeG1nFwTEbSDiCfNoiHVFJdp/3AXUyxpwd7q6dNoPcquHsU9gE420LjnU8+bD1aYez6mkRbZ0pOoLHq9peEJw27j993Coidfr9DOyXLM28AmCWHhOrF4MVAim81pmhzy1hamfy6xc5ye3IsnQdyMt/GOjH/e7jPxwWNICIfx/j6RXaa4KKMSZNVNcGhX0kCgYEAy811aJ+A95+W7FkIjd5uFB495jY33d1j36CU6FSCL0tmiJh8ZvD9XaJCW8ebLbPzWl59s/LXVs4oUKHauYecKgJhCE8xhn5vrOVzQYdED678LDS7b6+4tGvFM+bep7gKyuKu+g2yPBz6oS9Ggui48AjgTuFcgcs51l5vVDk9MTMCgYEAxgnXTd0JTfNDp3j/UdvnhnmBDORr+k5R7JHrNxO+it3drylhQ7ucNliRFqk5/hMHTmyPkzo9dSEN42UF8voXX6wZ4hkD5V+qhFaEt98LLmqPjBMEGEUwujDQZf2Bf8DkzFZ2Y7N+8IbzTnhtIIGCbq13myr2urJ6ePPBG2djO28CgYEAjCJHM9xRKnNSrEsABcTG/hBZUZ1ARs7+6HqbSTEqnuiCpTPsfkAAh0yVwlP60K8miqHkX0KAbRCuSdsw8VdcusoN/E+v5yGzGjhfStR+qSYSATd1FnPGVlCwNWLvAHYc/apm1EtsncbzUreWDVeGKo5/5d0x5ZFewJcIh+ofuF8CgYBI3TwTkP0oahX9W36Nbtyr1K7PwIeeDA0GftXNaP1VeLZlCVOZKUEbmdCgRtloizXH/BeDcw1DuEq03Omoca4B7H+FefC+B0nk8TRZtr4VcO2p+yEpkOORzf4PWIu6Jo3IRRPAMT3GX9DLkXGNYTlNYZO9SryHCr4XHJBzdcHEDwKBgQChVvIRDj5TmmO4srMrsMG6BUH+HrAV13Hz6NSKMN2RGH0WwXNC22EYCEWG0liSNRCopJnKEnzGvJxgQiDY9Lph8KKf6ibv6u70J0d+K8Ae7nlU4/Oa1pc9FcQUW2OrIJ8ceeAsOpOikUuGrKHwIwh9zKJmaRLj/bufnBzdG3nhcw==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdqL5vlmpQHlWrkpjZiz/hZq3HK6PKTCCy5/q06uWenHU9aPsq4nCfCR8LGVciqucq7wker+0WLmioE3evR8qYyUHnkycxXiEaTFSymSk9DhCtb3PGORDzmdNRlCndLmDLitkbbQzmD/qia1v0uxRayMDZn7qFsmI3kSgLmyv+1GidBulu5Y9DURgrWCMqpZBiTUSNy0SWj0u4BC0GHDPDmYZNHBhhilJqAyYNCE+74N8UeJqkpQalx3zQuXYg9Tm/OfyrRXWJPuETECU2rORtKNXqEK7iTFllRbh3nq1hLcFTVcgq7qNXUogMu3c34d7EPeGTbNOBSG4pE4x3mBYdAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmThg3JJ7BLnA5GVm6oxztaL2iq6k57LL8E37i5nY1JFxj", - "privKey": "CAASpwkwggSjAgEAAoIBAQCt7YDoUaRHlN11UD1G8tSsXoyJQp7PlX7QJIkd7kCeZHp6MwT22ATBjXQlGewovzoo4Mod7g3NjbCGE4Todurk8tSAq8d2lVZeEOrQ8aaC0TG58JP88FzIOzJHf3SrJCqo3uGQ37tlNYFIGHnS62/aWC7vcZLvBzZw8qUwbXaOjqaB48Rz92q5VbGEPRReYsDG4j8+0EqJaw/dSv7K6xgNiNgMSHioVVkrk/oYNssY1Ue7BEjUWhJJaDebsomZk3F4w40Ycp/Gttq7krCTmmdT75Kw1MVVaSrp00ORE66TJcS+i8PYAOe0frzuxfUmQMyq0y7I8LlrEwO2doSa7h0zAgMBAAECggEAOMI3/hiefsmi16Teymd2ZeXZAPYfs2h64Nv7bywQJGBv468AoLlwG+XYkD78ZXO6PBrXepr0IC9r+uUly2L7Vsmz9WWZiyZC8CGfL56ckzZHfwF2meWqsaE30ENUxIDh9wf9HnUUx3uFfAyYvO8eKmf6sSMkKyL0bjmRFNO0C+MRmNgw5bpx20KQqchsQQOlECUXs5g5Ro8a4szhwr85mflgR7kdMwW6pfNoBZYmJs9hHqXVLyk2y4XW8xPYOfgI+sdMnyUq0HYttJnlTnG/HMLwP4U+Gsxm2H+wzDqSx4uqcD8o7oXiRy+elvJs5hn2Ik4tcMzj/14kfCoKhter4QKBgQDfpuIrVOErEcwVdaCP+sUCq/d9/TK+MkAbUmNqVSagdarhZgI/si2qKVoOYRo1dpnneaHdHR5v5Eyh59qTKz0SGmmYqyw8NoyrEdFpSnzGSWddCQWM9NTLnUc7NQjY6SLei8XrQ/JtsZ7kqnhb16ItGYOaHLZLnGUvNvgC2saDwwKBgQDHFX5gFfxWvGVgrqjkwOMC7Cw4ViUJc86jXSuiTTnBhFILXkI8yk9nVjrzVAXbWMpSPQQZnUcHvDR9C6g8K3hwf+xv/6hBVW268oZT7wecGnzBGWXXgSTB30qaCLcqPe4MQGxedBAw0ISvT6BfWsVOotGvO/VsyWwtT5iqLJ+Z0QKBgBfLzNKpdE+91AYQfuXy25VeMLYSA50jAZkmmfdNWg/GlUjoLqMSVTN+tNtEz6ISnWt4kJVTLNLg6pprbeEsv5G2h7e7trgtYagt/CcEyuPaGYpXlGScBCwp7tNI4Ekb/R7KpmNS1m9/b5WK4cV72wCLb2otVeQTntx4L8k199s7AoGAKdFD+F7l4Do2eTZ214YEqSp+p17A7NlcgEgj0DW0egeXTDgCZc6BG02rmEz/5fEinl+eqtq0ftVzmQiH0Au5grf8LBJhf0e4gtpKiPreeFW/+rehAsFnvSlv/Cb0gnT7uasWmEh81iQWmtR49U6Vv0zICqzngnBUvrfHc4doBuECgYEA3JvoqCVOaBTZ8LYgZrnFPjlVRKvfOwTjK68ezxfg8VQZBrll/hM5rLjTEN+AccDB3a79cMV3FkXAKV7Wf+f9FFoMjGMtUwpicov1j+Zm6nlLd4B1iQyhruj+XN170cmxqYLJGw5AKRPGa4VcWpvJTOtF48pO4WZ+riKPRYjXK+g=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCt7YDoUaRHlN11UD1G8tSsXoyJQp7PlX7QJIkd7kCeZHp6MwT22ATBjXQlGewovzoo4Mod7g3NjbCGE4Todurk8tSAq8d2lVZeEOrQ8aaC0TG58JP88FzIOzJHf3SrJCqo3uGQ37tlNYFIGHnS62/aWC7vcZLvBzZw8qUwbXaOjqaB48Rz92q5VbGEPRReYsDG4j8+0EqJaw/dSv7K6xgNiNgMSHioVVkrk/oYNssY1Ue7BEjUWhJJaDebsomZk3F4w40Ycp/Gttq7krCTmmdT75Kw1MVVaSrp00ORE66TJcS+i8PYAOe0frzuxfUmQMyq0y7I8LlrEwO2doSa7h0zAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmPXXypPBfaZScvDjqtfhaAwnufqgBdva23ZS6vFnpRE8H", - "privKey": "CAASpwkwggSjAgEAAoIBAQCoasuDjj82xdcy8Y4SiDWVph7YOC0fX3MF+6NvmixvWiUomzN7Gvmklgeg8gMWcAmWZuu2cdKWltoqCu7Y78oFCXSkOCD6xKiP2q7gQG0yCS4b1YVAsK5vP6f4GEixO/orRA2uuqJgn/7iy/42uNGt0crr0e6AlgVSiGNyw/lgMg1GaFF7E9S21rO+/Ezezp/hB+R7rXgZ5n0YwivxblivZw0JEadhwn+ror7GpRPu/bJYLo3sVCqYM9CtAE02cNS3/gSYf8NV+nRFWY2DSIlsbpuH0awfFuEszIj42wCqyLos1D7+/UH1A+4DHaVsp2JYQZZVU9sl7oFoY0j2XoTvAgMBAAECggEAI7J3LoxBA9gNVAP1LCJo0S5jzUqi7cpqc/MxYh9YmcWOqLu0vrwp++O8/DUvyFq4/YMVJRedHkQdO9oTZDH3LPgjHAe1ndF/NPaSKIAfZQKjHk00sFCCuJvSe3iSN9bRoMgM6mMutbJT8ThxyqGD+AbGrxNRLTofKK41/gZh3iyFqqOYDlaiWc7mDGg2YVlGEjiaAAm7ZDHpF733gYuwVQa3f7dhKQA1ptmb4t20LQ9OjLbYeocQSDmNfljF+l+7xjsXSWrLxjf2nVhd/PYNfKFJGNGouaLovAaDzESz0g0zENzpeI1LRw12FwEg2711T9rpMVubOfjxhOIBOVkhYQKBgQDVrOXXmnUYYuwxECGyFSGR9Bb8p52azmo3HkEaDn6n7NxSi32LTVfCNbXLD2iiYtIL1ClysTQ9ZGF3Vv8RgWFOjHtQpC5WXdRaKZrNHvpejij8Vv91G+IC0tCoNi6NNR+jLvR0+VC5RSpZErp9jwyirqpqK65NVQaYGLOJOex2XwKBgQDJxu7O/BXUs5PSec9Uc542DiNXm6xuVYFfePbYp4RXtx1qTA/A4pPcrblyf/l4l4OVVtxysGXslsVl/6i68JLnM0pIGOq8ETLuiTu7cl22pCHM7hfw4kx4Mxy0IQDkV84Jxt0Je9ioFXktED9U9t12H8qSFvHbx4D+YQmEACXbcQKBgHAK+nakwnPoI0vS1qhn1jOPV6JiTg1H4YBHeAGuyhFJ7XnHNSyfgL4QpeP1j3te8B9Nv/IpI2hxw33te1B1lE248kyl2rpk9x3UJR0b+lMsnic7gzaoSUoLu2gJCT34Nj++NmdD+GU99GfCn1GJeimwByInB337cLq+cR4q5mhnAoGBAKSn2ai+vXHdOPvAuxfHYYvq7ZxIROWkkPY/1+/kg3Kw0ygy+YgFXXPvsC1nkUR/H7l2MF7G4+W1A1DA2Af02WwhxrQe4S6nOlC9XCkSorawKYT5pj/D63MLAplbdUbhABmqViWvEpXXMBM99vB2ozIJr1yXrLYUj4cF2KYHGN2BAoGAKWfXfO/SqZxm+iButeCs0aPY9X4moP3NKRxR5wWmEuXD0xuUMmYf2iaWytD0RAdIdBlurswebdArDFDfdku2WCX4yX/yWYhO6ejU1acfTROtXzUpGx3XqA3Of8Dg8vf6VbJfKtZi/IsaqhgDSZj6eI+CDgal4uy1w9EOYUT5m2o=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCoasuDjj82xdcy8Y4SiDWVph7YOC0fX3MF+6NvmixvWiUomzN7Gvmklgeg8gMWcAmWZuu2cdKWltoqCu7Y78oFCXSkOCD6xKiP2q7gQG0yCS4b1YVAsK5vP6f4GEixO/orRA2uuqJgn/7iy/42uNGt0crr0e6AlgVSiGNyw/lgMg1GaFF7E9S21rO+/Ezezp/hB+R7rXgZ5n0YwivxblivZw0JEadhwn+ror7GpRPu/bJYLo3sVCqYM9CtAE02cNS3/gSYf8NV+nRFWY2DSIlsbpuH0awfFuEszIj42wCqyLos1D7+/UH1A+4DHaVsp2JYQZZVU9sl7oFoY0j2XoTvAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "Qmb2ettwafKhxxyNNCAAwZVfCBnioFhoArRA6EZqcitg24", - "privKey": "CAASqAkwggSkAgEAAoIBAQDXzYCtBwWGMpp8RVuHik+S4QtSuO7jZbJbx+LRzEx1Et9rNDjc4LxGUpWzjKLRyKTgoj82j+0TILgnRcN6vAwagrf2Je53QxDXfZtDWC1JF/2FX2b7SL2Pv+cQ+4OIboJrYMf1FyLeg7Q6n8ea8S6xV5NPJB3up+WCc/ntwBEySt9HNhFXsod3EgyZqoHpgcizE3xeZcU78cLPDAYUQi7V1vDXdvSVtTNG+AFIUDFuc/uU0yHAvatcA5pWGsxxHwdweAkhnK+uSIK/rhVTFuT/tHR9xgxbE0jMWvuHQukPYcPLv869nxQPA9qQ7MGiWZWvJRttNgZcabAvrGyZvwiNAgMBAAECggEAL91c1QPhrco7iaS4kG+VBrbzk/2Avt8nmEPVg0MVEkKFW3nRwuv11oMqwRBIbM9cApb5/lgd9UgkkFFg8jATXy3vL6FqKvmtGp65eU5tfPDdQl/Or52Krf+aeKHQosogE0D8GNhw23nK19Xop+0mth7+hWc1XGHQ/gZLQPiA1+5rG8JE1fmcYKN/W1kZWlo3kuj5JDmwGug3vOGR+Gz1+aXkkvfTWmR+xp+YgCI9aMeDgOn0M49HffBvOH/fZivw2VEK8ul6x6d9JKadrAPTq0yDXU/aDu668mdIgdEjwV2bJz6A+STqx/DmVCUGAWzwhCgVKbcPP+m82/Su/v1u4QKBgQDwwXnsGPfyHyz89GilqlZ8NWoLmL3y1TDE0OuUYrKuvm95ZW/N7k5RG4MQLcXY9UlzI6Hs+xQEITg1q3V4g18E/E4e3PFf0HZ+yE7ltWc2qsubR102g6hlQoe5Us+JUCk3PVnkGoIHQkDP3KuXWWKJCgizHUMMHLvdl13+xVRKqwKBgQDld40TxSqxDzJpIcwiEXUXkPRSz7/6nY4VbokBgj1jg9KTU6ikblftqv4LdIWZOvI95ynKKTY1lEMPaSWujnLReMejeJSHN4dCKt83LYKZ6vS0+KRxjE71jXL6eMEMS4Zh6+vCfUA0Kfef9dnakVTZV1QJUdjlRiSCm/E/aWb5pwKBgQCrJwAD5eQuThdvZFkYnLWK63YN9HHktcZLxLIU9O1N6Lfat0/6N9WZN1O/JqsmB4pFvikZDY03Ol55WQDTwaDFLJBkxHEbyljS3JeqGYHcjSLdqqgLXyFRizBtgP9lAIWsbYL/9BBIFMN6gcfCeprgDTAOFVlavPqZF0iNG79GrQKBgQCUYvbr7fhpfzZOHfjvnvJlRut4EbhHzFLxMQWP4DTqgXhOpS7NBj3+BzE5HyS1rhSwSygO/w97HmEvOgOQGbXOF5ih8Xu65QGmnCq0d82Y0wNjc9aDRwRYbhwINMZBuSUxdWqD3pMCKJFk84rpeEmyMnK5hCAKQ42gmE8tfm+EyQKBgFdfx1FBrtCRBEY9fGZr7Kk5VgAThm076liVhpu2dr6Himpp6QImt+d6RpgeerGuGNrbcphQ5KHXDPPq/yyTsZHQ1mk6P8wME4ETBV2ACK2AH512jaVGyj5AwGI0FUeU+NLnY8o/AWAxCqShrWgKjOb6zVaVviKE3/YUNyEGGwRd", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDXzYCtBwWGMpp8RVuHik+S4QtSuO7jZbJbx+LRzEx1Et9rNDjc4LxGUpWzjKLRyKTgoj82j+0TILgnRcN6vAwagrf2Je53QxDXfZtDWC1JF/2FX2b7SL2Pv+cQ+4OIboJrYMf1FyLeg7Q6n8ea8S6xV5NPJB3up+WCc/ntwBEySt9HNhFXsod3EgyZqoHpgcizE3xeZcU78cLPDAYUQi7V1vDXdvSVtTNG+AFIUDFuc/uU0yHAvatcA5pWGsxxHwdweAkhnK+uSIK/rhVTFuT/tHR9xgxbE0jMWvuHQukPYcPLv869nxQPA9qQ7MGiWZWvJRttNgZcabAvrGyZvwiNAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmSasEmREwPDaLqB8nmrcP1w6bSd3Zg9LZeSUVzHwaxETB", - "privKey": "CAASpwkwggSjAgEAAoIBAQDC5DJehI8wKoPMeXuh6xs3IS9W/DLrz5pKUbrkU+YNUtCTLAW6J8CB1clGh3F8dweJ9vXlfmd6ihLyQ4EM58yFzYQrG5w6YGA2HlZO3KZQ1foDgbEwR58wy/mfGOO8zbftzkAP05siee8ruVS33mNsRpclntsarbZHNEPWAjWXyqKFTP1aJW/r+ovnOjBgTGrJLSb0rOrPt5F1muyTh2ymkZWlrBrQboU7YBeDZj7KK8FaN/4gNJmBT8qI7kXZCWMGk4SWamB3VJ4MAR51asom2WwQl4gqAcPLrw440CjxzSOiQWIJGPIHSw9AlEWW+xFXmbXIi4GEK0HRiUZ34zLFAgMBAAECggEAChS/vj/hID6yvpryGDgPGlTvG/LDt4rvkjSUFEd6uOm1vEckrLJttMmYNbu/1Q5bJ3nM0mgtdhs6S6nOPRqoa6tr0McG18Ywc9wx3rZvK/NFkXTd839g7qc+bEpfTV7eysBGdAsgFTJ1eq+FgFVSk0E7hEipUMH3kctUTveiSg2sFkGDhtYmGKBO1TaDXnbkCyQDaKspSDBP7fEDiP156fRwMRT8CDtW4BILaLs9wV8x0/PLYVlz6wf56i3/gNhvqDsbd3U8axVIQmrXullosqI0HFl+Uhqhakyw1umoLA3dsklAUmfUmGue/5fxLNCGJ0QFyTpVVf5+zOLRaS4rwQKBgQDywKhnTx2vrL4z3ltiWMsZfzD/mIse0QpIO4UhUboFsH7bsm/kn4/vwVUzFo1tXDG3Mp2Ph9AsP3PTlRcCSmPeGKyyNIP1et+MMxnucIBqrE7JFMwrq0YLdpDxynmPDlfZ1H0agA1YSUBxjf+ETFCCSfglHiZCwfoMobRgRdYVWQKBgQDNhuaajDABleXX2CLgYabi4cSu738jfeB6JGqQDVFWFqtoux0DziRG/AMT8RotqgQRd/skp27AlzNXTKePw9CGilBD6ogIL9S5EBkEbo1osD20dikaEjhbreqsi8CLQpsQs7eypKw5NrM2TLbUkRHxZCjPjTy500WJgnmnNxEfTQKBgQCObpYgz532dp+/JUdvQ/QfCK8COUnfkf27dhjd/Ort7anxVBgtB6ZXoZNQ/3mJ4h9Vg0BJeAGgBLb8PS0b7fP823NwuDl47lh+FXmwmpfufx1XBHnrYXoevbm79PYwBtVq/S9OPjYWSBykxBFZWcGfQLF1beQ7JT+G69Y+6pr7OQKBgB5HTHviQURKiBT3c5Po7wQnzKkVAX8CEWsNKGHWhHARYOlJ/6lK2k9W20E52Oh3TqggK/CndgqLe/XVhi4I5BSeFdsblzTVjxpAg98CRnTw2fZXHhEINCNViOgoopIhmuSoBV0dI34+T8KlJJ5GTQVqAxUospSRyoHKpg97bltVAoGAY49CkW6RUuSD2TlrdlOWRK2+0n4sJDuCGmLeYAMSm9kbASKQY2kqwgXBAU8Ch/SmGxYfAzhYd56MDsTbcoRS4eaw863uPFNkQOyHZa2gdnSho8Ry9PsSnM0geeRI3qA/F4y2ishDZnNahOcL4Xi9WXX+UGkSVjwLva9H9fzD2W0=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDC5DJehI8wKoPMeXuh6xs3IS9W/DLrz5pKUbrkU+YNUtCTLAW6J8CB1clGh3F8dweJ9vXlfmd6ihLyQ4EM58yFzYQrG5w6YGA2HlZO3KZQ1foDgbEwR58wy/mfGOO8zbftzkAP05siee8ruVS33mNsRpclntsarbZHNEPWAjWXyqKFTP1aJW/r+ovnOjBgTGrJLSb0rOrPt5F1muyTh2ymkZWlrBrQboU7YBeDZj7KK8FaN/4gNJmBT8qI7kXZCWMGk4SWamB3VJ4MAR51asom2WwQl4gqAcPLrw440CjxzSOiQWIJGPIHSw9AlEWW+xFXmbXIi4GEK0HRiUZ34zLFAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmVzpVsXAKu8ocLf1h6YxiGaoGzaoow7mgFhnSayUkrtos", - "privKey": "CAASqAkwggSkAgEAAoIBAQC70DVI9M/iZV6F64OHkYCzatlI32bP6bg0pyVnaEAQHEG8RYMm55w+SlDxfO7NE6MPYp1objt0PkB2MUvmhigmKsYlRoOO3kxPf0fgsB45WjRZO+gxhIejmGpfMmQS5+cYx2nWcv6WYXURlIMwb1YkV7AcvZIcqdNspSrlHVqP6BPfIC+dbRnf1xX/1Urj7Z0X6wUnWAFn8vsiFEy4t7ox66OC/uYQMB/lF8PkZcccAeZIlmflZ8LeFqtuwdUpBa2fS+00tZNd2Xav3OnnLU9fnEG8MfdYXqQFq9LD4e+nBEIARVFkaRo6vT7ujU8qdgA+y1bZYA2Y4MBTbC1tnf4RAgMBAAECggEAV0eS+6yJTzS8kI+6OC4uGTL2dx8asFR0/kMO5tdTrijzg4LqSBIqUehHZXIhp7wQcv3pGLbhekvTuRl/pEmELviBzKDQUnyMCgWkaY5u/UgmO7HTXe+w+R3DkSnhx8dtZd6GGNqn5Uq1FM5niQK0jX8SoMiYNinVzw+St5bEl0r9xnQ/0WQz2o1tb5wkX3WcRlWhp2i4OHuuCcV5CWrT0biXzAOGl+6releHceqWSkv57rIYMXOlSp5Eu0JoWsgdETn7kP0GRfHny9pqKNZ8sT/cw/Iq15+cUYELyMLfXLAPWsTkfdM7PVGSVTXnYzr+jsC7o0yqWZ1q/IAO73sk4QKBgQDzcqdvX7M6AmcN8Y91LYV+SvvtNu0igESFR1QcHnF7vPKSbDLIchimi/vxVfAZ548hVgvwoQ8nfocMCWf5ZCn8LSXaZVNcHJjFHnmaoyXeRxuSv1lSGuanBSDBRx02FYIr0K/CHjk+zjim+CHE/RYJez8G2ywmeVGx0f3wGUW5ZwKBgQDFfzdE7zfHiG4DpHvdrzHjyzygZzf4Pv/WtUQZnUSWCdC1D04lDXi/ivqWslnnIO2oJPDBElnlJ7fXIPh1PqJVtote5+xGbV0W/dW1laiUmmF/rsjJM2275mhCKEDbdp19C9d3LYKxhqK42vt4wFn20QDCizZGsTZjwWv92r/JxwKBgQDxKsS5rUlkjxq+Em32O/lBqlC1pzL1ebHngkjNbk8nsH9xFCSes4C+BHC6nFK1ptIAyTgc0cCsdEieYPcSdOquuZ8FIlmZJ28j31PCIBsUfsbO8iYvEx0pmgff0G4ctOP2Oc7Tc5NsJ2ix55+0gK+DBwfh599t4cNPb+KrJq4OwwKBgFHZpHVMUyi90SJvU+qPRjTrMQglXxviODOq0jtvY1JvZPD1E+TlTWrM1YgJCJtymSw7iw/pZBpFuLpO7sngmHS/f8logxK5FoCF2ME18jUMOmYpcQt55fuexQzOE/sgkKqXcsfws56RdvT3xIrJ5T8WZaM7ANaRcUIskm4V77BXAoGBALNV4aqbu6rnMKyl6i2JTvQPsoSAY865Z9M5o3WmGSyR/7DUOPwkX0hUtMZgVQXeto0ENodDg0vrwWsUvNc9tsB1yk84tE+NnAj2EecBxdcgygsyIJh1U+SYAlbEGK+gYuk+cD3K5i3+gcrMYQ3tzv8Ee9TYJkPKod/LEQgtmmhK", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC70DVI9M/iZV6F64OHkYCzatlI32bP6bg0pyVnaEAQHEG8RYMm55w+SlDxfO7NE6MPYp1objt0PkB2MUvmhigmKsYlRoOO3kxPf0fgsB45WjRZO+gxhIejmGpfMmQS5+cYx2nWcv6WYXURlIMwb1YkV7AcvZIcqdNspSrlHVqP6BPfIC+dbRnf1xX/1Urj7Z0X6wUnWAFn8vsiFEy4t7ox66OC/uYQMB/lF8PkZcccAeZIlmflZ8LeFqtuwdUpBa2fS+00tZNd2Xav3OnnLU9fnEG8MfdYXqQFq9LD4e+nBEIARVFkaRo6vT7ujU8qdgA+y1bZYA2Y4MBTbC1tnf4RAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmUnTYhJiofsHuBysR5163GP4a7AgXno2N6Xfz13gEhKLW", - "privKey": "CAASqgkwggSmAgEAAoIBAQDSO7EWwmXElyq2y87TjaG58rEm0ZZhhuIBVJWhvBFOqmacmaFaJWN3OR3QTtEZN6j9ymiOpDWkgz9Vh92Ozk9C6ay7BA9mKoqd2G0UESL9btPV9sQECqajhrscVQPq9amra2wy0ECjvKPMGQEBchnva81XT60Gg8Zm2ItzqopHZ76FsPYGPGPSSTJ1yv/J+dsyjBOmNRhR95b8fqsQGYJVLKwmfKxRDeXv9wMbB557g+/TOPodDeR4q02sgyr+ONOq0PU40qrL43PqtsQBRT2nLeoQl1DAHouia4AP1U1ZwTuNPd5YgCXvHqkr9YJUiagEOZn/a5xBQN4dzeDE6irnAgMBAAECggEBAJLVSBlaSxPkdNvZOyp8uGEURXCUX9DcEUvWlO+yV/A2iZaEorJAfNkPVmhgNCDFxE0FqsM9o420cW6+hxsvsyJL7O1tp4e23LvkJkMmuOaDGodNY5hjDAIYnuTp5+OaExf73kUbOJpjrY9mQ1KMK9sR0whRSMrNDKxWQAfYK940LQXQhAd28T0gB5Scie9l5oQnIRgmaQskhdOTYvFYyH0AEExR4vlx1+clQ4tV7FTf4irqJbh9b1+x/pvuWAOKxn4oGPyndi/K6HRNBIWu0HULWLHQJMWK57APWTBNiVKC2w72ZL7iLcme8eujIakrwSgUK5oHtqC12EggvsdZPgECgYEA/VxnvZlAP2w+CY3U62+SPIHipUIsizmswRNlwj/Qes8h8jcdOv8M0QibAgVHzIw4z6xXUbBlaGI88GfeM5EOpnn6GDt5wGGE/Pfe2L5+iljYm6j7qLj/MXt+m/0iw8Tl8Reb2fMjiQEuDT9XLsXip3ZtixusdBLqPRoRaSYBuQECgYEA1GxI8n5FwamjCjtD/W8jZMJnfCRVgGshRRs66S27STT0Q44rK6jyEY8unsmwob+Hk/884mbQEkmevv9YFRX1Bx2B2gQBgDe5u4ESW/xZe6zO5UOz+1nyw9KrW8W/VI5C/Vuii2Y+8Hnfp3s8KNwnwKgqV/m2qdNip0/wLGK5O+cCgYEA6JrVg3QXUCMIMa1NNXmRQIvekOpYCtpAiGJOojAELzvLZpzC8U8HbUIBTbGbYWe7IK6Q3Caec179o5k4nw8l7CFAQs8X0E+30KegqEz7z/gRpZdWtGhjogJHEt8r85/pm5aZN1fJ4BZ9ORxV5lM265gGqhgWE9rpwn8UTPzfyAECgYEAhT3288Qo1TUmw4AxQYK43LbkWoYf65FHKSXPafv5gg3pOYavpY8vZ7w8LfWtCYgt7rMm6Yw773ymSn+4LGG9dF0Z2jqxBk/t/KMVdQVwy5a1oDE7b+oX0KUQP1xmiw9BDdKwvmfACu8nTtKKBccyWDIjfVNxNE0XkIMfz3eNYPkCgYEA7DBOsXD/lca6cJ2gyb1QRrESqu2zVgzA6eU+Aj4W2f5JGJiYyED1+Z096PdU62Hz3YORW6STrxF/fvl5dpSY++FktTjQqNvoogCIaz1Tv7/2I6qmghWOV7ykznqJV757H7W01LIbkihkTUD5mtrSLLuhZC/eGixBmD+SC6WQbH4=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDSO7EWwmXElyq2y87TjaG58rEm0ZZhhuIBVJWhvBFOqmacmaFaJWN3OR3QTtEZN6j9ymiOpDWkgz9Vh92Ozk9C6ay7BA9mKoqd2G0UESL9btPV9sQECqajhrscVQPq9amra2wy0ECjvKPMGQEBchnva81XT60Gg8Zm2ItzqopHZ76FsPYGPGPSSTJ1yv/J+dsyjBOmNRhR95b8fqsQGYJVLKwmfKxRDeXv9wMbB557g+/TOPodDeR4q02sgyr+ONOq0PU40qrL43PqtsQBRT2nLeoQl1DAHouia4AP1U1ZwTuNPd5YgCXvHqkr9YJUiagEOZn/a5xBQN4dzeDE6irnAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmcMTe658zwQDgHHnyoRhATfhDtugTTbCxwGXNzkZAzdeZ", - "privKey": "CAASqAkwggSkAgEAAoIBAQD83VaAqAvo1ZRSQIR7kqik/V2i/9ywEVPdZhl7O+K5LvoD9FSGKGPYav3yjGQTKwvE5eki8Rq4VwthC99OaaogMUygpAsLLEiaoxoRRHvEM5tyzYlbPTlnAE9Xd0bNto34yLV/duaQFKh2wFrCypXZ4xEWz+vjRvfm3+/hSp6cqBTUGV6I9qsQO+8q7Sqxdh+wzC2U9P473pZ9X4EsqkUq04Ycvon4WVbdLGZkrxGWX9sKfRQW1xPVkZtVJlp/aDq9ZK8VB9dCnxNWOmJlolekfmc3tthe6gdcPEw270rSyRhxuqvPgvhgosu9FAkLKSwA2o5vHsKtKTGlmcpFSBqlAgMBAAECggEBAKRD7UPa5xG0XYwpWWclWOUFquSOroC6YO68uuTxfFGskMIs4RPd/S7EIoCEbyZ8mkKo0JDga+lAsqWynrhDsD8Fh6/7oSj69ZdvSSnagURt+hfUKdzZowakjuZVF+vfIc9yI2XQiesjYGT0hIFyNXK8LYfSPn0Ax152L1D9tpgw1fUF69pHbBI356m4pQfMq5hp3RSI0TvRlDrtmb3Eab5ItCDmtz0prqZIFv8y9HGa+3KZXunnhJyfTapheF0PAdoI/9+mkcU5yXZ9VgIX7RL8M75tUMWLiy3o/mpJFcQbneNkTyucEKzKyk32Z3olP+iqI5sX0R0MU/fyK4IyTOkCgYEA/7P7gwWQDbtomKKx3h7gsUbkopsBeculuhSVoyE1VZgJL9YQW8Xg4oQfNA61nsu8km7AJJsnGB4wkAqU8HxMZpk9jjLsI+GubRLnArLLrvHFwnKOqw/PeIxcrJMBzSHYkQ7aybcv7rolNbABdC13t46Y5Nyh/kpICmJUzLftH4MCgYEA/SiC98LtWmVo4i6D0vlM9Bn+gPffD/MV4G9eK2BiYeneawwuJkjhtU0X2BWG13V11KGHmkIm4eU5ILAJn2q91s4EQ+lSOwjW35PrgFEdWn+y5AkohL7onv2MNV6vLu/dKrT2sX96G91hiN2sYwztarGS34aoOnEDeEThUjzm3LcCgYAkbAecTxOI0TQB4dLCF9XbioSQoNGh/p75lWsHFHjbW0+br7sex13UBgvHx3yZRN30YbAexrbX2Z0DN26lnp7nUlaRRbGbHs9QnAupt7wJjEil/NlThmn/+sZMkpgEFxkY+GuzpdM/Bua78fkTClLuI3KlzsOITB5c1ErN6jjtbwKBgQCB8Bc44D4/lal93m4fDYKoD+eHfrJpV1W1OrRVA0W8B/P3cesGD4Z6LjW83V+2mz19g+M8FBQtAiCOXIyz3G/QHzIlQU7JqkHPw/auh/PPDZheXy0C5ZI0eONMSWsVZlxYnUW52TptrvVu8IiY1nvNtZMzU8RpKrSjOIeGVGgShQKBgFPUX9/rZjR7mbzZakbXwcEZCjSI1C57VMLIH3m/IfE0yhO5KsE8rSm7lpHy64K3rMH2hFViHlf+RyUrzIXyvVOEToQUWdDcT/QxbFNHFolPJCjcssQvejngG4IbO4clRMNmTjCdQZHJoz6o4XwKrFN3jalBPeLrTqSF1HyOV1bJ", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD83VaAqAvo1ZRSQIR7kqik/V2i/9ywEVPdZhl7O+K5LvoD9FSGKGPYav3yjGQTKwvE5eki8Rq4VwthC99OaaogMUygpAsLLEiaoxoRRHvEM5tyzYlbPTlnAE9Xd0bNto34yLV/duaQFKh2wFrCypXZ4xEWz+vjRvfm3+/hSp6cqBTUGV6I9qsQO+8q7Sqxdh+wzC2U9P473pZ9X4EsqkUq04Ycvon4WVbdLGZkrxGWX9sKfRQW1xPVkZtVJlp/aDq9ZK8VB9dCnxNWOmJlolekfmc3tthe6gdcPEw270rSyRhxuqvPgvhgosu9FAkLKSwA2o5vHsKtKTGlmcpFSBqlAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmUaekXpyXgarUkAuUq7LgeXYGFMxsiuy1zkUSiMDzY2ge", - "privKey": "CAASpwkwggSjAgEAAoIBAQCV/mBz7chASkoZWB9N0Z5y4bkAb52+88XLdfE1PsXDmgkPn3txZ5En5gq/pCQ/x7obG7zfb1Deppu1hnkK4imsCMW5Q8tfx3ar79SoX09gMUj0ULcQX84Ruhb/JXtjxHrY810CMVeBO/okMIS4JZPrTwfygodpcjz/7CRGT5fBn6D5yT2b2ADAjcmKFc+cVjMiU33ffLGXXc8dn3vUEI94M8fAtYc0Kso7deLv5wd1BnTS4sa87DMNSbexrmjZNmRCd9OvXvf66UH/rVdteU+8o6bwFbjtx73dfeacbz9SWqIHpnxBRk5TbhT/ahsRxa8gTPpZNWR+o/d3YOGUTDCjAgMBAAECggEAQFNpdg5B1SCHCrt6IVuGgmo/dupnUl8lMo6QNW+ITMygmiyhOg9adyv27B0u1pOHQtzwcTpCClqVaJIVEw/PI1JXyY5Dh/347N/b6aGGXxCD4xNCjyknLP8LobynYDABJ02nU6tphaj9K8wK/xZOi5nHJL/J5vTxKChTnjvAL27n9pqo8zXjEsNOrsdQoeVRT/aq2cexzFv6nfJvS0uZ+hfQ+96LcmBT52OFtf8ekoSp7CcLCZpjmgHW452Mi75vC3B1VRkIeAMyV6DM1knPclSI22Ph5YP/xH+SAVriin1j1i2Y6fAqSYmRLRLLlQZpDX/4d3d6AhyIF+FOOKTG0QKBgQDHXt31BVRedo2CJ/vOkpAwoNMVsyYHeEs/uQL1ZoSTHFR7K1s9Zv5s7M1VCQnpWxfSw4jipmPlHoYvKtxkUcH8f8uBGeVpHHODHsNWQTSTO3uBK8qaH81zvL752h0ElY2kDfZ4yUQjr83AaiKjbl7/0lWzyNWrZfpxVJkV+AOa/wKBgQDAmRd7P/oc9GpS1zw9GyJuOmIyNgsITS5eQ1s07brdNgUNe8Mvwuauz8QIipadE0o9tvRlBTIALhJ1VMZk2Pc7GBiT5R158lZkMCWDtEl+SwRN6swCpaDoEAdi8OAHkoUyaDa3awc/Og/Beh5F9nzfQHnohBiqDZTdQ0jcMC0eXQKBgHQxeuRZBdHEADbx/JRo4LYmlL8Z2LkTx69MsUe6RtvB8A6UtykzBGcRH55GlUs2Ns0z/mwxkxiuUH/e1/FzoL368Oy93fEDjuLFJAz6FZ0VVqZykjJ/BGtGfnr5Pl40lwccyB+fFSJDTIOul59uLNmliSMtkjHBTlOMfWfLUrabAoGAYQ5E+QU6g1DgK7LvVlPQPAAL8AWv9ZT/Yt1KnxeV7VgFn8/Ygr8TBNEKlstQLwPDi+ogqq+9jL2q65m3CKcVn5/68ryo6AUpZ/+jSAWYa55eIu3JtSPGPGunbUK5gtdhbA98U14KHuChg/yIOPWH4/FX/cZjr358oCwCEYPtmLkCgYEAvWLMa7TXajVv+2lb01VmpEOjMnl9FfGtYqnp5LQbbCV5xpecjwaT6nn9jXbuhhTDvARacR/7dYbPCG+hOE5kuDDZ0LeRZuhboRgq9meE5F2ujG2NMfkz0cz83KoDR2eHbVd2M00HRtxSez5AV9y2lVH/i2YWJkI3kIsx0X6m+NU=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCV/mBz7chASkoZWB9N0Z5y4bkAb52+88XLdfE1PsXDmgkPn3txZ5En5gq/pCQ/x7obG7zfb1Deppu1hnkK4imsCMW5Q8tfx3ar79SoX09gMUj0ULcQX84Ruhb/JXtjxHrY810CMVeBO/okMIS4JZPrTwfygodpcjz/7CRGT5fBn6D5yT2b2ADAjcmKFc+cVjMiU33ffLGXXc8dn3vUEI94M8fAtYc0Kso7deLv5wd1BnTS4sa87DMNSbexrmjZNmRCd9OvXvf66UH/rVdteU+8o6bwFbjtx73dfeacbz9SWqIHpnxBRk5TbhT/ahsRxa8gTPpZNWR+o/d3YOGUTDCjAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmdAKuU1tkALvTgazZtQtey8UQWqEZD7hEqYadQpfQficW", - "privKey": "CAASpwkwggSjAgEAAoIBAQCg2t7T0Wino9kgXWcv6reFcvY9QQEjcbq/+viXSOWBuMkIsgt3d1Hl4D6RKVWL0Nq51eWwubEzNeufN0XRbnZPvBX97xyD0TJhhjPWEf1iQep5+Ioea8hs1tCW47XkCDmfp54jR/6p5JCoWyQ5473I8nV+7r8I1aptygfnyAmGcHSaPf55t6ZlPbz4M6GCbxo/Op97N0xVlAEFIl7rYdYl3dCTFYUcN9TzhWTUxWKPhPkpEg+iO0W9luHnGtUebEV2lLeZskid/+NZfXxSayueS0B1mwGlPZlm53149RWXyVZtZihkggaXDXVUKStqWQt9o4W4xAa8JSuIol8ZnCKTAgMBAAECggEANlugf449wqERJ+nIjB3SpOtDoVGNU/AD/wqN5XoB7QOIFEMustGEwJ02J5IDUbtjnvdUppMp+bdYB7cDBhJBMxLJj8W1KiqQzvouHEJ6ETFbTpqZ+kvMMFOrq8IJ3qSU7IoVW7Dhs4IFDI+4P0PiB70/zYRa1F54OJ/UahRke6SN6Jepv3Jgdb5e5wC3NQNvgdJi/hPl/Y3d+T1vzctR9bPg/H12uY0OJTIjn+FkUAQIhiskkj4iGf1MKtd94Oojnh11OUcQoabipQH/4hf7K9Js2uizr0d5qKZ0VQBPJwyf1qlP09WNPwZJIcHh0/cYePA4XosKlWhspEFv4NIC2QKBgQDNx02e/nthoFJ7gGH75I8QANTHgvHiAvtkpfPTFCHVbzkQK89FeeQLE+0rgbEXOoRSlDTmKYRoLxU8Qk0mP6lF6PD1Tgxq1f/+zuROzRTOCvdIbpMLB42jdOuuOLCdFjPngn9dwdmUpB+sYM4J3HdHnesfXSTyGKtfG924HoRojwKBgQDIHNF7qUkZr9w1uDsWPYunAXKhUw7MMznvew7FC1WBb4WpPwgpeZ5cKa9ywviwV7EbKh6FM0g0uP5ArFUXBlcHpEh78x/xfipbiER8P73OGxsuvphdWozSWm47zCv2rnV2RwaqfN3nt937WRCIg10tR/y454RL8Z2J7c1DJER/vQKBgQCSdobC4bKDvA65JJmZJgbFhzHrh0IOcbzo2E2BMVUbivx8jBINC0LKt7YZP0gCln3UIPS91VMOrGRa7X3n+WvL/I50qsafzA1XGX7ar5FdTeTPwxQZx5iCfRe6e1MJm+H5p6Jr4yuwZli84nIEBs1HRhkxy6QeRHzFRxo6kE4B9QKBgClb02v0g/g8IY40wnmJRNjCctem2/MWT04Qp+/PtN9olj5xmZVA3pr7vphAdbe0mBUeMmqjO7Qx29KwC3ITzF729Egx6pM12TlLw6POZMM5VPfnSoRY16wOJqRTQW7dhcdpTJZl8lMW7FkrgkBErjhSnYf1yaEMkdvU+0x6LXIdAoGAEs340X3I2wgWm3gpuhhaEbkg6d8XoilBSNd2J7djFHlPJy7vrNYWcKyvj4GDvOJS7mH4mFXFxM9AVD0xIVeXRK3M/tj9V5Ak6e8uXmyY03CLz38gqHVL0VfU7z5LV71oseASYNmK80+RyhiTZz/+6MdKGoAPSnNfr6DqfViVT7U=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCg2t7T0Wino9kgXWcv6reFcvY9QQEjcbq/+viXSOWBuMkIsgt3d1Hl4D6RKVWL0Nq51eWwubEzNeufN0XRbnZPvBX97xyD0TJhhjPWEf1iQep5+Ioea8hs1tCW47XkCDmfp54jR/6p5JCoWyQ5473I8nV+7r8I1aptygfnyAmGcHSaPf55t6ZlPbz4M6GCbxo/Op97N0xVlAEFIl7rYdYl3dCTFYUcN9TzhWTUxWKPhPkpEg+iO0W9luHnGtUebEV2lLeZskid/+NZfXxSayueS0B1mwGlPZlm53149RWXyVZtZihkggaXDXVUKStqWQt9o4W4xAa8JSuIol8ZnCKTAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmbjYN3j93tRphkpoPYYNPtVRQ1PfWKBXbTjvPb1YaQie4", - "privKey": "CAASqAkwggSkAgEAAoIBAQCvPzx0PZ7I7g7946nS71ihYr3S44ilmZ9Zb6Dkt+nTf4s51ZXF8c1vbeH1rVLqImSXk0nQyuyxtZbXzjhfzz2lkIHWrld6/nFJWrDj4iL3+dWyrbGduzGH5RxIxo90zq9/pvlQomxWLW7C3dfNqTcyEuZPlh09wAsvuc6ymokDjia0WJ48n8tejgv1XotGmJPriDTSQ3lyecMXh4z+fM2EVwSum+8Sl8MxE17WdhBTduYcEzylCjO55ved6yhUEGQ33KcgUxDLOKve0ZWjvipPha3QyKYFs0F4oQhxXJ1KvvTQu6vIvqEexRPufMtF6USmfNWzJ7VEnoPErKRAw7r9AgMBAAECggEAGDdEw0tAhcNfjvXGob8xIBvk3x9R4pA31MP4F6LSTMdzFarN52xiVuN4Ndqden0GKWvQ52kjC+trzKZSY+rfOeGeD2xH6lb+kIRXrSWyb1G2ldoqkQEs9vpRzjyh1iI5XgpUqS/IiJ/+ji7ZgzG+zsyNxrGXmNDQuueSCFwSUss3CRgeLY8X3jf1tNqw8RrVJMK8dQOn1udJVHwi+4BwDoEvWD8JQ/iwTK6Nw9PI9oClKG26zk0zc/nw3QiqQuuRjjh3LEVW4wBnCSiehRIYl8YY9kXq11R9dsLlMl/P1ak5YsC3e4ZqPehJXc/rN37/ypEl88gPSxQRf6hywomYAQKBgQDnhFjXFvif4H1QL4xU7JlDp4wJ3iTSvJFhS6R0Rs8yqSSdwezcC9OGlglFsxAmOSpy8ax3unxyMhBUUjS5X4ZsAr45eYe2ma9pru0bMqat8V+e882Mtx1eL/AEQwUylENAlYs6rVasmQdWVXYlLuO+Kh+YR9XPr8E68eT7M9nMjQKBgQDBx4rhOEY5lvJRkrNK0uFJClIR27FIyWGXSXiYgGa4SRHlwNtbTxQsK86NT63rnwCWMDMdrbg451jDNi9xV21ZLLG5gOZmnzz9Ku1Ool/rWpE7SnfmukBEqXkLuJpLWYGcX4oo2IaMXzl23fcD9UNIs/hc8M29PQYWg1jHfv7kMQKBgQDHCNqvn4oDOKXDB/2nDPj+Vs5ntVkG6yI4+STa6f07WnqmPY/55RjmvZofF8AsfDzoMKjLDcHrEutC8qFtNJiFxx3un3JzI1DQlJg3J6ZwJ/DC4Gq4LLzMun2nzE5tm1Tt8yKNQXQgUjcim7pEYTldxS0AZ9GDCWAf4tGuvHbkCQKBgDa7tOd+bJ9xmkoeJJQ60jU+PAYdRornjrAbqXtxsRHWWb7KZWr6ABml2faiDd7ij1jcjmOQoNs5xSGGWYorBpDMhfp+hRVxXtmnWVX/mRYyA5l6pDlAXEzIjY8Y+kPUKT7Q4YY9+msFroZ7lXzBttp/MuSVg5cy+Fg9i0L2BOrRAoGBALSoamMnSuj9U+XySBgzR4CFnEt//x09sv7and2Ds6R9v8KTG7hfxXQeSCSrhHzht7zGUOl7AialE8DvwmFQGdk/MXXXGLNLc9BKxDsm0SOhnfpd+3gj1tETZ1MNQwvEvH05/YxisUM0Tf7jaxAoRPQCTK5RnI+/SPRM4WKxxAk3", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvPzx0PZ7I7g7946nS71ihYr3S44ilmZ9Zb6Dkt+nTf4s51ZXF8c1vbeH1rVLqImSXk0nQyuyxtZbXzjhfzz2lkIHWrld6/nFJWrDj4iL3+dWyrbGduzGH5RxIxo90zq9/pvlQomxWLW7C3dfNqTcyEuZPlh09wAsvuc6ymokDjia0WJ48n8tejgv1XotGmJPriDTSQ3lyecMXh4z+fM2EVwSum+8Sl8MxE17WdhBTduYcEzylCjO55ved6yhUEGQ33KcgUxDLOKve0ZWjvipPha3QyKYFs0F4oQhxXJ1KvvTQu6vIvqEexRPufMtF6USmfNWzJ7VEnoPErKRAw7r9AgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmYUkVMesm7KC9DddFyoUr96c9ktBV5TX3PJU45BKHT8i6", - "privKey": "CAASqAkwggSkAgEAAoIBAQCvad/4sTNPjoJXFONa+0WXqCiWAr551BwdkHFVEKXS2E8AhtSqFcdU5CANEZUca0c0RPTJV2RhQJuc7HjVbx3jAADyLUwvPWEv+oFAHdgz+/mopVeJjVhmEYWV33LkSwcRFhJdJ3m0gA9c1FRFZbFOe/q/lULyEr0d0sQOTmludDyjWs5S7kkDuv32E0q23wXfEDvLpime3KDO5XslXGVKR957hziJ4z3ZnHII2Exaqbxp2yUoZ2gWrqzTDhZS9Vkxh++yJIRLCwF3co1z0E8BXOYFEec0ON0S607D4AfCvyY1Z9FB+NeBJNPd6Hn1UE6rK8MRRQaQpWLoKw+TQEnRAgMBAAECggEAe2qEeJdEQK9FqTs7E2JC/ocDtzfLCDBib7KW6oDCCuzB+N7kdZ7JFkNDAa7jOJGKEY6Ko7ZnG723Pttp0NFTN8li4QFZ3srSvE0F7zSQT1LzvuJGCrN2BKpDUMVcMp9PI4hh90S07nhDVs7VU9ZOv6efLng4F9VzVa5a3q3wpBLdTJkhzxHCCnA/mMYGhrzemL00RWhxe3Y3HUCfume6W4HdlScvoBL71GZsDTdvq/jKY0DTW4tSK8MS6OulBm4a4flTNCah/+RKsnxaTK59B+TvW+4LrPtAIpCvTNVvvhGtw1LV1M23S0/h9Ti35GObETPF0ydsIL5lBSozYFxoIQKBgQDi2YRIUepYumHzE/J//D5Eyu9oVjYFOILzwz4UnnnJGOJkNaH6kCz4KNXNc7RHgx2+UVGhzHTjK7XKb9cdb2pBul0yMibL5fwUx9TLSiJFiQttVsQG9Uar0w++11SqIVFmVMOG5/sKBu8M4rPnGPuPwKHMPiub3UROPawQGMtkNQKBgQDF9E7U7eLD4Yk42QOPLLZr7NUrbnBK7kkjmsjOf3iHz3srJveGnwMZu9KqeDqh8We8hB2uEAmVY9eXViGb0NDfTTcgKS6N6IyBlbGm+YpF4lPDUP2QU1a4gcEw9IYkyuTRP+Bd/DXmH06hvmtBDV5aiFe2oL9S6k6mS71o7cWKrQKBgQCKaFCvl1s2e7GbkAYbVJnhezgLHt6i3NH5TJyqE+8WZVpr7dVAfYsSdkfMrNXH9BXHsvHtmEOQ/3BRbV+AlCPuqniGUdcd/NqLC0moJzk11+Hi+ldsL2bJG2O1+serbdyuZPVPcGbYvVZJNGCzlaiXEt8lMKGG3b/5ROOghqBCKQKBgQCVu2o1nYq9Z8eH/H64ubVyhT3pECxYQU2JZPcnWzwsXkBoL51jcrvBp1R+JVsUS6mP6s8YboERQug8TKY3WgfkIF/mL8BLDu/YxQYPqwlwOvXo80YY+TDLdzpOcWdWRTI3JP3tmWybmGq95W7zUc1g5WiTd5vAeALtvrSSveeCMQKBgBqRcvMPYIvioU4wX+hfUTK4pZswbZAKXn53UVqVLo3aPXI9NCtIwuJt1uPzrk3knd92zGyHQIL1BbJd+ntrFmgySzq9NfcfN8Aw2qBCbMaftY2VZPcCMl/ZKniI73g3sS3PTyfTtaTLDmaGnTTs9FTwj910BP2qcqM1CyNj9bEc", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvad/4sTNPjoJXFONa+0WXqCiWAr551BwdkHFVEKXS2E8AhtSqFcdU5CANEZUca0c0RPTJV2RhQJuc7HjVbx3jAADyLUwvPWEv+oFAHdgz+/mopVeJjVhmEYWV33LkSwcRFhJdJ3m0gA9c1FRFZbFOe/q/lULyEr0d0sQOTmludDyjWs5S7kkDuv32E0q23wXfEDvLpime3KDO5XslXGVKR957hziJ4z3ZnHII2Exaqbxp2yUoZ2gWrqzTDhZS9Vkxh++yJIRLCwF3co1z0E8BXOYFEec0ON0S607D4AfCvyY1Z9FB+NeBJNPd6Hn1UE6rK8MRRQaQpWLoKw+TQEnRAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmPC4nWM2FExdfdtFPj2Mo8CgD9V39EGYJ1Uy2KfJByQ76", - "privKey": "CAASpwkwggSjAgEAAoIBAQCmJ+uNs+bUavj6HxRVCsoCb7NsfAs/RPm+KyoYP9zMAYAeo389S20cdVJmLEUcv47a3DsvK7OlKBlMlS30nYK7vv+y8t7yk4bgUIcKPRSUWUgRnjPBiC2VA0rdxHCtp5f5BsPoOayN137bv+jPanPWba6B1mN1zQMLZt6Mm+Kgf3xWlSJv8boUx066s7zAGQ1+4neOfejdSboFA6faRjPBZQyePgYkaObgeaGpkJN7KnlLcmMdN+yqvRTAo2Lzzty3GmmNg5OsPwm8Sh3tsGvRtC8Mk0m1KpamIaxOlEQiT3pAj/XLmvn+I19HSAU42ydHzDaaQCor/Wlyi0pxWd3HAgMBAAECggEAdUfKQYxRi3Aya8JSRLDH5C5aFGH+Qlt6eNvY66LwQ+NvPrEjF+3Mh4Dcd5gZ9G/V8u/uqp4LQLFsIh1OgdJIPCNWM0axTcIKOv08RGLWytu2PhFP8PQhUIQxbRXCfyDD6Zf34kwLW1dXiN8OApHeT+W9fpIIRFdAJeUng1JpBeWzgeys+JjBitjdrl8BEeDaecYGTDTVcL7Qle98J09vuBU0AEjiL3wHZ1NOqC6e5LdqJTDBBwd7qwlmJYKKTP7p80y7AV46QPkhGg0IuxtSFz053Vy5zgxNmEhOLG/6t9OViTqr5q5z6ruW1fDDFSup5VwT4OucGRj8i7mcJOh+YQKBgQDPkA4EB+jHgyOs6r9cywa10Y3eKZSiTn9q9vc6YiTmzf/vM+3edzXBTp2DgE+NUTNrLX/KBjCcZ4MJO9pXv7isC1M4RzoheNcEXt/lQiwM40NWtyoBtZmuQsd9VzRoRkeEUfRD30cIGftyLynQ+T9Vs4ohO0i1nIPTHodRVdRydwKBgQDM7jHmf/RkrYdRIUYWmqEDmxC30bRdzwXNuxkgtodjH3O5by2dRIna4VHHC/0Fajr5tDsozLE4y5onapgoLUR9BIeQ0zdHpfhknyRmPJTAI7tmOUAtyk5Ag8V21UAnQa89/2LkiSOXK563k7IbcfhR8QUpyqmHtydXjpTjfn/zMQKBgAbcJfpwIHtnlChE4eo5M5GSyXOMQENVANUSMH2XfMy8BjdrqfLuUbJ/3KjZ9sce5eom6NBOgBDLQwNtHPxFc98LyMZVZFBy4/hbAl9bXoVWhYU6LIM980RVJK650RuZJwfyhXYwzPIxmaPedy1W74bvliMfCHooIBs8KRDBG3JlAoGAZIGV+6RZql7o9MNK6p8fxPLyOhUhTrjP8dyHMGIU+GpeiV2bk3wf2DeVsfeROmylS/423YW2jVJd4mMHCP1aj63/BupwPDWMI11hrrqbgbiEmlgNv+duhXmbCPMBqb8vQUrVp5wS1ntQNly7h3ZYAWghziNVDfin1Ota3lAWVKECgYEAmZZxr4Rjqrdl+Opo/UBO7z8hZCiMVCydjOej5rV0LPeJvPUN26hZxbzI8T/kIjN+ShwXErkbOdm3YLSiqSBD2V84FiL8D4GF/fQstq9kWMHe05bABegFdZPG3eRUTTeVca/CoryBSxfrYv43sNE1wPhBC/mcaKl4a+Nf7fb93tE=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCmJ+uNs+bUavj6HxRVCsoCb7NsfAs/RPm+KyoYP9zMAYAeo389S20cdVJmLEUcv47a3DsvK7OlKBlMlS30nYK7vv+y8t7yk4bgUIcKPRSUWUgRnjPBiC2VA0rdxHCtp5f5BsPoOayN137bv+jPanPWba6B1mN1zQMLZt6Mm+Kgf3xWlSJv8boUx066s7zAGQ1+4neOfejdSboFA6faRjPBZQyePgYkaObgeaGpkJN7KnlLcmMdN+yqvRTAo2Lzzty3GmmNg5OsPwm8Sh3tsGvRtC8Mk0m1KpamIaxOlEQiT3pAj/XLmvn+I19HSAU42ydHzDaaQCor/Wlyi0pxWd3HAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmRD7p61F8WLQxxaXSUkhSiCJD7BvEkSBEJEmDRKsFqKx2", - "privKey": "CAASqQkwggSlAgEAAoIBAQCwqroKmBCeb9LTRqENrnRehdCTTk8eJxRaF6SGUiQDlL7waBkXKGzA3FyA8Z4wlx5zUCjKtwObpRd5Ijvp1bWvivc61kA+RSAMtSaSBXlfzRZs35h+GxYNWvBfZZBsgoEVTQ0zOr+nvPy4C+ADuSJpZ54fP0ZjDnFDy+qj5hDUW2S4qiror1Q/kH0JNwCmEdcQeoo8JgZwvdl8pFegiOouamHXqOi6pw/nrWZBgGC4kInmz1Ui6lszowSI+39nDsRKYFequ8vnhCYz5linF2p+I1q2V6JwNEc5eGXNMSUwdxulM4+zJ5agQ3e5vDjQ+AeRO7NwpWyHyekCfdK66KztAgMBAAECggEBAJ03scxPuypj9UhTqGuWfrTHfPA6VipNOM1cEOwAGVCehLVIzltPfEi9Ugzl+JLhSRXxlfuglrNiXdtM3eigaMlJb+6KUC2aMoVciHCWModQ6c4FxZ0j2aIU9ajPp5EJKnqcUUzv0TMi+fuHhdmKXddTgOHp22e3qJBe3fbxfLSdDb/jqVzQeaOg4p7xvwn9cghRNWkOqRqMhBL/5FIw03+HCsapAgLB9i6+y038dnbhlJWqVhfIdoTRpc23s7vlra418U+Khkak+F9rymdPDoh26biTyenQSTGhoXDGozeuletJyz5x2uszP31WV1jPkECJa6kW6eyQWCRHDZ3gJIECgYEA3sNF5sogTGM7EMfoUgSSMYYbTrs9Gz5Uk26h/8yOZl+/eEzO3gFgzVTlu2bC/jJj777gRg23oNMu87f+Uay/gAcdz3c15NYm2wUHvFciAk1kE8QNhnIvkC2Kzk+kORgWD8x3qHOuaSLT/Khmfc7V23DqgWWKyFOyF8NoXmI3ob0CgYEAywbCQXdQRwqmCbaZlVcUBJKVY+3R5l882ZXcqtBrCOShQ1Mx3M+2m417ZYQDulDDddLanTG8WkuhFLVLO9iOK0CVgMbQt314cTPa8Ro3hW0T7kYGOGlATSpO3Y+46/9MfOsyehT4WdPK0lXu/Rcnow2IKLNQJOCa4eorZu2ksvECgYEAnqBXCn0seri+urhfyufOYs2obGwQm3HLMCE74rd7P5M2+SdYt+YrVIv7+3K1r+WaHILDmZ7y/+biLFL9GpP02eo3ZCDzk7ybdqMiWw+A/Dq35Qtaxj5ReE215iv4OV/Zde6X1rBpphxS8DvKoBPFXboOg44XQYe37gwMKgmuq9ECgYAdOO7S73J9lznI4iB/D1aRRev8wylYKFMg2mI1r+QIFqhjgWEG8FrPTvD47qR+t8s6dUwEHjmHIaWgzmtyxLvJ2/To4TT/hC7G1HjqBSUCrm2U+T1B91xK/xD08Q/j4A5JWK0eR1Br1YE2/yl0AlYxMOxtN0oM1MtWQxdWLFRtcQKBgQDZu5bIG3PrKR2ySDnTwlY9nwPqKSVkzSlzipF3xs0cXtZv+caONI3oAeX4V0CUZ/q47wvZ7b36z3iEQUQqnfDDQaGafqHOaKpTVBuBrRGfwRb4sO/9OLAU2vyYvmoYzuSkBCVNqB3pP8Hc3/Yoz8jJ/Dk8UBkHJuNPGNdNCrhFvw==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCwqroKmBCeb9LTRqENrnRehdCTTk8eJxRaF6SGUiQDlL7waBkXKGzA3FyA8Z4wlx5zUCjKtwObpRd5Ijvp1bWvivc61kA+RSAMtSaSBXlfzRZs35h+GxYNWvBfZZBsgoEVTQ0zOr+nvPy4C+ADuSJpZ54fP0ZjDnFDy+qj5hDUW2S4qiror1Q/kH0JNwCmEdcQeoo8JgZwvdl8pFegiOouamHXqOi6pw/nrWZBgGC4kInmz1Ui6lszowSI+39nDsRKYFequ8vnhCYz5linF2p+I1q2V6JwNEc5eGXNMSUwdxulM4+zJ5agQ3e5vDjQ+AeRO7NwpWyHyekCfdK66KztAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmWEukYe7VLXaSFG27QMR2qMKLuSJGBiiFQCVA2yrm4LGx", - "privKey": "CAASqAkwggSkAgEAAoIBAQC9IEf/7M1moPkYTNHUkIQbeqbPS586tVtUxJiqZmfua8G3lhEeee41V5P/4I9ytGIqWldkLHBynG3asN4mPM5gXhz6vJdvROB4b9FmGea6XRc3zd1ZWZe7aaL/zL0bdKFLxCprjaF5ijW11tl7fD1XuBtvgh9FyJpOQi8AfcMQfy1sBbr1TX+a0C7mshEjMfN3B3OKSo8eA2RqAcXq4GkVdhBFuMyCFzhMKV43B8s/MKEgkAAALLNpEfw7kZc2pHIqAk3T4tYd/cEFCrFPcsjxo+jBicZg7kZEA+9AFLfB8734o4jJi215wTPwCVkc5b6ssEP7yH3BGK5KsVHpHjObAgMBAAECggEAXx/UdvnhGeSPRVSmGXcSq0uWiR8tGHdNV6aGbvaRAc97IN6+/4gucu/4xbNqEzR9R3YnDIB5knvxmRRqt+rPlpLfmpGuzU1kZc9AEE2oykW2PuAxnBY/BgmM7YJJ/3w7AIPLHkufUyVb/Hjy7HRB2lQEoKJfHldWnVQWlfWrXijrgLf9FqwcNC+H5j0WCga/A0Tbf6pTGrB16MtaiuThZdy/U0S8pWL1ByMB6mjsYORxXBBa5QSHecyltqQCn3mBnbZdI7t0Q1O9wo07bHpz7QxqxlAume1IY2uPSSpI8QMHBMBNKNpa3525tBGhZVCNPyDrXXF6NRAwgC4Zkqz0eQKBgQDse+5GJhocm5TO9sZ3N+PRPL9PeMn2WPURClCFm7iS1zG+oDzBxYu8ZOPAjQi+gx0+xlfcOn+/TFidFBqRLleZBzg1+9Qys62Xe4IdNOZpseZz+sdg2N8PYqBXBoPEyIK0DQt1CP9kSqktG2soKdsU/52mug6MmtvEHX9zmDvVdwKBgQDMu9gRwsXsAenSApks7xzDXMXbA7/myitn3/NU9zD/s58+wD0otKRRPhNONDbON69uH0aB925OkEUZQ10TCYS70QS02K5/4uB7gBT+PW94eFCU/jf+jLpVjz7WzsZ/Lz8h44W74BzrAx3TW3IfKEGytfLCy8/kaJES1oK5oTjr/QKBgFfW5LeLuZE8vPZvNWLdELMMpGcJj8MAYe71bNlj8Rgh9KlA7bBwBypwMyS3fjL9kqRZmhMEa6UL37Jg4Eli9Ei0JM3wf25hzS4CQ19D4f4KhXY5BUvU4m3djX8lvVYfwGTOn53WPL7s+I/3qkLd4TGYjN98JqFVeCINbuTp+/ebAoGBAMeVFz25MliwRNCF1+0F7HRGrFqlfR3vWAEbQItDrnCXGlaB8R0NfGH2sbs7C3JctpgTxRhNrSrJWZMXKFS2or61NHFYCkSBV3UNl2mBWnmGUIfui4eKiNt/mTKuwLKbzF+s/WH5SDeSAjFYpBfblrAwz0c2iKORjFtg4m8zy9nBAoGBALiyvnOoHqnuh8vpSu/icuEROGHpmcp8PyGMlbkc0I5dmIehOaJw1bLfWoL6GLChufwDO6djpIao8UJAPCgHsxYMXwZjxkhZEQ3RL79tg/YiulRR5O25lXNk61vMALIcUmQeCow64IfBcY0PVDgtQJXb2QhCCd8s3MOURCkqKHa3", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9IEf/7M1moPkYTNHUkIQbeqbPS586tVtUxJiqZmfua8G3lhEeee41V5P/4I9ytGIqWldkLHBynG3asN4mPM5gXhz6vJdvROB4b9FmGea6XRc3zd1ZWZe7aaL/zL0bdKFLxCprjaF5ijW11tl7fD1XuBtvgh9FyJpOQi8AfcMQfy1sBbr1TX+a0C7mshEjMfN3B3OKSo8eA2RqAcXq4GkVdhBFuMyCFzhMKV43B8s/MKEgkAAALLNpEfw7kZc2pHIqAk3T4tYd/cEFCrFPcsjxo+jBicZg7kZEA+9AFLfB8734o4jJi215wTPwCVkc5b6ssEP7yH3BGK5KsVHpHjObAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmSe3T2985rtAoiy39uahpYsjEJd8GK7sFgXAMbnX4AQY2", - "privKey": "CAASpwkwggSjAgEAAoIBAQDt/lYSWdZdTVdNmEc15XXHZFPtPutZEdhJzzltTuN38Sl2jWymsn8CagHepR3Scs9FwzV/tn81ROZtRS3YcNZBa7w54tWSb5V2Jvt6kyGstqBQqoWnuyEmKqYUPasvQEY0Xg69IHsdtvL5XnheQAtOGiIQZCwyrwJBU36Oi0Vgp9+n42hK9tDwcrqKweYL8/Xr9V3LT2OufZ+UXd3NRsd2G2u2bLQbBzjkbo5MjEWGJwbLTDRwg+VI+lIz9vqnMk+Hq32ymwUqqmumzmkkJ0hQWTxQmt0vBbOtSdknB4be7B34LZaf6HMlm04J0gJHELqwtRK97mD18t8yZBL0eCeTAgMBAAECggEANGE94FwVagOTq2hQg/Q0r+XM8vJeKgRbbiNFqGEsf0F8trL5rtaqTYW3U6FTpvXN2LTWGX25EahQbsxDAtgSz+M+Uh8ykkAszQxXXOr1BmZLcnWVZQ0yhovscZgBDS1ARlZNOCLl9exGHcxFAblmw5HM3X6um5kZDfeqawUMB/F9+Oei5rx0DpI6z00zJjT096Xu/TbOTSDLJUhGqHUC2dI3bo+VjVlBzSLdot+cXfsxiP0kWz6Aico/ftWEZNoG6eXNJBpQifPvCEMGImMIwI0jpolW4cUdu8tSO2q5QbpCHhUHwAQC5jmQTrHoX3pl4s6aE37xxCmm6w9TMZDpwQKBgQD/axU/3KRs60wmdD9uLcGmP2BIRt0PeNiF/FO3p8GTJCHwN5/uKtxnh54oilWSKYuAPRO2MAZenf1Z9LsPt8vkBYeGN0gx4CW6AeH8hT24VqJUbjniPYEZFnLc8ooN9uFxe4/eukq0RVCPwz79U3HHupXYXoHSm4bpA3qDJ/iNLQKBgQDuiRgQmzNjZgPo034F71/5aDxeBCF6DMTmXwruVU3pzGsMcuP8X4FNWDAL7AGci3h2uI5MsS0YVwZ1vjjg9EFv1xsuQR5+BiTWD2BQIKqbaaL0B6Hp2ZfsNvsVpJIehksRJgUfUJtHtearYxL3AzCsTV0MDFgooupqhx3X6WP/vwKBgFmrXmptK8yRTsqxRROJPNMArOyy9CjaZCmlzD5NxsfBh6it3pfetEIkeoIBDsmhjDgZOTJc6d+N18QdBw8dl5cV2d5kyhO4fYYv4wakQGbXA2ZgzDGBJjGIkArBm3YLllog5wFqpY9kRkQyZ4rIIMnd131+sFUgBN0JO5mQDtKBAoGAZm28RbU/ddliqGHY5deKkOCvu3duoKhHDN2XJgy/bjv3Y9saB09DiODrkNMBRiWlzuUlRc13HdKQ1ZKffgmk58+ovk38OAWPX9QueXntiNrtvHhikLZ9RFO/seV/UVg9d9mprW7BnyN/L+1VQXi/N93orLnISXrbym7G4+Y2qKUCgYEA2Mw4KUSDpHP1CJmSqqQog09vPhPPT0mU/8fj+GdVxwP6nv6u8z0Re/49ly9+1RD0dyXerKQOAGd9rvAjkmmfqr+S18gNRAkepW7DPiveL1rvRH0O1fdhSUAKDXDiWou9mdPUH6U0290AjjbI+ZAh83Af2VmJOvPW+hs1sCvkF6Y=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDt/lYSWdZdTVdNmEc15XXHZFPtPutZEdhJzzltTuN38Sl2jWymsn8CagHepR3Scs9FwzV/tn81ROZtRS3YcNZBa7w54tWSb5V2Jvt6kyGstqBQqoWnuyEmKqYUPasvQEY0Xg69IHsdtvL5XnheQAtOGiIQZCwyrwJBU36Oi0Vgp9+n42hK9tDwcrqKweYL8/Xr9V3LT2OufZ+UXd3NRsd2G2u2bLQbBzjkbo5MjEWGJwbLTDRwg+VI+lIz9vqnMk+Hq32ymwUqqmumzmkkJ0hQWTxQmt0vBbOtSdknB4be7B34LZaf6HMlm04J0gJHELqwtRK97mD18t8yZBL0eCeTAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmZ2rBVRQBbSKaDDAY7WhzcoN9AjvcebTvUPhRFyAk6Lj8", - "privKey": "CAASqQkwggSlAgEAAoIBAQDQ4LLRPd47qKngVWvUziPeFPDPISVIi8HgUFJ1Vq/CbLKZDrB20JjLvXwA0RpYkFfZHC84Fjmh4+LwYuV5qJy+5X7IdtnXjUO2yXyykkJ55jDZb7ZEx/5jrYSK+FY7GRaYpQU5dP9R33FnRdVm7GfSETmSwKv/NRQe9KTLKM+K9NvkLPfemau1YaGA2lF4h1TgRbHZYaz/Pp05Y2wv/iRXPRX4wE3k7b7k9oH9TL1UkoNn7L9G9/ZXA7zO8F51YNk0kGfYSmZSIcT6CyfU6SgZdojOYyQQ1KrUKy5lbByaHRh5PW5H8f6f1p8V6Ja/iUMwUg2YQobkKKsY4G3sB86jAgMBAAECggEBAMQsdOU91O11D+7oazjXTipywmPWfnyu/axd48PeYX4Ztnc3q5Y7fXXEhaUCvlq1XjxDUzm67e/U5rvcNidXq7dCNRuzPA9M1m7it2HDKfnwrqpYV/grWQlm2xfl+p7Qhj9gpRJ8hprvX0Od+7oJh8xsbwUcPa2XvUkBfZBsyNd4RAcDR3i7hRo77be1KSVfsGmNFnI1eGkSCS/QPG4aiQk7Od62V5ie0/lHM0hDlUnrl2SRqJGQRdwCp4DPH0TmxhPe4WMl277Pmfd8SRgcRW2WNiLdNXY+PK0cecvkEyr9kpK8eJ0g9loAOkFdcWTHwYplq8NSyVtnMKwvcji3FtECgYEA8h4Pxj9NH8teqfWqIQlDDGeDKxAE3EyPgREGCaFngV/OJ06fB05zt5uyoX8tLXotzSQjjgRk25d1577OkahDJGtcgq/zBDLE1XiaE8eICXn0/MksaMT2ZY0Pp/UEWql9cnnLY3ts/+q8qFloe6Gi+thdUYtWl+1hPhZonKtxn3sCgYEA3Nq5u+fdILAh+DCcWCtAtyZhsXENNvxmJAgVdsK/nbsrl01EiBYXFouxxIl9peHNJaEs4OAMky8EZjYbTZbcFltjh2KdPJiJ2vel30iQBjjPS46uQHh1s5f7SsdXJ2xI3YiheuK0ySiS35vl6AZbrXCAn3Kwi9aOR1wDm+XkEPkCgYEA3Ch7vZBICBY8YR2y8tFiN4BUpK6vTMcNYpZhQBaVcO32HoX+U32B+b5JY1KqeQT1aulmrzfNomQKYY1+drJjQ1WgzHFD8Fhd5aMBr+SrDbrpC4e+qxIW32ayis5ghDREjviy+iX8ioUfwZFzUaA7/A8MZB7owcOnvfZQb83xxssCgYALEfefYJbn7Yw2WZFspfZfd9ALyePkrrAb/D+/LTHXoSslMV1PCPRtT+FAPbgLmY7j5PlP6EsZEZFB4lJqCDbN9BTAE4RYJjk6vZEV6Rg3B5/0ZJl9Z8xWjTauX+GRe08Hs7KMa1KuhpceGD1k7PSpc+suktwglkeZchZIOTS+WQKBgQDvzWu1RFnaWiSpdkVguMP+W8RqAhH+4QW9NJC9S7RrXZ/OvEOdBp8BX8Gm0FbBKEpUl6hreaf/sPm7fudw+fNTDmRZL8yNOTbrCc9CpkZcgl/lm6Q5t7z8Ot2thlZNmTgdIkdXX9jIh+1BmuKpynzP31unvXWczzCnyiy81l+HAQ==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDQ4LLRPd47qKngVWvUziPeFPDPISVIi8HgUFJ1Vq/CbLKZDrB20JjLvXwA0RpYkFfZHC84Fjmh4+LwYuV5qJy+5X7IdtnXjUO2yXyykkJ55jDZb7ZEx/5jrYSK+FY7GRaYpQU5dP9R33FnRdVm7GfSETmSwKv/NRQe9KTLKM+K9NvkLPfemau1YaGA2lF4h1TgRbHZYaz/Pp05Y2wv/iRXPRX4wE3k7b7k9oH9TL1UkoNn7L9G9/ZXA7zO8F51YNk0kGfYSmZSIcT6CyfU6SgZdojOYyQQ1KrUKy5lbByaHRh5PW5H8f6f1p8V6Ja/iUMwUg2YQobkKKsY4G3sB86jAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmdbyUiDo6RdJKSP4yiM7FkycMWx1DycYf5prttoCi7nkH", - "privKey": "CAASqQkwggSlAgEAAoIBAQDU1+WqcQTmVY8dZbZAMaprvG8QzsVGX+ssQT1jDQs8G+GQdGK6yg4OwCW6ZGebGg8Wcnhm9izafQGc94rk7su4+jKkDttQv+GEpZf8yv//jdTzV1bhpDIdE7+yMZgN5ViR3YrFi10vs7Sl+E9KvBQJPD1vsDngjsa3rGLG9CoKtY7EpXi2Rhe7Un8+tp0J7uIJRtyZ0Tgu0K3cLir7Fl3YVBv1PzsY2qaUA3ZfQNy6Yux0ANltU0oO+P2DTknfkuCIC7+QpzECV9z8eIc8vQzRSFLXKRDKqN+1Ucmf698VBdGVUt7Nmyo3q/jeHJWyKce0hArUGhwij5/uoYg9TZx9AgMBAAECggEBAK8KUuVmBxqKWKVbhZOrhLUPheOzuMeUkKqXiK2SB6BKaanMHXnyO7djzGNKuW3z816Ji31ZjS+uSIpXhhGaVU5t7QHA+hqhgwz8xk7uf7QiZ3QsatYsm84P9MHOSXd8Gufy43Jsl5loV/N6j3Mt0+h4cyoMKr0Djmd1TNLD8GNWxhahfGdhFsBEp20EcAe0BTMwHVUgKKOpu7kElAiJUYRVeqvozNC+kkzTLC5m8dcNW9Y5mRNHv6IpBDmUBCxnUubDakbCh8rI759+XNBE3SrDDBiZE9CO2DSZV/QM1lCIMnjcoEmENVZj3XcJYoAiGrMAVqGlxQYZmjQTBI0TzCECgYEA+gnxXm70lJCxPjRMqzI+4RPUeKc+TwIq1RLPOP8OP70VpG1TvBGny9YkuBxP5xsDB3W2S8jRYao3nwmKq3AJurfUt9GRJY1OreN3iX7aEBMMoPe5HTEZOrTm72RCt8eaeezxJuOLYxJLD3PYT/JHuuUD9d5VDHawE7OLlYbyEGkCgYEA2erwlOoRZ1hh/2eddfeRZEkeKhur625wt+29Ga2T3HmvLzVorHsq/krEE+qqZTDNk9I64dylbCYS6qJ6cJzEBlhyfimyQOooYiuUlcP/FvuXEAoOc1BwhmRNl1DkszX2XUMhNyWDNVCWJym47M1ksHlTExMOHV2cz8d9IToHqPUCgYEA1+KH1XpFkJSRhFzRqaq7YcimVfpIsRz08H3KD7MgkWXn7s06VBKGZ1eg4poHX0oSRnmbCTn9lq7KUXWCll0o+V9JueCmyt6EBV1103CERQa9i6n32b2PxAF3t1BAzr73oLg0ytgCfGrKBjCGnxhYWITt83agxh8gDhKivVsDW6kCgYAwfJvnJmWU7w9u+qkIdHs/Kx2xFNMd4UbnRdiLfBmoNtMJ2AJgTk90oUIbhF1BgqhbOa2sT6Hm/Fm9J0XDBL6BAvEGrVRiKTevEC9RW3jIrlYgVXx9n+pJnMu+3VrlnR4iBiu/z3LwS+v87sWcut6qfXREjDrZwdiASszGtdi6eQKBgQCWc2lKE2IapSzG8BF0ai6d6T8PmgwOcLnwSD6YU4Oe1lKaTBbtaS5KaemPnSAV7CJ0KR9Xf7qxDcdZBB7RnSBIIhi8sz0UW8s529nDKYhSrVi7iY1EQVK2YmXjDg+q76qTl2+/C+6EyfJHrU4tUBDbpjjOEZxcwwMYV6vydiR2sA==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDU1+WqcQTmVY8dZbZAMaprvG8QzsVGX+ssQT1jDQs8G+GQdGK6yg4OwCW6ZGebGg8Wcnhm9izafQGc94rk7su4+jKkDttQv+GEpZf8yv//jdTzV1bhpDIdE7+yMZgN5ViR3YrFi10vs7Sl+E9KvBQJPD1vsDngjsa3rGLG9CoKtY7EpXi2Rhe7Un8+tp0J7uIJRtyZ0Tgu0K3cLir7Fl3YVBv1PzsY2qaUA3ZfQNy6Yux0ANltU0oO+P2DTknfkuCIC7+QpzECV9z8eIc8vQzRSFLXKRDKqN+1Ucmf698VBdGVUt7Nmyo3q/jeHJWyKce0hArUGhwij5/uoYg9TZx9AgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmVoKJ8hSEjXCfeGUVdRxrQYGPCAR8GEmvS9TcgAQThG4L", - "privKey": "CAASqQkwggSlAgEAAoIBAQCubDylrliDnatd6ZVnl3T73E9gxl+KMV9Y4Gr9GVQcGYyYNlJMr6SezMxfk8hzkxyIlJjnzI4lXWPqAxloRdmps/7f4tCSnOTSUo+ZDsEjazVNYGntxjaOoEnSzkM+MbvOlq3McvW3XSmMDbRVOU79tfNP6PMDZHOxXnRMDaPRh7U23xVE08/gfjnOXQSEg05+EhnKzizkWeqJvRbfRKbY7ZnCL8fNl9Wt75PoxJnSdhdcpL6Qba4q6UkaEx6MJ0EidT2LClSCRg+/z+CtuFi4RgYTBJejdn4COK/Ho7h+MMZi5ZBx7uhZZ/6wefh5hWc+vUCozAQnfLsg66c6C34VAgMBAAECggEAZL+yXEUTbZrCJHHK0dZjRSOhWhXbk7gnCfA+/EkIE18Snc0qxo7h+LP1DPQQ4elEnwOuOp4mMSD7mG0H3PoT2vlULEAYF8e2SGJV/aPPHcVMOZCKP0SxuLqPScvIfYE+qPrSEvkIQ0z1taco1d1Pai8SBsNYs0nvpbEYXeG3EUxsrVcB7Dh68ydbHYlhUKMZXe0cNHyndrK9Jox9Z41kKxYcKpbzORi1pfnd+vIYC1+3geoWGaJTW0xnzpXpUl4EmG52NMD8uX6dpbJXpIpdMcKdCspUH2riSPOGTDwthvKW3Ews7fxGmEblDR7pYClrWPCn6UYiKTZxiZesNh6FlQKBgQDblX3MrWlbhG8ih18SFdJSlagy2OB5ecgf/zkJRsVNmyTO2+wv0dTsTLcwFafe7S0srqLxHc1ZEzryy+TxOHfplB5orxLYookAjZ8WaoZC50avQfkHMEIeJmo3OAem9yzbPJ6dVarR1TX8ZpID4g0d2ge4CWo+VwzY94VeEkDgLwKBgQDLWWn/s73pRJjl+Ll56ItwEdajSZf6VXLJFBIMeQn8GJ8o9pFn7YGUVbhkxBQPd1UjO0Eidn+YL/CywfH9ePcRIHPaOjI22Cl74hnCz5ManDrTH/S78Tu1CQeWKacbUaFTxLgNqo6z/NZww2CCgV051IJZJHvqBp2DQSYHRm9Q+wKBgQCQtaYgGzBRxadQBBKdYpAnKMWeLNtScvV2UMaP3HnuuQ263ah7ozdFOxGGuN7WxUt+JODxMgjAaTHyDHkml2Y/IwQfTTGIXyUWnj53kWBF+xDUMxAgsqcAI6TgGya/3ClNmleVrH1Up8RaQGZ99J1cTPHFUT8ZMlkfK5BS/IiQtQKBgQCpYGTGM7Tv489nXnE/dc8PHgymHdqVDS97BVizQu5qKSgJOreK1W2lXHEmnZwH9eHYYrayOfm1jdjzTFCATI2emmVlVCwXOp3zLjU+6x8gfxkQWgHDuf99n3PORAuI2cmCuMyFtZb/nI4RhuuQSKiaTsPz9Euydqgkd9NxI938mQKBgQDRcf0BavVuHEZcSegoI0wVLynCUapeCbnUFv32Cer9/V/YV8/ZcjTS1P/YlhZB5L16LguXDVfgXa03zeTeItjsW6feR3ZJo3WzwS9CEEuLg4OFu36hXsO/Tncv8MGO8weVYLGjeHhrbaPWmoWGhmsm7VP6430O1MAFSoZj6zyBzw==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCubDylrliDnatd6ZVnl3T73E9gxl+KMV9Y4Gr9GVQcGYyYNlJMr6SezMxfk8hzkxyIlJjnzI4lXWPqAxloRdmps/7f4tCSnOTSUo+ZDsEjazVNYGntxjaOoEnSzkM+MbvOlq3McvW3XSmMDbRVOU79tfNP6PMDZHOxXnRMDaPRh7U23xVE08/gfjnOXQSEg05+EhnKzizkWeqJvRbfRKbY7ZnCL8fNl9Wt75PoxJnSdhdcpL6Qba4q6UkaEx6MJ0EidT2LClSCRg+/z+CtuFi4RgYTBJejdn4COK/Ho7h+MMZi5ZBx7uhZZ/6wefh5hWc+vUCozAQnfLsg66c6C34VAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "Qmc5QckxBKLCgQVhMBisisvainagMKYDEeHQkXzPU5ppgx", - "privKey": "CAASqAkwggSkAgEAAoIBAQDBXIuC7gUixPiZ5TkXpTbZMzAt2fdfNV8gci9Rs7mhdtcAIWoMTTqcEEvuY0qA4VeoyiHOZrwWijcN1qhL/iWIwm+35ceqARa4WKzxqdFNUUqrcQ41tm5D3hk4Mc7+26KhRf5Rb6w4ZQxnadRHD26GcYOtGP+LLTKGFreWJOFcbwGCnsDkrW2f/jhsVeDwXAohfuQw2x9YkhhW8GBKeXnVrzdKneX98j2TmdQ7XxbqpmtYbSRscvvYjK3sliHJ4iFpqpXpHXIRuflgmcWOjNMQXBs1tNZpo5aWrjFngotGIqLWoMMCi3WgoMfhby246KpsgmtFDgQB0fdol1HmPeaFAgMBAAECggEAZQW03fL9O+0s9TqNWY032sKjqVD3rQZ1bL47erQrh/BO5AKRJVw0AtWA1kuJ4UvaQJValDuYiS4tFU3RH+LoOUtckve6GVf4RtgNgzT15S9Tk769bdKiSVMAWhuryft2PEwVUvbFQ7GHiYABKB8n35Xu9cDZwh0bCHNV91vNYjyygNg+VcUZK9TP6djcZB4YvdWv/WtpKgcAOOcPtgZbp3GzFHL/BsSNBHGg6Lynt9StYaZ6mqPYeYd6klTa2bVe/xkVrJ1cCPkWcFBk700JoPw/La2bARHjew46BSLf69wDWTQfl2QdUMNHVjwIZfIQN2dfldJFx2bYGAEhi7NIAQKBgQD8GKDmhw7ot1o5ha1Wl+Uvx7tfgtOmnouybrX1Ce0B8peDfD4MAUhy8nVwEjyoGVlzbY89odT3hSgp9KGD5h+38spAo9Rr2Hb5V2QxehlHZCewr+bESuL02bvxZnAxSFjJV0B6/A9anyBWKzw4hSviEYjvE+W/VYJLccZaP8V+yQKBgQDEWxPezoVJNVYIAKvX4ssX3nDfc+zdh+aAOcI/+CEmrWZ1E3kRD8bVFt1LFND2zIrlU/1FhuuhiL8KtsN4slonCdYhEi6kUnbsG5SkWfB8+TM391qvrX1eLBD0u/NzPfBBaeg7BQWFUSsXFWVzRfZG2VzXm3222AABJYVpcD5b3QKBgQCnkweBtc1nTFohWoa6xQWIGVCoUKK4YzOhTI6PcCWn4cZtlKz59fBe2GTQNo8zfoZDgFRzN5wFXPIx0Xd74gC7mhxvk3ekqKONY1YqvWsIVb88Z/ESEmWDNSkFcn6pg9nhHKq0FdFu/8/S97J0L7HX+Kf5pFRYN1MBK4QagcGaYQKBgQCdEV3rtLfZv9h5vk+3+asMBNu1Yz3uV2+C0rEYCpw6HCsBK/qEM2KRwiBylswxH51bpLvMigiixohLQbdLLSAAalXnTmwQ9gY7CDT24xsEXTMjabIZJWZLlmRZ4J71aG5vZRBnZbTs1+joJi1o8GX4dpdVwQPm5xHZ2PHHTgoT4QKBgHU4FGuQxOvbPXe2kEoxtonjTXZFd3ALLn7WHVCrlBEYkj58j0aIx7RsF/1dlVPM4j9SzAldk5Eze3cZdsu+Ae8wfLerxMwmQEWlwNGmttp45t6sfVq5Y0Sas5bOSRTfUQFXzrAxFo+5ZETYKN6OkDnEv3xbTQTnKaPV2pcqFx52", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDBXIuC7gUixPiZ5TkXpTbZMzAt2fdfNV8gci9Rs7mhdtcAIWoMTTqcEEvuY0qA4VeoyiHOZrwWijcN1qhL/iWIwm+35ceqARa4WKzxqdFNUUqrcQ41tm5D3hk4Mc7+26KhRf5Rb6w4ZQxnadRHD26GcYOtGP+LLTKGFreWJOFcbwGCnsDkrW2f/jhsVeDwXAohfuQw2x9YkhhW8GBKeXnVrzdKneX98j2TmdQ7XxbqpmtYbSRscvvYjK3sliHJ4iFpqpXpHXIRuflgmcWOjNMQXBs1tNZpo5aWrjFngotGIqLWoMMCi3WgoMfhby246KpsgmtFDgQB0fdol1HmPeaFAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmSBxDrxA5Cs9XCPHZkTQML8frDa7uFoCRHQDtGrkXCcC6", - "privKey": "CAASpwkwggSjAgEAAoIBAQDB1rGs8/efBrlhYevYWLmiREq1AmubenyJHL58MAWwgk/wFQZbTEHGQQQZT0AoR4vtcn1seODzUKxNCH4tpYOK+F7/ey0gtoJwL3YI0ZwL3kPIq2MJW2ZnqUZ/v/VkX5QtTUPfyfZfQnRYcZ54pWYHdFzqUbOwN5wD+VBWq+V6Fu2aIFuP7/RUPeRZ83CXHxsD54hTEDlBmc9lut3rVtFB0s6wvO+pRmt9uLUg93GtG/oA+qeEUXNj0l6XtoZq0CdJ2kj8bhKrz/oqx63g3kg7Pyv1L8Aa/MC7GjJ+o4R3bIG+sk7lDsm2bMI5HkfrEVgE+FlEvCS2FaDTk/jVHaDTAgMBAAECggEAOqCjELqhlJnGDCw/1ynOy8N4DRN0VIxRim8FNi6YKfDgGK9jQs3nvvz/LmCH+SbarbDJOru83hryYkJFV60OAkRpB0DMP260ORZBzx0G45gQTGt6AuSALq5GQnFe2UMHYERUWSWOvPUul2mWEsuD9pE9YSng/VV0fMc1g2FugORTm2SYenOA/tZfm6kPo/H9Y0aiVEKv5O9Js8GhGa35zCrF0Rhf/awthNLQWb0+vi92iBafrEilZfuSpD9mgbttRaCTaN00YwC4749jxlQyIwuUEodWwAy3NFQ1SuoiWNfUJbmHtuHf+2citsLBeUWcev7QWf4t+aFHQFUh7sIo0QKBgQDl3uRHHPV2DwfRbcyThpeGAg/SkgnsnE+CuSKCtTdK0l55Ck0EIeElsgB15FCinntJ3ZADU9l9gtf4toqM7OeqowwQ+8kEaqzMPILOicDs1SHUAKtxzUJjD3WuGnEgYKd2JjV7JDvuQRHwyht2Qhaken0Va+wJIal+5tLZ+z47hQKBgQDX30pkLCc2QJHrUyca42jaZVAvh8W/no8nXMN7LAJ/xPhGGi2tnTGfkUnIg/+8IZWLzS5198swPwm7nH1UN7QQq0atC4Ld2Lf8dn3cmyAJjcRll4lt9AB6VAJp5rkAAud9VJXCn2e0J5elXoM3YLgW2VlpFUy2JfUmEAh1kDr+dwKBgAZYVbLE0N22Yn/caQY1c99GFUu5rj5yvhscoyA6glE1Z1gt+ZxAlydkN3EJoVQrzblnPT9qRBmbz/xUhZSIQYjLQV0CpjTSAP0OOooa8VFYPLvOXO0iPk/fsF7i6fZ71IOFYHqKsIDOGQGtgn6MKnXVz7gUp4pE/Jm9I1rS/Y/FAoGAXxJhEfL8JgGUAj7x5v6mjCC4iuZR6g1r4JsTIKkGRL071qvq2B5131++TggMVg+4bASmZKAIJaxtnenSrIeHzxuPmeCK9ydeCFsrHUBYgLyl9VQi24DtwPJEyd0qNt4Qk3rwJfHMW2Rgfh08zuPSz4VTwlr2GPZonCXNg/FMegsCgYEAt3cw21h3X3VHmAxgWEU11XjNjDKyCrWUW4aYEUjGso0M5dksFLDSNezCls/k7Urp8agl1wRE9Okr1RILcle5bbhPVqQ1oXobANSjIEVhCwmCjnOuYMBJA/tQdKHqyOFe3Z/Sq42n7ydMdimSmBb+kf5uwLL8hWjPNDZJrxBl+YI=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDB1rGs8/efBrlhYevYWLmiREq1AmubenyJHL58MAWwgk/wFQZbTEHGQQQZT0AoR4vtcn1seODzUKxNCH4tpYOK+F7/ey0gtoJwL3YI0ZwL3kPIq2MJW2ZnqUZ/v/VkX5QtTUPfyfZfQnRYcZ54pWYHdFzqUbOwN5wD+VBWq+V6Fu2aIFuP7/RUPeRZ83CXHxsD54hTEDlBmc9lut3rVtFB0s6wvO+pRmt9uLUg93GtG/oA+qeEUXNj0l6XtoZq0CdJ2kj8bhKrz/oqx63g3kg7Pyv1L8Aa/MC7GjJ+o4R3bIG+sk7lDsm2bMI5HkfrEVgE+FlEvCS2FaDTk/jVHaDTAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmXmdPtQ6bT9pNyHKjfDUB7of3cmXQtJDBYKKeUGTEn27d", - "privKey": "CAASqAkwggSkAgEAAoIBAQD6xCvltFpdm2M8dlK6d6laqnIjuZRUtxFn+sdK2LqCsJVgg/a/eT6t8R8vKphjRN5piOzVJzo8NZC2X9UW4TeL6MBNRKrWzCFsTKwdEQM14Npjcu3wufi00p8ZoaG/JeEucXmmRzK10cXTYTG1hFcBWxWi4IHwsHM8+mNCNoK46S4Ly7ehQTi+1QLWG/lvd2FLN26FFNEhadikTd7+Psq7pvYC4XIAieU3LturRbfvGKzqEUwFTrayFsmxTo+KfiLDOyjCPjkwLL7+VsDmZUpHKkAetZpVx2NSBLD0p42qUyG4AjplfCQFi+TaJZqIEqrXAdumNWYgjBNR/hkAt7MlAgMBAAECggEBAJJ06D5sKyroifjSElcddCejzK3YwS0JDn1wFd083xFdGKEZ8Y66vUTRwqjFc+LmYg+5DLkhA/4OOsqJBecq+koYUdfO9wgkiJC75vnC6eEZxfK3OQiTVRImwQ0zPUhqUy3Q0H+wrYlLTwK5jVK6TCZakDRkcv+jzmoawsX1GDvtrCV0MzQkZL24tz1SDDSBDCijaEtyfqBqWOLLhDjocvYuEKsse+pu2PfSMH36/0dLaEwf2h5tib4OUyuTfA5knjbcEfsSFZHQvf84Y55baOuJbf3kHqEOKJ2WG+IYpAsVnpCN5Cwthj9k4TCpDaWCBsNFmXKBNZ8ZNJqXf+J30bECgYEA/aP8hDGYnbtMM9PZUzsS9RyzVx1k/IyJ9bOOD+RbHzomf7YTWeOv3bvbFzoyF1acmN+cY192F2w7t9Owckk2qSk1VuKHXjw/E3v6xLRnJTvhi1/+i7887A9jghm3MvfPtsyArRagcWxD2mnHum7QeQQbBG+Om64XVE/N2Z9JZlcCgYEA/RlXIPlprSo8gKKwmBa2+mPB6VgLXZCB3hcp+MC3hzzmbPtOup42Ap8g4YVpk5QNbizOfWEsYoT3O4jEFE6SKlzSvhvvWnw/G4ZGq7QnIp3ZKY2pn5vHenjri/RZzO9JATQ+nGjgqFHGvUXXUZ8bNq8oP0Fjc9hrdv2c7b5tLOMCgYEAkB85Ighodt/xWdW7vG5pxDttsEd0lYhp7+H6DA+us1zAeXsFHeOhj7XptRYNVnORgdA1tcWNfZuzhy3TKe1uEMrokxke4C4NjU26XUFBBsgyzZZbNh8RR/Uqjsd78IsdTPqA91lPC4QAPkAzDD1hWhI6I9gbyVwvx2mdR1YaR/sCgYB7N/UFJqfeGCvwbEQRJy3Z5Oso0SZnXMz89MYIRrqS6oE8GXUQwamFyTbW1H67zF5lfwbgX4ieRiGfKExdnormeN5Yk30Jzmdi3RJW0ZQj9DkfU8p62/pXk7sJHeMCNJSUM30v5JdLGtTonLHhGNbE3q13bjwe0AQxn/Lgg87fBQKBgFIJuMQ+RsrwK7XJ+Zm288rKo6kbRx8SVm0mjhCHaZCpcJbhlFB+8yvE++n+TKtgDp+pFrjkQf8yYO9p6+B/tCbQ6JHNCfdl4INMGAaCEAxqLnZPzB5ToxRYYHJ6RDVcBtX2inpZAKr1uKra/cLCWWg/cJLH3ozITCL5tHOMWevE", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD6xCvltFpdm2M8dlK6d6laqnIjuZRUtxFn+sdK2LqCsJVgg/a/eT6t8R8vKphjRN5piOzVJzo8NZC2X9UW4TeL6MBNRKrWzCFsTKwdEQM14Npjcu3wufi00p8ZoaG/JeEucXmmRzK10cXTYTG1hFcBWxWi4IHwsHM8+mNCNoK46S4Ly7ehQTi+1QLWG/lvd2FLN26FFNEhadikTd7+Psq7pvYC4XIAieU3LturRbfvGKzqEUwFTrayFsmxTo+KfiLDOyjCPjkwLL7+VsDmZUpHKkAetZpVx2NSBLD0p42qUyG4AjplfCQFi+TaJZqIEqrXAdumNWYgjBNR/hkAt7MlAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmcndsRHcVLGUgUMVjXczHf4qU84WtjFb8cKjgQHxPBkjA", - "privKey": "CAASqAkwggSkAgEAAoIBAQC4olOupPQAcn9YS66bRt+6p671m14N78z8Hv0pM+kF8QmA8G9DR9z+GJxFwZanQkIiKcwpw+o26lPYI8XGBh/ohTWTRgqIV3Ra/SGyIgMEIKjUJcoy0+/rfS6AgRsDuGWYWPTGs3i4OCc7CB22in+I61FX6zzHtxIiD4ADpjLoFxIdVEoOjp2Q3IErn9dyUx9rMqP80+WzxYLxtlaEX4Kt1vRATFk3yeg/6l6g5e6XctqD1gyIPaXfYyXDQAKQtDnGuc6Yf3SNvZjjtRImeEb4gfhBL9KTIvL0fq7cnNrQXuneupMQ5ho0naObLj0LBdJ/dVcIrET+WZvkdD8aTe9TAgMBAAECggEAfziq/L4Au4YppUeQ6sGtS8pbTjVeW7AOyPL5cjioqkVqTQRfRjbwWc3PcGlyS5HmS/ANFAJBEtHoMBiGIGr79ZZEUlSC0Wuha0jcvQeemGuAqZ3Yc6mBufwp3LYZTTj0GEPDdl8YIaffsFdpUeyg8FFlXXVkDiFhR0Ly7JDKpwyBG+p4aeqMmPYIYqx64yy8ddqooW6XBBCXkkMUDWwE4Dd7Xpokx//DKVmOmu85NOkCtqycptaidkdFKVobRw9zDF+7xprs+kSJOOF+nIAHaOStzuc4x8Am4WJqXNNTX2yGty2gEoMQKjrAu5FtDdNqGvrwbfQ/Yt1etHZFjh5yIQKBgQDzIVSw64Ca/6roWTrVBlRd/OTvWzjMb2vfkRpQIq6KpMpxP8qbQYV+nX83RoadpcHAk4kBoimC8L05NGTZjLuHMZ9fAs+/wVXxyhmdHZbCG7yUMF7f/j/IBc5yxeFRYZs2gWuVhdPrFqz8HAnTY0rHC+JTbBLxxIpA2X44RgZAqwKBgQDCaE/7hpAH6+TXtaXdeS7uxJ/efZQFccuB6giHCsM+dylEZKtPfs6sLpjeYQo/IaOLEhpvBwflOc8eOeS8uzYr42vo4FqvZ/nWB/vmm26QV697YQD6HabqBm8iL5NpIGWg9At/7doxfZfiRVcioIdDvgNx22hNr8QTqPBS1jUb+QKBgCX8HB4z/Pi6XvpEDpP/lCjG/QGEUABonALmyaShdoGEs3g0DjRpbTDV7G03YIq6veWXZz1RF4k0kWuhiuwON7Ish4ixiMGdtA69k3jfiZE0Aido0znNoCtg9NsrnUM4q6Y9XBCVQwGknkwZGVPkXGdyrN55sRACs9Lj5/tkvU9XAoGBAKMkUIJ+QN406lzO9fsul+ENJi/a6F3NSf+iuzdAI+qGqx3W8SAMBTne/LAZdTTXcNvi/EXR+6E0awgtgzOSU3pvJf5OUCvEsJcZKh4yr4z32K5MEDrUqV7YuWhRzn25DzALvJ7FpoZDpDLhB6dqWTjS+ycP/a674mqxKcQKOJVZAoGBAKqFFYjIOGNpv46dgXLcwN08GUTzUkOuqs96b5mmxLGE3CGAlSUeeoaKLRlh9QvjpMQF63BBiPcYetbB/NRWL+225lhAyQfxWSJKRNlKos7g/VuF0CNtTGT9FYhZkekBuWbHiIe03SYlzAEIF9FtR6W0AWBK/zv/gsQHwdIoZbbw", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4olOupPQAcn9YS66bRt+6p671m14N78z8Hv0pM+kF8QmA8G9DR9z+GJxFwZanQkIiKcwpw+o26lPYI8XGBh/ohTWTRgqIV3Ra/SGyIgMEIKjUJcoy0+/rfS6AgRsDuGWYWPTGs3i4OCc7CB22in+I61FX6zzHtxIiD4ADpjLoFxIdVEoOjp2Q3IErn9dyUx9rMqP80+WzxYLxtlaEX4Kt1vRATFk3yeg/6l6g5e6XctqD1gyIPaXfYyXDQAKQtDnGuc6Yf3SNvZjjtRImeEb4gfhBL9KTIvL0fq7cnNrQXuneupMQ5ho0naObLj0LBdJ/dVcIrET+WZvkdD8aTe9TAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmTSPkZzHvNmHtcqwE9nGZjebbmFsrPAvRYPXxrbe64o3v", - "privKey": "CAASqAkwggSkAgEAAoIBAQDTvih1SAj4ioy/YtpD6Ewu0SvPlH2gF5GsUZ0eEJNdBwV7PWfXv+F36MdVxL+f4wFekW/BDp0kBFjDtnjoktrcusd6vpeSXndY4v4y/3DPLzGPpyldZIKEjuBRCm0PdUcl2mLXoH2hEFo49Xlk8h1eyXK3yscPJmRU9ANlHCmgIo/lIDdISosCn3BQwCx60FhVkfEiO6Rt/vzMH8j9QswLoqpAiz+XQ8iDqHNmsDpwliIOELmWgiwV61DVzOXPzvyHl+MhCLvvr8UokL2jxOmldAiIScXGl8dnMIKrhG714UIBcq+4757XF8fuL78nViSaTe1GB4AoSCSspQWvbq4dAgMBAAECggEAOcp5wmDRyfwOpCG3zrb1LAX8/h/aFbq5EJ4J0u3VOpuy/ErrL7B4OkD3Psp/PoU3l3b8WGXDr9Pb4jbIUznZsEruLOsd9V4BFuqFVKfxQyrvTPTjzlCjasiQIq5Ey+ZHb+Zl+dIc17vd1BPzeQC30WoL/GvE3rasxZ7/2jXQiprE7e73HABDtje4t3BKVtdc5+2Za5K0mVJU9AF9PWdp+SRNuvbhrr9wj1DPuzJVmOFkgGHWY8LBtUlfixTvH1cOBb+jXSOf7+yOjPNi7s9MpHVWdejM1Sk/TYKGfkv9/j3esLdwdCI8P7pupzzNtuFCjRMdZ+4Oul13YlfKULEsAQKBgQD4aEw4V88SRbM2WARFJhlWq/WqsHV6UBcSq6N4Uj+EmxUqOIMG6JIOcOjDazBQk5mjAZMMZM0i2jzBrSEE/Mtpeywj8c77xs7Kx415cgzdIKM9+oFcHbDYEBGxkekiFm6u5Gg+dsbh86gSTxcbUP+R0ORGg1bLq+YmzJPLLL6DAQKBgQDaNvjvSE9lAlDXW/H2qEI5PxfBNf2iuWhVNHkmDFb/0Gk4ietx5J7Q8oqTHWdts/QpQ/WsFWVTT9Fastj8qEaMubO0z7inl6IDMBv8KKRKsaQq14d7iT4eDvJzB4JdUGZ/Ch5OHTW2onMnOJnZnLuCn6ek+Io5igT3glXGIdTXHQKBgQDx1ri98d8Lbwg21CH0IE9y7h9SelElL2wHJUsVDR4Bv+ovHK2TwEDSBmLWPjjfeZON+y5qVojQcZ/M/vyymlp+6wfiRry4qqkRCo5Vug+ECQ5kfMoMIGvXLm3Lbr6GDUjcxEoo5gJiYJE0ogNg+M6X68MSUzPhPg3noCwTFhC0AQKBgQCMrxxWyHvHV3LfJXwd1eS8G50pB7H6Eybcp/PjP9lnG+p6dRDCYO6zL2t/5VklNPuZDyN4SmMFD1Sd8OhMHAFAAQmG7NTT18Kv43hnXZxuO5DnvgSu9JCDuIc++fxmRMuP4+od2l8i3CD5jFhEH/QUBvKCPWqAJieFmxXJo04hUQKBgFCkYPBQZhgaoGbRkvjRoh5Nz7pYZ8nfIzCt4B1NJyIJNUGyRBIuamC3Rm3mBF0fLg3bM2fYC4YdfInTeii1u/UXLlYgw1u5/O/xN5/CliLRX5ALDtm+noWFqxImlCGmkV4ZEOxHO+kuaxG0V1mzcMkYvCj2u2YbPKMrHWZ5PN8D", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDTvih1SAj4ioy/YtpD6Ewu0SvPlH2gF5GsUZ0eEJNdBwV7PWfXv+F36MdVxL+f4wFekW/BDp0kBFjDtnjoktrcusd6vpeSXndY4v4y/3DPLzGPpyldZIKEjuBRCm0PdUcl2mLXoH2hEFo49Xlk8h1eyXK3yscPJmRU9ANlHCmgIo/lIDdISosCn3BQwCx60FhVkfEiO6Rt/vzMH8j9QswLoqpAiz+XQ8iDqHNmsDpwliIOELmWgiwV61DVzOXPzvyHl+MhCLvvr8UokL2jxOmldAiIScXGl8dnMIKrhG714UIBcq+4757XF8fuL78nViSaTe1GB4AoSCSspQWvbq4dAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmX7K6pwskrZB8hvqwTgukFTR6o3DHiVui9PokxuJULnvr", - "privKey": "CAASqAkwggSkAgEAAoIBAQDPYkOtFBtPpJqDmAF4bp8dieu7d1/WlaMw2VynFmL9AhcydjDFp72N+YRztF8e0jBz46VkBGQ9PwZHkVgWNv/X7pUOELfEaUpg/KCx+X8uxLv7hjUb8ne2MVP1tO+Axl6cBtSbtQSqTxCTxK2I/4JRmnLNWcZ65nRdD/1CrBkGI2fwFnRYkSexGyFlDrhJtfpiZAB/lPk9GPcxeU6i2MKrx9S/vpj/DwV3QENlF/YSwdUMxbaaFkQHgeYVNHAIUP2HsErj9xDCNbzh51B4uLxIahOWs/exHvHBXab+xu8BPjIsCxaKKQ264jWssa8dDBRJ5O+eRDPwXj4TFdoMO9JXAgMBAAECggEBAI7qfRENxjSAjysY2gqQ0X6dyaKLhbRvsuK7KKrNNrJ9elcANGRCUNNCnRDPwK2Q1GtI+nWOwTWj9UPk2fuVM4Mvm/DxfHMSzHtCHcwI0Kj+Uz3nIzp7QhyAqgeuBU+NZS3JV0Nm4CwuCJKM+7ppuvlZorv5nlqb7p0jo7kKuMQM6znKsw3YzrOJLBvrCxWAo4ISpYuFHW132bpleO5ZY0Ipx7Auiz43C0jnq/v2ly7nrrPlp8xqbRJ37RM/RQyn+f/qTAWAJvR9LrHZQvP3+qln57+QQkAFqhPeYhXpXaJQQ4IPuEfAiLcAm0IWgrULRI2LHboQ6HW146GItKyTBYECgYEA/kCAKZnrryYGaKIYQJpzWqFLv+z6KQVtqXoHizkEsaNv9oHXl8zOauQLm3pKMs6Zgd3Pt633UOm8glfnqcOwft858t8h1hgd+p+xvz+LWx6sVJTcJ3PvyhuF1I2oGQ8Jl0oKlWP+2LYqkFK2Wp4BWTWhvopb9sX3QaXh75nOhbECgYEA0M9F3caJjrFagtEGWB7tDPogqoLPLrQdFLRAhYRu6ds33btclvgmv4g852F7X8qo5Nv/p/uMbcWRVczfzoV9BO1/6YMewT9Nc/OrpLFCVkt8k90XFVJiRTowfBoJb2Aojiw8zRs7XiwjWicdydTHnCBCIVYHd8hxIsrc3KeS8ocCgYBG1A8gB7oJa+1jHqzk6mHyQHbKu6ig3ttC2DTbywGMvvwEzv0RU8O5MVgucu3So41OCU3BXJxGFScnpHdr6pDzdxo8l35klwla9TveDES1GKFnWqTN9NU7F1m78c5/VJoWZFD4dwfatTy8Qd589gFoKbGqU/70iweraRu81LscsQKBgGLg5BrC+zyg61VrGe/8pRAyGenki6t4CxVUzgDr14HSF0BeitfKpr6oCv8egEe6NgQ50XSAf90zY0EYBRtMxwjgVmQDfTrReSHhT3RrpBgtIs76MQYdvv89MNxzj+g3xrycYiZWMOTFTfBQ+aArrGJYPDiA/oRQXJK3MaMjj0hdAoGBANyYagDRItp4Pv5j8b5nXmZq7l+9BxerzPwSzK147Wh24OBd6PRHxi9wJRrl9MeExJlf/CF764OnLodGhLb233v4iRG6LFb5j8XExFFNrVtnNbjdxOMKUFzG8FTd7UNcVO/8o2nk/qsuAAingYXMbkzhEl3soB591zs7GetvYDVc", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPYkOtFBtPpJqDmAF4bp8dieu7d1/WlaMw2VynFmL9AhcydjDFp72N+YRztF8e0jBz46VkBGQ9PwZHkVgWNv/X7pUOELfEaUpg/KCx+X8uxLv7hjUb8ne2MVP1tO+Axl6cBtSbtQSqTxCTxK2I/4JRmnLNWcZ65nRdD/1CrBkGI2fwFnRYkSexGyFlDrhJtfpiZAB/lPk9GPcxeU6i2MKrx9S/vpj/DwV3QENlF/YSwdUMxbaaFkQHgeYVNHAIUP2HsErj9xDCNbzh51B4uLxIahOWs/exHvHBXab+xu8BPjIsCxaKKQ264jWssa8dDBRJ5O+eRDPwXj4TFdoMO9JXAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmTcEGm5kj2vjQUbXTXo315ATwkUaggyJtHWsRVcUKN7PB", - "privKey": "CAASqQkwggSlAgEAAoIBAQD1Zl0OXDb1tHPDquEh/+xKF19vIbjim4xWDc9sK2h9OATAvGjuTcczW/YJ+swqrPA6zin+J1XrCThWDYBuQj9GNKR8illHZriqlwppVe2+7bLWca2dviLXmGh0lUlwJ6u7qQduPZuP4gZADS8YarIbJveUZGIksFF/php+/60OZk7nonapFlb3Sn3IR3ke9CS526pH9wA91A4UrvgKOtpS8K4NjScTOZHc62sttvg6/ssiWBsvXnwu9hhAbXCwfK1//A0wuLznpHdbg/WijnnEAijSBnapdgcpYIQnrLfxCYF+goYifIDlgAIz6pRvoZbtdUcZ5QgoyPo+w7Po8TSLAgMBAAECggEBAI5CUR/KBXJaseF0Zh63pdstwX1DJ1L2qVwZlW03nNM6bkbs8kdzf08euHsAkOsMZhcw/NcBJqWiKq54FUPV06h3TAOGkEr8GYGLHdYColhUo5/9NpCDcN9a0vMCuBf0Z3Hagxw9SrkWZlkrS2n0MFvdMxkrOFncfOJrAGEvBruZIO9h4ESHQku74zvnkaRTVPtvL5OKFKkWb8v5/5oJ/p5gAOCV5lo1OzFNvjPmHJTJWck2NwFMvQCrRNnh7/E7/1/GmBECK5/HpaZQRc0PnGER5U9/LEbfJU+4khHR3VzVr9d5Pn3lJhBmTmLaWitnWH4Bt4XDkbO52rjoD/TEuQECgYEA/wi/OsalFb/ThCsX0UJh+GukMjY646RMGCGvMVEPLkYZi0An0pL0Vbx6io7C/mcxBdz+8qSbv30UJMLbjC0IiGWVsWSamsQxr/O/z4R5F0ByTqyW5KrMVNJMxmjhKRoKTeWV1pTpY5uIbnd1xX/twxmQO6iFz2Nn7TTTeYHo2xECgYEA9lRGsWkaXbZ6zzD6TiK5I2taDnLqk9GSkwPp3cSWgm6iou7hNqLobRVUeOUAepbnYraa3lEOUyoJlrzBiIdBJUsXObs641MiGU1rjbBm712tdR3ppGPSUEromrCy+3gn0LLmZHe32gfZSkEdOyQWxdR5CJbeqMroT89S3tZe/dsCgYAmb5YKcKeuqHNjRu9W/U8wlmBvpNapOjixpln178Z+7depseiOhtFGHprFSRDAMKMlxBG0VfSXHm2rwKY/8QWJMO4nhwb57jmiz/SHfOqXA4J2svIm0krrOaqSeHn+rMsCxGgZp+WoumcMZvqb4lTeA3tGUnagM9YU3NJGTLrgUQKBgQD1pDEi9davISvytbrGdGX/ZixWQE6gvdrW9I4g8svMohtZM7Iu0+HH9f9Y17TUiuuPSt3BWT9Zu4/4W577UTWrxOgSUB13WA2nAceBcioUBWzWX9AAePLf0vOGXzL9BmNeASkzgxc6O516KNjHg0OaYDmaUSkVVdK409ymD0yHBQKBgQCeBJSjjHUnpCOtXTSuPw7L/f/R4jZebP3LCqnMYnMAWe6w5VIBU0y0Qty7SsnDkWkSHTBjCo3uO+FzHh4Vh94VcOrtZETowPsXK0+Hnrmptus+7zoLwZQd6CYSNpxE0BpfUjv7YoPJMQLuYCKU4DNs4bgSt53SUJ7+qLjGbbbPpA==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD1Zl0OXDb1tHPDquEh/+xKF19vIbjim4xWDc9sK2h9OATAvGjuTcczW/YJ+swqrPA6zin+J1XrCThWDYBuQj9GNKR8illHZriqlwppVe2+7bLWca2dviLXmGh0lUlwJ6u7qQduPZuP4gZADS8YarIbJveUZGIksFF/php+/60OZk7nonapFlb3Sn3IR3ke9CS526pH9wA91A4UrvgKOtpS8K4NjScTOZHc62sttvg6/ssiWBsvXnwu9hhAbXCwfK1//A0wuLznpHdbg/WijnnEAijSBnapdgcpYIQnrLfxCYF+goYifIDlgAIz6pRvoZbtdUcZ5QgoyPo+w7Po8TSLAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmWfHjXRK9udKSkHtKhguehBAzW75LeHyPxRLP1ZykgdPG", - "privKey": "CAASqAkwggSkAgEAAoIBAQDaCLdVLSfGmXFz2qwciYslW7GQv8oMy3pkBotMPzdXIhSYXB4p8/HjRyAeX0DeCF9vC1SdaHXc9p70katM/K9AcwMod74QIUVI9dCS0oQKKW4oI+/Az5nYXEC/C1Lu70JnOVWxt4R9rTU087UJkPDHs3DsVN5bQ4R6wvQe9crotxRvRiXvOd3TnR+u/B6bzQgRBqglj8GUYSmLe+iE6e+WXnJhRcA4unyixljSXD3rmaZf3GI4GSGhLnauJbwWpmmyTfH56J23rnJLQnQJuygaMtreDQrfk2u9C8rSxY7pFNQCDahkMolicUsfgegN0hZfVTokHypbB7Teb49e1iaDAgMBAAECggEBAIQfuAkzneDpZyjPoHCCoQF4eTfAIQ5z16z5kjwYKs7wZg6V8+l0XGZf2YTOMB7ccAh4k0P340SNZnHKPEYg8Ypap9VECrb3kmbOHyB51W3bAVftvwHWS+IitVGP6SfFcTXgNp/FF9KYvZ9i95febyp1AL8WBtDDL2q87PY9+EHgYQTqv5yT1OGXhGN7dRMxyDGfe8OEZVjpsQ+hhA4/c3rPh+uZ3ugZZ4CnaistyNMywPZGYVoPqQg8/HRspSL97GctmuCKph7qQyKGVvFg0ExJgEQXVNSjVw0z7LXmDipYsz+3qR7dLGAQvpfXTc2szAhbOE0cqKUUlz0qt9M0MKECgYEA+flgc2Y93ls8/MtZbvzwbbi/VX3pfXKikqdizTAshwJzcbHMEohzHRnKb6hNk/P/V37c7NJXZWa/k3LPmz0Xk7wtSXSHpmk/+GEMpwjjQ7urRSg8HMHjN96U5amqmA6pZAYsv0tIouCoR2+XPi0YY2meIRpwtbeR4npxBbhnGQkCgYEA30o7oh1jy93VnCWHRg4g7CmV0H0ufgJfAcjQg07Wo+fquiu7QfmBHqyWGJCkAcT9pC4Ia8o7Xa17m5Syq9RyIPlTJMmr9KCrFQCyj9Tr1p59MK1FbkpLTCrRimaYmR/dQJBTfaaISSZipXEKyw5VhAz28tLBLTevwdn72YfT4isCgYBFvVk3WNLx8ip1rJXq7Q52zhAzXcmCgjTxDVn3PPVvRTPICH6SvRbAi616sU3TdUNLuc0RFS3k0GGqVWGuQcEOKnXIBIbD2qFKPmk1QLmG8Bi8VplOvJkTwTlxSYCao5yGl2JsjChbqKnKJEvhwNsJATJoseO4DtrYgKh/nA7HYQKBgQCd5diVo0LW/1/2s3MdTxBo8F9It70QzoxwrpkEwdN2xKFwVUxuMwnjrxfU9zODLNJQL101HCUu8Wbfdh+C8xBh0O3CrfozWwqgJ4Ydv+umMR1GNsFKZK8qhXz36eUvIyFKbsUbrY/iaoqHg5CmVtSSNLjMrcx9NUvMQWGfSjXDUQKBgHQUaT18/o+WEETXKLdyA6L4BUWSNzqTtt8qvDLffV+9/6YwO9fAjN/6NyRp4iBFtje5uuw5e7SfraWY34GA9tHv6ClGDYPQB2p8MwNq+8TyGkAxiPb6kqYIMGxxeXnwwac1ITK2FlAmFrazn2WmXUChB0MIiapyLGOjHLB+sH9l", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaCLdVLSfGmXFz2qwciYslW7GQv8oMy3pkBotMPzdXIhSYXB4p8/HjRyAeX0DeCF9vC1SdaHXc9p70katM/K9AcwMod74QIUVI9dCS0oQKKW4oI+/Az5nYXEC/C1Lu70JnOVWxt4R9rTU087UJkPDHs3DsVN5bQ4R6wvQe9crotxRvRiXvOd3TnR+u/B6bzQgRBqglj8GUYSmLe+iE6e+WXnJhRcA4unyixljSXD3rmaZf3GI4GSGhLnauJbwWpmmyTfH56J23rnJLQnQJuygaMtreDQrfk2u9C8rSxY7pFNQCDahkMolicUsfgegN0hZfVTokHypbB7Teb49e1iaDAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmYnNic1ZXYYFnE1GLFE6s5PJat7SkDRuYigxycHdvoeSb", - "privKey": "CAASpwkwggSjAgEAAoIBAQDqtF5hmd8vOpqbbwV21tg14xX++zqKXLH6GEsERFfmdzIvwEqXuaYwubrGbTONETICm5KDzaY/lQClV+R8Um6AmR1dL9j9lOyzeZE4f66l+LVu6n0jxE0T6Ik6nsHtIzOqO1aKpLNO+219KBswnBWc9lv7HD2e/OKlpOBDqX/YxpM1sz8J8A8TzNCrkufP6OSCdZ/s5XwPEh1o4tYXdJ9BSAfdKzUBgRtbs16Q7vWrVfxD01pmvvVnkIHVR+eL3d+3n19ZmMlGAm8HpP52fYTa7ABixDxdBelQN92/cdaIwePvAdGpqLv8fCEVCLwrQc4BQi2BePp5n5WYHTwEFW4hAgMBAAECggEAEeyWbKPArK2wEwDGjQ3ZUzw1eNSc4uYzXWMvj3Lct6gQuB7aU34FGCGHBxJd5n8Sr6pL5S72bFKnyvjMZUYyVDXdTTmTO8J81TQKiCMQJnK5AHB+ABZEwKl4mXZ4XvDaSDzh3hK38uc2tGE0umChMeyKl8HPXu33LSlLSz+NmPNj5wAallohy0JLSvM9hImvxE7kFTtSZCQUctz56WTnQdS7VAejmSPAqUheLlgq+eDbqzqJbMJOj+iXrT3DUxx54CIMlyd3vZg/LGI6PQxLNdrRBG5Tn+z0cn3T+407pClLdr6+ShJGBgzw1Su9NelWIJaXlsGquVXipj2fzAH58QKBgQD4yXq/k89BENdGI94h4ewICRXdc2ElgGK2xURUDKncb9HC7foNv/XR9ma87gwYq+uM/pgF6DCNi2VzVy9UHsollAOjdsn5y7C2y9HobWgArtv4YUe7oOdzI1UrP2htvFNNxjdDcYhGmeXlDOZcxW5AzQGJu/dclwIbH1vY0g3k7QKBgQDxgl4qHzB//VQJNpuimN2X/G9XDknKvm8guLjPXDr/2jWBthyPM6ow16qroAEGUtkXwBSt4y4GW/sz1vhf0sVrPIWMqkkPxMrnNQ1qQ3gECWHgQCBHVTqgJU0qOF2+c+WG3SGpB91YrIzHJqQRDjtHixEC1uJDdDRrYFfqqAqbhQKBgAzbhM9/2Rc4wpdqZSGFJoinx4yBWQTyJKfjfAuH+ANfeAzF9cVeJVsri9W5y8A+qlbIFZ1AibnW+XBDkjubt8DHbIS3L+sL/t8Dm56SgOyAHPgyNt3Yi/2kVtN8XG5HbFq5osOGi49yhrIWv5UN0wvgTHMM1tTfLQmvzjRfbr5lAoGAVvyC2B8Vw/PFse/WTNFMdzK4E54U3A6NTjbace2hXogE36xtSvLr6N21Hk3qMJHkmZZYnG0IJcg5iWlzWmg7LS3GWGz5FdHm1zIXm9+jOaj7dN8EAU1kaUwmJ//XXAK4eEPrnMs1YXv81LpJO89pcJJZVTF6m5seSlKQN/fAolUCgYEAw3j6b3Lgi1Z8jioss9xDmMPvf913W97N9lt877SXWSJwvcDik6cuRS6FCs2NBgUUcbQ2Sct/WqVbDNBCDU0PjpOFmlHjWUl9CZ0M4A5uiPyzoJbICE6Rz7AxAiQbGxRCjMe8l10rCkb5twQcxwnWFmWuCfp4C0IrXmD21Xlvwug=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDqtF5hmd8vOpqbbwV21tg14xX++zqKXLH6GEsERFfmdzIvwEqXuaYwubrGbTONETICm5KDzaY/lQClV+R8Um6AmR1dL9j9lOyzeZE4f66l+LVu6n0jxE0T6Ik6nsHtIzOqO1aKpLNO+219KBswnBWc9lv7HD2e/OKlpOBDqX/YxpM1sz8J8A8TzNCrkufP6OSCdZ/s5XwPEh1o4tYXdJ9BSAfdKzUBgRtbs16Q7vWrVfxD01pmvvVnkIHVR+eL3d+3n19ZmMlGAm8HpP52fYTa7ABixDxdBelQN92/cdaIwePvAdGpqLv8fCEVCLwrQc4BQi2BePp5n5WYHTwEFW4hAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmfQa5Q8TzoTugCs1GTfwB5ckb5MwEuqHdQu8LdBSmcqXk", - "privKey": "CAASqQkwggSlAgEAAoIBAQDx0kP+8QIqEa1V8vmxNxn/bx2r+I42wn8MZytziUKLzXPJIfIRejdcEO/U5jqDQNIr1ltIQkO/P+K8U4/ThlLfJ/pqkOYHNFtU1kGPVnMMDHkr2RvOG9MPUC61aCyhCYAUpUsHwSraBVxfQkDUTUOCMqwn0FUqpErM2v+naPGBYyn2owsN1SMLE/JxyZyB1VhoqyMpbfUZY0nYL+OqF7jveVM+30cfdfGHSkojgJCyb9Y8EXRCPRLCX2uQ8g5zgln8CyELd4p+wgSJ35YS2pUGlZ9Ev9UK+YZNCF97NIhtepFNLHmrYemVKrDokDP7fl2XEwmtjFdNq4SiorQdTHJBAgMBAAECggEAf/a8dJQkiQ6BoxHIf7ag00KBeRc2alPR10Zg/+qKhGBb/PsxlX4O/XEY+Jg8LmiGzxvHgh1OrE2qNe4iFdTm1Z/aK7oxf259Rg968dbVWnLfTAy/YfnnXhsYHHbb5vuYA1TUt23It0ZO8zmkBLQ+HQ+jeg4Mg1wdGPpqfrRR2B0SKbR/aG6c9EDhYXIDNnkX1Wfd4y+GqlHN0YrURbFVIzJUu93yrhP7DaM9CYBlmdjul/5NdfX+zU9XjqHJVw7/zxkJjWwfm8UgNVgjsFJlHhfcTKsacRUAiYuwljo84OOZUhshWV321LIby4BKMNFPEr6uqh14ZzTFPiczx8yoAQKBgQD6xYh4fpelVf21JMnjtWuiEkFLbTSKqFMPp6djCIfiBuJOFF3paJCHtnCGsK++/aEQ3mlxlJ6kYaPRcQUAle1RJtd8C+xhRqB3mjjwO0Cl1vTJgYMNMW1E0mTzhMVT1gRdt0+3Uii7k4Z/MWRacm/zFuE5GF9mxG51jabjULX/0QKBgQD23PYgucYaTEgZQrLMhN3TSQKmlGG8dE7S4QqdchmXkaqCMnU2LW2GYthirsZQa4hByUamG9j6T9tldpuPP0Fc8KfbwLohiwIatQHwIQUT2uhkG+HX/+/tZYZZ79vIf9Yl9fhAwBnSChRGhS5n90jhZ/CbyEk1PZxmDBe75r7XcQKBgQDzwQRZU3vmC0L0S9EuVM9Nl37+aSU0Tk+GnQlYexdR/i0FhkiOs8QhFpYkZiQ+etyPwBEwhSz7Tall0Pzyx8kJI787ZX+cQoGCIFeOM5owWVRRdmFDdrLmvbfA+WKxjgtqaN/EqsjLI6gNhJ4uSKRG3wuHawh4pSFVhJ4ewPpXsQKBgQDx0cVwjUqXnD3MMOABI+4/+HcWQqfy+WP1gujpDkovhUunulHDPoDZcZ5SHK67PHr/JnGEaicEHJHoNGVxzx7yMfPcelBaZ1cqXkGFvnLA3mFjH0T+WAHpZNhU5XdAUqmuCeKjWwpwC9uMsQ2iXkQQOccicvHzq2S3OgVN1V0AoQKBgQDBGXOTN/at01B5ooKS3/O/3zBoERfgnmAsPhnkYCl+yn6eSY6tU31do+oUXxXLVTt79goSSC9sQDzA9TSBBo1NTGzFElP1zSBEwhgU+l+KTdjKH0qHI9/8ks1G4SoSNFxAj+MrRVsy9glqdmJIgKmPqdwlnq/7hB9l/FUC65LsfQ==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDx0kP+8QIqEa1V8vmxNxn/bx2r+I42wn8MZytziUKLzXPJIfIRejdcEO/U5jqDQNIr1ltIQkO/P+K8U4/ThlLfJ/pqkOYHNFtU1kGPVnMMDHkr2RvOG9MPUC61aCyhCYAUpUsHwSraBVxfQkDUTUOCMqwn0FUqpErM2v+naPGBYyn2owsN1SMLE/JxyZyB1VhoqyMpbfUZY0nYL+OqF7jveVM+30cfdfGHSkojgJCyb9Y8EXRCPRLCX2uQ8g5zgln8CyELd4p+wgSJ35YS2pUGlZ9Ev9UK+YZNCF97NIhtepFNLHmrYemVKrDokDP7fl2XEwmtjFdNq4SiorQdTHJBAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmNcR5oXnGP4VSX5AjaRcPepFMz27ZGupEJXyq2BMpRp5g", - "privKey": "CAASpwkwggSjAgEAAoIBAQDQ+zxtRDJm934HN6L/8q8EFnXghF2cvpECCpBc+/+yR4l81QkKblB3/LWDeAcx4YSDipJZ8D7Wb18R4R3BfcJ2AoMnvoavX64tnTfFOYiUMri4txnDDCVfYzo2CC5QGTqwzwAdC4olqHpb7oVTIAzuQHOQSdH7nEsmy19SmXo2kQlZdU4b5G1x42qyFIRY2F5i+d9fx9WtwuYmCOL/QaPSZOF8j1uxuPzeHzc8zyVHXqJfvbhh7MOowCuawecp2N7OvYUsNJdtej5U0xgEdyICTivIPNOIvRKoozvMcmA0RZto6X+RFQX4c1Avc7cvA0CwKW2fLvsW+SB4FP5cur9dAgMBAAECggEABtl1LY+ip0VNWCc2rHTjz5p82pL8DnsgfZSjDqkjxFAb7X9+AF2FPamGuXxhn/zoPvd3vILnTFfyIb/jHchla0DB07em6nCUYOJaRZiRJWpUK5m0unPXdbzm14aFHhL1nX3rXwhVys4u1HyI2iSex+BM6VnCDCEfRXI8+ZQWMVuYaEyCTuhvl01jJ2LuPsDdQGffMXuiubwZSmvjwOlauUvXVCy6l5riwj/UyoEyIDp0bWY3LbaewblqHGvVGKBBX3pdNE4fadFtgeJBETMCr17EwKRWrNEo28w9Tm8A11s5GntOcGnpk6h/OIdBDPqXqBoQ3CnkJOsI+TmL5nTa8QKBgQDzbFZr1YKAEvKM41xJ6lqAKh7jXw+1SK2LgvatFMDnvsirsRJs/s2D/PQaTnQu+fG82LQbIj8a0wGLd/UKTle6jldssA0YU6KooW08/+1RaaaViSXik91pYel4gxyaN55ie3zlJ9lDiRTximQ+RTHr+w1qk38jcn93YOlYcZTNCwKBgQDbx1mvz9QK9Xf2gT3+KfMkJiEcQit38s9kWQnY3BGzqAq8Oo631gVpAy8KiYey6jKohj7wNPnhDBISVJ/eta6rMiDjujVa4IkBhakyzMCm1nSQHordv/oocnQHna2TplvYbSMUAjgvatS7AbnzZzz+k4iXCmfSEE/1z9aoTUNWNwKBgCcqymkFbL8QzWgv+RyHkdJHdLrfA9cGf64P/4Lv8O4Y+47sqetRwF25aMmG0Bjy7JuXPruS8hZt1zTKs2naGzGQT67UUPcWFfkOKFaFU3kjB8PN0oO3iQu4zmkup36E7n4oInt4wvOj7fPDcce3OIYg2hLI8s8QUEQ0Gre5ZtjrAoGAZhqsWRiVq221COmsUltM4VtxgH5hUX2VyknvYDeFZdDJA/+0dEXTB6F6Bkw0pfNWC6MqtE/4UwxXjPqRt1byyggk7YeB6DFulS1ymO41Bo2VY6s82p6o6oeZzjv7+x+LhfXWGSKa1bStFiBMMn+g/6itCXbFGvuHGm0vjcsvYGsCgYEAvDl+6IL0r1HNoyXZZgJGM8GVW1PpIJlHVE5X1X/LDtfiNNAhxXUt05+8uuryeAWpA5hr/Bt7/ds2k9wJFUHAU7VRJkhZ3J64vtl7MJro+cSNEZ8H5O1Y+53SBuAQk9Z5wOwZjjHf30l+wYoTwoKJ15DIPOIKGYyhE0Bx4HOBBdU=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDQ+zxtRDJm934HN6L/8q8EFnXghF2cvpECCpBc+/+yR4l81QkKblB3/LWDeAcx4YSDipJZ8D7Wb18R4R3BfcJ2AoMnvoavX64tnTfFOYiUMri4txnDDCVfYzo2CC5QGTqwzwAdC4olqHpb7oVTIAzuQHOQSdH7nEsmy19SmXo2kQlZdU4b5G1x42qyFIRY2F5i+d9fx9WtwuYmCOL/QaPSZOF8j1uxuPzeHzc8zyVHXqJfvbhh7MOowCuawecp2N7OvYUsNJdtej5U0xgEdyICTivIPNOIvRKoozvMcmA0RZto6X+RFQX4c1Avc7cvA0CwKW2fLvsW+SB4FP5cur9dAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmPpTw7SQru7aWTmsZMhBPmprCWqnqXdqFbra14bnCYWYg", - "privKey": "CAASpgkwggSiAgEAAoIBAQDSzlp3EXYtalGQfbDDu8eE0y4Wn+YyVB8uOk7CVmDonlwp3brJ0/fNaP3mDG/Y24YdvmgpCuzNaxvEIYz19Ji5MKUE9wY9D3gUoJN8jRIAfuKIUnxOLxd+Rva3gp7Yn/5Y9aOurFjlWWNs1jimmqfDTnA5wmEKvIDuoWjMjq4fg150SIUjtG/SKg25lUCL2n6WYK21HIsObVWLbh88Hy3lQCbBU/4CcYakF6TEzrcgSPZ9hz1oirrznu5jpO2NoqT3vSN5NtiUrGcEdyk4L0efwqYKDIEjGjXS9SWiJYc+ri2vxsOy6rmoFafugGcX8cnnPLmqGhzqXngDLkQG1DqBAgMBAAECggEAPAp+Ba+5gxHnDUpfUEBpgVFMrTD5tZf0EYyV5hAIJfkEsv/uNZHj4GNo/V7JdHCB8HLM4/OyoodBL0mHBn6WCRjrx1A8PKPtRaK+nxjm6bE6AC3OLc6H2HWJy5aue3CGVvwPlK6N2zTsdpFFLV6bLatnl2vfi9lIt67NVIXG3j5dxo2OlPqXKLkLnMEwjo5hGj86Z+Q0O3Z0fxe3CApr373sVxkHDgChagVoraguFubOPsEOHBnjO5THZmNUhlEFDRydX5aCTYerzo8vs1G7vd6I9MjKkPAwRlGJ+XtmwmXbaX9pFjwiHjkiVFUAXxw5dmhuzYcfN0kZSsvWDv94AQKBgQD4UbSG18nyOFS6/njfebrtwJSkhQIyk54O/1KAguQUqOx4CjYhuhGeQlu5yTLQoWUO8E/7K5zdjOsCYCHcmI1W8TRolyOo0o78ATvSrcwy1gkaYgk2duac9WiBV6FVDtxlcsX1pou4vt4S8LXDHVb2ToMcretZpdx9WJAQkP1GYQKBgQDZU5qWmwG4uJQ6bloa6NQ4sgY7gXpf8fRHUk+Y6W6cyRkeiBVo+3BmoY0kE5kbJUQAfbpOJ2ynQzV1T19CFqnKJHbd4WQM5Q9+Lsa7QUSCmAvLoaAPe8AoLYlYoFodpRKEvjZk6myMhcSP85/XPkPsAA25GdXFZspUlPzuU48oIQKBgFzJ4yRT9BE/vWGWf0I6cYAv6xtC3Fxbzr8Z5xFAV8vkh2AfqLSXm8fAUhgtN4DAHkwjvi9Dz7z10Ec19tFAa+gl/4hpmZiW/XjrWRhTey8vzXz/TyP78BaMmT1jqlRnVjHOXmx5jFI/eCopqjG7f+hP1CxeTMhV8vsfoc2e8BVhAoGAWkAt4n1cqal9ZQaOxL4L47+Kdwu+FjoUh8nW5FmMZe/dTqCUw5QniXdtdZ3t5ygCpXGQ/QPCS3PNr3nWxUtEF34tHteLBQ/a7zvdq8Xe/ZzGyTnFjqiFlCnU78kno0f5+MZFMINpsLGcf2tc5bYl3svm5wejjuaw/48fuplYygECgYBbaRw9VV4XUBu+/nlcjSNdrj2N6gHxilYwUX4hTAT4ZDVxxJXZZHlxp20jHd3L135M6VAEHjA5b1+tUbe90GxOdM6xi9iKn36VHtjd7K/p0NWzGd86Ny8sqOZN0Q3surcqXvT2nFyuO6q7jDtAN2b0BSo9K9PnSiN50lEVcmBX0A==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDSzlp3EXYtalGQfbDDu8eE0y4Wn+YyVB8uOk7CVmDonlwp3brJ0/fNaP3mDG/Y24YdvmgpCuzNaxvEIYz19Ji5MKUE9wY9D3gUoJN8jRIAfuKIUnxOLxd+Rva3gp7Yn/5Y9aOurFjlWWNs1jimmqfDTnA5wmEKvIDuoWjMjq4fg150SIUjtG/SKg25lUCL2n6WYK21HIsObVWLbh88Hy3lQCbBU/4CcYakF6TEzrcgSPZ9hz1oirrznu5jpO2NoqT3vSN5NtiUrGcEdyk4L0efwqYKDIEjGjXS9SWiJYc+ri2vxsOy6rmoFafugGcX8cnnPLmqGhzqXngDLkQG1DqBAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmSPUjpXC8ogoXp3AouY35AiLYTJWBbXR8uCmQY6QqS1rV", - "privKey": "CAASqAkwggSkAgEAAoIBAQDLlhiibulDWV/h9BweAmYkNDnyeU9Q+cn6xI5xCrrmEraPCvaQWcNBPbIoldERlE93d/ynP59y8Kixqhh9bXFHzP/EwXavvbO0OMxABtJZOx+lhWGl82HIjrvz44TFNEmXZBUhv/3BXEm0yLOPznW6ccdVNicOZLQoi46mHpH7BYe3zjQuX2cEp4wRG5pKB5X3QKKBmWn1SlskzthP5v3So7aQh2MfhGtetU9sJ8vNliFHST77QIKCyF4f6secNuyojekiHu7CUX55q7o1IVdZnrrEXZX2aMormAfqPhKQFPJYyO6aeWyHwMtxiiYnUQbokFpA1TQ1C6tOvc6ZXNsjAgMBAAECggEAcilS2xMyvs+JUt1ePv29ZSPcMroP3iqUNoiuD3mi4I0xzfip1rxfH2CHXPbV6/OstCOWi/rDYOLO1gG6BeuvEEJGUoDiGx5XfQI0ltq8bckXr+uhnDtkY+CWSOcWdrchZUF8EBbnJtyngDbjagquPcS7sG7Ta+DQncPUVBbkaUvqSpp4M5rqwECtEA9kUZQIMp/pJB9ZQ/2Ob2cyoI5SmUvk0by0XP8Hb7GBtrf6S4+BbUaOkj9jDfAH6Y0pN1ubJ3tkd38FAFtorWsJAdkvrQliFUjfjk+ANmWR6iIEGJMXsabUmrmXDIhzpv+XMMNpyI0L37l0DlkJ9hUEwql0wQKBgQDusySod6rD02pmSX4gTzf2RNaAOghTwiW6W4YyXOokRQPyrbvXUqhnlYJGBpB8Iy74h6+nJtCBPOTOhFSDjL0isTcG5KV4MWzoRT2l0PhOA03KccTBAgxHQSc2ACJIJr1yiuj62/ToEtmc7e4Eb5HeYzgUqig5Z9LHjxXu+UZeswKBgQDaV3RnUqb+3tlir2mB8Srr7ZjLMMjYw657YtvFCHpljuqUPbt+6k1K6uTsnJ/fTrfRg9Fn168YA8Kc5tqcW5ECjN4Q5/MdGWVBVuLByE1OAZ3Hm9g6++tIWLMpb+z2tgduKUA/hfdm64X6HqoBsxporYhj0Mpn0hrO8L7FlrXJ0QKBgQCDWHQNd3uxsb3UdxA9+xlSG+LkQAqg/C4Cc6ZORC5astdPTCYWf9dG2FAM9EPA6yNHgnI3SfZlhvpoYQyYLnNMibM7ycj7cEb7ME6R1YEsfEjr4tpfUh8rfkBzSHOUvCx2wNUeZLZIlUbFQW89ZZ8gffw38sGbhPPI94UcMHJ2XQKBgE06s9y8Gn96ObAzVYF12XW8A9iTN+ecR4IzNIMb/Zcglw66SzCYFaDTNwgOWmo1QMWl95LgcnlvEw5GhbralI8vXnjiYla/ndYfsnNSsy1NWw64rCIo608auLyGb23Qcw5fHu+ZJipMUoZnBEE3pbay8tRDjORuJ7dc5k2jgkeRAoGBAID3nD/tbyg617LB/tO1rnKlfkUGmLocoyzzX+reQHVRaabjAS5Ip5a/7g5gtLyFERlImbEzmCbZm03PomZgHLHMPE6Jlo1uTsggf8Ax/ldR3Q3ZficWwQ05gZGflezsUVayewoqDbrW+qxXI56v8l/dnoVs/5FjlY3aFZOFO2pV", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDLlhiibulDWV/h9BweAmYkNDnyeU9Q+cn6xI5xCrrmEraPCvaQWcNBPbIoldERlE93d/ynP59y8Kixqhh9bXFHzP/EwXavvbO0OMxABtJZOx+lhWGl82HIjrvz44TFNEmXZBUhv/3BXEm0yLOPznW6ccdVNicOZLQoi46mHpH7BYe3zjQuX2cEp4wRG5pKB5X3QKKBmWn1SlskzthP5v3So7aQh2MfhGtetU9sJ8vNliFHST77QIKCyF4f6secNuyojekiHu7CUX55q7o1IVdZnrrEXZX2aMormAfqPhKQFPJYyO6aeWyHwMtxiiYnUQbokFpA1TQ1C6tOvc6ZXNsjAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmeQD4Y7ahTYxRrDERmgTD4QTUxEgXoMKcoM5TJ9LXDKfB", - "privKey": "CAASqQkwggSlAgEAAoIBAQCrq+NH4sQ+lgXdZgQ8G/xD01fkuFuuHpaRX3a1QRsVP30KxhTNHNAuRq1+2RFSIfT42jGvEV29xFKAAZpY70P/6uPRurxn/uqCAroPJYV3FrCYS/b2N3lJOBrG6e/TzM2Gp0Aj5TbJwzA2k2vWHxjnyacVyzxSQtO5wPfiv+seP6RVSnBYQnJkeyjxDyhUG9D1n4q8TX8Bdjfgnubh73rg+jGwfmY7R6YQVCRNkwGnjesJHNvE0fgKt9HQYnDbgwQ4b1GeKPgYPpc+gOLOI5ctY/ScgXcHrI4N86o4uJsMBWo/kmTD1T5PJjGtdHRByzLrUElsJnKb+0rXiDS2WroXAgMBAAECggEBAKI4bjAauAjQKUCaSzwl0c6h4nduQqwZmXxLslf66sW8VcOdhECCjrJ79SxdoIF1NxEE1lgxV9yfrLnrSdfqWN53Lsqb47d96knqm7j+Ys0y8rMnbXoi14h57Mu0ef0xlbE9UF3bFle4C1I3InqWrikxo6Lzhs/Q+FOaZmOtqVbNjhdP88oIZANK/ceCYdba4mHENqxl8yupcuYplKdZJc7udOd97zGwebqlhsF9AJ0/OINUo5yeH4lBhjZmLyzvgUnNbWRqQbIrFHUJERb8LiYadNDuGURgKdSb9ARe9+/9L+mYTI1tNs7F0hACMnJxLRb/q3rxW2zPR3C2v2LB6+kCgYEA3NMofdiNfkeCav4OnpHpTV9G+uj3xEKtTvqdNVxrJEXbeRMYzZqVY9TP2v3UdSCDD1zjXays/+INljFFForSQFech9UIuuIDR4IVpmHiPwzmmYHf2A2tVZvjOFIe9sJMXS7nL193ojDuNvy6XCN5V4Vi0MYnM2XdaP+gKIa252sCgYEAxwRXiGjyEjRD8XMiwpe+o7IY+5DDG0he2f5jxI7ObEqVupQeAknQbvjDfXAv55bWlV8kQOWgcs5d4mpJoZ43adO1R9VFu15VkyhCJ3XYL1AlNj/543Rd9y6/Kxm1yYu42EHUrpluiWE2Rt46+INd+Ztj/yzYZoCM8F2GiBoW3wUCgYEAvZKxYkg0QEKXnc55MnxE811mDCVP/zbWncTcjWDHwh4OqkRQuMGKmmeqAXCDogHFQb0Wm+aPpiSkUVn+27lVglM0WA/1LKq28f6lI29I0aP7m7E5P7uOIL5xNHqbhm+LKzwG0E5+38ht2NriChOSKiaijGRwZtl+WJOLJP9xqf0CgYAGoQ9lXNGLZ7BHr6UdxD42Z61LW+QT2ZJHQqECIBuiIc3g/CQPwXOu7pxcZktCNJULPrMPclao3FTmQNIZDxMbdFDahrEe76J8F2A0vkkoMkw7BWCGgg7LOARoJCAZCY1rrq2t7zBuZQ2QyMBAHOgZc2KeUlkW+Ps42nSrveq7HQKBgQCwrPtKocpxsVTd+VuFtki3AtUvM2NfQBhtc5FT8WKVvO5PKBIPyG6hBmfGeOrxiK8lISO6MVDaU4FRZ7hc7bQUD7IpzGvNpZo8Vnsav11bgi3VO7ElKnHJN+MrqFYywasCDOnIVBGBBr0m+qnhr7pEgJeHuVnlcmynkZZJob5zWw==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrq+NH4sQ+lgXdZgQ8G/xD01fkuFuuHpaRX3a1QRsVP30KxhTNHNAuRq1+2RFSIfT42jGvEV29xFKAAZpY70P/6uPRurxn/uqCAroPJYV3FrCYS/b2N3lJOBrG6e/TzM2Gp0Aj5TbJwzA2k2vWHxjnyacVyzxSQtO5wPfiv+seP6RVSnBYQnJkeyjxDyhUG9D1n4q8TX8Bdjfgnubh73rg+jGwfmY7R6YQVCRNkwGnjesJHNvE0fgKt9HQYnDbgwQ4b1GeKPgYPpc+gOLOI5ctY/ScgXcHrI4N86o4uJsMBWo/kmTD1T5PJjGtdHRByzLrUElsJnKb+0rXiDS2WroXAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmRu9rXt1KoHES8dwvoN4Ca5PkHVEHsyFY3F7CwY1ZUTPA", - "privKey": "CAASqAkwggSkAgEAAoIBAQDfElzt5+t4UjWuHtDpwPvjIwQfrgn4+OvnKhBeK4yJuLlC3Crl5kS4cMkiwnbpsIGAF00BO0GQi7j0EY9g6bEsSnmKvpAovoekcVACQZzbsbe5zp1KYqbSawkzPrQO0OUR6UaIPEx4LZ6fpP8AJZLfOsJ1s+ETJ9qOQZa+IkrHgpHU629WdQoZt9tJ5jKLprWgp24CvBnErLnUPDXhkT60TrF5mSMukJJ/hsnbgzjs83J1AfhRp3wCE3X3606cUnT/0IpI+flLA7KvxBN8eVIA4zWFFr4hGZxtzNnxHImcwJ1l4Fj2vu20Ks8vozjBYOauVoUD02gVKBbsYdw82Ec7AgMBAAECggEAKBbiwJbHiK4tm4dKQFhAbIekfBUJEceajcbPfj0RWsbp9Iwg4YRKoWMTor2UJVdlTqHhYvFFTTbvHF3ziJU3FCCmSzsIKWpkcjczC9TC3fDIdgod1np4RKSb2KvSLD96i4eC94TusUJxmXtLoLkf9iJXRFP5hTnKW3qKHs2G5ufIQCp3QEgJaCJ+uGd2npZ+5x1pMZAF7HsShONyiDWT+63sF0TXrh4OJh1e5+SLCjlJY4luXDWYkelihWbWuWEe8lHAmBorBdRAPJG0ssl/rzSeEdQQHNY/WBYgXq1OhrrwffEKXBStrH1lIteYdAtPoTV6ilHB+S3Sj1LgWDaRgQKBgQD+q4X2PwGJ2PECog657PzWM1w8QQgbwnydl2N+iLrg+lOBdDE6QpJtOCmuTUAjlTbBONUhunsBSugHZtSUkSeJ2NBEuHf4qvqB9o78PGT+fILHMei8uJi7uJY6hNWbzfKR//RRdxZ/ZIJGK0eEZ5HO3K2s9tnS9jRg4OwWbo64mwKBgQDgPJhNmr+rMtMhRqMF5iYfpFIvvF+KGq8RJ0kk/PACf0wtxzjlryOnuy45Tkjuga3rr3xOJKwi6j+5MGxntH12cfkfZnR1Y1KnmTbNVs91R1ZGR08s0Omjfl1YauSdeQxmDyoOQXjV65vOb1L7bvwBa5DafUwVeFUikQ7tg7cF4QKBgQDFy8iHIhZ6zwEZj26qn1McttVbgxLeJKcO6yb+fwnOdP5onCsj2dLKe4V7+EnpmRnm5tI6mRCyR1CBdy+CmF7CJKBVz4R2oa1hRXN2mx3BvkkAl1XxRdpyaoJbvxH9Ke7N0KMcpsbVeOXpw/GO97X6mdFWdn9l54109RzIq2O0IwKBgCRvlCvf/k76JjZc/PZjbERt9fDNwhR1u4alBIyfEPzG5ID3wzYHHFsP3jXvk4g1yCXo0OD9sn7F427bAHJlcJGDeYBxrHC6n96d1brN5U3gNpOa2LGmjKBFUzOfwuAXoD0hL6s7VkAkVZ/YlPpIEWjFqrbl7yv57pN8UJmlcmLhAoGBAJXAhN56TY7mc/w6fKm8tD46zao2iynMREOUO3Wpa9wWtRWyAQwg44SW9mpuBuFVtuZ7KgumKX8BWcHsz5lu78KD+zZ7ko8UhtEDXbk22KVxsWYEgUNmaTkTA3ziDWq8Y2QJ0gW1HJ3G1PTu/0FH3b43LxxBnmSaR/wQmnKEvvoo", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDfElzt5+t4UjWuHtDpwPvjIwQfrgn4+OvnKhBeK4yJuLlC3Crl5kS4cMkiwnbpsIGAF00BO0GQi7j0EY9g6bEsSnmKvpAovoekcVACQZzbsbe5zp1KYqbSawkzPrQO0OUR6UaIPEx4LZ6fpP8AJZLfOsJ1s+ETJ9qOQZa+IkrHgpHU629WdQoZt9tJ5jKLprWgp24CvBnErLnUPDXhkT60TrF5mSMukJJ/hsnbgzjs83J1AfhRp3wCE3X3606cUnT/0IpI+flLA7KvxBN8eVIA4zWFFr4hGZxtzNnxHImcwJ1l4Fj2vu20Ks8vozjBYOauVoUD02gVKBbsYdw82Ec7AgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmYqz4o7nrf6tNUo7ymPzLUPM5SpF7QvXK71kGNnLYw8Ra", - "privKey": "CAASpwkwggSjAgEAAoIBAQCgNnacFOUn73TenCv+AqNqSUFVf6rTHKOXq1JdxpiOvnCTVs/jGzFwZv7ytKMtfJf2o479TQL5anFnjOos6m2hxFcmF/W9XdcUEK7UiAD0n45RaGTOisE1ifZ4hUcdl5W1kmKHmbwvwcRRxXuoBvGYXq2ZJe0fWqaAxHHSzyVved4Sg80NzwX5mIwRK6Hg9GYCxj6vpRd5GAaEgxIk6aRnhV4zWOIYPnGu7ifUFz2mEyzB0a4ihZ2P/cLxr85Jr+GmJ4bwb5nxNtx5EOHVpAo/JFUenqc9bTowTvfWbOC5zsW/1FtmL8cFHUdVjVGDLfn8go8ZJ6tbMoTsbvDqNUNVAgMBAAECggEAO5Kk3frDDutySIhHr2bpvs7IdXNIYMGobvAsa2Q6O/HCSHciS+9Dnekeab8TYgmPNA2zUKq/LWEQFBIIzXTKGTm5shd8r9Jh9DsT10FPIabms4ye11Iu76qCNGRSgkVoTKDG9GcM27EwP7uv9FXIpgCmimjY2CzL9tuU+289G0rdzCDhYXpyTIPj8ZBu4sBp6pPBcLsjMuzzdyJ1OjczrSvIPh7hW3LQLS3i6Da4bq1yImqwyqN7tK94W4vRAxRBFjls6sdMDFZfOJST6l+qvHwcv6ZjwFQS+eJOPLcfqAxypBxOqw+Kmv90xJDJiJkq+jgsTnrJkYCW7KeMRaCgbQKBgQDO8ar/BlXMDoAzdGysm+cIFeZyNKQY4QLsJlH+VmpDXxdZnnkIJeQ6dfnbyatCaqHaqCKkx/2W06ZZOXiDjL6a2L1ZElfXszylYfhapvGDKIVqOIg1a1KJCRCE/MVzpGqmmj2td1O7PInGKTvA9zwTkLnWanNm41m+9q1gPFIeSwKBgQDGMOxUGkeSHAauoloKd5mPxbbm6rWm/a/Jh6gF/DuEU4lpNh3eZb07DpNq6g8xb0Fd3hcA+kJfoOZwrIA73SYCvAirO966ZgolSNRNakQV/QLEPgkL3FaOwocyDeG/LfN3uxvjFdIqCFzhWPhdRwjGNUrSeeownEGGsgZCVHqg3wKBgQCSE6sFi75CZTX/nD4d9Yq2fWcG1LvEyAhdE4urQeqOlfAQlbmPk9evoJl3mLpoDocjpq2VrYoGzm3M67FzAoWFHltCJZ2WJ/I2N5qsus0eLRtH6JHVS2WeT6S2iwsB31xdL+E7slCLiWcjVvXT93ETyoQzoz7EsNUn5E5r8QhyUwKBgHpYrzuH8ZC/3lwl+yGlDVYUvsE0OSk6SC9HoDD5saARla0ubCfjdHqll9mTXgetX5PbyyWeWCUChd8ejhbmgVWE0HEsh2VYIoE7wVt880UDqJaOmTUKMyDz81OyAB7t9fN+vUtlKBUsjnHKY5/pfwAk2+isvCZ//29wLK77yavPAoGAYR3tpLA7LB91/YWuS8D3xrda7cvLm3nVsik4tgWl0VgrewyGJjKWCvsV/SupVBIn62PlJzMSLDUzQDcWw/9mZ9isAKUBZPAI7lYzuuw3D6j3/05I9vKvHRUtAH9mNuYNu0lfqwyCuMoV1LD1wTfgaHSF1N8y4EoqQrcUBM1qrXI=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCgNnacFOUn73TenCv+AqNqSUFVf6rTHKOXq1JdxpiOvnCTVs/jGzFwZv7ytKMtfJf2o479TQL5anFnjOos6m2hxFcmF/W9XdcUEK7UiAD0n45RaGTOisE1ifZ4hUcdl5W1kmKHmbwvwcRRxXuoBvGYXq2ZJe0fWqaAxHHSzyVved4Sg80NzwX5mIwRK6Hg9GYCxj6vpRd5GAaEgxIk6aRnhV4zWOIYPnGu7ifUFz2mEyzB0a4ihZ2P/cLxr85Jr+GmJ4bwb5nxNtx5EOHVpAo/JFUenqc9bTowTvfWbOC5zsW/1FtmL8cFHUdVjVGDLfn8go8ZJ6tbMoTsbvDqNUNVAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "Qmf4BuSEF3q31HTpBuse7aPwpaYzJ2e7FHiGSHsmhA3uZz", - "privKey": "CAASpwkwggSjAgEAAoIBAQC40RyUzcdSIEiXOd5cb6Aw1rH2WhagwIX4p158NPDx57pDbjKVMgZXNJk6Mmnk4h0WFVxugFJtG9l730y5ErUa4kNFOMzG85urHM9pvWZAtqwggI44uITxST8mYGXwOT3jIJaPqnVmSQ/8TlAbQ+4tTEH2TfJaQwZ6uhn3h3RIQGyUm2iYrEa2eFV5x0aUXln4Q5xJb0Agns62QXJ+m73QaJhp3/x9Ul5umMpDRwWgfMZWhiHi0Uo/FwcZpmvNSeEZlnAyYHMulNkpXMH6UYj0vzHxH03BRkBAVpZl8aD1asKtOI2H7XHirqCcRoSLmPOTKgrGktDqsHzl6izy+KTfAgMBAAECggEAV+aNE3DztenI3LQXQBuPMutJ5QNf88DddzATTjvXxRYTjvKgeDk8rslDf1xu5P9uGgy604uQqHgwbiv8T6HIJSssF4Y2TwGaLj4boA0GzwySvTqnae7IvAG8WUJL+X8gIiBju5y1DZr+UV/l1bHvW/gC/2R/OdLbCA/vPb1c3ueF7k21ACoDN6CE2u+IxX+FfFmtvwlzo0zYobgNYMIvWaeM7qjMRHH/p4KTrutKcgGhZhq/0hXymRaX+Uc/aKyZkVVOvKPzHOQ1cucpv1Hzocl6dAnBgEOC0tm5/ZG98t27xRSef3zeElarzkmwh4vNDDNh7g7HLkPY6LWeF77ZAQKBgQDndF/9iwKyDw+7zPs9QH1PReW6joZaUY7UFFsHpatUYSVMt0TDOo2WrGcr+l3B82oYqQ88SOlH/JPURPQbsv3KB0r9OtfcF7RTxwFg83KGl3SyZzaTxchFIPF1hjp/KpcVQBdGJmFOybBg4ZomCkTr27ctOtP/uFoR+nx8m1SNwQKBgQDMapiG2Oj5UNQ8emHcYKPDuIiKF1y2VNUK/PXht1GN++D/VMykXWjXxytEnn/UrLHNbmuH2dJrXJIjNfH1TNcRpsM6LrgtsWLTex9WIeT7Afgvbs3sR19FkAHssbEXVx/W/nK0y0mADyXZ2NG7UZMzKdpELjF1+WV8tFj8RM8anwKBgFLpsH1OL/ADTzqSaqn9kSY1vt7+sYhnUQgOJrHtmhuHFWqO+HYLYq9IIUlyzeVtwmMFJO0OnWrpQze2X9AQZbPauvVOAAfbAgFE9+x4KV2noelK6hUzs9N3wqe8JvZpFmhJZkz98Lvdqm56QtM/uILZWZw9R7aCntlz5uZoanjBAoGAbcrTIZpfh4lidRlGdpdxXi4/J+xkX4ow4zX62sEbjKc8sedaAu4o4byYAMMg5Znb5froxo639fJCi6btzlL3MQPg199ADUq5Sd1Xd2u9ERR9uPxKnh23jiVK41aNR3wEHfWMpo6Ja763Fcre2z11UoWoNfaZmkPZvqEfKl/K3QECgYEAzNCm3YWypjjJ6ny/VHCWmjLZbgJN6kNbhfj/djr2As016K0DJqII0E66QWT1BjnMe4uBkabNvBCTybb4o1n5C2vRgvYtVJ/TJsmPKLxrEvARiYD0N1+v24P1G3BTpnVAqVIihMuTdHQIkBmqlGHOUmOfZ8CJb+lda4wMoAIxJSw=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC40RyUzcdSIEiXOd5cb6Aw1rH2WhagwIX4p158NPDx57pDbjKVMgZXNJk6Mmnk4h0WFVxugFJtG9l730y5ErUa4kNFOMzG85urHM9pvWZAtqwggI44uITxST8mYGXwOT3jIJaPqnVmSQ/8TlAbQ+4tTEH2TfJaQwZ6uhn3h3RIQGyUm2iYrEa2eFV5x0aUXln4Q5xJb0Agns62QXJ+m73QaJhp3/x9Ul5umMpDRwWgfMZWhiHi0Uo/FwcZpmvNSeEZlnAyYHMulNkpXMH6UYj0vzHxH03BRkBAVpZl8aD1asKtOI2H7XHirqCcRoSLmPOTKgrGktDqsHzl6izy+KTfAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmQnr4zPrD7UJRyLx8p3hkd4EXghTjwBHjnSVKwLtP3gFC", - "privKey": "CAASpgkwggSiAgEAAoIBAQDaDKzUGU5PCmN9cfxfi2K6LSf6Ro/LEJmBI4+aAZWRW5c8WJdqNIME7kQ0vLwr4ZjxVizntHjKr8so1CEVlet5YxQg2ezhsvnniTIYgoAo/tuLt8rZqEL6Kg0I9SYFN/zWHbfsJbGqFA20HoUSjPnO6MiSYrXHLzlJwXX/P0twRJGW1a0YjI+A01kI0XrTYfXNBgSPqd1gd/nWqDDMuulF+Y8dcXPRoUHEAQI4q7nMUyTSFX7Qhv9U7fKIQ1cGCEOKjWyB5jt8xK+cfKWJ9Mb1eLvPu/ZBnfjqW2g8LnE1TbZ1LvFmnbQZgPWZ12AfAsFprswMcIHNxoCnz3yxmUoHAgMBAAECggEABS4p6PwU7THM/Uz49vgjx1KNUZfdkLB7RSMoJTuGZyaq6CceqcpHlpVmj24wdkZs0McAWBzkhcQ5amXnx1CBgKfG8aTbyNzsrQCIbSakjtTHOIGMUzF5LeJT3vOcDKGw4xFfrj+TAfxp+u6CsNcilDTZlwi7UtkfXk43VHIXg7pCE/CxBstrlu48/Rsxvymzk7QHAkUneG0oVWJnkcPQNlnntC8XpQMqapxcDdIWjhXctouPn2hdVDC2KpyC2MrQKvbDb7bCVi6qntnryEe+DmX45TaR6TPfvnh1oc0ov02Tf64TJPieql+tjerlG23YoGWJH0wdezY+R/jIc5r1WQKBgQDzneebNpmQqduM0/4HI2LuI17J2OebKt7voQDm7q9ER7hsIPTUxXt9dx1RyxVZpXxNKFWJ3ITUtx/cLC4fxXb7CHxzWNx6nhAsnjwJeHbGiNw3x0c6fY0Tn6CV83WS2+XngItH4frT78nhnYFbnD4HByUXCfmlV6fR0UpTfkNUswKBgQDlIhKTh3IHKtmO0k8edbfhH0cXergbebp01Agjh6mnmMnsjI0XYn+iI1QK1ClN32mzlqHXMtGKtc2zKX6yhDMT7kPglVugKQWaQWkd7RHCQDaxN7tpwiDaYjVL6TqrTa24Dy5Lyrem8j/i5Bey5+TH08u31QuyUQGqCNJePnLnXQKBgB1cy9yGUS4BewfXSUfc+QCQ3MzhStEF8sbZFf2/iPpm1pCZzEiU4NR3dd405wbeDkRSdzTdklj9FWb5IDoOF9Ab7rwMWs6gnHx0OfI+RbqaJkjGyQwAs+9Ijxdjt6kSvfwQHzlzwEKpJSD/VecPxt4b+1lyh1dpYD3GxvmXP1BHAoGAWCd5uiS8LCHCPf6PzgpASm58LX5bYsa8g8Int3O0Q/S2izmv9rVAoaKx7NCfa4Ru6FclwOOeVp2HnEx0oD3YYOykVL1h2QavTx+nT4or8O4/nILyqce0WBC8rI34sntaQJwmlaZSbfp5tdNHgt9Q18iWcg2XSG1+FGr8dKHWF0kCgYAM6YV4r4GzAI+TgqI63fiRH3WhtE+RYhW9geUcGpI3PcGATee/R5xGZ9pYtNRHrm4DQ5EqTBiIZNKmBLmpanilhmoeju0+19fojk5tqlft/YrMOxfSNZkLNbm2Yww4vNHTCDvw8XpDlAjS4yyxRSDkucuCdsP6yJF9IyTqQsbrWA==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDKzUGU5PCmN9cfxfi2K6LSf6Ro/LEJmBI4+aAZWRW5c8WJdqNIME7kQ0vLwr4ZjxVizntHjKr8so1CEVlet5YxQg2ezhsvnniTIYgoAo/tuLt8rZqEL6Kg0I9SYFN/zWHbfsJbGqFA20HoUSjPnO6MiSYrXHLzlJwXX/P0twRJGW1a0YjI+A01kI0XrTYfXNBgSPqd1gd/nWqDDMuulF+Y8dcXPRoUHEAQI4q7nMUyTSFX7Qhv9U7fKIQ1cGCEOKjWyB5jt8xK+cfKWJ9Mb1eLvPu/ZBnfjqW2g8LnE1TbZ1LvFmnbQZgPWZ12AfAsFprswMcIHNxoCnz3yxmUoHAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmbF4cqzgjRthALpoDKwj6W1gDLVQVorkrmHVgGdgWkPzJ", - "privKey": "CAASqQkwggSlAgEAAoIBAQCyGullIgkT2WDF2RH2SCfhwqyRU1EN1OtIAUSo1AxlcYgGAPBVWDhO9Scce+nnGvlJY5OAUd7j1/QX/6/1/DPsRyKNjTqtUVrDZ+MUGYH8js/sSlDKeIcgjNXJ71EFPsqiGg0QS2PojpDkNf7jNDtCxEVTgGYC4wWb2r4cx/wcHVdO6uarCRGU9Pz29lNtNuqgLouLf+cLhi7G8kUisR42NWEP6qC/r4nb8GOttCcjrTS7tWgJUD/QBBlmYwJvmsVbJKQ4gtdYTrcn6yxoj0KrN05HMIw2cRfL5bgMaXO7Lm4IhnQDfYjrJVyfj/ZTJ9SICUG1SSgy0x9gwqK5HibNAgMBAAECggEAZ3RiZjBi/XijUclJObmoEOc3viKbTmGDWYwDCd5CZRqRXItnDuvzqUmVsmH3+Boe+5Yvs7XatpZWXypSV5xrvK+FTpvenZZIFoFd0esPKlj6RdLVIwbn1ux3spikg1t58LcZJ4HjQs6tMyJ6MBfC5IGFk39dwgeE1oc1LxqrQth/KzSYEYZgGU0bcgpNdAu3hHH1PBHsMkJ+3/mDhpOQbj3QWIm9wqRTbWWGehw3TnggwYVPC+xLQauFqbw5LkisVfZBlfN0D+fsbeivtBJzq+MZeZWi7BqTvAbPq4gJonxe13CApp6sXyUAup+Z7VOuDRs6wGt/OqUl+W/pmrqBMQKBgQDp1l5LmR+bj+F30FqnwWGB8znsPhf2CZgWy15IwavuzKwCCT8Z7uTISldE79kQWdYUF8gAuLbSNycUmM+b3XIYPo/8jXqWPooQeJ/M7umrJ3H8PINbNO7Q2XcxBjOFkZAj2P8drSqpL6Amqsay30+sd9ETaKf1WtN2JrWYFuQ81wKBgQDC/E2jt58nh7DZRTwMFtS2YutAAvtESU1xnwrGtuODmD7Xyazo8QDWKXb8XepAKxW6K4oWeW/SDXPtjbsxIdeVlYCQc+G/ekF8+oYp8MvDeq/mR13/JmCuW2b/aNNBj2Q7eRo7vWEVYl33M+qdZbYiFiDw3STmTQ/E/wxZPg2A+wKBgQCtrU50D9LuE7t+5f2vQ25Mun52/NeHIjEYHQx2NYKh5tqK2JtJg6nhKXYP+aTbBB6A5fjisE75a4VXQvhP5/XqE+2Vwu8d0G1zNmRaLcjYGoAKvFdD0tjdvedNPjHeLvND7NPvEsLwzjLBBW53RG1Ex+k95Sl6jm8o/i86OyZiGQKBgQCYlgTT96AOuTsF7A4/j6ZKTEK4xxyGpa57GfC+7ORCWOPkzigH6oGzFqPMfloQeSb5l5TqXYHKKUjtP5qbqlYg8uu3H1gsFaol+Y8ARzXN9batSHAgeZHzIAgMG6YmieXwPKbw1RSiPWY3S2NwZOYQ6qxAkW6M4wVSLh0lwU+j/QKBgQCIA0BuYWxcxPoeEiKc9JyFAPKt8ISCV0BVNYm5pYgw5vjEfNO9d1iZFZfB/ECdvRuuWxeiKgy8+l78XPjIqFyDZ7z1gI1XTz75HULbWUgjpv88BikhAY5/qjhri84Gzjfa4tZmEZKxu3K7ii07ZKp4o7k+6rosm4AZpOKi5WpBfA==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyGullIgkT2WDF2RH2SCfhwqyRU1EN1OtIAUSo1AxlcYgGAPBVWDhO9Scce+nnGvlJY5OAUd7j1/QX/6/1/DPsRyKNjTqtUVrDZ+MUGYH8js/sSlDKeIcgjNXJ71EFPsqiGg0QS2PojpDkNf7jNDtCxEVTgGYC4wWb2r4cx/wcHVdO6uarCRGU9Pz29lNtNuqgLouLf+cLhi7G8kUisR42NWEP6qC/r4nb8GOttCcjrTS7tWgJUD/QBBlmYwJvmsVbJKQ4gtdYTrcn6yxoj0KrN05HMIw2cRfL5bgMaXO7Lm4IhnQDfYjrJVyfj/ZTJ9SICUG1SSgy0x9gwqK5HibNAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmdycNxu2hJK68SQovw5Yju9FnuLB3NqY6mk15ATr4nkYa", - "privKey": "CAASpwkwggSjAgEAAoIBAQCp4KItjrRxtJU1AjHPhNvcH77MSh+XhLBMZaOOTfsmJ/9FtkUwMvWCVcfgUhi+CMyNqZRbTovcqr1U9hn1szLUjxhOPJ6DoFVZxZGARypXw3pTzZkLLYTJmOmNnQYG52Go74DmPS5E9x/iPxiD/xZTSsfAOKTgD+TmSRWxm4PXL4953NDtY8TUIdXSXnSgRpnJr99OugJDxwgW4HCSmcYYGo/j+xfjd8hwW2x3Zk663i5FlYpGDms+V39k0TDRFbCf3gcSZ1nOlR8CVMiQNeqZFgOGPfxOqKuOAfFg502ghaRXFQR9ZTO8HMOiN/b2m9alg0DGVZQ+yhktLuQ94JwPAgMBAAECggEAek1mlWQLV12KmppU4DGn1GfqhsvKyNxXzPjT8u0Dpune6AKc92GIzegSOdcBRzewhUEUtVPsb9dg7h0sfW8hZlULS7Bq8xrot/P8mB0kSAFNPa5kw95mnnl/lFv7bdcBwY2FAL4FZNOCWfHRJZ7uJNNO0n41fbcTthPiEXeESNQhWdVczNMycqod21CJLWuyBGv9LVTamWR4fFB2/ixjJNKVAVh0WTBk9LMURwgRaNRzwq+3NTZW4B+brZTLAAVav+xxIaJZWVv6irGwtXCvZTxpTL0vuTS41YH4oAhvWVvl/XG/CokEAh8T3CEzR23FN1A9Bgm31PwbSOeiWjbZuQKBgQDavlxkOb5/SioGpPpmMBdTTt5UO3oiCvVrV0yZuOpAX7+1fSBegksdfa7XOkixochmiIgbtuTRS5YEOmevE+YPEEHstzpQ//7lJOijzCaxLHTLVPYVrLxXktnGb9IWygna24BGkhAVvIt8dfwg8/3nsBoQ3omIVrDV5u5ycbQJIwKBgQDGz53Anqi4jEKPv60NBbj81LYR3RtAvPM4OlbaWLAeERaHR9xC4nWEgwvEDm0BWrQ77cGZATZ7QK6nstrKue8khkxbIKbibWJJWJ9F57yTZhvwq5sNayjJRbIsuKkubdvXaJp5ZDBz8L0XK693Rj4MGXp17ulZ8L9fwPcARt0uJQKBgGmKsb92EREPsqlUDrEhgQ+kHSfdLregO/vXulDtZLE8wZ4KyoRvL1kCXErih1KVscCvHaTpoQvPAYn2uDJEUptwB670VUHh0pWzMkBd70lLHutAih+5IYLLiyHwsBho0Up04DasoPAr8c1SjB1GPHr+gAUlqoxK77W1X9V+QRSrAoGBAIP47c8fgwB+mvCxXD54vgOXcAULsTuYMhvxHhZzKPXMghfrK9t6WGhOVVEgAlwTyfC+MvVOSMwoc8f+gh5wrr6gJ6+WTTGhSs1FdvUAj72I2qM4RwTxTXHOQihNrICVjInBdkl+qGtOMzdeWGvkxOtjPldq8Jwzo9X8UfptEAXBAoGAKaDO1B5bpFsiBlQKZmzeg/6vBa8D/F27YVP+bxoqeY9HlSBDtDxI540vyGwE1tJSql3gPvCZzKPxpXYPeXHweKp100YIYnRokLlXJpofA0dyJLKY3DPGAPTv4cOkgspw7Ckr6vsJL0/MK/kW0WfmSptAggdDwjgeM6mtcVlOsI0=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCp4KItjrRxtJU1AjHPhNvcH77MSh+XhLBMZaOOTfsmJ/9FtkUwMvWCVcfgUhi+CMyNqZRbTovcqr1U9hn1szLUjxhOPJ6DoFVZxZGARypXw3pTzZkLLYTJmOmNnQYG52Go74DmPS5E9x/iPxiD/xZTSsfAOKTgD+TmSRWxm4PXL4953NDtY8TUIdXSXnSgRpnJr99OugJDxwgW4HCSmcYYGo/j+xfjd8hwW2x3Zk663i5FlYpGDms+V39k0TDRFbCf3gcSZ1nOlR8CVMiQNeqZFgOGPfxOqKuOAfFg502ghaRXFQR9ZTO8HMOiN/b2m9alg0DGVZQ+yhktLuQ94JwPAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmSSEcBgyeaT8U6HykT9ErKHUKumaSYPBo7mdqTwcvq55U", - "privKey": "CAASpwkwggSjAgEAAoIBAQCv+kI/GSApNKExW7zcz7lmyTvX22eEiLhvwQv/LuGSknIqlmC8IqoQg+8l/MulZ3rKeD4uuS+VDDkNPcDDONHIjtlu1qKIYwec5SUiNhPTVpNiCeRkyG90XikwPEXak3OOyqPO17TtvK8n4/Pk63ehBix5c2GNoK0m4b7ThQzgUoOzwQ+AYRq4U41ma3pircE8JKYbywGmJFJdswvVjg9eGUgADfS9JhM941ZgQVvqCnX/yrxjZTMw5HVS+MBh0ZFHa6BK3rTF4yrQp7n2u5Tj2efs1Ci07DJ5hAuHZOn3JnX+UNLn0HMm/Bg/hvWOrc8zYzcZv9eVLfPCGVaUDUP1AgMBAAECggEAJrQ2Ccau6iEnKsHwgeg18MNlpA4fcGjZl8qvpspa1m/bKD62u+or2UILQSGecJyXxxw3IPOd4Xw0uBLS6J0AlsnETLpsOO7+56UGS8X1ClBKTg+66eeji8aB7Jf1DSPNEKTE7mNG6drL80wRglG/l+zRr0yPMiUasCiKXd8ve86Mu4iBLUdStBSHQAIMWBqTOP2oo10oW8Z54dGMQ9kSXcbtUFsD/5FJATQ1AlMPluHqyDf/fwqxAN2DSx2kZ8zIVFVcpK3250wWSY27Mplvq8VTdp4CjVpqWXxb710Oh969FPgYvNMHVHfuRfBuBiRsBlJKaVWtFWrNtWO9vngSAQKBgQDbE4OzCWppyqQ5DzUOWnI9COdjIHfqb1ynom2X2knr3Op7+YNpI0yUDygOr6OunKycU6uR3oWrJRvO6hdKWYYlFWu5BhSqHCSPCjJl/2OERRAkqmUxgu3dOM/WzJjcNLXB0aZ4LNxyS5x8ltSlpLR8PA1ZIliSFhaErHqTyqAywQKBgQDNoyp8po2Mmd5xNbF154BVJHuARdnu87ypV0otBW/ce6dxSDa1nKsht9I5C7nh1rwzN+srGrunhokHcSP3BOYmYG5bPn2/+bhmUo2KPIcXBzPFZEtaT2xYzn3DNZx1G/dY2ukgEZfS1KM6FcTj635P07zdcLTFWks0HbqNwA1CNQKBgHCU67ZDHXN2VsSX4w0YP+LLw5U2Z0mLpxLiru09mYVjRwEk7XpHUKA51b0OV9Bw5WeEvAO/VfPoowzHUea8cOp3wp8X1+C/i64ScGnoP60GjNA63Lv/69smyfA5vkhTsiADbEgPzc3Su31vSaJCLRo3BikLNHcGcNYHiQqQM5lBAoGBAJlNnS0Ulc5OH9FScBwwHDJdYlz8tj44I1wzoS7zMLO0093WMkMuqz4V5nl0zn0ZM3ETrRSTd3arC5kqtd9AHbxag6suaV0ndFuEC9UUzrlSOzxbSvnm4CVMu+E+JIgB82KgwM+Rjhg1QgLZm9E3DRHCDrkffwTqDcqqpxtqI/hJAoGAeS3nCY38KdaHbGTT7TmSe6HSt/kfb1NIygKTYEWNDc3kR9jubfk81/iwtAAvLfE4FY82TwC+78wyfpRa8Kt8m0GZLmyf0/8ACQd1RSig5P5ORKN9Er3nUXtvnr6tfkMELuPBWC9Rs0Vky8JIPJCfXEsQEOOGcuZDLsWF9Oe7Vwo=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCv+kI/GSApNKExW7zcz7lmyTvX22eEiLhvwQv/LuGSknIqlmC8IqoQg+8l/MulZ3rKeD4uuS+VDDkNPcDDONHIjtlu1qKIYwec5SUiNhPTVpNiCeRkyG90XikwPEXak3OOyqPO17TtvK8n4/Pk63ehBix5c2GNoK0m4b7ThQzgUoOzwQ+AYRq4U41ma3pircE8JKYbywGmJFJdswvVjg9eGUgADfS9JhM941ZgQVvqCnX/yrxjZTMw5HVS+MBh0ZFHa6BK3rTF4yrQp7n2u5Tj2efs1Ci07DJ5hAuHZOn3JnX+UNLn0HMm/Bg/hvWOrc8zYzcZv9eVLfPCGVaUDUP1AgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmU8D5FgAjyxrSaobQT2FhMhx8Gxx117K8U8cPNYX3FRNX", - "privKey": "CAASqAkwggSkAgEAAoIBAQDIgr95M7CC0YOSJKhNn0g3V7GXwdBkd8HoGr+cFWKps4ghZeR+VYUjK5Fu93TuBEdeNac5KOAGfHLwN8uSdT4eN+F1zcb75/E+1RC2X2z47PaRMg8vXQ5AmTkXr6EX3E/IKxR/QaURuK1obTBiMAVzhJ4FVuYmk3VVpqKWO6U18I6pT2rfARtlEJYG6GHvqnSyyju/9TKL79VjD2wJrmNCS9l+yXSKAL46AsBHmyTzlLn1/cbRTFmG08Nm7PSokCpOV3ZTdVSPrJLpha39yMW/Ex5ICC6aSCB08lhCSUA4DoMOcYulOWO0q4rB33UokBzjAZAsZMlLhWSoBd99sSLvAgMBAAECggEAKrReH2w43cPNp+SSy+VutgrBUjb/MUaoT8zSnmWXm9kW1zYiUh3Yu0LeOKoPh1n18USwFuZzwC3lNPBNNSYvUrRIGpT3GlOt99ndM1pjlSiy4v2sakQBcxSvKjJHtxM/ErzKIshSZdHVbPZEZcUghBfsp+p4HiMtzE4vNpwBddkjQBxxAzLuua81W4fXdVcuqpH/1z1vCuzxgX8MpEjepGR0RIBPA1UsV8yb4I0w2xD3XO4nS++p0tZD0Z8SyBYcVRx0boUQAhghtLXhs4qv4VDkk1uvfU4A63NyFd4OQMwd3eGOnqxYQq+M9vRNIP+CenVfO7WiQEY1SOl5PtZCwQKBgQD4PCycyQ/5b82WnoAF6JOClgkAZ9wJXVx3Uz7QPQyoPoF33D1SowkMjEL44s8IsxSLDcU8Dnrqji9qjcbDgepK/6AN8vgzWwnitoX3+X56z5uGaY9EZphWk4VEnYQZwQZmQCrnLFaIxnqzLTxa+gW6VFvahvaKu8to4th6aVo0XwKBgQDOyGe1eqIyDMaXecXORNATt5M5x+FKMEzIy/sL6P7ZCKYqJ7DJP+qtlteqGcOccvDlxCKaM8eIrQDPHIRFyAexsKQ7UG577zHle2EE90LbYxKvOwPP4hRm2cDBc/suCtnrGjeYCHCTRcoj8TKsCkmdd97wNOmkPoFa+YtoFOYbcQKBgQDhNaa77+ZgRUDeP5qiwajitsAf8Bo/HMbBM3MvddO/6EWJuvSfvm59RduU9iEjIWWn6qxgmjqGBs2Z/FqyEXHA7T4GqcLoxNWpLDNLEL3hKe1N+wMR6YqYMWqdH9Mzkl398oV6Ck3P9VJosMerOl5r+BEFp6CRqWMYG4aPOHmwPQKBgGo9LoNf8UszoyiaCNXUJu+qZnrOReJ+9ERKAL56w8ywE+ceo0aSjzkGgeFEAWs05q212m1NYxvGft7p8M+FWOajMY3D4i/Mkd8sR4lsnC3pNeVPtcKtjfvVrqH1u7xJGPMgciWrWGNh/NwAhR883duIhcL1/IBFGOKryUL9UcgRAoGBAOG2J2RTW0NyhldTOciRpRuMTNTpr4LTGE9S//5S40i7YNv4ZFzUCabPXCyRRYyXUbhegeWmlNgfqRtNF+QyH98Y0ZezGJzQ1S0uQSnUeoqGcA1ohczWuOXESe1MgVS0Gsoo6Ytb4muYzlFZ97Y9gBM0KhaQ3GzMzkvNnX+3hqZv", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIgr95M7CC0YOSJKhNn0g3V7GXwdBkd8HoGr+cFWKps4ghZeR+VYUjK5Fu93TuBEdeNac5KOAGfHLwN8uSdT4eN+F1zcb75/E+1RC2X2z47PaRMg8vXQ5AmTkXr6EX3E/IKxR/QaURuK1obTBiMAVzhJ4FVuYmk3VVpqKWO6U18I6pT2rfARtlEJYG6GHvqnSyyju/9TKL79VjD2wJrmNCS9l+yXSKAL46AsBHmyTzlLn1/cbRTFmG08Nm7PSokCpOV3ZTdVSPrJLpha39yMW/Ex5ICC6aSCB08lhCSUA4DoMOcYulOWO0q4rB33UokBzjAZAsZMlLhWSoBd99sSLvAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmbdrCRXAWsGxXTzaTkyL7o84PXkLQ8iyjWAJqXQrXoBBB", - "privKey": "CAASqAkwggSkAgEAAoIBAQCgS5u787XHG8wm+wPq1JFbNR+VBdQz2VTygxOZ5DMJn2ZWLbXEES94yzO4UaVA2/umXtTiiM97VPFBgYC3sV8rGg302qiNApY8z+OlGasXuoZBW6aWlFU9F9j/8jIEKEGtgqHIYNLDPjjFVa8308TCDC6otJmzCRuCJnDvYRkRaVlha1u3OSwxcKBVG4EGBpK1uOw4IpfP4zBkZAw4Ig5YzVA/QciJJ8TNRB2BwIv3f3NHWN1wV4v0MJIdj1JW5kLaU0WeQxxAgI6DJ9KG5h1Lxi3tqgudLAGF1mpU2VXBu6Mrnpqm/MPKHuoHrA259MSSIQZJ+75cDo9YpzTOvTQRAgMBAAECggEBAJSH2uObHQpFeLN6DxQvKg2AuSYGQ65TqQIacTQ9HwnAmTwrmOz4G6vrZp5ZkS37aUCtSMgsi0011WOkk1gjVBMFTn9fiaU4C2yIGeGnWkFfhf3T5hZLlnxIt7vaeXwerVUQ4cZh6YofAs3f6r9pTD2eujF7P5yFSOcdpbI6n9bf9plHJlVNwlOWvFrF0uO5lD4fPs1D6XWlKFuz2G6LOAvexZfbGmac3fpcrgY60rlXXOAueC35puyuha56RQtwKXHMC3/DcKExlRNXZhQByV9GrZ5WVWgSkouE1vbmFYNiLBaPWH4fFXgbkCvxlxiZKiVkx+vIRg70c8ZjAxFJDMECgYEA1TQr89ewSszz57am3oVaia+cB2vuy1QSDzxzyeGjbIpf+hbuxaZ7uHKngHV7QhJoC4OZ/+crfM8HvjSXObK6ZVWiNm+a59J81CGhV8XqzxO6WRNiGKSuNfxYjFv6BOqMi8lT/nIG5Mg+IjuZ6HVYcK5lLKS3vRlfR7uofE0hXVkCgYEAwHimE/oTXTLbWDCA/w207RJSyCvyroZ8+UXeSq7fKHkSFYAaPjq+NN520sR+E7g8uZqP4ZF6Pv/FAV3hVpuB5+wtk/2uPFO702rre7Rw3B2Hq0QM48njXfcTAiFfC94LRhKdcSnr7aOKnwAyl8EP6FSqXNLoMKnvV0iFDX/tHXkCgYBbwl6ATf4z003OFlBvSNmUlJ4Em7FklURIhm4XHyOk3VE9Y41UR7jLw5zPrsBjyWQ6QGORPb77smbUt/G2BXQvlNGBuDrlNzQ+YFL+YdITWZxEJhF8JbRMy9SYZCWQ5BmlN/sMcasB4CTNuvUclRSBOq2UrzfdDQRy7RMwnEmV0QKBgQC+CuLBOt0/2uVlkI7uR9RrePowF+TJmpVvdCNnTn+d8N2ASTqgU1RX04kz1zw9sF6VTR3gNcqkxdr53H6RC38bRsJCK+uMOYlt2VamkKYXUTkSTGEF0eQkdb9ZDSZSC27KQ7sdb606uY44LPPHj6NrXZ3RhZYp5sEiR8LIb5Xq0QKBgGU22UnE2iIgRKsWZ1BYQebnp2wmI7QZiU5ZmVWNfuM3kyjaA7IjLPK3zI/wi9NV8GnnpVWVOGoFeQIg4mAMfHSkCHEPG+8Ib0cAu2yHEv+i8yn1xwZqSuUx1aEjlczGCFIUCy4X30DIdwtpsVlUlAdE1p1br68firIz6W3OKyn+", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCgS5u787XHG8wm+wPq1JFbNR+VBdQz2VTygxOZ5DMJn2ZWLbXEES94yzO4UaVA2/umXtTiiM97VPFBgYC3sV8rGg302qiNApY8z+OlGasXuoZBW6aWlFU9F9j/8jIEKEGtgqHIYNLDPjjFVa8308TCDC6otJmzCRuCJnDvYRkRaVlha1u3OSwxcKBVG4EGBpK1uOw4IpfP4zBkZAw4Ig5YzVA/QciJJ8TNRB2BwIv3f3NHWN1wV4v0MJIdj1JW5kLaU0WeQxxAgI6DJ9KG5h1Lxi3tqgudLAGF1mpU2VXBu6Mrnpqm/MPKHuoHrA259MSSIQZJ+75cDo9YpzTOvTQRAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmWWYbfs1jFnZuKP6ky3Zf9A46W8R8tYA9eTqRdPobWFHh", - "privKey": "CAASpwkwggSjAgEAAoIBAQC7z4AoGK8K+kzoHhz5McSmP5IbsItENCi14CECnItsPXZlrUCywWMcXpIjh/FyMSq1P8d+EQryuYxnhdP3Kc5cYVshK7aaYsf+Pfy1sVzCfp59snGLKcKzetWLIDZsMYpnYp9yv2+ArxqGCYG7OikYgc2729ub4PkZpsBcQmsYy0lGyrCEId+dre0yGIHA4tYBzGuGfIGGfhLulyNbmDHdVkiAPyP6+VQvklvuzYim4KdDPIiklxbGIgQLM2WdrOHtS7PA9j5BzThzyjzUhe4JEsyS22oddbIzFai0rmYX06SDQcSY0PYWmpzOrCwmptNN/4XNK4OiR83Ttl6a7RtzAgMBAAECggEBALp71TL7H4P0+TxZ+kbtxeeVo8xexkoYyHufauee7Umy1ccr+twD7heTR+SD7ZiHfXKvO7TP02EkIGgCmHAJUOClwsjzEMPHZfHrNuxqikKNW25QKzIVa0CvrS4R9DgGEPmLEevsbhkGxX1mHyz7GSc+bDwmmK70+iMgUkzJnnHkZMsublJRd/Jc06VpogkI6SzR5pIHwc/t6B6U+WuZ+ohiJsEWiXrTEvD6RcNt/zHZAwmkatj/xkWcRRNhvS/Irb+ZkxgivXsk5XeNNK1BitK9Vnii+tVfoTTervKgMx18RN0hL9Fx4I01MwZNEnCpKxO+Xwg5oOwHY/TihgTesJECgYEA4y2YC8Eg0LIl+pnrNWUvja6RtJDSFIKGWOIAB1v+zBzszlDdFISLAeyZfZgwLzdOZZjTNuJp2V+pUsU/qTLqccIeI5u66cgRB2oI8O+g7XH472P7Ay/n0dFVj42fS6yZam8o+HRcK8av+f9F7YG0Fvs/ilrKg2Fcd4/IgenxXJ0CgYEA06NOqdNk2L3s3rgRkuBLoQ2gBmca0gKLcJOBfv2shiJ9y7dIBefSZxyMT6938U3NGvvzA8QB6/QZvz2BrgM9wS8bVIv25VtQ6tSatARXiTVfVVfAuEgErDEQWCdyqFerjDz4gu2yRUS6sg9rr7lJ1cXEGhsc2h5wzSQwGVMDc08CgYAIkorfPq1nUqGeQDqg7C2MMh8rah+TSI2bQwPvQyhtOVYyPtjo0kuQigYMuDZxQawCp26o7ohB/JseFXVehB5WppWOkGzQL4188yJdPR2ceCWFmwc4ypD72ONapGRzbZLockNghLuJp1iynVBdMvzBtT9jkCN+K6lalaFiTZqe/QKBgETOdFW8V64r2WXznCsPZyc+YceTH+IlV6ZLHq/l04BsmE9yECVzYDGL04ZYuvsl20gpn7GauTE4VGKboZysixhSs2UCeEvLK3ydkIp0Wu1N/+ekNxDywSombXTrplha4Hggnn8avnnMxZH8d3tTF1E8EeyW4gN8IBph6I1jMtz7AoGANXAUXM+PBA9xe7QUs4ZegR8reObEW7sUJ4rOmqSoTB5HIFHU6L37QJiAaHAF9S1Ncvgo7v+/GHRvA+O/FYy731kW/SJfHkkmPtfUl3u+gtdIdiyYswV1gxK/8PRB2dlbkrqITIBfHGqnSVVnIVqhy/wc7YIXu4H2mrpvrd389nM=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC7z4AoGK8K+kzoHhz5McSmP5IbsItENCi14CECnItsPXZlrUCywWMcXpIjh/FyMSq1P8d+EQryuYxnhdP3Kc5cYVshK7aaYsf+Pfy1sVzCfp59snGLKcKzetWLIDZsMYpnYp9yv2+ArxqGCYG7OikYgc2729ub4PkZpsBcQmsYy0lGyrCEId+dre0yGIHA4tYBzGuGfIGGfhLulyNbmDHdVkiAPyP6+VQvklvuzYim4KdDPIiklxbGIgQLM2WdrOHtS7PA9j5BzThzyjzUhe4JEsyS22oddbIzFai0rmYX06SDQcSY0PYWmpzOrCwmptNN/4XNK4OiR83Ttl6a7RtzAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmeQvuGyVN9VWyVmKZt1U6eiy578rF6kiU3W24nVXi7Z1d", - "privKey": "CAASqAkwggSkAgEAAoIBAQChA9xZ7KGxG0ti+VUNB1KH0Z8F37F6zlRqdwtrD/k5RRz91T+DEwifasCLgs8pdpl/I9CwlASXwPcbgVNj4GHlptxzdF6zeSuP3QDlhB/jyTpRa2lw2S2zYhBaTGsVeMlmGRV8LFpjhnCdW2kO0+oGEofF6Tcj3g9J+ul96aNi89FBHYYSERS+MCHRYvCuiHOHXtDI0yYfXR19rAcvzFOIHX0EoeNfKd1zwDv6p5kJnPR3n0DtGsNw8A0tW8w6l8fQd6Y97EmqPRzBQQ1Gag4zGfW/eBu59lNonghfjWJpBRH7TXlvy0w4WILbnEaUOFoeN1+fYQVMMfZTVA/+ePgbAgMBAAECggEAP8XMr50miYQa/q9sPUXKLVscFfJ8U/yGuMg/sH7aIhG6otqkViDiyGk6q8b6kByWPSINVPK7QvO9q5o0UhmcDJ5jMCNGIuV6GHfbFAyZqNmZjIfzcivCiwrrGSitPQrjEdobhVv3zPWBgwGiganzRcZvGjb9jOo1ugJ0GlfAS79PLIbzEflQj1P2sFULk47IXT+jwv+RqyUH8SN91wWblCamC7O+fIz/4jT5ff8iwtbkflGw2Gbf0iBY1CRqEHAx2zTFch8WeJb8KkVbkONy2jP0zCVPIpH16gw4FcKvQZZAMdZLI98DZ4MCH5J4JyJA0M7HyW5KXh5xNCaHjL8GIQKBgQDMTUam/4YhaUx17p3C8Q+zOO9TzmUzQQuoMvaG3Xs9Ekp5ygAbcHCNQUZ/thIHBS7kKDSRq62Tm1PXcnboAoQ98dSxpy1CSda4v3gAC4TF66+Tx94KFjLXZelgBIHXOzcqB7YuDXXHTmUz53V+8lVZ/rIhPVq5ZbZeWYWIUWEEkQKBgQDJwnTuY7yVQc9nZd/loTPLKibFqw2YgGGVG0GjIYhXWQiS31PL7wtnde+p9B69bBkEMnBiK20WBjsBgOP7asHJIiidb3BjuNUKQYqBatZBJt8OPmS/6SAZGY5DAlgntpoAhyX8HixNHeHQ5VUMHwmStVXWvHZvSGYy47oyUFTX6wKBgQC/IMArNTvHgBoe7ie7GwgkE+yaC6nTZFPCfELz8rn7bWQtQdQN14gELgAFNFDzLl8q5Y4ghWqyf4rVMOmardgHl3jy5kJKFIgDeGSMLjp9artsVnwcFZ5kspu8zxqlP2mhMWu287KuzWGSSEQ8iftdYRBGVn7MmSIebEOnPvKzcQKBgQCuOyolT6XkMv+7p+Mw9wO2N8Fhw/SqtHsQe4g0KtoFrFJWG1vO6bCseNEtsC33oGj+EdyxOhUrBthf1QGL9UZBvijaxAiHZW88Oxsz5aH+g2Xuc/0nKVfZtRMAVP7x1KOrPwqTbS8OrXZ7of/OxuLKeaQWG4wfT6NJ4RTDLFIIXwKBgFYydorZxccoBmtQzWhQA/I/Xgm/JO+k1DG7XzHLvOztwCwazRoBiVQ+ecIc1c/tsDQD3DuZZle+oFWYlfR3yrpBO9v2JaOeD8wMb5BBg067VAZ7CcYdbTccl3ncObaawWttJC4Njx5GElrUZa+aJwzL5LnGq2offNDw4BNGyUDo", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQChA9xZ7KGxG0ti+VUNB1KH0Z8F37F6zlRqdwtrD/k5RRz91T+DEwifasCLgs8pdpl/I9CwlASXwPcbgVNj4GHlptxzdF6zeSuP3QDlhB/jyTpRa2lw2S2zYhBaTGsVeMlmGRV8LFpjhnCdW2kO0+oGEofF6Tcj3g9J+ul96aNi89FBHYYSERS+MCHRYvCuiHOHXtDI0yYfXR19rAcvzFOIHX0EoeNfKd1zwDv6p5kJnPR3n0DtGsNw8A0tW8w6l8fQd6Y97EmqPRzBQQ1Gag4zGfW/eBu59lNonghfjWJpBRH7TXlvy0w4WILbnEaUOFoeN1+fYQVMMfZTVA/+ePgbAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmV7Hr7Yi74neZktqNghgrGYNV9X8zys1YTm31npSdVCrv", - "privKey": "CAASpwkwggSjAgEAAoIBAQDCF5axKAn+m/82xRDrewOR/LWE9THASE9CDihLm8FnCRAQw091KwzDOsMC3I/+lCHwxNNDLLFpgpSFJ1JEftdpMj4h7JnUn+Rx09PW9JUtii8PZmbpJD0w56eu1kachHrVMuZvYNA6ge+ukMMQpL3Za9udHk7a+CnCITG9N0F3WGk9p7GPpPlSazR/DgJiaqh4W11k+UAYejcs5Bj3QU1ndkGWhfd0bBxepK+otk9qXWSwaaOKxFCVrSBnv+hxTkaenWJy6oAE0GoQNQdsRxS2yHlgZEMLjmpD2hnUn4GfX6BLwn9/LY5fBaT56SIgu2JxThgro5mxkB4T79x//S+3AgMBAAECggEBAJgQhx3RMtNqQPAWQYVc4ZU1GrpKqGnvvTkRgnyKUWJ6dT3M56nyypMCrNrHF4HraRQMAUD1+SGjDt2rywajIf3nQUqu5m7xvrd3sNcO1PnS87/rCOHMZKy2MmgGtVfXa60xrdzBSyMrvi9Ud5/Ikn2PxYY5wqpIF99ixmdqrT3khjhzb4BRL6zW0xvKYOuuv7LULNC4261IlrBn6QUpaSKopCVGDiajZYpKYLJAgYyRXlvBEQnKACMRwt6uJ/kH6b6A8mlMV20OWrjHSce2F1MU02ZGSj8IblRK0FJvtVWhdOgOaM8WodtwIx7fgag45xTC4d6H3PnwRHJfG9GinnECgYEA8oT0TWcV6OdWmJOsqV4IdbcWatM1U2uQnZ2A5t+3MBsAZDS+725VgtSrWzAB8saVmDCOanMNq2Gtq5wdS9aVlR/SATQwRmVeGFG2ae7qtS2CpBwwIu2bD3bcR8AEVtR9uCJQyAeBQj+Hy75+wj/KR5mKquMTMXVkovAwn/27B70CgYEAzOGEHcU3+4SLnrVbXHoZfRrk9oiaZBWQELchk16D1LSQjf0fnJOjDGIO5L83qx7f9xzAdyGmYc9YbD9j1mTGnqgsSfhdRDxPcynExbhQOQAQNB7U7KCQlFzs4sbRWpHPrRpNAcxLZiDlyhSLEZLFnMOXUct2UGTeGYjmmmhHwoMCgYAIyeask2rI2PFbcCaWsLCvy2XFk0fgcQp5m8abF0plNOVLvFmbBa2Voy1ejZvUd3veWwweMXMyXcTUbkDlia48DD4pCwIg2vWQ/g0VQ7I/xJlyZw8bhO7UnaMX+o5tsx+nN58j0JnPk8vRB2NCmNs0wwyyaq48YZu3B+tLMP/BJQKBgALHeFxTBYxi4uX3PdMGUPwydjKl7bo31Kl1Yn42RQGIpYFXkqs0EX0kg2E0+tNWauFWQYIcMb6X6nIldfw9h7g1PcyPEuzPCKDeSy4Hbwcm6hFa7bZ8AxoQHKKC4eohmjiV57+Dfu5WuedA2hYV8JpMyOuyH9u9Uon0InSrv3VzAoGAT7iFhk5+t0Tcn4vmFjNKBTSBBdwGet3mHVbjVpit3IMT6dShxhpOxharIGypdWKLdptRwyIfNWoZ8P12DqujOsJ7mKn+uoAnD3Pcor6Ph5ZVbnVhaUU7nnxZ22gZZPbpZyWQQgbDHxq1DTGGpVW59q0CZ13+CE8QFKeRscOHW/E=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCF5axKAn+m/82xRDrewOR/LWE9THASE9CDihLm8FnCRAQw091KwzDOsMC3I/+lCHwxNNDLLFpgpSFJ1JEftdpMj4h7JnUn+Rx09PW9JUtii8PZmbpJD0w56eu1kachHrVMuZvYNA6ge+ukMMQpL3Za9udHk7a+CnCITG9N0F3WGk9p7GPpPlSazR/DgJiaqh4W11k+UAYejcs5Bj3QU1ndkGWhfd0bBxepK+otk9qXWSwaaOKxFCVrSBnv+hxTkaenWJy6oAE0GoQNQdsRxS2yHlgZEMLjmpD2hnUn4GfX6BLwn9/LY5fBaT56SIgu2JxThgro5mxkB4T79x//S+3AgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmX3mkJEm2E5qnMS3zfSUGLLvJrTrWP3cB2x47CzV7x1H2", - "privKey": "CAASpwkwggSjAgEAAoIBAQDOd2RrB4TRtSdUarzog++Ma3tnWkq/kCiMGbZ175EnEkVImcCtKuVjoS+2qXDYoMR85yj1Q7tR/Me5iFINqK/ZZRBcmvokwWPLlNUCsE6Ig7YJCmUvuRaRGKYmJ6mJLVd98yhqeU7Pu7A0kXp07orpVmje+KbA7WCeZqsOQXHgTweqirqoBMZf6kV5p/Ya3mngpqmzCPhZTQSI7Fx1f/XkAh24CXeU2lnNlzpXLHVzGT68HQ9v0QCRh+bneB7tBEkwKLE5g2VjALjaDj65mElGkn+8ZyttztzNb8/ujrWkNBtKR7u0wWIgcgfs7fBf7c8i9HFs+9nZraZEdoLPuYGfAgMBAAECggEAMhwkER34DHWtH/3v73bmEuybPNBbR/cTAD3VXPZSAmuayS4X52970Rxz2h9xtgH+7lmkRTK1Kgbx6oO9dnc0hszSlcc/YuBU+jobINXtmZBuA++z80s2wOx8ltIVgaexjm4PpxfeGujwsTGFyQ+EQ3GnbkZnInf6dTdx2Lnli4zy1nom/7MrLxqXhGYf5iRdNMiPwx5dxSUsAWBhRvMMhJAyX+Mq0ft2FLxTk1eotHNXWtTwjPHEsHDjEEZT758uMsxqXYWKM/rrabrBhB1MM2nVww++G0Q6zdiE82kG5XaPR9unYqyjVMFO0srY6DgYXxyNVANS4rDEAjhPzI8qoQKBgQD5Tx0M0cJdtXYmGtqLaVKly9Gpe63lMVjwCOFTudLCi0QnfQ1bB+oEb+SKOegrGD3yRRF77a8iRtkqQc4ujoUTjyBjzJ7XH5+zSjqPrLFa/z5caLV2Agycj36RWCJV2L9XsMg9xk+HXxYiKEA4ft1NkrQqXMgXRSBl/KgdTyk+1wKBgQDUAe1AwBxQnb4VR3iX+rUjsl6JQBvwC60d4/vtSV4nFr4sDJuQo169uE3FI57OMyJwlBM3B+e1YEGPNafPojRv5EP+mGG+XIhmFiY6k4D7l+37p8/ymKAAJvhgrU9mKQniXZx5a29Hf/j8t63yzN2jRzs3gcE/uzdsddTdvyfieQKBgEkcZUWEIf6/H1XPXDWz/lO2sNaF+ZoT3aQOxp16Cg+ZLbRy3L7MVFlWwuuyTZ6NrmTk0lrIeiqQIlFdGOzYSLhSqcn6kL4/fOLkKsZFe4FXBt+sqUJhGXe0MQbIlNEeDgbWRfKvvFTTkrcTnLm0oouEMSeXK+p/ECA4dsiZlVvjAoGAMBIvxZrJ0M2zqAeIpI1IPUvYe655pzg+jKSBHxCftKVHgZ1qOKWSedosaCLng0G88WHh6Xx1YX7t3pb/8eiJk0Vi1XufzhYVJ3CmQmnnuSR95a3rTMqmnOI5N1KUyklL4HPxYualWMT/o+3SF1e0ea1RFAjr1JOSwZkGJzGMzaECgYEAhmHEv1HWQus7MFSiDPCEGekDbAbrqQOVHznLM7oU9PoVT8PPYm4qajoe9+kaGdR1wDgUcOIP2FstUAGrUCil/5EJbg+r3T1sH1TdIVEOalFNmoddYGYK00xdIyuHkZ5ADQP4oahM5XbpWm/2n3VNcTgY7qHoKqU7psVwLQklSqM=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDOd2RrB4TRtSdUarzog++Ma3tnWkq/kCiMGbZ175EnEkVImcCtKuVjoS+2qXDYoMR85yj1Q7tR/Me5iFINqK/ZZRBcmvokwWPLlNUCsE6Ig7YJCmUvuRaRGKYmJ6mJLVd98yhqeU7Pu7A0kXp07orpVmje+KbA7WCeZqsOQXHgTweqirqoBMZf6kV5p/Ya3mngpqmzCPhZTQSI7Fx1f/XkAh24CXeU2lnNlzpXLHVzGT68HQ9v0QCRh+bneB7tBEkwKLE5g2VjALjaDj65mElGkn+8ZyttztzNb8/ujrWkNBtKR7u0wWIgcgfs7fBf7c8i9HFs+9nZraZEdoLPuYGfAgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmRVSpRP7xqAD6DygdDs41Tou7kC4ZMcbXZusRsBmJPVVH", - "privKey": "CAASqAkwggSkAgEAAoIBAQC1pz29v/97ld9dVd/jUrCM0ZIGABwqG1UvpPXffF1zWlHBHyoheUXi1QD+QDNFV06ajEMP1j0jGeo5bYn2ggZf5duxeT5x5cRO1yWF/j54uprOlbg1/8+TRgRZrKFDmXY2p3ovxwg/dqBFZ+H8EDTWA3Wvq5VFPMzHlm0HCernyxnERxtC6vqC601wIsU2TTEGqHzEPi4yvIgfdu3Hse92j99nBUo6jftjMxv50IMLZM091DPfNlQCBRKwrxmArm2o591B/+Z7xsaNreQKIw5rDBnwT/QXzZuKf8hkcnXYek1PIiY4FngtHuWj0zjNDaNtClI/6kR4pCYgBFoIaoz1AgMBAAECggEBAIVQTMaSPsyTTE8yc9JgYEOoljMjJ4hbcOQ7e1rd6bN7qJ5D4eaZGwoC6uytbzNHhN91as4Xm9zD6xrkYijwef8tMVOJOKPcTXrS+K3izjRKNszAImY27D8YVp79S4jR+mjX9ptTxaDVzX/CYp5bwnsCJP+cvDsJCPy9UBynUad0MQVQnYCS6/gl8xSlxh7oaZ3oZzEMFZWzXmgc+w5agaNL+XEzWhLi5PD+DxZoCMnRUg6+0AiHJcOFnDTyRRi4bymerDJNVScOG7Jx6QLJQ9LwP8iNMa5zWoSPSR2E9m+Z1JioxXtAjIKfz4jxhbMIBibXy7euBOu+WP5k5nkwLu0CgYEA4a30MyabKClXN4jaflef7Q6csqSQFJt7oaxC6DzFT21bxvbJB5gyuwJXUX6MnIne7zl56j43usMDyij4Gsmm2Ukug7fdUnbNfzfXZHVIqCDRI12LKM0gdhpbnnchtKg5Is8A+vdz5a9NOtVFhY3KpZbFvLgf6V1jf9Klhtfl+B8CgYEAzg8LZLzcGcM3UTA+t/4ExW2jPNVi1dn+Wff6aL+X8sQMn+cD3xcQlPs394eBqvqh2/57ifdlHCKnrIGnuscTCM4VTbfhOwIGDd1PagxE3SRcG7yShsVO79GhcWkHFyUmditCVUue5UXJWj2Er4YQZ6EVwicC7SYKO6T8ZS6ZKGsCgYEAkcaI0CWm4Zlaoh+/aw702e6vX2GXRAhvIq6gBV2D4lt0hh/RGRvB4TSQ7K4+67rPC13oF1wbKYNgxkwSf1M0eHSiHCk/SE4/TWbnthdgWGHiVeLNygw+ZKt/9OtlFUn4pjhqnLIM5heHXnJ21t8RQEcU8WNKEbbmV6HclC6PeOcCgYATbYGyfsf1ud0mT3kqWc3TW3Hvk2LdLM95ZhL6+011OxzBmsNXrlIG6eSt9t235CeMmWLGcEfdLjtG3XaV+p0F0IBbsoGO0bMGbZ5GLl/zxbDVgKMEB+hYXhhtm+xqNzt4Gr4HUrjpfvnsAy7Wabp0OtDVXF4/Q73lP7n4RDt2fwKBgEHE+gsQoYBS8CY5UJHmHbft4oQ2YbGEe9DwKwXlB0y65154okg8Qk/fldwG4W/4PujOvO+rqHZ+zw8i25TV9uvYToDeobFj84y+MInWS65Gx7Ky1NQP6gGO4+2CxzWsvF0GSbdwqW4iqtwYgj8StFsxjGCY5Rkrx8pjbpXbImmC", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC1pz29v/97ld9dVd/jUrCM0ZIGABwqG1UvpPXffF1zWlHBHyoheUXi1QD+QDNFV06ajEMP1j0jGeo5bYn2ggZf5duxeT5x5cRO1yWF/j54uprOlbg1/8+TRgRZrKFDmXY2p3ovxwg/dqBFZ+H8EDTWA3Wvq5VFPMzHlm0HCernyxnERxtC6vqC601wIsU2TTEGqHzEPi4yvIgfdu3Hse92j99nBUo6jftjMxv50IMLZM091DPfNlQCBRKwrxmArm2o591B/+Z7xsaNreQKIw5rDBnwT/QXzZuKf8hkcnXYek1PIiY4FngtHuWj0zjNDaNtClI/6kR4pCYgBFoIaoz1AgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - }, - { - "id": { - "id": "QmVG3c9ejpUUoAVg5T3TjcS1tXGWwmG6dmbgkSLxvp3jgt", - "privKey": "CAASpwkwggSjAgEAAoIBAQDN3x153zheqZd1EMVM/ty8j24QeAIdqvntIFMTrhVgqtGV6NiCLTytiXo/qOFa4eN0V0AXpK6xXS46NAFkOej4rDG3b1d9eUKADndja98ywtbMdKjddtuVWvIa/0M5TgBvtv0JSAi3qyOUM2GmBkRn9L4GsBvj440I82gASkNSsxKUa7LCxAz3mK9pCZ5QVWZGRIMjQzNpB3uR12vwXKouBf/Rnx9GIg7G2a7f6GTWWlNr+BLynURMaWnOu6mtVnCmPGAXysLZgodzw1aLdF6im57a/pd9zw0jcI5d/ffSHii51/337+ffdizSwTnAJnCGN4Y7Vpq5l4SJXFerT6V/AgMBAAECggEBAIeRtqJryX4k5fUULykt6ARP22YC8TnCPsTVdX/PMoqu0keKxxCqY3vPvW4wcv5bJGKXlkA7lUJ9HxT67DOpIu6mzjKCorWg5ZbYb+xLu/Z8ceC/rffw7lbjRe1bTVRuNkFa2jSDeCIjE9HjKBmhpOhkNcLHtAYU8eoEB+ew/7ZzwXiCTJUNFeTqb6dzuvhRpipD0ARCGGWUbbt18osWl1IPDNDkQfcCAcDSO1Mb2fC07uS2D3Hj9h0wUFN/lfzg64mIs1Lxh6+ctdYO3ln/n2hpYAT/QgoqrkpPCZy7PrWkTCSTW/Su9/NWqaTV3F9MzJnKkzexyF2442qhA15fpwkCgYEA7KWSUQg1hRoRPXrKpaSmF/iXW1d/+VOLRXWTR9Fpvi/PDQpgLyVMGLL5N+ENjSlcZCbk7hbffjSgoU6mSZqD92vKHN3kbFPxHf8Fk2g2V+GY2850pYlXX44IlOg1aeAX0Zx+SiWzWtCA9CFxwTh7gSpx/HOrFubiF2oO0OzXHuMCgYEA3rU8Ea67DKfBTPzlM9Wc/EpazVvdKC7/wPgjJfKbkTnqk6fjx0q+jLq3LzHTdobf7w7ydWs5U+8W4/c/oXmiGNVXwpEfTDAKYIkyKoMwlp/V2XaRlEVin4+SvlCvxMeo2x3N3kTnul6vGfT0O2tWFjDP8Uz4VnQVJz+Iqq1AJbUCgYAifqwKVcj/YuJadNivNoXjfqAJd4K3BD+L22yhjlv8lhl3TCjjFmu2Ofhr9ck052+JRcYfEoR3cBJuEPnaRsSvvy2R8aJHTCEcfzz/1LP/MWpHuBt2ucNbsWd81TBcA4dVTZt3EXHIbhYt/+YGBUazeE1vQCkTSIpyYUpRmARvgwKBgAO87wEs+Z7AwhHUvNQd5cCmTtfbjt65yzkl8REV/V52pmVMEBqsOn6KM8DrCS2YHfIZQiCOaCvse2ngIIVJUVsxWYO+g9P3inUMWHc2NH6SuDgqMU9Xysv60O+40vpuj3r+CRKN/YW3SSEaZ28H4i4FK7hVHmX1FNXPzy9uMQFxAoGAcRL/1MJiXG9XPdUUuRnlHgMgObeKyYjvy+1JyRgYhIMudpk1HWvo/+v7SIZwDm0NVA3fEgWhB1BNUxwbijrxw5TGH29fnXa+NFATFcmvyfwSVvbEB8Ml5E4X1S6r2EE5lHYIoa1fTV03LMJVuSrN12kqOyV4Bj6XTaJ0ZQ/+vi0=", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDN3x153zheqZd1EMVM/ty8j24QeAIdqvntIFMTrhVgqtGV6NiCLTytiXo/qOFa4eN0V0AXpK6xXS46NAFkOej4rDG3b1d9eUKADndja98ywtbMdKjddtuVWvIa/0M5TgBvtv0JSAi3qyOUM2GmBkRn9L4GsBvj440I82gASkNSsxKUa7LCxAz3mK9pCZ5QVWZGRIMjQzNpB3uR12vwXKouBf/Rnx9GIg7G2a7f6GTWWlNr+BLynURMaWnOu6mtVnCmPGAXysLZgodzw1aLdF6im57a/pd9zw0jcI5d/ffSHii51/337+ffdizSwTnAJnCGN4Y7Vpq5l4SJXFerT6V/AgMBAAE=" - }, - "multiaddrs": [], - "multiaddr": {} - } - ] -} \ No newline at end of file diff --git a/test/switch/transport-manager.spec.js b/test/switch/transport-manager.spec.js deleted file mode 100644 index f3389c9578..0000000000 --- a/test/switch/transport-manager.spec.js +++ /dev/null @@ -1,295 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) -const Multiaddr = require('multiaddr') -const PeerInfo = require('peer-info') -const sinon = require('sinon') - -const TransportManager = require('../../src/switch/transport') - -describe('Transport Manager', () => { - afterEach(() => { - sinon.restore() - }) - - describe('dialables', () => { - let peerInfo - const dialAllTransport = { filter: addrs => addrs } - - before(function (done) { - this.timeout(10e3) - PeerInfo.create((err, info) => { - if (err) return done(err) - peerInfo = info - done() - }) - }) - - afterEach(() => { - peerInfo.multiaddrs.clear() - }) - - it('should return all transport addresses when peer info has 0 addrs', () => { - const queryAddrs = [ - '/ip4/127.0.0.1/tcp/4002', - '/ip4/192.168.0.3/tcp/4002', - '/ip6/::1/tcp/4001' - ].map(a => Multiaddr(a)) - - const dialableAddrs = TransportManager.dialables(dialAllTransport, queryAddrs, peerInfo) - - expect(dialableAddrs).to.have.length(queryAddrs.length) - - queryAddrs.forEach(qa => { - expect(dialableAddrs.some(da => da.equals(qa))).to.be.true() - }) - }) - - it('should return all transport addresses when we pass no peer info', () => { - const queryAddrs = [ - '/ip4/127.0.0.1/tcp/4002', - '/ip4/192.168.0.3/tcp/4002', - '/ip6/::1/tcp/4001' - ].map(a => Multiaddr(a)) - - const dialableAddrs = TransportManager.dialables(dialAllTransport, queryAddrs) - - expect(dialableAddrs).to.have.length(queryAddrs.length) - - queryAddrs.forEach(qa => { - expect(dialableAddrs.some(da => da.equals(qa))).to.be.true() - }) - }) - - it('should filter our addresses', () => { - const queryAddrs = [ - '/ip4/127.0.0.1/tcp/4002', - '/ip4/192.168.0.3/tcp/4002', - '/ip6/::1/tcp/4001' - ].map(a => Multiaddr(a)) - - const ourAddrs = [ - '/ip4/127.0.0.1/tcp/4002', - '/ip4/192.168.0.3/tcp/4002' - ] - - ourAddrs.forEach(a => peerInfo.multiaddrs.add(a)) - - const dialableAddrs = TransportManager.dialables(dialAllTransport, queryAddrs, peerInfo) - - expect(dialableAddrs).to.have.length(1) - expect(dialableAddrs[0].toString()).to.equal('/ip6/::1/tcp/4001') - }) - - it('should filter our addresses with peer ID suffix', () => { - const queryAddrs = [ - '/ip4/127.0.0.1/tcp/4002/ipfs/QmebzNV1kSzLfaYpSZdShuiABNUxoKT1vJmCdxM2iWsM2j', - '/ip4/192.168.0.3/tcp/4002', - '/ip6/::1/tcp/4001' - ].map(a => Multiaddr(a)) - - const ourAddrs = [ - '/ip4/127.0.0.1/tcp/4002', - `/ip4/192.168.0.3/tcp/4002/ipfs/${peerInfo.id.toB58String()}` - ] - - ourAddrs.forEach(a => peerInfo.multiaddrs.add(a)) - - const dialableAddrs = TransportManager.dialables(dialAllTransport, queryAddrs, peerInfo) - - expect(dialableAddrs).to.have.length(1) - expect(dialableAddrs[0].toString()).to.equal('/ip6/::1/tcp/4001') - }) - - it('should filter out our addrs that start with /ipfs/', () => { - const queryAddrs = [ - '/ip4/127.0.0.1/tcp/4002/ipfs/QmebzNV1kSzLfaYpSZdShuiABNUxoKT1vJmCdxM2iWsM2j' - ].map(a => Multiaddr(a)) - - const ourAddrs = [ - '/ipfs/QmSoLnSGccFuZQJzRadHn95W2CrSFmZuTdDWP8HXaHca9z' - ] - - ourAddrs.forEach(a => peerInfo.multiaddrs.add(a)) - - const dialableAddrs = TransportManager.dialables(dialAllTransport, queryAddrs, peerInfo) - - expect(dialableAddrs).to.have.length(1) - expect(dialableAddrs[0]).to.eql(queryAddrs[0]) - }) - - it('should filter our addresses over relay/rendezvous', () => { - const peerId = peerInfo.id.toB58String() - const queryAddrs = [ - `/p2p-circuit/ipfs/${peerId}`, - '/p2p-circuit/ip4/127.0.0.1/tcp/4002', - '/p2p-circuit/ip4/192.168.0.3/tcp/4002', - `/p2p-circuit/ip4/127.0.0.1/tcp/4002/ipfs/${peerId}`, - `/p2p-circuit/ip4/192.168.0.3/tcp/4002/ipfs/${peerId}`, - '/p2p-circuit/ip4/127.0.0.1/tcp/4002/ipfs/QmebzNV1kSzLfaYpSZdShuiABNUxoKT1vJmCdxM2iWsM2j', - '/p2p-circuit/ip4/192.168.0.3/tcp/4002/ipfs/QmebzNV1kSzLfaYpSZdShuiABNUxoKT1vJmCdxM2iWsM2j', - `/p2p-webrtc-star/ipfs/${peerId}`, - `/p2p-websocket-star/ipfs/${peerId}`, - `/p2p-stardust/ipfs/${peerId}`, - '/ip6/::1/tcp/4001' - ].map(a => Multiaddr(a)) - - const ourAddrs = [ - '/ip4/127.0.0.1/tcp/4002', - `/ip4/192.168.0.3/tcp/4002/ipfs/${peerInfo.id.toB58String()}` - ] - - ourAddrs.forEach(a => peerInfo.multiaddrs.add(a)) - - const dialableAddrs = TransportManager.dialables(dialAllTransport, queryAddrs, peerInfo) - - expect(dialableAddrs).to.have.length(1) - expect(dialableAddrs[0].toString()).to.equal('/ip6/::1/tcp/4001') - }) - }) - - describe('listen', () => { - const listener = { - once: function () {}, - listen: function () {}, - removeListener: function () {}, - getAddrs: function () {} - } - - it('should allow for multiple addresses with port 0', (done) => { - const mockListener = sinon.stub(listener) - mockListener.listen.callsArg(1) - mockListener.getAddrs.callsArgWith(0, null, []) - const mockSwitch = { - _peerInfo: { - multiaddrs: { - toArray: () => [ - Multiaddr('/ip4/127.0.0.1/tcp/0'), - Multiaddr('/ip4/0.0.0.0/tcp/0') - ], - replace: () => {} - } - }, - _options: {}, - _connectionHandler: () => {}, - transports: { - TCP: { - filter: (addrs) => addrs, - createListener: () => { - return mockListener - } - } - } - } - const transportManager = new TransportManager(mockSwitch) - transportManager.listen('TCP', null, null, (err) => { - expect(err).to.not.exist() - expect(mockListener.listen.callCount).to.eql(2) - done() - }) - }) - - it('should filter out equal addresses', (done) => { - const mockListener = sinon.stub(listener) - mockListener.listen.callsArg(1) - mockListener.getAddrs.callsArgWith(0, null, []) - const mockSwitch = { - _peerInfo: { - multiaddrs: { - toArray: () => [ - Multiaddr('/ip4/127.0.0.1/tcp/0'), - Multiaddr('/ip4/127.0.0.1/tcp/0') - ], - replace: () => {} - } - }, - _options: {}, - _connectionHandler: () => {}, - transports: { - TCP: { - filter: (addrs) => addrs, - createListener: () => { - return mockListener - } - } - } - } - const transportManager = new TransportManager(mockSwitch) - transportManager.listen('TCP', null, null, (err) => { - expect(err).to.not.exist() - expect(mockListener.listen.callCount).to.eql(1) - done() - }) - }) - - it('should account for addresses with no port', (done) => { - const mockListener = sinon.stub(listener) - mockListener.listen.callsArg(1) - mockListener.getAddrs.callsArgWith(0, null, []) - const mockSwitch = { - _peerInfo: { - multiaddrs: { - toArray: () => [ - Multiaddr('/p2p-circuit'), - Multiaddr('/p2p-websocket-star') - ], - replace: () => {} - } - }, - _options: {}, - _connectionHandler: () => {}, - transports: { - TCP: { - filter: (addrs) => addrs, - createListener: () => { - return mockListener - } - } - } - } - const transportManager = new TransportManager(mockSwitch) - transportManager.listen('TCP', null, null, (err) => { - expect(err).to.not.exist() - expect(mockListener.listen.callCount).to.eql(2) - done() - }) - }) - - it('should filter out addresses with the same, non 0, port', (done) => { - const mockListener = sinon.stub(listener) - mockListener.listen.callsArg(1) - mockListener.getAddrs.callsArgWith(0, null, []) - const mockSwitch = { - _peerInfo: { - multiaddrs: { - toArray: () => [ - Multiaddr('/ip4/127.0.0.1/tcp/8000'), - Multiaddr('/dnsaddr/libp2p.io/tcp/8000') - ], - replace: () => {} - } - }, - _options: {}, - _connectionHandler: () => {}, - transports: { - TCP: { - filter: (addrs) => addrs, - createListener: () => { - return mockListener - } - } - } - } - const transportManager = new TransportManager(mockSwitch) - transportManager.listen('TCP', null, null, (err) => { - expect(err).to.not.exist() - expect(mockListener.listen.callCount).to.eql(1) - done() - }) - }) - }) -}) diff --git a/test/switch/transports.browser.js b/test/switch/transports.browser.js deleted file mode 100644 index 735921e719..0000000000 --- a/test/switch/transports.browser.js +++ /dev/null @@ -1,52 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) - -const PeerId = require('peer-id') -const PeerInfo = require('peer-info') -const PeerBook = require('peer-book') -const WebSockets = require('libp2p-websockets') - -const tryEcho = require('./utils').tryEcho -const Switch = require('../../src/switch') - -describe('Transports', () => { - describe('WebSockets', () => { - let sw - let peer - - before((done) => { - const b58IdSrc = 'QmYzgdesgjdvD3okTPGZT9NPmh1BuH5FfTVNKjsvaAprhb' - // use a pre generated Id to save time - const idSrc = PeerId.createFromB58String(b58IdSrc) - const peerSrc = new PeerInfo(idSrc) - sw = new Switch(peerSrc, new PeerBook()) - - PeerInfo.create((err, p) => { - expect(err).to.not.exist() - peer = p - done() - }) - }) - - it('.transport.add', () => { - sw.transport.add('ws', new WebSockets()) - expect(Object.keys(sw.transports).length).to.equal(1) - }) - - it('.transport.dial', (done) => { - peer.multiaddrs.clear() - peer.multiaddrs.add('/ip4/127.0.0.1/tcp/15337/ws') - - const conn = sw.transport.dial('ws', peer, (err, conn) => { - expect(err).to.not.exist() - }) - - tryEcho(conn, done) - }) - }) -}) diff --git a/test/switch/transports.node.js b/test/switch/transports.node.js deleted file mode 100644 index f9e464c03d..0000000000 --- a/test/switch/transports.node.js +++ /dev/null @@ -1,237 +0,0 @@ -/* eslint-env mocha */ -/* eslint no-warning-comments: off */ -'use strict' - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) -const parallel = require('async/parallel') -const TCP = require('libp2p-tcp') -const WS = require('libp2p-websockets') -const pull = require('pull-stream') -const PeerBook = require('peer-book') - -const utils = require('./utils') -const createInfos = utils.createInfos -const tryEcho = utils.tryEcho -const Switch = require('../../src/switch') - -describe('transports', () => { - [ - { n: 'TCP', C: TCP, maGen: (port) => { return `/ip4/127.0.0.1/tcp/${port}` } }, - { n: 'WS', C: WS, maGen: (port) => { return `/ip4/127.0.0.1/tcp/${port}/ws` } } - // { n: 'UTP', C: UTP, maGen: (port) => { return `/ip4/127.0.0.1/udp/${port}/utp` } } - ].forEach((t) => describe(t.n, () => { - let switchA - let switchB - let morePeerInfo - - before(function (done) { - this.timeout(10 * 1000) - - createInfos(9, (err, peerInfos) => { - expect(err).to.not.exist() - - const peerA = peerInfos[0] - const peerB = peerInfos[1] - morePeerInfo = peerInfos.slice(2) - - peerA.multiaddrs.add(t.maGen(9888)) - peerB.multiaddrs.add(t.maGen(9999)) - switchA = new Switch(peerA, new PeerBook()) - switchB = new Switch(peerB, new PeerBook()) - done() - }) - }) - - after(function (done) { - parallel([ - (next) => switchA.stop(next), - (next) => switchB.stop(next) - ], done) - }) - - it('.transport.remove', () => { - switchA.transport.add('test', new t.C()) - expect(switchA.transports).to.have.any.keys(['test']) - switchA.transport.remove('test') - expect(switchA.transports).to.not.have.any.keys(['test']) - // verify remove fails silently - switchA.transport.remove('test') - }) - - it('.transport.removeAll', (done) => { - switchA.transport.add('test', new t.C()) - switchA.transport.add('test2', new t.C()) - expect(switchA.transports).to.have.any.keys(['test', 'test2']) - switchA.transport.removeAll(() => { - expect(switchA.transports).to.not.have.any.keys(['test', 'test2']) - done() - }) - }) - - it('.transport.add', () => { - switchA.transport.add(t.n, new t.C()) - expect(Object.keys(switchA.transports).length).to.equal(1) - - switchB.transport.add(t.n, new t.C()) - expect(Object.keys(switchB.transports).length).to.equal(1) - }) - - it('.transport.listen', (done) => { - let count = 0 - - switchA.transport.listen(t.n, {}, (conn) => pull(conn, conn), ready) - switchB.transport.listen(t.n, {}, (conn) => pull(conn, conn), ready) - - function ready () { - if (++count === 2) { - expect(switchA._peerInfo.multiaddrs.size).to.equal(1) - expect(switchB._peerInfo.multiaddrs.size).to.equal(1) - done() - } - } - }) - - it('.transport.dial to a multiaddr', (done) => { - const peer = morePeerInfo[0] - peer.multiaddrs.add(t.maGen(9999)) - - switchA.transport.dial(t.n, peer, (err, conn) => { - expect(err).to.not.exist() - tryEcho(conn, done) - }) - }) - - it('.transport.dial to set of multiaddr, only one is available', (done) => { - const peer = morePeerInfo[1] - peer.multiaddrs.add(t.maGen(9359)) - peer.multiaddrs.add(t.maGen(9329)) - peer.multiaddrs.add(t.maGen(9910)) - peer.multiaddrs.add(switchB._peerInfo.multiaddrs.toArray()[0]) // the valid address - peer.multiaddrs.add(t.maGen(9309)) - // addr not supported added on purpose - peer.multiaddrs.add('/ip4/1.2.3.4/tcp/3456/ws/p2p-webrtc-star') - - switchA.transport.dial(t.n, peer, (err, conn) => { - expect(err).to.not.exist() - tryEcho(conn, done) - }) - }) - - it('.transport.dial to set of multiaddr, none is available', (done) => { - const peer = morePeerInfo[2] - peer.multiaddrs.add(t.maGen(9359)) - peer.multiaddrs.add(t.maGen(9329)) - // addr not supported added on purpose - peer.multiaddrs.add('/ip4/1.2.3.4/tcp/3456/ws/p2p-webrtc-star') - - switchA.transport.dial(t.n, peer, (err, conn) => { - expect(err).to.exist() - expect(conn).to.not.exist() - done() - }) - }) - - it('.close', function (done) { - this.timeout(2500) - - parallel([ - (cb) => switchA.transport.close(t.n, cb), - (cb) => switchB.transport.close(t.n, cb) - ], done) - }) - - it('support port 0', (done) => { - const ma = t.maGen(0) - const peer = morePeerInfo[3] - peer.multiaddrs.add(ma) - - const sw = new Switch(peer, new PeerBook()) - sw.transport.add(t.n, new t.C()) - sw.transport.listen(t.n, {}, (conn) => pull(conn, conn), ready) - - function ready () { - expect(peer.multiaddrs.size).to.equal(1) - // should not have /tcp/0 anymore - expect(peer.multiaddrs.has(ma)).to.equal(false) - sw.stop(done) - } - }) - - it('support addr 0.0.0.0', (done) => { - const ma = t.maGen(9050).replace('127.0.0.1', '0.0.0.0') - const peer = morePeerInfo[4] - peer.multiaddrs.add(ma) - - const sw = new Switch(peer, new PeerBook()) - sw.transport.add(t.n, new t.C()) - sw.transport.listen(t.n, {}, (conn) => pull(conn, conn), ready) - - function ready () { - expect(peer.multiaddrs.size >= 1).to.equal(true) - expect(peer.multiaddrs.has(ma)).to.equal(false) - sw.stop(done) - } - }) - - it('support addr 0.0.0.0:0', (done) => { - const ma = t.maGen(9050).replace('127.0.0.1', '0.0.0.0') - const peer = morePeerInfo[5] - peer.multiaddrs.add(ma) - - const sw = new Switch(peer, new PeerBook()) - sw.transport.add(t.n, new t.C()) - sw.transport.listen(t.n, {}, (conn) => pull(conn, conn), ready) - - function ready () { - expect(peer.multiaddrs.size >= 1).to.equal(true) - expect(peer.multiaddrs.has(ma)).to.equal(false) - sw.stop(done) - } - }) - - it('listen in several addrs', function (done) { - this.timeout(12000) - const peer = morePeerInfo[6] - - peer.multiaddrs.add(t.maGen(9001)) - peer.multiaddrs.add(t.maGen(9002)) - peer.multiaddrs.add(t.maGen(9003)) - - const sw = new Switch(peer, new PeerBook()) - sw.transport.add(t.n, new t.C()) - sw.transport.listen(t.n, {}, (conn) => pull(conn, conn), ready) - - function ready () { - expect(peer.multiaddrs.size).to.equal(3) - sw.stop(done) - } - }) - - it('handles EADDRINUSE error when trying to listen', (done) => { - // TODO: fix libp2p-websockets to not throw Uncaught Error in this test - if (t.n === 'WS') { return done() } - - const switch1 = new Switch(switchA._peerInfo, new PeerBook()) - let switch2 - - switch1.transport.add(t.n, new t.C()) - switch1.transport.listen(t.n, {}, (conn) => pull(conn, conn), () => { - // Add in-use (peerA) address to peerB - switchB._peerInfo.multiaddrs.add(t.maGen(9888)) - - switch2 = new Switch(switchB._peerInfo, new PeerBook()) - switch2.transport.add(t.n, new t.C()) - switch2.transport.listen(t.n, {}, (conn) => pull(conn, conn), ready) - }) - - function ready (err) { - expect(err).to.exist() - expect(err.code).to.equal('EADDRINUSE') - switch1.stop(() => switch2.stop(done)) - } - }) - })) -}) diff --git a/test/switch/utils.js b/test/switch/utils.js deleted file mode 100644 index 0348bcb5b9..0000000000 --- a/test/switch/utils.js +++ /dev/null @@ -1,76 +0,0 @@ -'use strict' - -const PeerInfo = require('peer-info') -const PeerId = require('peer-id') -const parallel = require('async/parallel') -const pull = require('pull-stream') -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) - -const fixtures = require('./test-data/ids.json').infos - -exports.createInfos = (num, callback) => { - const tasks = [] - - for (let i = 0; i < num; i++) { - tasks.push((cb) => { - if (fixtures[i]) { - PeerId.createFromJSON(fixtures[i].id, (err, id) => { - if (err) { - return cb(err) - } - - cb(null, new PeerInfo(id)) - }) - return - } - - PeerInfo.create(cb) - }) - } - - parallel(tasks, callback) -} - -exports.tryEcho = (conn, callback) => { - const values = [Buffer.from('echo')] - - pull( - pull.values(values), - conn, - pull.collect((err, _values) => { - expect(err).to.not.exist() - expect(_values).to.eql(values) - callback() - }) - ) -} - -/** - * A utility method for calling done multiple times to help with async - * testing - * - * @param {Number} n The number of times done will be called - * @param {Function} willFinish An optional callback for cleanup before done is called - * @param {Function} done - * @returns {void} - */ -exports.doneAfter = (n, willFinish, done) => { - if (!done) { - done = willFinish - willFinish = undefined - } - - let count = 0 - const errors = [] - return (err) => { - count++ - if (err) errors.push(err) - if (count >= n) { - if (willFinish) willFinish() - done(errors.length > 0 ? errors : null) - } - } -} diff --git a/test/transports.browser.js b/test/transports.browser.js deleted file mode 100644 index 88614078a0..0000000000 --- a/test/transports.browser.js +++ /dev/null @@ -1,450 +0,0 @@ -/* eslint max-nested-callbacks: ["error", 8] */ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -chai.use(require('chai-checkmark')) -const expect = chai.expect -const PeerInfo = require('peer-info') -const PeerId = require('peer-id') -const Mplex = require('pull-mplex') -const pull = require('pull-stream') -const parallel = require('async/parallel') -const goodbye = require('pull-goodbye') -const serializer = require('pull-serializer') -const wrtcSupport = self.RTCPeerConnection && ('createDataChannel' in self.RTCPeerConnection.prototype) -const tryEcho = require('./utils/try-echo') - -const Node = require('./utils/bundle-browser') -const { getPeerRelay } = require('./utils/constants') - -describe('transports', () => { - describe('websockets', () => { - let peerB - let peerBMultiaddr - let nodeA - - before(async () => { - const peerInfo = await getPeerRelay() - peerB = new PeerInfo(peerInfo.id) - peerBMultiaddr = `/ip4/127.0.0.1/tcp/9200/ws/p2p/${peerInfo.id.toB58String()}` - peerB.multiaddrs.add(peerBMultiaddr) - }) - - after((done) => nodeA.stop(done)) - - it('create a libp2p Node', (done) => { - PeerInfo.create((err, peerInfo) => { - expect(err).to.not.exist() - - nodeA = new Node({ - peerInfo: peerInfo - }) - done() - }) - }) - - it('create a libp2p Node with mplex only', (done) => { - PeerInfo.create((err, peerInfo) => { - expect(err).to.not.exist() - - const b = new Node({ - peerInfo: peerInfo, - modules: { - streamMuxer: [Mplex] - } - }) - expect(b._modules.streamMuxer).to.eql([require('pull-mplex')]) - done() - }) - }) - - it('start libp2pNode', (done) => { - nodeA.start(done) - }) - - // General connectivity tests - - it('.dial using Multiaddr', (done) => { - nodeA.dial(peerBMultiaddr, (err) => { - expect(err).to.not.exist() - - setTimeout(check, 500) // Some time for Identify to finish - - function check () { - const peers = nodeA.peerBook.getAll() - expect(Object.keys(peers)).to.have.length(1) - done() - } - }) - }) - - it('.dialProtocol using Multiaddr', (done) => { - nodeA.dialProtocol(peerBMultiaddr, '/echo/1.0.0', (err, conn) => { - expect(err).to.not.exist() - - const peers = nodeA.peerBook.getAll() - expect(Object.keys(peers)).to.have.length(1) - - tryEcho(conn, done) - }) - }) - - it('.hangUp using Multiaddr', (done) => { - nodeA.hangUp(peerBMultiaddr, (err) => { - expect(err).to.not.exist() - - setTimeout(check, 500) - - function check () { - const peers = nodeA.peerBook.getAll() - expect(Object.keys(peers)).to.have.length(1) - expect(nodeA._switch.connection.getAll()).to.have.length(0) - done() - } - }) - }) - - it('.dial using PeerInfo', (done) => { - nodeA.dial(peerB, (err) => { - expect(err).to.not.exist() - - setTimeout(check, 500) // Some time for Identify to finish - - function check () { - const peers = nodeA.peerBook.getAll() - expect(Object.keys(peers)).to.have.length(1) - done() - } - }) - }) - - it('.dialProtocol using PeerInfo', (done) => { - nodeA.dialProtocol(peerB, '/echo/1.0.0', (err, conn) => { - expect(err).to.not.exist() - const peers = nodeA.peerBook.getAll() - expect(err).to.not.exist() - expect(Object.keys(peers)).to.have.length(1) - - tryEcho(conn, done) - }) - }) - - it('.hangUp using PeerInfo', (done) => { - nodeA.hangUp(peerB, (err) => { - expect(err).to.not.exist() - setTimeout(check, 500) - - function check () { - const peers = nodeA.peerBook.getAll() - expect(err).to.not.exist() - expect(Object.keys(peers)).to.have.length(1) - expect(nodeA._switch.connection.getAll()).to.have.length(0) - done() - } - }) - }) - - it('.dialFSM check conn and close', (done) => { - nodeA.dialFSM(peerB, (err, connFSM) => { - expect(err).to.not.exist() - - connFSM.once('muxed', () => { - expect( - nodeA._switch.connection.getAllById(peerB.id.toB58String()) - ).to.have.length(1) - - connFSM.once('error', done) - connFSM.once('close', () => { - // ensure the connection is closed - expect( - nodeA._switch.connection.getAllById(peerB.id.toB58String()) - ).to.have.length(0) - - done() - }) - - connFSM.close() - }) - }) - }) - - it('.dialFSM with a protocol, do an echo and close', (done) => { - nodeA.dialFSM(peerB, '/echo/1.0.0', (err, connFSM) => { - expect(err).to.not.exist() - connFSM.once('connection', (conn) => { - tryEcho(conn, () => { - connFSM.close() - }) - }) - connFSM.once('error', done) - connFSM.once('close', done) - }) - }) - - describe('stress', () => { - it('one big write', (done) => { - nodeA.dialProtocol(peerB, '/echo/1.0.0', (err, conn) => { - expect(err).to.not.exist() - const rawMessage = Buffer.alloc(100000) - rawMessage.fill('a') - - const s = serializer(goodbye({ - source: pull.values([rawMessage]), - sink: pull.collect((err, results) => { - expect(err).to.not.exist() - expect(results).to.have.length(1) - expect(Buffer.from(results[0])).to.have.length(rawMessage.length) - done() - }) - })) - pull(s, conn, s) - }) - }) - - it('many writes', function (done) { - this.timeout(10000) - - nodeA.dialProtocol(peerB, '/echo/1.0.0', (err, conn) => { - expect(err).to.not.exist() - - const s = serializer(goodbye({ - source: pull( - pull.infinite(), - pull.take(1000), - pull.map((val) => Buffer.from(val.toString())) - ), - sink: pull.collect((err, result) => { - expect(err).to.not.exist() - expect(result).to.have.length(1000) - done() - }) - })) - - pull(s, conn, s) - }) - }) - }) - }) - - describe('webrtc-star', () => { - /* eslint-disable-next-line no-console */ - if (!wrtcSupport) { return console.log('NO WEBRTC SUPPORT') } - - let peer1 - let peer2 - let node1 - let node2 - let node3 - - after((done) => { - parallel([ - (cb) => node1.stop(cb), - (cb) => node2.stop(cb), - (cb) => node3.stop(cb) - ], done) - }) - - it('create two peerInfo with webrtc-star addrs', (done) => { - parallel([ - (cb) => PeerId.create({ bits: 512 }, cb), - (cb) => PeerId.create({ bits: 512 }, cb) - ], (err, ids) => { - expect(err).to.not.exist() - - peer1 = new PeerInfo(ids[0]) - const ma1 = '/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star/p2p/' + ids[0].toB58String() - peer1.multiaddrs.add(ma1) - - peer2 = new PeerInfo(ids[1]) - const ma2 = '/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star/p2p/' + ids[1].toB58String() - peer2.multiaddrs.add(ma2) - - done() - }) - }) - - it('create two libp2p nodes with those peers', (done) => { - node1 = new Node({ - peerInfo: peer1 - }) - node2 = new Node({ - peerInfo: peer2 - }) - done() - }) - - it('start two libp2p nodes', (done) => { - parallel([ - (cb) => node1.start(cb), - (cb) => node2.start(cb) - ], done) - }) - - it('.handle echo on first node', () => { - node2.handle('/echo/1.0.0', (protocol, conn) => pull(conn, conn)) - }) - - it('.dialProtocol from the second node to the first node', (done) => { - node1.dialProtocol(peer2, '/echo/1.0.0', (err, conn) => { - expect(err).to.not.exist() - setTimeout(check, 500) - - function check () { - const peers1 = node1.peerBook.getAll() - expect(Object.keys(peers1)).to.have.length(1) - - const peers2 = node2.peerBook.getAll() - expect(Object.keys(peers2)).to.have.length(1) - - tryEcho(conn, done) - } - }) - }) - - it('node1 hangUp node2', (done) => { - node1.hangUp(peer2, (err) => { - expect(err).to.not.exist() - setTimeout(check, 500) - - function check () { - const peers = node1.peerBook.getAll() - expect(Object.keys(peers)).to.have.length(1) - expect(node1._switch.connection.getAll()).to.have.length(0) - done() - } - }) - }) - - it('create a third node and check that discovery works', (done) => { - PeerId.create({ bits: 512 }, (err, id3) => { - expect(err).to.not.exist() - - const b58Id = id3.toB58String() - - function check () { - // Verify both nodes are connected to node 3 - if (node1._switch.connection.getAllById(b58Id) && node2._switch.connection.getAllById(b58Id)) { - done() - } - } - - const peer3 = new PeerInfo(id3) - const ma3 = '/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star/p2p/' + b58Id - peer3.multiaddrs.add(ma3) - - node1.on('peer:discovery', (peerInfo) => node1.dial(peerInfo, check)) - node2.on('peer:discovery', (peerInfo) => node2.dial(peerInfo, check)) - - node3 = new Node({ - peerInfo: peer3 - }) - node3.start(check) - }) - }) - }) - - describe('websocket-star', () => { - let peer1 - let peer2 - let node1 - let node2 - - it('create two peerInfo with websocket-star addrs', (done) => { - parallel([ - (cb) => PeerId.create({ bits: 512 }, cb), - (cb) => PeerId.create({ bits: 512 }, cb) - ], (err, ids) => { - expect(err).to.not.exist() - - peer1 = new PeerInfo(ids[0]) - const ma1 = '/ip4/127.0.0.1/tcp/14444/ws/p2p-websocket-star/' - peer1.multiaddrs.add(ma1) - - peer2 = new PeerInfo(ids[1]) - const ma2 = '/ip4/127.0.0.1/tcp/14444/ws/p2p-websocket-star/' - peer2.multiaddrs.add(ma2) - - done() - }) - }) - - it('create two libp2p nodes with those peers', (done) => { - node1 = new Node({ - peerInfo: peer1 - }) - node2 = new Node({ - peerInfo: peer2 - }) - done() - }) - - it('listen on the two libp2p nodes', (done) => { - parallel([ - (cb) => node1.start(cb), - (cb) => node2.start(cb) - ], done) - }) - - it('handle a protocol on the first node', () => { - node2.handle('/echo/1.0.0', (protocol, conn) => pull(conn, conn)) - }) - - it('.dialProtocol from the second node to the first node', (done) => { - node1.dialProtocol(peer2, '/echo/1.0.0', (err, conn) => { - expect(err).to.not.exist() - setTimeout(check, 500) - - function check () { - const peers1 = node1.peerBook.getAll() - expect(Object.keys(peers1)).to.have.length(1) - - const peers2 = node2.peerBook.getAll() - expect(Object.keys(peers2)).to.have.length(1) - - tryEcho(conn, done) - } - }) - }) - - it('node1 hangUp node2', (done) => { - node1.hangUp(peer2, (err) => { - expect(err).to.not.exist() - const peers = node1.peerBook.getAll() - expect(Object.keys(peers)).to.have.length(1) - expect(node1._switch.connection.getAll()).to.have.length(0) - done() - }) - }) - - it('create a third node and check that discovery works', function (done) { - this.timeout(10 * 1000) - const expectedPeers = [ - node1.peerInfo.id.toB58String(), - node2.peerInfo.id.toB58String() - ] - - PeerId.create((err, id3) => { - expect(err).to.not.exist() - - const peer3 = new PeerInfo(id3) - const ma3 = '/ip4/127.0.0.1/tcp/14444/ws/p2p-websocket-star/p2p/' + id3.toB58String() - peer3.multiaddrs.add(ma3) - - // 2 connects and 1 start - expect(3).checks(done) - - const node3 = new Node({ - peerInfo: peer3 - }) - node3.on('peer:connect', (peerInfo) => { - expect(expectedPeers).to.include(peerInfo.id.toB58String()).mark() - }) - node3.start((err) => { - expect(err).to.not.exist().mark() - }) - }) - }) - }) -}) diff --git a/test/transports.node.js b/test/transports.node.js deleted file mode 100644 index 3b4faf9696..0000000000 --- a/test/transports.node.js +++ /dev/null @@ -1,723 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -const expect = chai.expect -const parallel = require('async/parallel') -const series = require('async/series') -const rendezvous = require('libp2p-websocket-star-rendezvous') -const TCP = require('libp2p-tcp') -const WS = require('libp2p-websockets') -const WSStar = require('libp2p-websocket-star') -const WRTCStar = require('libp2p-webrtc-star') -const wrtc = require('wrtc') - -const createNode = require('./utils/create-node.js') -const tryEcho = require('./utils/try-echo') -const echo = require('./utils/echo') - -const { - WRTC_RENDEZVOUS_MULTIADDR -} = require('./utils/constants') - -describe('transports', () => { - describe('TCP only', () => { - let nodeA - let nodeB - - before((done) => { - parallel([ - (cb) => createNode('/ip4/0.0.0.0/tcp/0', (err, node) => { - expect(err).to.not.exist() - nodeA = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }), - (cb) => createNode('/ip4/0.0.0.0/tcp/0', (err, node) => { - expect(err).to.not.exist() - nodeB = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }) - ], done) - }) - - after((done) => { - parallel([ - (cb) => nodeA.stop(cb), - (cb) => nodeB.stop(cb) - ], done) - }) - - it('nodeA.dial nodeB using PeerInfo without proto (warmup)', (done) => { - nodeA.dial(nodeB.peerInfo, (err) => { - expect(err).to.not.exist() - - // Some time for Identify to finish - setTimeout(check, 500) - - function check () { - parallel([ - (cb) => { - const peers = nodeA.peerBook.getAll() - expect(err).to.not.exist() - expect(Object.keys(peers)).to.have.length(1) - cb() - }, - (cb) => { - const peers = nodeB.peerBook.getAll() - expect(err).to.not.exist() - expect(Object.keys(peers)).to.have.length(1) - cb() - } - ], done) - } - }) - }) - - it('nodeA.dial nodeB using PeerInfo', (done) => { - nodeA.dialProtocol(nodeB.peerInfo, '/echo/1.0.0', (err, conn) => { - expect(err).to.not.exist() - - tryEcho(conn, done) - }) - }) - - it('nodeA.hangUp nodeB using PeerInfo (first)', (done) => { - nodeA.hangUp(nodeB.peerInfo, (err) => { - expect(err).to.not.exist() - setTimeout(check, 500) - - function check () { - parallel([ - (cb) => { - const peers = nodeA.peerBook.getAll() - expect(Object.keys(peers)).to.have.length(1) - expect(nodeA._switch.connection.getAll()).to.have.length(0) - cb() - }, - (cb) => { - const peers = nodeB.peerBook.getAll() - expect(Object.keys(peers)).to.have.length(1) - expect(nodeB._switch.connection.getAll()).to.have.length(0) - cb() - } - ], done) - } - }) - }) - - it('nodeA.dialProtocol nodeB using multiaddr', (done) => { - nodeA.dialProtocol(nodeB.peerInfo.multiaddrs.toArray()[0], '/echo/1.0.0', (err, conn) => { - // Some time for Identify to finish - setTimeout(check, 500) - - function check () { - expect(err).to.not.exist() - series([ - (cb) => { - const peers = nodeA.peerBook.getAll() - expect(Object.keys(peers)).to.have.length(1) - expect(nodeA._switch.connection.getAll()).to.have.length(1) - cb() - }, - (cb) => { - const peers = nodeB.peerBook.getAll() - expect(Object.keys(peers)).to.have.length(1) - expect(nodeA._switch.connection.getAll()).to.have.length(1) - cb() - } - ], () => tryEcho(conn, done)) - } - }) - }) - - it('nodeA.hangUp nodeB using multiaddr (second)', (done) => { - nodeA.hangUp(nodeB.peerInfo.multiaddrs.toArray()[0], (err) => { - expect(err).to.not.exist() - setTimeout(check, 500) - - function check () { - parallel([ - (cb) => { - const peers = nodeA.peerBook.getAll() - expect(Object.keys(peers)).to.have.length(1) - expect(nodeA._switch.connection.getAll()).to.have.length(0) - cb() - }, - (cb) => { - const peers = nodeB.peerBook.getAll() - expect(Object.keys(peers)).to.have.length(1) - expect(nodeB._switch.connection.getAll()).to.have.length(0) - cb() - } - ], done) - } - }) - }) - - it('nodeA.dialProtocol nodeB using PeerId', (done) => { - nodeA.dialProtocol(nodeB.peerInfo.id, '/echo/1.0.0', (err, conn) => { - // Some time for Identify to finish - setTimeout(check, 500) - - function check () { - expect(err).to.not.exist() - series([ - (cb) => { - const peers = nodeA.peerBook.getAll() - expect(Object.keys(peers)).to.have.length(1) - expect(nodeA._switch.connection.getAll()).to.have.length(1) - cb() - }, - (cb) => { - const peers = nodeB.peerBook.getAll() - expect(Object.keys(peers)).to.have.length(1) - expect(nodeA._switch.connection.getAll()).to.have.length(1) - cb() - } - ], () => tryEcho(conn, done)) - } - }) - }) - - it('nodeA.hangUp nodeB using PeerId (third)', (done) => { - nodeA.hangUp(nodeB.peerInfo.id, (err) => { - expect(err).to.not.exist() - setTimeout(check, 500) - - function check () { - parallel([ - (cb) => { - const peers = nodeA.peerBook.getAll() - expect(Object.keys(peers)).to.have.length(1) - expect(nodeA._switch.connection.getAll()).to.have.length(0) - cb() - }, - (cb) => { - const peers = nodeB.peerBook.getAll() - expect(Object.keys(peers)).to.have.length(1) - expect(nodeB._switch.connection.getAll()).to.have.length(0) - cb() - } - ], done) - } - }) - }) - - it('.dialFSM check conn and close', (done) => { - nodeA.dialFSM(nodeB.peerInfo, (err, connFSM) => { - expect(err).to.not.exist() - - connFSM.once('muxed', () => { - expect( - nodeA._switch.connection.getAllById(nodeB.peerInfo.id.toB58String()) - ).to.have.length(1) - - connFSM.once('error', done) - connFSM.once('close', () => { - // ensure the connection is closed - expect( - nodeA._switch.connection.getAllById(nodeB.peerInfo.id.toB58String()) - ).to.have.length(0) - done() - }) - - connFSM.close() - }) - }) - }) - - it('.dialFSM with a protocol, do an echo and close', (done) => { - nodeA.dialFSM(nodeB.peerInfo, '/echo/1.0.0', (err, connFSM) => { - expect(err).to.not.exist() - connFSM.once('connection', (conn) => { - expect( - nodeA._switch.connection.getAllById(nodeB.peerInfo.id.toB58String()) - ).to.have.length(1) - tryEcho(conn, () => { - connFSM.close() - }) - }) - connFSM.once('error', done) - connFSM.once('close', () => { - // ensure the connection is closed - expect( - nodeA._switch.connection.getAllById(nodeB.peerInfo.id.toB58String()) - ).to.have.length(0) - done() - }) - }) - }) - }) - - describe('TCP + WebSockets', () => { - let nodeTCP - let nodeTCPnWS - let nodeWS - - before((done) => { - parallel([ - (cb) => createNode([ - '/ip4/0.0.0.0/tcp/0' - ], (err, node) => { - expect(err).to.not.exist() - nodeTCP = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }), - (cb) => createNode([ - '/ip4/0.0.0.0/tcp/0', - '/ip4/127.0.0.1/tcp/25011/ws' - ], (err, node) => { - expect(err).to.not.exist() - nodeTCPnWS = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }), - (cb) => createNode([ - '/ip4/127.0.0.1/tcp/25022/ws' - ], (err, node) => { - expect(err).to.not.exist() - nodeWS = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }) - ], done) - }) - - after((done) => { - parallel([ - (cb) => nodeTCP.stop(cb), - (cb) => nodeTCPnWS.stop(cb), - (cb) => nodeWS.stop(cb) - ], done) - }) - - it('nodeTCP.dial nodeTCPnWS using PeerInfo', (done) => { - nodeTCP.dial(nodeTCPnWS.peerInfo, (err) => { - expect(err).to.not.exist() - - // Some time for Identify to finish - setTimeout(check, 500) - - function check () { - parallel([ - (cb) => { - const peers = nodeTCP.peerBook.getAll() - expect(Object.keys(peers)).to.have.length(1) - expect(nodeTCP._switch.connection.getAll()).to.have.length(1) - cb() - }, - (cb) => { - const peers = nodeTCPnWS.peerBook.getAll() - expect(Object.keys(peers)).to.have.length(1) - expect(nodeTCPnWS._switch.connection.getAll()).to.have.length(1) - cb() - } - ], done) - } - }) - }) - - it('nodeTCP.hangUp nodeTCPnWS using PeerInfo', (done) => { - nodeTCP.hangUp(nodeTCPnWS.peerInfo, (err) => { - expect(err).to.not.exist() - setTimeout(check, 500) - - function check () { - parallel([ - (cb) => { - const peers = nodeTCP.peerBook.getAll() - expect(Object.keys(peers)).to.have.length(1) - expect(nodeTCP._switch.connection.getAll()).to.have.length(0) - cb() - }, - (cb) => { - const peers = nodeTCPnWS.peerBook.getAll() - expect(Object.keys(peers)).to.have.length(1) - expect(nodeTCPnWS._switch.connection.getAll()).to.have.length(0) - cb() - } - ], done) - } - }) - }) - - it('nodeTCPnWS.dial nodeWS using PeerInfo', (done) => { - nodeTCPnWS.dial(nodeWS.peerInfo, (err) => { - expect(err).to.not.exist() - - // Some time for Identify to finish - setTimeout(check, 500) - - function check () { - parallel([ - (cb) => { - const peers = nodeTCPnWS.peerBook.getAll() - expect(Object.keys(peers)).to.have.length(2) - expect(nodeTCPnWS._switch.connection.getAll()).to.have.length(1) - cb() - }, - (cb) => { - const peers = nodeWS.peerBook.getAll() - expect(Object.keys(peers)).to.have.length(1) - expect(nodeWS._switch.connection.getAll()).to.have.length(1) - cb() - } - ], done) - } - }) - }) - - it('nodeTCPnWS.hangUp nodeWS using PeerInfo', (done) => { - nodeTCPnWS.hangUp(nodeWS.peerInfo, (err) => { - expect(err).to.not.exist() - setTimeout(check, 500) - - function check () { - parallel([ - (cb) => { - const peers = nodeTCPnWS.peerBook.getAll() - expect(Object.keys(peers)).to.have.length(2) - expect(nodeTCPnWS._switch.connection.getAll()).to.have.length(0) - - cb() - }, - (cb) => { - const peers = nodeWS.peerBook.getAll() - expect(Object.keys(peers)).to.have.length(1) - expect(nodeWS._switch.connection.getAll()).to.have.length(0) - cb() - } - ], done) - } - }) - }) - - // Until https://github.com/libp2p/js-libp2p/issues/46 is resolved - // Everynode will be able to dial in WebSockets - it.skip('nodeTCP.dial nodeWS using PeerInfo is unsuccesful', (done) => { - nodeTCP.dial(nodeWS.peerInfo, (err) => { - expect(err).to.exist() - done() - }) - }) - }) - - describe('TCP + WebSockets + WebRTCStar', () => { - let nodeAll - let nodeTCP - let nodeWS - let nodeWebRTCStar - - before(function (done) { - this.timeout(5 * 1000) - - parallel([ - (cb) => { - const wstar = new WRTCStar({ wrtc: wrtc }) - - createNode([ - '/ip4/0.0.0.0/tcp/0', - '/ip4/127.0.0.1/tcp/25011/ws', - `${WRTC_RENDEZVOUS_MULTIADDR.toString()}/p2p-webrtc-star` - ], { - modules: { - transport: [ - TCP, - WS, - wstar - ], - peerDiscovery: [wstar.discovery] - }, - config: { - peerDiscovery: { - autoDial: false, - [wstar.discovery.tag]: { - enabled: true - } - } - } - }, (err, node) => { - expect(err).to.not.exist() - nodeAll = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }) - }, - (cb) => createNode([ - '/ip4/0.0.0.0/tcp/0' - ], { - config: { - peerDiscovery: { - autoDial: false - } - } - }, (err, node) => { - expect(err).to.not.exist() - nodeTCP = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }), - (cb) => createNode([ - '/ip4/127.0.0.1/tcp/25022/ws' - ], { - config: { - peerDiscovery: { - autoDial: false - } - } - }, (err, node) => { - expect(err).to.not.exist() - nodeWS = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }), - (cb) => { - const wstar = new WRTCStar({ wrtc: wrtc }) - - createNode([ - `${WRTC_RENDEZVOUS_MULTIADDR.toString()}/p2p-webrtc-star` - ], { - modules: { - transport: [wstar], - peerDiscovery: [wstar.discovery] - }, - config: { - peerDiscovery: { - autoDial: false, - [wstar.discovery.tag]: { - enabled: true - } - } - } - }, (err, node) => { - expect(err).to.not.exist() - nodeWebRTCStar = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }) - } - ], done) - }) - - after(function (done) { - this.timeout(10 * 1000) - - parallel([ - (cb) => nodeAll.stop(cb), - (cb) => nodeTCP.stop(cb), - (cb) => nodeWS.stop(cb), - (cb) => nodeWebRTCStar.stop(cb) - ], done) - }) - - function check (otherNode, muxed, peers, callback) { - let i = 1; - [nodeAll, otherNode].forEach((node) => { - expect(Object.keys(node.peerBook.getAll())).to.have.length(i-- ? peers : 1) - expect(node._switch.connection.getAll()).to.have.length(muxed) - }) - callback() - } - - it('nodeAll.dial nodeTCP using PeerInfo', (done) => { - nodeAll.dial(nodeTCP.peerInfo, (err) => { - expect(err).to.not.exist() - // Some time for Identify to finish - setTimeout(() => check(nodeTCP, 1, 1, done), 500) - }) - }) - - it('nodeAll.hangUp nodeTCP using PeerInfo', (done) => { - nodeAll.hangUp(nodeTCP.peerInfo, (err) => { - expect(err).to.not.exist() - // Some time for Identify to finish - setTimeout(() => check(nodeTCP, 0, 1, done), 500) - }) - }) - - it('nodeAll.dial nodeWS using PeerInfo', (done) => { - nodeAll.dial(nodeWS.peerInfo, (err) => { - expect(err).to.not.exist() - // Some time for Identify to finish - setTimeout(() => check(nodeWS, 1, 2, done), 500) - }) - }) - - it('nodeAll.hangUp nodeWS using PeerInfo', (done) => { - nodeAll.hangUp(nodeWS.peerInfo, (err) => { - expect(err).to.not.exist() - // Some time for Identify to finish - setTimeout(() => check(nodeWS, 0, 2, done), 500) - }) - }) - - it('nodeAll.dial nodeWebRTCStar using PeerInfo', function (done) { - this.timeout(40 * 1000) - - nodeAll.dial(nodeWebRTCStar.peerInfo, (err) => { - expect(err).to.not.exist() - // Some time for Identify to finish - setTimeout(() => check(nodeWebRTCStar, 1, 3, done), 500) - }) - }) - - it('nodeAll.hangUp nodeWebRTCStar using PeerInfo', (done) => { - nodeAll.hangUp(nodeWebRTCStar.peerInfo, (err) => { - expect(err).to.not.exist() - setTimeout(() => check(nodeWebRTCStar, 0, 3, done), 500) - }) - }) - }) - - describe('TCP + WebSockets + WebSocketStar', () => { - let nodeAll - let nodeTCP - let nodeWS - let nodeWebSocketStar - let ss - const PORT = 24642 - - before(async () => { - ss = await rendezvous.start({ - port: PORT - }) - }) - - before((done) => { - parallel([ - (cb) => { - const wstar = new WSStar() - - createNode([ - '/ip4/0.0.0.0/tcp/0', - '/ip4/127.0.0.1/tcp/25011/ws', - `/ip4/127.0.0.1/tcp/${PORT}/ws/p2p-websocket-star` - ], { - modules: { - transport: [ - TCP, - WS, - wstar - ], - peerDiscovery: [wstar.discovery] - } - }, (err, node) => { - expect(err).to.not.exist() - nodeAll = node - wstar.lazySetId(node.peerInfo.id) - node.handle('/echo/1.0.0', echo) - node.start(cb) - }) - }, - (cb) => createNode([ - '/ip4/0.0.0.0/tcp/0' - ], (err, node) => { - expect(err).to.not.exist() - nodeTCP = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }), - (cb) => createNode([ - '/ip4/127.0.0.1/tcp/25022/ws' - ], (err, node) => { - expect(err).to.not.exist() - nodeWS = node - node.handle('/echo/1.0.0', echo) - node.start(cb) - }), - - (cb) => { - const wstar = new WSStar({}) - - createNode([ - `/ip4/127.0.0.1/tcp/${PORT}/ws/p2p-websocket-star` - ], { - modules: { - transport: [wstar], - peerDiscovery: [wstar.discovery] - } - }, (err, node) => { - expect(err).to.not.exist() - nodeWebSocketStar = node - wstar.lazySetId(node.peerInfo.id) - node.handle('/echo/1.0.0', echo) - node.start(cb) - }) - } - ], done) - }) - - after((done) => { - parallel([ - (cb) => nodeAll.stop(cb), - (cb) => nodeTCP.stop(cb), - (cb) => nodeWS.stop(cb), - (cb) => nodeWebSocketStar.stop(cb), - async () => { - await ss.stop() - } - ], done) - }) - - function check (otherNode, muxed, peers, done) { - let i = 1; - [nodeAll, otherNode].forEach((node) => { - expect(Object.keys(node.peerBook.getAll())).to.have.length(i-- ? peers : 1) - expect(node._switch.connection.getAll()).to.have.length(muxed) - }) - done() - } - - it('nodeAll.dial nodeTCP using PeerInfo', (done) => { - nodeAll.dial(nodeTCP.peerInfo, (err) => { - expect(err).to.not.exist() - // Some time for Identify to finish - setTimeout(() => check(nodeTCP, 1, 1, done), 500) - }) - }) - - it('nodeAll.hangUp nodeTCP using PeerInfo', (done) => { - nodeAll.hangUp(nodeTCP.peerInfo, (err) => { - expect(err).to.not.exist() - // Some time for Identify to finish - setTimeout(() => check(nodeTCP, 0, 1, done), 500) - }) - }) - - it('nodeAll.dial nodeWS using PeerInfo', (done) => { - nodeAll.dial(nodeWS.peerInfo, (err) => { - expect(err).to.not.exist() - // Some time for Identify to finish - setTimeout(() => check(nodeWS, 1, 2, done), 500) - }) - }) - - it('nodeAll.hangUp nodeWS using PeerInfo', (done) => { - nodeAll.hangUp(nodeWS.peerInfo, (err) => { - expect(err).to.not.exist() - // Some time for Identify to finish - setTimeout(() => check(nodeWS, 0, 2, done), 500) - }) - }) - - it('nodeAll.dial nodeWebSocketStar using PeerInfo', (done) => { - nodeAll.dial(nodeWebSocketStar.peerInfo, (err) => { - expect(err).to.not.exist() - // Some time for Identify to finish - setTimeout(() => check(nodeWebSocketStar, 1, 3, done), 500) - }) - }) - - it('nodeAll.hangUp nodeWebSocketStar using PeerInfo', (done) => { - nodeAll.hangUp(nodeWebSocketStar.peerInfo, (err) => { - expect(err).to.not.exist() - // Some time for Identify to finish - setTimeout(() => check(nodeWebSocketStar, 0, 3, done), 500) - }) - }) - }) -}) diff --git a/test/transports/transport-manager.node.js b/test/transports/transport-manager.node.js new file mode 100644 index 0000000000..71d776fc2c --- /dev/null +++ b/test/transports/transport-manager.node.js @@ -0,0 +1,56 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +const { expect } = chai +const TransportManager = require('../../src/transport-manager') +const Transport = require('libp2p-tcp') +const multiaddr = require('multiaddr') +const mockUpgrader = require('../utils/mockUpgrader') +const addrs = [ + multiaddr('/ip4/127.0.0.1/tcp/0'), + multiaddr('/ip4/127.0.0.1/tcp/0') +] + +describe('Transport Manager (TCP)', () => { + let tm + + before(() => { + tm = new TransportManager({ + libp2p: {}, + upgrader: mockUpgrader, + onConnection: () => {} + }) + }) + + afterEach(async () => { + await tm.removeAll() + expect(tm._transports.size).to.equal(0) + }) + + it('should be able to add and remove a transport', async () => { + tm.add(Transport.prototype[Symbol.toStringTag], Transport) + expect(tm._transports.size).to.equal(1) + await tm.remove(Transport.prototype[Symbol.toStringTag]) + }) + + it('should be able to listen', async () => { + tm.add(Transport.prototype[Symbol.toStringTag], Transport) + await tm.listen(addrs) + expect(tm._listeners.size).to.equal(1) + // Ephemeral ip addresses may result in multiple listeners + expect(tm.getAddrs().length).to.equal(addrs.length) + await tm.close() + expect(tm._listeners.size).to.equal(0) + }) + + it('should be able to dial', async () => { + tm.add(Transport.prototype[Symbol.toStringTag], Transport) + await tm.listen(addrs) + const addr = tm.getAddrs().shift() + const connection = await tm.dial(addr) + expect(connection).to.exist() + await connection.close() + }) +}) diff --git a/test/transports/transport-manager.spec.js b/test/transports/transport-manager.spec.js new file mode 100644 index 0000000000..91a976eb3b --- /dev/null +++ b/test/transports/transport-manager.spec.js @@ -0,0 +1,139 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +const { expect } = chai +const sinon = require('sinon') + +const multiaddr = require('multiaddr') +const Transport = require('libp2p-websockets') +const TransportManager = require('../../src/transport-manager') +const mockUpgrader = require('../utils/mockUpgrader') +const { MULTIADDRS_WEBSOCKETS } = require('../fixtures/browser') +const { codes: ErrorCodes } = require('../../src/errors') +const Libp2p = require('../../src') +const Peers = require('../fixtures/peers') +const PeerId = require('peer-id') +const PeerInfo = require('peer-info') + +describe('Transport Manager (WebSockets)', () => { + let tm + + before(() => { + tm = new TransportManager({ + libp2p: {}, + upgrader: mockUpgrader, + onConnection: () => {} + }) + }) + + afterEach(async () => { + await tm.removeAll() + expect(tm._transports.size).to.equal(0) + }) + + it('should be able to add and remove a transport', async () => { + tm.add(Transport.prototype[Symbol.toStringTag], Transport) + expect(tm._transports.size).to.equal(1) + await tm.remove(Transport.prototype[Symbol.toStringTag]) + }) + + it('should not be able to add a transport without a key', () => { + expect(() => { + tm.add(undefined, Transport) + }).to.throw().that.satisfies((err) => { + return err.code === ErrorCodes.ERR_INVALID_KEY + }) + }) + + it('should not be able to add a transport twice', () => { + tm.add(Transport.prototype[Symbol.toStringTag], Transport) + expect(() => { + tm.add(Transport.prototype[Symbol.toStringTag], Transport) + }).to.throw().that.satisfies((err) => { + return err.code === ErrorCodes.ERR_DUPLICATE_TRANSPORT + }) + }) + + it('should be able to dial', async () => { + tm.add(Transport.prototype[Symbol.toStringTag], Transport) + const addr = MULTIADDRS_WEBSOCKETS[0] + const connection = await tm.dial(addr) + expect(connection).to.exist() + await connection.close() + }) + + it('should fail to dial an unsupported address', async () => { + tm.add(Transport.prototype[Symbol.toStringTag], Transport) + const addr = multiaddr('/ip4/127.0.0.1/tcp/0') + try { + await tm.dial(addr) + } catch (err) { + expect(err).to.satisfy((err) => err.code === ErrorCodes.ERR_TRANSPORT_UNAVAILABLE) + return + } + + expect.fail('Dial attempt should have failed') + }) + + it('should fail to listen with no valid address', async () => { + tm.add(Transport.prototype[Symbol.toStringTag], Transport) + const addrs = [multiaddr('/ip4/127.0.0.1/tcp/0')] + try { + await tm.listen(addrs) + } catch (err) { + expect(err).to.satisfy((err) => err.code === ErrorCodes.ERR_NO_VALID_ADDRESSES) + return + } + + expect.fail('should have failed') + }) +}) + +describe('libp2p.transportManager', () => { + let peerInfo + let libp2p + + before(async () => { + const peerId = await PeerId.createFromJSON(Peers[0]) + peerInfo = new PeerInfo(peerId) + }) + + afterEach(async () => { + sinon.restore() + libp2p && await libp2p.stop() + libp2p = null + }) + + it('should create a TransportManager', () => { + libp2p = new Libp2p({ + peerInfo, + modules: { + transport: [Transport] + } + }) + + expect(libp2p.transportManager).to.exist() + expect(libp2p.transportManager._transports.size).to.equal(1) + }) + + it('starting and stopping libp2p should start and stop TransportManager', async () => { + libp2p = new Libp2p({ + peerInfo, + modules: { + transport: [Transport] + } + }) + + // We don't need to listen, stub it + sinon.stub(libp2p.transportManager, 'listen').returns(true) + sinon.spy(libp2p.transportManager, 'close') + + await libp2p.start() + await libp2p.stop() + + expect(libp2p.transportManager.listen.callCount).to.equal(1) + expect(libp2p.transportManager.close.callCount).to.equal(1) + }) +}) diff --git a/test/utils/bundle-browser.js b/test/utils/bundle-browser.js deleted file mode 100644 index a5140da92a..0000000000 --- a/test/utils/bundle-browser.js +++ /dev/null @@ -1,103 +0,0 @@ -'use strict' - -const WS = require('libp2p-websockets') -const WebRTCStar = require('libp2p-webrtc-star') -const WebSocketStar = require('libp2p-websocket-star') -const Bootstrap = require('libp2p-bootstrap') -const SPDY = require('libp2p-spdy') -const MPLEX = require('libp2p-mplex') -const PULLMPLEX = require('pull-mplex') -const KadDHT = require('libp2p-kad-dht') -const GossipSub = require('libp2p-gossipsub') -const SECIO = require('libp2p-secio') -const defaultsDeep = require('@nodeutils/defaults-deep') -const libp2p = require('../..') - -function mapMuxers (list) { - return list.map((pref) => { - if (typeof pref !== 'string') { return pref } - switch (pref.trim().toLowerCase()) { - case 'spdy': return SPDY - case 'mplex': return MPLEX - case 'pullmplex': return PULLMPLEX - default: - throw new Error(pref + ' muxer not available') - } - }) -} - -function getMuxers (options) { - if (options) { - return mapMuxers(options) - } else { - return [PULLMPLEX, MPLEX, SPDY] - } -} - -class Node extends libp2p { - constructor (_options) { - _options = _options || {} - - const starOpts = { id: _options.peerInfo.id } - const wrtcStar = new WebRTCStar(starOpts) - const wsStar = new WebSocketStar(starOpts) - - const defaults = { - modules: { - transport: [ - wrtcStar, - wsStar, - new WS() - ], - streamMuxer: getMuxers(_options.muxer), - connEncryption: [ - SECIO - ], - peerDiscovery: [ - wrtcStar.discovery, - wsStar.discovery, - Bootstrap - ], - dht: KadDHT, - pubsub: GossipSub - }, - config: { - peerDiscovery: { - autoDial: true, - webRTCStar: { - enabled: true - }, - websocketStar: { - enabled: true - }, - bootstrap: { - interval: 10000, - enabled: false, - list: _options.boostrapList - } - }, - relay: { - enabled: false, - hop: { - enabled: false, - active: false - } - }, - dht: { - kBucketSize: 20, - randomWalk: { - enabled: true - }, - enabled: false - }, - pubsub: { - enabled: false - } - } - } - - super(defaultsDeep(_options, defaults)) - } -} - -module.exports = Node diff --git a/test/utils/bundle-nodejs.js b/test/utils/bundle-nodejs.js deleted file mode 100644 index 842224cac8..0000000000 --- a/test/utils/bundle-nodejs.js +++ /dev/null @@ -1,96 +0,0 @@ -'use strict' - -const TCP = require('libp2p-tcp') -const MulticastDNS = require('libp2p-mdns') -const WS = require('libp2p-websockets') -const Bootstrap = require('libp2p-bootstrap') -const SPDY = require('libp2p-spdy') -const KadDHT = require('libp2p-kad-dht') -const GossipSub = require('libp2p-gossipsub') -const MPLEX = require('libp2p-mplex') -const PULLMPLEX = require('pull-mplex') -const SECIO = require('libp2p-secio') -const defaultsDeep = require('@nodeutils/defaults-deep') -const libp2p = require('../..') - -function mapMuxers (list) { - return list.map((pref) => { - if (typeof pref !== 'string') { return pref } - switch (pref.trim().toLowerCase()) { - case 'spdy': return SPDY - case 'mplex': return MPLEX - case 'pullmplex': return PULLMPLEX - default: - throw new Error(pref + ' muxer not available') - } - }) -} - -function getMuxers (muxers) { - const muxerPrefs = process.env.LIBP2P_MUXER - if (muxerPrefs && !muxers) { - return mapMuxers(muxerPrefs.split(',')) - } else if (muxers) { - return mapMuxers(muxers) - } else { - return [PULLMPLEX, MPLEX, SPDY] - } -} - -class Node extends libp2p { - constructor (_options) { - const defaults = { - modules: { - transport: [ - TCP, - WS - ], - streamMuxer: getMuxers(_options.muxer), - connEncryption: [ - SECIO - ], - peerDiscovery: [ - MulticastDNS, - Bootstrap - ], - dht: KadDHT, - pubsub: GossipSub - }, - config: { - peerDiscovery: { - autoDial: true, - mdns: { - interval: 10000, - enabled: false - }, - bootstrap: { - interval: 10000, - enabled: false, - list: _options.bootstrapList - } - }, - relay: { - enabled: false, - hop: { - enabled: false, - active: false - } - }, - dht: { - kBucketSize: 20, - randomWalk: { - enabled: true - }, - enabled: true - }, - pubsub: { - enabled: false - } - } - } - - super(defaultsDeep(_options, defaults)) - } -} - -module.exports = Node diff --git a/test/utils/constants.js b/test/utils/constants.js deleted file mode 100644 index 33eaf45cb4..0000000000 --- a/test/utils/constants.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict' - -const PeerId = require('peer-id') -const PeerInfo = require('peer-info') -const nextTick = require('async/nextTick') -const peerJSON = require('../fixtures/test-peer') -const multiaddr = require('multiaddr') -const promisify = require('promisify-es6') - -let peerRelay = null - -/** - * Creates a `PeerInfo` that can be used across testing. Once the - * relay `PeerInfo` has been requested, it will be reused for each - * additional request. - * - * This is currently being used to create a relay on test bootstrapping - * so that it can be used by browser nodes during their test suite. This - * is necessary for running a TCP node during browser tests. - * @private - * @param {function(error, PeerInfo)} callback - * @returns {void} - */ -module.exports.getPeerRelay = promisify((callback) => { - if (peerRelay) return nextTick(callback, null, peerRelay) - - PeerId.createFromJSON(peerJSON, (err, peerId) => { - if (err) { - return callback(err) - } - peerRelay = new PeerInfo(peerId) - - peerRelay.multiaddrs.add('/ip4/127.0.0.1/tcp/9200/ws') - peerRelay.multiaddrs.add('/ip4/127.0.0.1/tcp/9245') - - callback(null, peerRelay) - }) -}) - -module.exports.WS_RENDEZVOUS_MULTIADDR = multiaddr('/ip4/127.0.0.1/tcp/14444/ws') -module.exports.WRTC_RENDEZVOUS_MULTIADDR = multiaddr('/ip4/127.0.0.1/tcp/15555/ws') diff --git a/test/utils/create-node.js b/test/utils/create-node.js deleted file mode 100644 index 3b9cf43bf2..0000000000 --- a/test/utils/create-node.js +++ /dev/null @@ -1,41 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -chai.use(require('dirty-chai')) -const PeerInfo = require('peer-info') -const PeerId = require('peer-id') -const waterfall = require('async/waterfall') -const Node = require('./bundle-nodejs') - -function createNode (multiaddrs, options, callback) { - if (typeof options === 'function') { - callback = options - options = {} - } - - options = options || {} - - if (!Array.isArray(multiaddrs)) { - multiaddrs = [multiaddrs] - } - - waterfall([ - (cb) => createPeerInfo(cb), - (peerInfo, cb) => { - multiaddrs.map((ma) => peerInfo.multiaddrs.add(ma)) - options.peerInfo = peerInfo - cb(null, new Node(options)) - } - ], callback) -} - -function createPeerInfo (callback) { - waterfall([ - (cb) => PeerId.create({ bits: 512 }, cb), - (peerId, cb) => PeerInfo.create(peerId, cb) - ], callback) -} - -module.exports = createNode -module.exports.createPeerInfo = createPeerInfo diff --git a/test/utils/echo.js b/test/utils/echo.js deleted file mode 100644 index 31aa584b4c..0000000000 --- a/test/utils/echo.js +++ /dev/null @@ -1,11 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const pull = require('pull-stream') - -function echo (protocol, conn) { - pull(conn, conn) -} - -module.exports = echo -module.exports.multicodec = '/echo/1.0.0' diff --git a/test/utils/mockUpgrader.js b/test/utils/mockUpgrader.js new file mode 100644 index 0000000000..5e72e2de2a --- /dev/null +++ b/test/utils/mockUpgrader.js @@ -0,0 +1,6 @@ +'use strict' + +module.exports = { + upgradeInbound: (maConn) => maConn, + upgradeOutbound: (maConn) => maConn +} diff --git a/test/utils/try-echo.js b/test/utils/try-echo.js deleted file mode 100644 index b8874b7494..0000000000 --- a/test/utils/try-echo.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict' - -const pull = require('pull-stream') -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) - -module.exports = (conn, callback) => { - const values = [Buffer.from('echo')] - - pull( - pull.values(values), - conn, - pull.collect((err, _values) => { - expect(err).to.not.exist() - expect(_values).to.eql(values) - callback() - }) - ) -} From 10c8553c58601411be644183fd60e13649e1da7f Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Mon, 21 Oct 2019 13:36:05 +0200 Subject: [PATCH 03/92] docs: add stream wrapping example (#466) * docs: add duplex wrapping example docs: add iterable types from @alanshaw's gist * docs(fix): add feedback fix Co-Authored-By: Vasco Santos * docs: clean up based on feedback --- doc/STREAMING_ITERABLES.md | 134 ++++++++++++++++++++++++++++++++++++- 1 file changed, 133 insertions(+), 1 deletion(-) diff --git a/doc/STREAMING_ITERABLES.md b/doc/STREAMING_ITERABLES.md index a58727be9e..2249344cea 100644 --- a/doc/STREAMING_ITERABLES.md +++ b/doc/STREAMING_ITERABLES.md @@ -7,11 +7,143 @@ - [Iterable Streams](#iterable-streams) - [Table of Contents](#table-of-contents) - [Usage Guide](#usage-guide) + - [Transforming Bidirectional Data](#transforming-bidirectional-data) + - [Iterable Stream Types](#iterable-stream-types) + - [Source](#source) + - [Sink](#sink) + - [Transform](#transform) + - [Duplex](#duplex) - [Iterable Modules](#iterable-modules) ## Usage Guide -**Coming Soon!** +### Transforming Bidirectional Data + +Sometimes you may need to wrap an existing duplex stream in order to perform incoming and outgoing [transforms](#transform) on data. This type of wrapping is commonly used in stream encryption/decryption. Using [it-pair][it-pair] and [it-pipe][it-pipe], we can do this rather easily, given an existing [duplex iterable](#duplex). + +```js +const duplexPair = require('it-pair/duplex') +const pipe = require('it-pipe') + +// Wrapper is what we will write and read from +// This gives us two duplex iterables that are internally connected +const [internal, external] = duplexPair() + +// Now we can pipe our wrapper to the existing duplex iterable +pipe( + external, // The external half of the pair interacts with the existing duplex + outgoingTransform, // A transform iterable to send data through (ie: encrypting) + existingDuplex, // The original duplex iterable we are wrapping + incomingTransform, // A transform iterable to read data through (ie: decrypting) + external +) + +// We can now read and write from the other half of our pair +pipe( + ['some data'], + internal, // The internal half of the pair is what we will interact with to read/write data + async (source) => { + for await (const chunk of source) { + console.log('Data: %s', chunk.toString()) + // > Data: some data + } + } +) +``` + +## Iterable Stream Types + +These types are pulled from [@alanshaw's gist](https://gist.github.com/alanshaw/591dc7dd54e4f99338a347ef568d6ee9) on streaming iterables. + +### Source + +A "source" is something that can be consumed. It is an iterable object. + +```js +const ints = { + [Symbol.asyncIterator] () { + let i = 0 + return { + async next () { + return { done: false, value: i++ } + } + } + } +} + +// or, more succinctly using a generator and for/await: + +const ints = (async function * () { + let i = 0 + while (true) yield i++ +})() +``` + +### Sink + +A "sink" is something that consumes (or drains) a source. It is a function that takes a source and iterates over it. It optionally returns a value. + +```js +const logger = async source => { + const it = source[Symbol.asyncIterator]() + while (true) { + const { done, value } = await it.next() + if (done) break + console.log(value) // prints 0, 1, 2, 3... + } +} + +// or, more succinctly using a generator and for/await: + +const logger = async source => { + for await (const chunk of source) { + console.log(chunk) // prints 0, 1, 2, 3... + } +} +``` + +### Transform + +A "transform" is both a sink _and_ a source where the values it consumes and the values that can be consumed from it are connected in some way. It is a function that takes a source and returns a source. + +```js +const doubler = source => { + return { + [Symbol.asyncIterator] () { + const it = source[Symbol.asyncIterator]() + return { + async next () { + const { done, value } = await it.next() + if (done) return { done } + return { done, value: value * 2 } + } + return () { + return it.return && it.return() + } + } + } + } +} + +// or, more succinctly using a generator and for/await: + +const doubler = source => (async function * () { + for await (const chunk of source) { + yield chunk * 2 + } +})() +``` + +### Duplex + +A "duplex" is similar to a transform but the values it consumes are not necessarily connected to the values that can be consumed from it. It is an object with two properties, `sink` and `source`. + +```js +const duplex = { + sink: async source => {/* ... */}, + source: { [Symbol.asyncIterator] () {/* ... */} } +} +``` ## Iterable Modules From af364b070bb9095e5456c2c1775e8c0ea4c68fb0 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Mon, 21 Oct 2019 16:53:58 +0200 Subject: [PATCH 04/92] refactor(async): add dialer and upgrader (#462) * chore(deps): update connection and multistream * feat: add basic dial support for addresses and peers * test: automatically require all node test files * fix: dont catch and log in the wrong place * test: add direct spec test fix: improve dial error consistency * feat: add dial timeouts and concurrency Queue timeouts will result in aborts of the dials * chore: fix linting * test: verify dialer defaults * feat: add initial upgrader * fix: add more test coverage and fix bugs * feat: libp2p creates the upgrader * feat: hook up handle to the upgrader * feat: hook up the dialer to libp2p test: add node dialer libp2p tests * feat: add connection listeners to upgrader * feat: emit connect and disconnect events * chore: use libp2p-interfaces * fix: address review feedback * fix: correct import * refactor: dedupe connection creation code --- package.json | 14 +- src/circuit/README.md | 23 +- src/circuit/circuit/dialer.js | 2 +- src/circuit/circuit/stop.js | 2 +- src/constants.js | 12 + src/dialer.js | 98 ++++++++ src/errors.js | 8 +- src/index.js | 197 +++++++--------- src/pnet/index.js | 2 +- src/switch/README.md | 6 +- src/switch/limit-dialer/queue.js | 2 +- src/switch/observe-connection.js | 2 +- src/transport-manager.js | 20 +- src/upgrader.js | 336 ++++++++++++++++++++++++++++ test/dialing/direct.node.js | 236 ++++++++++++++++++++ test/dialing/direct.spec.js | 211 ++++++++++++++++++ test/node.js | 13 +- test/upgrading/upgrader.spec.js | 370 +++++++++++++++++++++++++++++++ test/utils/mockCrypto.js | 24 ++ test/utils/mockMultiaddrConn.js | 43 ++++ 20 files changed, 1480 insertions(+), 141 deletions(-) create mode 100644 src/constants.js create mode 100644 src/dialer.js create mode 100644 src/upgrader.js create mode 100644 test/dialing/direct.node.js create mode 100644 test/dialing/direct.spec.js create mode 100644 test/upgrading/upgrader.spec.js create mode 100644 test/utils/mockCrypto.js create mode 100644 test/utils/mockMultiaddrConn.js diff --git a/package.json b/package.json index db94adc390..4abc96c890 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "npm": ">=6.0.0" }, "dependencies": { + "abort-controller": "^3.0.0", "async": "^2.6.2", "bignumber.js": "^9.0.0", "class-is": "^1.1.0", @@ -48,15 +49,17 @@ "err-code": "^1.1.2", "fsm-event": "^2.1.0", "hashlru": "^2.3.0", - "interface-connection": "~0.3.3", + "it-pipe": "^1.0.1", "latency-monitor": "~0.2.1", "libp2p-crypto": "^0.16.2", + "libp2p-interfaces": "^0.1.1", "mafmt": "^7.0.0", "merge-options": "^1.0.1", "moving-average": "^1.0.0", "multiaddr": "^7.1.0", - "multistream-select": "~0.14.6", + "multistream-select": "^0.15.0", "once": "^1.4.0", + "p-queue": "^6.1.1", "p-settle": "^3.1.0", "peer-book": "^0.9.1", "peer-id": "^0.13.3", @@ -73,6 +76,7 @@ }, "devDependencies": { "@nodeutils/defaults-deep": "^1.1.0", + "abortable-iterator": "^2.1.0", "aegir": "^20.0.0", "chai": "^4.2.0", "chai-checkmark": "^1.0.1", @@ -80,7 +84,9 @@ "delay": "^4.3.0", "dirty-chai": "^2.0.1", "electron-webrtc": "^0.3.0", + "glob": "^7.1.4", "interface-datastore": "^0.6.0", + "it-pair": "^1.0.0", "libp2p-bootstrap": "^0.9.7", "libp2p-delegated-content-routing": "^0.2.2", "libp2p-delegated-peer-routing": "^0.2.2", @@ -88,7 +94,7 @@ "libp2p-gossipsub": "~0.0.4", "libp2p-kad-dht": "^0.15.3", "libp2p-mdns": "^0.12.3", - "libp2p-mplex": "^0.8.4", + "libp2p-mplex": "^0.9.1", "libp2p-pnet": "~0.1.0", "libp2p-secio": "^0.11.1", "libp2p-spdy": "^0.13.2", @@ -96,6 +102,7 @@ "libp2p-websockets": "^0.13.0", "lodash.times": "^4.3.2", "nock": "^10.0.6", + "p-defer": "^3.0.0", "portfinder": "^1.0.20", "pull-goodbye": "0.0.2", "pull-length-prefixed": "^1.3.3", @@ -104,6 +111,7 @@ "pull-protocol-buffers": "~0.1.2", "pull-serializer": "^0.3.2", "sinon": "^7.2.7", + "streaming-iterables": "^4.1.0", "wrtc": "^0.4.1" }, "contributors": [ diff --git a/src/circuit/README.md b/src/circuit/README.md index 5c67474ddc..36b659f7ff 100644 --- a/src/circuit/README.md +++ b/src/circuit/README.md @@ -1,6 +1,6 @@ # js-libp2p-circuit -> Node.js implementation of the Circuit module that libp2p uses, which implements the [interface-connection](https://github.com/libp2p/interface-connection) interface for dial/listen. +> Node.js implementation of the Circuit module that libp2p uses, which implements the [interface-connection](https://github.com/libp2p/js-interfaces/tree/master/src/connection) interface for dial/listen. **Note**: git history prior to merging into js-libp2p can be found in the original repository, https://github.com/libp2p/js-libp2p-circuit. @@ -24,15 +24,18 @@ Prior to `libp2p-circuit` there was a rift in the IPFS network, were IPFS nodes ## Table of Contents -- [Install](#install) - - [npm](#npm) -- [Usage](#usage) - - [Example](#example) - - [This module uses `pull-streams`](#this-module-uses-pull-streams) - - [Converting `pull-streams` to Node.js Streams](#converting-pull-streams-to-nodejs-streams) -- [API](#api) -- [Contribute](#contribute) -- [License](#license) +- [js-libp2p-circuit](#js-libp2p-circuit) + - [Why?](#why) + - [libp2p-circuit and IPFS](#libp2p-circuit-and-ipfs) + - [Table of Contents](#table-of-contents) + - [Usage](#usage) + - [Example](#example) + - [Create dialer/listener](#create-dialerlistener) + - [Create `relay`](#create-relay) + - [This module uses `pull-streams`](#this-module-uses-pull-streams) + - [Converting `pull-streams` to Node.js Streams](#converting-pull-streams-to-nodejs-streams) + - [API](#api) + - [Implementation rational](#implementation-rational) ## Usage diff --git a/src/circuit/circuit/dialer.js b/src/circuit/circuit/dialer.js index 2407c40f15..2c250e2c22 100644 --- a/src/circuit/circuit/dialer.js +++ b/src/circuit/circuit/dialer.js @@ -6,7 +6,7 @@ const waterfall = require('async/waterfall') const setImmediate = require('async/setImmediate') const multiaddr = require('multiaddr') -const Connection = require('interface-connection').Connection +const { Connection } = require('libp2p-interfaces/src/connection') const utilsFactory = require('./utils') const StreamHandler = require('./stream-handler') diff --git a/src/circuit/circuit/stop.js b/src/circuit/circuit/stop.js index f420c8f6e8..e18ef028f8 100644 --- a/src/circuit/circuit/stop.js +++ b/src/circuit/circuit/stop.js @@ -3,7 +3,7 @@ const setImmediate = require('async/setImmediate') const EE = require('events').EventEmitter -const Connection = require('interface-connection').Connection +const { Connection } = require('libp2p-interfaces/src/connection') const utilsFactory = require('./utils') const PeerInfo = require('peer-info') const proto = require('../protocol').CircuitRelay diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000000..72c442d275 --- /dev/null +++ b/src/constants.js @@ -0,0 +1,12 @@ +'use strict' + +module.exports = { + DENY_TTL: 5 * 60 * 1e3, // How long before an errored peer can be dialed again + DENY_ATTEMPTS: 5, // Num of unsuccessful dials before a peer is permanently denied + DIAL_TIMEOUT: 30e3, // How long in ms a dial attempt is allowed to take + MAX_COLD_CALLS: 50, // How many dials w/o protocols that can be queued + MAX_PARALLEL_DIALS: 100, // Maximum allowed concurrent dials + QUARTER_HOUR: 15 * 60e3, + PRIORITY_HIGH: 10, + PRIORITY_LOW: 20 +} diff --git a/src/dialer.js b/src/dialer.js new file mode 100644 index 0000000000..2177ccdad6 --- /dev/null +++ b/src/dialer.js @@ -0,0 +1,98 @@ +'use strict' + +const multiaddr = require('multiaddr') +const errCode = require('err-code') +const { default: PQueue } = require('p-queue') +const AbortController = require('abort-controller') +const debug = require('debug') +const log = debug('libp2p:dialer') +log.error = debug('libp2p:dialer:error') + +const { codes } = require('./errors') +const { + MAX_PARALLEL_DIALS, + DIAL_TIMEOUT +} = require('./constants') + +class Dialer { + /** + * @constructor + * @param {object} options + * @param {TransportManager} options.transportManager + * @param {number} options.concurrency Number of max concurrent dials. Defaults to `MAX_PARALLEL_DIALS` + * @param {number} options.timeout How long a dial attempt is allowed to take. Defaults to `DIAL_TIMEOUT` + */ + constructor ({ + transportManager, + concurrency = MAX_PARALLEL_DIALS, + timeout = DIAL_TIMEOUT + }) { + this.transportManager = transportManager + this.concurrency = concurrency + this.timeout = timeout + this.queue = new PQueue({ concurrency, timeout, throwOnTimeout: true }) + } + + /** + * Connects to a given `Multiaddr`. `addr` should include the id of the peer being + * dialed, it will be used for encryption verification. + * + * @async + * @param {Multiaddr} addr The address to dial + * @param {object} [options] + * @param {AbortSignal} [options.signal] An AbortController signal + * @returns {Promise} + */ + async connectToMultiaddr (addr, options = {}) { + addr = multiaddr(addr) + let conn + let controller + + if (!options.signal) { + controller = new AbortController() + options.signal = controller.signal + } + + try { + conn = await this.queue.add(() => this.transportManager.dial(addr, options)) + } catch (err) { + if (err.name === 'TimeoutError') { + controller.abort() + err.code = codes.ERR_TIMEOUT + } + log.error('Error dialing address %s,', addr, err) + throw err + } + + return conn + } + + /** + * Connects to a given `PeerInfo` by dialing all of its known addresses. + * The dial to the first address that is successfully able to upgrade a connection + * will be used. + * + * @async + * @param {PeerInfo} peerInfo The remote peer to dial + * @param {object} [options] + * @param {AbortSignal} [options.signal] An AbortController signal + * @returns {Promise} + */ + async connectToPeer (peerInfo, options = {}) { + const addrs = peerInfo.multiaddrs.toArray() + for (const addr of addrs) { + try { + return await this.connectToMultiaddr(addr, options) + } catch (_) { + // The error is already logged, just move to the next addr + continue + } + } + + const err = errCode(new Error('Could not dial peer, all addresses failed'), codes.ERR_CONNECTION_FAILED) + log.error(err) + throw err + } +} + +module.exports = Dialer diff --git a/src/errors.js b/src/errors.js index 5219891a3b..de75f094ff 100644 --- a/src/errors.js +++ b/src/errors.js @@ -8,10 +8,16 @@ exports.messages = { exports.codes = { DHT_DISABLED: 'ERR_DHT_DISABLED', PUBSUB_NOT_STARTED: 'ERR_PUBSUB_NOT_STARTED', + ERR_CONNECTION_FAILED: 'ERR_CONNECTION_FAILED', ERR_NODE_NOT_STARTED: 'ERR_NODE_NOT_STARTED', ERR_NO_VALID_ADDRESSES: 'ERR_NO_VALID_ADDRESSES', ERR_DISCOVERED_SELF: 'ERR_DISCOVERED_SELF', ERR_DUPLICATE_TRANSPORT: 'ERR_DUPLICATE_TRANSPORT', + ERR_ENCRYPTION_FAILED: 'ERR_ENCRYPTION_FAILED', ERR_INVALID_KEY: 'ERR_INVALID_KEY', - ERR_TRANSPORT_UNAVAILABLE: 'ERR_TRANSPORT_UNAVAILABLE' + ERR_MUXER_UNAVAILABLE: 'ERR_MUXER_UNAVAILABLE', + ERR_TIMEOUT: 'ERR_TIMEOUT', + ERR_TRANSPORT_UNAVAILABLE: 'ERR_TRANSPORT_UNAVAILABLE', + ERR_TRANSPORT_DIAL_FAILED: 'ERR_TRANSPORT_DIAL_FAILED', + ERR_UNSUPPORTED_PROTOCOL: 'ERR_UNSUPPORTED_PROTOCOL' } diff --git a/src/index.js b/src/index.js index b973739b6c..3d5cf8c219 100644 --- a/src/index.js +++ b/src/index.js @@ -13,20 +13,22 @@ const nextTick = require('async/nextTick') const PeerBook = require('peer-book') const PeerInfo = require('peer-info') +const multiaddr = require('multiaddr') const Switch = require('./switch') const Ping = require('./ping') -const ConnectionManager = require('./connection-manager') const { emitFirst } = require('./util') const peerRouting = require('./peer-routing') const contentRouting = require('./content-routing') const dht = require('./dht') const pubsub = require('./pubsub') -const { getPeerInfoRemote } = require('./get-peer-info') -const validateConfig = require('./config').validate +const { getPeerInfo, getPeerInfoRemote } = require('./get-peer-info') +const { validate: validateConfig } = require('./config') const { codes } = require('./errors') +const Dialer = require('./dialer') const TransportManager = require('./transport-manager') +const Upgrader = require('./upgrader') const notStarted = (action, state) => { return errCode( @@ -61,64 +63,49 @@ class Libp2p extends EventEmitter { // create the switch, and listen for errors this._switch = new Switch(this.peerInfo, this.peerBook, this._options.switch) - this._switch.on('error', (...args) => this.emit('error', ...args)) - this.stats = this._switch.stats - this.connectionManager = new ConnectionManager(this, this._options.connectionManager) + // Setup the Upgrader + this.upgrader = new Upgrader({ + localPeer: this.peerInfo.id, + onConnection: (connection) => { + const peerInfo = getPeerInfo(connection.remotePeer) + this.emit('peer:connect', peerInfo) + }, + onConnectionEnd: (connection) => { + const peerInfo = getPeerInfo(connection.remotePeer) + this.emit('peer:disconnect', peerInfo) + } + }) // Setup the transport manager this.transportManager = new TransportManager({ libp2p: this, - // TODO: set the actual upgrader - upgrader: { - upgradeInbound: (maConn) => maConn, - upgradeOutbound: (maConn) => maConn - }, - // TODO: Route incoming connections to a multiplex protocol router - onConnection: () => {} + upgrader: this.upgrader }) this._modules.transport.forEach((Transport) => { this.transportManager.add(Transport.prototype[Symbol.toStringTag], Transport) }) + // Attach crypto channels + if (this._modules.connEncryption) { + const cryptos = this._modules.connEncryption + cryptos.forEach((crypto) => { + this.upgrader.cryptos.set(crypto.tag, crypto) + }) + } + // Attach stream multiplexers if (this._modules.streamMuxer) { const muxers = this._modules.streamMuxer - muxers.forEach((muxer) => this._switch.connection.addStreamMuxer(muxer)) - - // If muxer exists - // we can use Identify - this._switch.connection.reuse() - // we can use Relay for listening/dialing - this._switch.connection.enableCircuitRelay(this._config.relay) - - // Received incomming dial and muxer upgrade happened, - // reuse this muxed connection - this._switch.on('peer-mux-established', (peerInfo) => { - this.emit('peer:connect', peerInfo) - }) - - this._switch.on('peer-mux-closed', (peerInfo) => { - this.emit('peer:disconnect', peerInfo) + muxers.forEach((muxer) => { + this.upgrader.muxers.set(muxer.multicodec, muxer) }) } - // Events for anytime connections are created/removed - this._switch.on('connection:start', (peerInfo) => { - this.emit('connection:start', peerInfo) - }) - this._switch.on('connection:end', (peerInfo) => { - this.emit('connection:end', peerInfo) + this.dialer = new Dialer({ + transportManager: this.transportManager }) - // Attach crypto channels - if (this._modules.connEncryption) { - const cryptos = this._modules.connEncryption - cryptos.forEach((crypto) => { - this._switch.connection.crypto(crypto.tag, crypto.encrypt) - }) - } - // Attach private network protector if (this._modules.connProtector) { this._switch.protector = this._modules.connProtector @@ -153,7 +140,8 @@ class Libp2p extends EventEmitter { this.state = new FSM('STOPPED', { STOPPED: { start: 'STARTING', - stop: 'STOPPED' + stop: 'STOPPED', + done: 'STOPPED' }, STARTING: { done: 'STARTED', @@ -175,7 +163,6 @@ class Libp2p extends EventEmitter { }) this.state.on('STOPPING', () => { log('libp2p is stopping') - this._onStopping() }) this.state.on('STARTED', () => { log('libp2p has started') @@ -201,7 +188,7 @@ class Libp2p extends EventEmitter { this._peerDiscovered = this._peerDiscovered.bind(this) // promisify all instance methods - ;['start', 'stop', 'dial', 'dialProtocol', 'dialFSM', 'hangUp', 'ping'].forEach(method => { + ;['start', 'hangUp', 'ping'].forEach(method => { this[method] = promisify(this[method], { context: this }) }) } @@ -234,13 +221,21 @@ class Libp2p extends EventEmitter { /** * Stop the libp2p node by closing its listeners and open connections - * - * @param {function(Error)} callback + * @async * @returns {void} */ - stop (callback = () => {}) { - emitFirst(this, ['error', 'stop'], callback) + async stop () { this.state('stop') + + try { + await this.transportManager.close() + } catch (err) { + if (err) { + log.error(err) + this.emit('error', err) + } + } + this.state('done') } isStarted () { @@ -252,11 +247,12 @@ class Libp2p extends EventEmitter { * peer will be added to the nodes `PeerBook` * * @param {PeerInfo|PeerId|Multiaddr|string} peer The peer to dial - * @param {function(Error)} callback - * @returns {void} + * @param {object} options + * @param {AbortSignal} [options.signal] + * @returns {Promise} */ - dial (peer, callback) { - this.dialProtocol(peer, null, callback) + dial (peer, options) { + return this.dialProtocol(peer, null, options) } /** @@ -264,50 +260,28 @@ class Libp2p extends EventEmitter { * If successful, the `PeerInfo` of the peer will be added to the nodes `PeerBook`, * and the `Connection` will be sent in the callback * + * @async * @param {PeerInfo|PeerId|Multiaddr|string} peer The peer to dial - * @param {string} protocol - * @param {function(Error, Connection)} callback - * @returns {void} + * @param {string[]|string} protocols + * @param {object} options + * @param {AbortSignal} [options.signal] + * @returns {Promise} */ - dialProtocol (peer, protocol, callback) { - if (!this.isStarted()) { - return callback(notStarted('dial', this.state._state)) + async dialProtocol (peer, protocols, options) { + let connection + if (multiaddr.isMultiaddr(peer)) { + connection = await this.dialer.connectToMultiaddr(peer, options) + } else { + peer = await getPeerInfoRemote(peer, this) + connection = await this.dialer.connectToPeer(peer, options) } - if (typeof protocol === 'function') { - callback = protocol - protocol = undefined + // If a protocol was provided, create a new stream + if (protocols) { + return connection.newStream(protocols) } - getPeerInfoRemote(peer, this) - .then(peerInfo => { - this._switch.dial(peerInfo, protocol, callback) - }, callback) - } - - /** - * Similar to `dial` and `dialProtocol`, but the callback will contain a - * Connection State Machine. - * - * @param {PeerInfo|PeerId|Multiaddr|string} peer The peer to dial - * @param {string} protocol - * @param {function(Error, ConnectionFSM)} callback - * @returns {void} - */ - dialFSM (peer, protocol, callback) { - if (!this.isStarted()) { - return callback(notStarted('dial', this.state._state)) - } - - if (typeof protocol === 'function') { - callback = protocol - protocol = undefined - } - - getPeerInfoRemote(peer, this) - .then(peerInfo => { - this._switch.dialFSM(peerInfo, protocol, callback) - }, callback) + return connection } /** @@ -342,12 +316,28 @@ class Libp2p extends EventEmitter { }, callback) } - handle (protocol, handlerFunc, matchFunc) { - this._switch.handle(protocol, handlerFunc, matchFunc) + /** + * Registers the `handler` for each protocol + * @param {string[]|string} protocols + * @param {function({ stream:*, protocol:string })} handler + */ + handle (protocols, handler) { + protocols = Array.isArray(protocols) ? protocols : [protocols] + protocols.forEach(protocol => { + this.upgrader.protocols.set(protocol, handler) + }) } - unhandle (protocol) { - this._switch.unhandle(protocol) + /** + * Removes the handler for each protocol. The protocol + * will no longer be supported on streams. + * @param {string[]|string} protocols + */ + unhandle (protocols) { + protocols = Array.isArray(protocols) ? protocols : [protocols] + protocols.forEach(protocol => { + this.upgrader.protocols.delete(protocol) + }) } async _onStarting () { @@ -373,21 +363,6 @@ class Libp2p extends EventEmitter { this.state('done') } - async _onStopping () { - // Start parallel tasks - try { - await this.transportManager.close() - } catch (err) { - if (err) { - log.error(err) - this.emit('error', err) - } - } - - // libp2p has stopped - this.state('done') - } - /** * Handles discovered peers. Each discovered peer will be emitted via * the `peer:discovery` event. If auto dial is enabled for libp2p diff --git a/src/pnet/index.js b/src/pnet/index.js index a6480dca27..db79e3bc44 100644 --- a/src/pnet/index.js +++ b/src/pnet/index.js @@ -1,7 +1,7 @@ 'use strict' const pull = require('pull-stream') -const Connection = require('interface-connection').Connection +const { Connection } = require('libp2p-interfaces/src/connection') const assert = require('assert') const Errors = require('./errors') diff --git a/src/switch/README.md b/src/switch/README.md index 2ceec4bbeb..3ca9e9d595 100644 --- a/src/switch/README.md +++ b/src/switch/README.md @@ -81,7 +81,7 @@ tests]([./test/pnet.node.js]). ##### `switch.connection.addUpgrade()` -A connection upgrade must be able to receive and return something that implements the [interface-connection](https://github.com/libp2p/interface-connection) specification. +A connection upgrade must be able to receive and return something that implements the [interface-connection](https://github.com/libp2p/js-interfaces/tree/master/src/connection) specification. > **WIP** @@ -151,7 +151,7 @@ a low priority dial to the provided peer. Calls to `dial` and `dialFSM` will tak - `error`: emitted whenever a fatal error occurs with the connection; the error will be emitted. - `error:upgrade_failed`: emitted whenever the connection fails to upgrade with a muxer, this is not fatal. - `error:connection_attempt_failed`: emitted whenever a dial attempt fails for a given transport. An array of errors is emitted. -- `connection`: emitted whenever a useable connection has been established; the underlying [Connection](https://github.com/libp2p/interface-connection) will be emitted. +- `connection`: emitted whenever a useable connection has been established; the underlying [Connection](https://github.com/libp2p/js-interfaces/tree/master/src/connection) will be emitted. - `close`: emitted when the connection has closed. ### `switch.handle(protocol, handlerFunc, matchFunc)` @@ -365,7 +365,7 @@ In order for a transport to be supported, it has to follow the [interface-transp ### Connection upgrades -Each connection in libp2p follows the [interface-connection](https://github.com/libp2p/interface-connection) spec. This design decision enables libp2p to have upgradable transports. +Each connection in libp2p follows the [interface-connection](https://github.com/libp2p/js-interfaces/tree/master/src/connection) spec. This design decision enables libp2p to have upgradable transports. We think of `upgrade` as a very important notion when we are talking about connections, we can see mechanisms like: stream multiplexing, congestion control, encrypted channels, multipath, simulcast, etc, as `upgrades` to a connection. A connection can be a simple and with no guarantees, drop a packet on the network with a destination thing, a transport in the other hand can be a connection and or a set of different upgrades that are mounted on top of each other, giving extra functionality to that connection and therefore `upgrading` it. diff --git a/src/switch/limit-dialer/queue.js b/src/switch/limit-dialer/queue.js index 344997adf9..94d38c5bf6 100644 --- a/src/switch/limit-dialer/queue.js +++ b/src/switch/limit-dialer/queue.js @@ -1,6 +1,6 @@ 'use strict' -const Connection = require('interface-connection').Connection +const { Connection } = require('libp2p-interfaces/src/connection') const pull = require('pull-stream/pull') const empty = require('pull-stream/sources/empty') const timeout = require('async/timeout') diff --git a/src/switch/observe-connection.js b/src/switch/observe-connection.js index c6e928c04e..45f87cd0ba 100644 --- a/src/switch/observe-connection.js +++ b/src/switch/observe-connection.js @@ -1,6 +1,6 @@ 'use strict' -const Connection = require('interface-connection').Connection +const { Connection } = require('libp2p-interfaces/src/connection') const pull = require('pull-stream/pull') /** diff --git a/src/transport-manager.js b/src/transport-manager.js index 371f2f57b8..136dc8c8a2 100644 --- a/src/transport-manager.js +++ b/src/transport-manager.js @@ -13,14 +13,12 @@ class TransportManager { * @param {object} options * @param {Libp2p} options.libp2p The Libp2p instance. It will be passed to the transports. * @param {Upgrader} options.upgrader The upgrader to provide to the transports - * @param {function(Connection)} options.onConnection Called whenever an incoming connection is received */ - constructor ({ libp2p, upgrader, onConnection }) { + constructor ({ libp2p, upgrader }) { this.libp2p = libp2p this.upgrader = upgrader this._transports = new Map() this._listeners = new Map() - this.onConnection = onConnection } /** @@ -45,7 +43,9 @@ class TransportManager { }) this._transports.set(key, transport) - this._listeners.set(key, []) + if (!this._listeners.has(key)) { + this._listeners.set(key, []) + } } /** @@ -57,11 +57,13 @@ class TransportManager { for (const [key, listeners] of this._listeners) { log('closing listeners for %s', key) while (listeners.length) { - tasks.push(listeners.pop().close()) + const listener = listeners.pop() + tasks.push(listener.close()) } } await Promise.all(tasks) + log('all listeners closed') this._listeners.clear() } @@ -76,8 +78,12 @@ class TransportManager { if (!transport) { throw errCode(new Error(`No transport available for address ${String(ma)}`), codes.ERR_TRANSPORT_UNAVAILABLE) } - const conn = await transport.dial(ma, options) - return conn + + try { + return await transport.dial(ma, options) + } catch (err) { + throw errCode(new Error('Transport dial failed'), codes.ERR_TRANSPORT_DIAL_FAILED, err) + } } /** diff --git a/src/upgrader.js b/src/upgrader.js new file mode 100644 index 0000000000..e470d81c1c --- /dev/null +++ b/src/upgrader.js @@ -0,0 +1,336 @@ +'use strict' + +const debug = require('debug') +const log = debug('libp2p:upgrader') +log.error = debug('libp2p:upgrader:error') +const Multistream = require('multistream-select') +const { Connection } = require('libp2p-interfaces/src/connection') +const PeerId = require('peer-id') +const pipe = require('it-pipe') +const errCode = require('err-code') + +const { codes } = require('./errors') + +/** + * @typedef MultiaddrConnection + * @property {function} sink + * @property {AsyncIterator} source + * @property {*} conn + * @property {Multiaddr} remoteAddr + */ + +/** + * @typedef CryptoResult + * @property {*} conn A duplex iterable + * @property {PeerId} remotePeer + * @property {string} protocol + */ + +class Upgrader { + /** + * @param {object} options + * @param {PeerId} options.localPeer + * @param {Map} options.cryptos + * @param {Map} options.muxers + */ + constructor ({ localPeer, cryptos, muxers, onConnectionEnd = () => {}, onConnection = () => {} }) { + this.localPeer = localPeer + this.cryptos = cryptos || new Map() + this.muxers = muxers || new Map() + this.protocols = new Map() + this.onConnection = onConnection + this.onConnectionEnd = onConnectionEnd + } + + /** + * Upgrades an inbound connection + * @async + * @param {MultiaddrConnection} maConn + * @returns {Promise} + */ + async upgradeInbound (maConn) { + let encryptedConn + let remotePeer + let muxedConnection + let Muxer + let cryptoProtocol + + try { + // Encrypt the connection + ({ + conn: encryptedConn, + remotePeer, + protocol: cryptoProtocol + } = await this._encryptInbound(this.localPeer, maConn, this.cryptos)) + + // Multiplex the connection + ;({ stream: muxedConnection, Muxer } = await this._multiplexInbound(encryptedConn, this.muxers)) + } catch (err) { + log.error('Failed to upgrade inbound connection', err) + await maConn.close(err) + // TODO: We shouldn't throw here, as there isn't anything to catch the failure + throw err + } + + log('Successfully upgraded inbound connection') + + return this._createConnection({ + cryptoProtocol, + direction: 'inbound', + maConn, + muxedConnection, + Muxer, + remotePeer + }) + } + + /** + * Upgrades an outbound connection + * @async + * @param {MultiaddrConnection} maConn + * @returns {Promise} + */ + async upgradeOutbound (maConn) { + let remotePeerId + try { + remotePeerId = PeerId.createFromB58String(maConn.remoteAddr.getPeerId()) + } catch (err) { + log.error('multiaddr did not contain a valid peer id', err) + } + + let encryptedConn + let remotePeer + let muxedConnection + let cryptoProtocol + let Muxer + + try { + // Encrypt the connection + ({ + conn: encryptedConn, + remotePeer, + protocol: cryptoProtocol + } = await this._encryptOutbound(this.localPeer, maConn, remotePeerId, this.cryptos)) + + // Multiplex the connection + ;({ stream: muxedConnection, Muxer } = await this._multiplexOutbound(encryptedConn, this.muxers)) + } catch (err) { + log.error('Failed to upgrade outbound connection', err) + await maConn.close(err) + throw err + } + + log('Successfully upgraded outbound connection') + + return this._createConnection({ + cryptoProtocol, + direction: 'outbound', + maConn, + muxedConnection, + Muxer, + remotePeer + }) + } + + /** + * A convenience method for generating a new `Connection` + * @private + * @param {object} options + * @param {string} cryptoProtocol The crypto protocol that was negotiated + * @param {string} direction One of ['inbound', 'outbound'] + * @param {MultiaddrConnection} maConn The transport layer connection + * @param {*} muxedConnection A duplex connection returned from multiplexer selection + * @param {Muxer} Muxer The muxer to be used for muxing + * @param {PeerId} remotePeer The peer the connection is with + * @returns {Connection} + */ + _createConnection ({ + cryptoProtocol, + direction, + maConn, + muxedConnection, + Muxer, + remotePeer + }) { + // Create the muxer + const muxer = new Muxer({ + // Run anytime a remote stream is created + onStream: async muxedStream => { + const mss = new Multistream.Listener(muxedStream) + const { stream, protocol } = await mss.handle(Array.from(this.protocols.keys())) + log('%s: incoming stream opened on %s', direction, protocol) + connection.addStream(stream, protocol) + this._onStream({ stream, protocol }) + }, + // Run anytime a stream closes + onStreamEnd: muxedStream => { + connection.removeStream(muxedStream.id) + } + }) + + const newStream = async protocols => { + log('%s: starting new stream on %s', direction, protocols) + const muxedStream = muxer.newStream() + const mss = new Multistream.Dialer(muxedStream) + try { + const { stream, protocol } = await mss.select(protocols) + return { stream: { ...muxedStream, ...stream }, protocol } + } catch (err) { + log.error('could not create new stream', err) + throw errCode(err, codes.ERR_UNSUPPORTED_PROTOCOL) + } + } + + // Pipe all data through the muxer + pipe(muxedConnection, muxer, muxedConnection) + + maConn.timeline.upgraded = Date.now() + const timelineProxy = new Proxy(maConn.timeline, { + set: (...args) => { + if (args[1] === 'close' && args[2]) { + this.onConnectionEnd(connection) + } + + return Reflect.set(...args) + } + }) + + // Create the connection + const connection = new Connection({ + localAddr: maConn.localAddr, + remoteAddr: maConn.remoteAddr, + localPeer: this.localPeer, + remotePeer: remotePeer, + stat: { + direction, + timeline: timelineProxy, + multiplexer: Muxer.multicodec, + encryption: cryptoProtocol + }, + newStream, + getStreams: () => muxer.streams, + close: err => maConn.close(err) + }) + + this.onConnection(connection) + + return connection + } + + /** + * Routes incoming streams to the correct handler + * @private + * @param {object} options + * @param {Stream} options.stream + * @param {string} protocol + */ + _onStream ({ stream, protocol }) { + const handler = this.protocols.get(protocol) + handler({ stream, protocol }) + } + + /** + * Attempts to encrypt the incoming `connection` with the provided `cryptos`. + * @private + * @async + * @param {PeerId} localPeer The initiators PeerInfo + * @param {*} connection + * @param {Map} cryptos + * @returns {CryptoResult} An encrypted connection, remote peer `PeerId` and the protocol of the `Crypto` used + */ + async _encryptInbound (localPeer, connection, cryptos) { + const mss = new Multistream.Listener(connection) + const protocols = Array.from(cryptos.keys()) + log('selecting inbound crypto protocol', protocols) + + try { + const { stream, protocol } = await mss.handle(protocols) + const crypto = cryptos.get(protocol) + log('encrypting inbound connection...') + + return { + ...await crypto.secureInbound(localPeer, stream), + protocol + } + } catch (err) { + throw errCode(err, codes.ERR_ENCRYPTION_FAILED) + } + } + + /** + * Attempts to encrypt the given `connection` with the provided `cryptos`. + * The first `Crypto` module to succeed will be used + * @private + * @async + * @param {PeerId} localPeer The initiators PeerInfo + * @param {*} connection + * @param {PeerId} remotePeerId + * @param {Map} cryptos + * @returns {CryptoResult} An encrypted connection, remote peer `PeerId` and the protocol of the `Crypto` used + */ + async _encryptOutbound (localPeer, connection, remotePeerId, cryptos) { + const mss = new Multistream.Dialer(connection) + const protocols = Array.from(cryptos.keys()) + log('selecting outbound crypto protocol', protocols) + + try { + const { stream, protocol } = await mss.select(protocols) + const crypto = cryptos.get(protocol) + log('encrypting outbound connection to %j', remotePeerId) + + return { + ...await crypto.secureOutbound(localPeer, stream, remotePeerId), + protocol + } + } catch (err) { + throw errCode(err, codes.ERR_ENCRYPTION_FAILED) + } + } + + /** + * Selects one of the given muxers via multistream-select. That + * muxer will be used for all future streams on the connection. + * @private + * @async + * @param {*} connection A basic duplex connection to multiplex + * @param {Map} muxers The muxers to attempt multiplexing with + * @returns {*} A muxed connection + */ + async _multiplexOutbound (connection, muxers) { + const dialer = new Multistream.Dialer(connection) + const protocols = Array.from(muxers.keys()) + log('outbound selecting muxer %s', protocols) + try { + const { stream, protocol } = await dialer.select(protocols) + log('%s selected as muxer protocol', protocol) + const Muxer = muxers.get(protocol) + return { stream, Muxer } + } catch (err) { + throw errCode(err, codes.ERR_MUXER_UNAVAILABLE) + } + } + + /** + * Registers support for one of the given muxers via multistream-select. The + * selected muxer will be used for all future streams on the connection. + * @private + * @async + * @param {*} connection A basic duplex connection to multiplex + * @param {Map} muxers The muxers to attempt multiplexing with + * @returns {*} A muxed connection + */ + async _multiplexInbound (connection, muxers) { + const listener = new Multistream.Listener(connection) + const protocols = Array.from(muxers.keys()) + log('inbound handling muxers %s', protocols) + try { + const { stream, protocol } = await listener.handle(protocols) + const Muxer = muxers.get(protocol) + return { stream, Muxer } + } catch (err) { + throw errCode(err, codes.ERR_MUXER_UNAVAILABLE) + } + } +} + +module.exports = Upgrader diff --git a/test/dialing/direct.node.js b/test/dialing/direct.node.js new file mode 100644 index 0000000000..071ad900aa --- /dev/null +++ b/test/dialing/direct.node.js @@ -0,0 +1,236 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +const { expect } = chai +const sinon = require('sinon') +const Transport = require('libp2p-tcp') +const Muxer = require('libp2p-mplex') +const multiaddr = require('multiaddr') +const PeerId = require('peer-id') +const PeerInfo = require('peer-info') +const delay = require('delay') +const pDefer = require('p-defer') +const pipe = require('it-pipe') + +const Libp2p = require('../../src') +const Dialer = require('../../src/dialer') +const TransportManager = require('../../src/transport-manager') +const { codes: ErrorCodes } = require('../../src/errors') + +const mockUpgrader = require('../utils/mockUpgrader') +const mockCrypto = require('../utils/mockCrypto') +const Peers = require('../fixtures/peers') + +const listenAddr = multiaddr('/ip4/127.0.0.1/tcp/0') +const unsupportedAddr = multiaddr('/ip4/127.0.0.1/tcp/9999/ws') + +describe('Dialing (direct, TCP)', () => { + let remoteTM + let localTM + let remoteAddr + + before(async () => { + remoteTM = new TransportManager({ + libp2p: {}, + upgrader: mockUpgrader + }) + remoteTM.add(Transport.prototype[Symbol.toStringTag], Transport) + + localTM = new TransportManager({ + libp2p: {}, + upgrader: mockUpgrader + }) + localTM.add(Transport.prototype[Symbol.toStringTag], Transport) + + await remoteTM.listen([listenAddr]) + + remoteAddr = remoteTM.getAddrs()[0] + }) + + after(async () => { + await remoteTM.close() + }) + + afterEach(() => { + sinon.restore() + }) + + it('should be able to connect to a remote node via its multiaddr', async () => { + const dialer = new Dialer({ transportManager: localTM }) + + const connection = await dialer.connectToMultiaddr(remoteAddr) + expect(connection).to.exist() + await connection.close() + }) + + it('should be able to connect to a remote node via its stringified multiaddr', async () => { + const dialer = new Dialer({ transportManager: localTM }) + + const connection = await dialer.connectToMultiaddr(remoteAddr.toString()) + expect(connection).to.exist() + await connection.close() + }) + + it('should fail to connect to an unsupported multiaddr', async () => { + const dialer = new Dialer({ transportManager: localTM }) + + try { + await dialer.connectToMultiaddr(unsupportedAddr) + } catch (err) { + expect(err).to.satisfy((err) => err.code === ErrorCodes.ERR_TRANSPORT_UNAVAILABLE) + return + } + + expect.fail('Dial should have failed') + }) + + it('should be able to connect to a given peer', async () => { + const dialer = new Dialer({ transportManager: localTM }) + const peerId = await PeerId.createFromJSON(Peers[0]) + const peerInfo = new PeerInfo(peerId) + peerInfo.multiaddrs.add(remoteAddr) + + const connection = await dialer.connectToPeer(peerInfo) + expect(connection).to.exist() + await connection.close() + }) + + it('should fail to connect to a given peer with unsupported addresses', async () => { + const dialer = new Dialer({ transportManager: localTM }) + const peerId = await PeerId.createFromJSON(Peers[0]) + const peerInfo = new PeerInfo(peerId) + peerInfo.multiaddrs.add(unsupportedAddr) + + try { + await dialer.connectToPeer(peerInfo) + } catch (err) { + expect(err).to.satisfy((err) => err.code === ErrorCodes.ERR_CONNECTION_FAILED) + return + } + + expect.fail('Dial should have failed') + }) + + it('should abort dials on queue task timeout', async () => { + const dialer = new Dialer({ + transportManager: localTM, + timeout: 50 + }) + sinon.stub(localTM, 'dial').callsFake(async (addr, options) => { + expect(options.signal).to.exist() + expect(options.signal.aborted).to.equal(false) + expect(addr.toString()).to.eql(remoteAddr.toString()) + await delay(60) + expect(options.signal.aborted).to.equal(true) + }) + + try { + await dialer.connectToMultiaddr(remoteAddr) + } catch (err) { + expect(err).to.satisfy((err) => err.code === ErrorCodes.ERR_TIMEOUT) + return + } + + expect.fail('Dial should have failed') + }) + + it('should dial to the max concurrency', async () => { + const dialer = new Dialer({ + transportManager: localTM, + concurrency: 2 + }) + + const deferredDial = pDefer() + sinon.stub(localTM, 'dial').callsFake(async () => { + await deferredDial.promise + }) + + // Add 3 dials + Promise.all([ + dialer.connectToMultiaddr(remoteAddr), + dialer.connectToMultiaddr(remoteAddr), + dialer.connectToMultiaddr(remoteAddr) + ]) + + // Let the call stack run + await delay(0) + + // We should have 2 in progress, and 1 waiting + expect(localTM.dial.callCount).to.equal(2) + expect(dialer.queue.pending).to.equal(2) + expect(dialer.queue.size).to.equal(1) + + deferredDial.resolve() + + // Let the call stack run + await delay(0) + // All dials should have executed + expect(localTM.dial.callCount).to.equal(3) + expect(dialer.queue.pending).to.equal(0) + expect(dialer.queue.size).to.equal(0) + }) +}) + +describe('libp2p.dialer', () => { + let peerInfo + let remotePeerInfo + let libp2p + let remoteLibp2p + let remoteAddr + + before(async () => { + const [peerId, remotePeerId] = await Promise.all([ + PeerId.createFromJSON(Peers[0]), + PeerId.createFromJSON(Peers[1]) + ]) + + peerInfo = new PeerInfo(peerId) + remotePeerInfo = new PeerInfo(remotePeerId) + + remoteLibp2p = new Libp2p({ + peerInfo: remotePeerInfo, + modules: { + transport: [Transport], + streamMuxer: [Muxer], + connEncryption: [mockCrypto] + } + }) + remoteLibp2p.handle('/echo/1.0.0', ({ stream }) => pipe(stream, stream)) + + await remoteLibp2p.transportManager.listen([listenAddr]) + remoteAddr = remoteLibp2p.transportManager.getAddrs()[0] + }) + + afterEach(async () => { + sinon.restore() + libp2p && await libp2p.stop() + libp2p = null + }) + + after(async () => { + await remoteLibp2p.stop() + }) + + it('should use the dialer for connecting', async () => { + libp2p = new Libp2p({ + peerInfo, + modules: { + transport: [Transport], + streamMuxer: [Muxer], + connEncryption: [mockCrypto] + } + }) + + sinon.spy(libp2p.dialer, 'connectToMultiaddr') + + const connection = await libp2p.dial(remoteAddr) + expect(connection).to.exist() + const { stream, protocol } = await connection.newStream('/echo/1.0.0') + expect(stream).to.exist() + expect(protocol).to.equal('/echo/1.0.0') + await connection.close() + expect(libp2p.dialer.connectToMultiaddr.callCount).to.equal(1) + }) +}) diff --git a/test/dialing/direct.spec.js b/test/dialing/direct.spec.js new file mode 100644 index 0000000000..bfc77b4d5a --- /dev/null +++ b/test/dialing/direct.spec.js @@ -0,0 +1,211 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +const { expect } = chai +const sinon = require('sinon') +const pDefer = require('p-defer') +const delay = require('delay') +const Transport = require('libp2p-websockets') +const Muxer = require('libp2p-mplex') +const multiaddr = require('multiaddr') +const PeerId = require('peer-id') +const PeerInfo = require('peer-info') + +const { codes: ErrorCodes } = require('../../src/errors') +const Constants = require('../../src/constants') +const Dialer = require('../../src/dialer') +const TransportManager = require('../../src/transport-manager') +const Libp2p = require('../../src') + +const Peers = require('../fixtures/peers') +const { MULTIADDRS_WEBSOCKETS } = require('../fixtures/browser') +const mockUpgrader = require('../utils/mockUpgrader') +const mockCrypto = require('../utils/mockCrypto') +const unsupportedAddr = multiaddr('/ip4/127.0.0.1/tcp/9999/ws') +const remoteAddr = MULTIADDRS_WEBSOCKETS[0] + +describe('Dialing (direct, WebSockets)', () => { + let localTM + + before(() => { + localTM = new TransportManager({ + libp2p: {}, + upgrader: mockUpgrader, + onConnection: () => {} + }) + localTM.add(Transport.prototype[Symbol.toStringTag], Transport) + }) + + afterEach(() => { + sinon.restore() + }) + + it('should have appropriate defaults', () => { + const dialer = new Dialer({ transportManager: localTM }) + expect(dialer.concurrency).to.equal(Constants.MAX_PARALLEL_DIALS) + expect(dialer.timeout).to.equal(Constants.DIAL_TIMEOUT) + }) + + it('should be able to connect to a remote node via its multiaddr', async () => { + const dialer = new Dialer({ transportManager: localTM }) + + const connection = await dialer.connectToMultiaddr(remoteAddr) + expect(connection).to.exist() + await connection.close() + }) + + it('should be able to connect to a remote node via its stringified multiaddr', async () => { + const dialer = new Dialer({ transportManager: localTM }) + + const connection = await dialer.connectToMultiaddr(remoteAddr.toString()) + expect(connection).to.exist() + await connection.close() + }) + + it('should fail to connect to an unsupported multiaddr', async () => { + const dialer = new Dialer({ transportManager: localTM }) + + try { + await dialer.connectToMultiaddr(unsupportedAddr) + } catch (err) { + expect(err).to.satisfy((err) => err.code === ErrorCodes.ERR_TRANSPORT_DIAL_FAILED) + return + } + + expect.fail('Dial should have failed') + }) + + it('should be able to connect to a given peer', async () => { + const dialer = new Dialer({ transportManager: localTM }) + const peerId = await PeerId.createFromJSON(Peers[0]) + const peerInfo = new PeerInfo(peerId) + peerInfo.multiaddrs.add(remoteAddr) + + const connection = await dialer.connectToPeer(peerInfo) + expect(connection).to.exist() + await connection.close() + }) + + it('should fail to connect to a given peer with unsupported addresses', async () => { + const dialer = new Dialer({ transportManager: localTM }) + const peerId = await PeerId.createFromJSON(Peers[0]) + const peerInfo = new PeerInfo(peerId) + peerInfo.multiaddrs.add(unsupportedAddr) + + try { + await dialer.connectToPeer(peerInfo) + } catch (err) { + expect(err).to.satisfy((err) => err.code === ErrorCodes.ERR_CONNECTION_FAILED) + return + } + + expect.fail('Dial should have failed') + }) + + it('should abort dials on queue task timeout', async () => { + const dialer = new Dialer({ + transportManager: localTM, + timeout: 50 + }) + sinon.stub(localTM, 'dial').callsFake(async (addr, options) => { + expect(options.signal).to.exist() + expect(options.signal.aborted).to.equal(false) + expect(addr.toString()).to.eql(remoteAddr.toString()) + await delay(60) + expect(options.signal.aborted).to.equal(true) + }) + + try { + await dialer.connectToMultiaddr(remoteAddr) + } catch (err) { + expect(err).to.satisfy((err) => err.code === ErrorCodes.ERR_TIMEOUT) + return + } + + expect.fail('Dial should have failed') + }) + + it('should dial to the max concurrency', async () => { + const dialer = new Dialer({ + transportManager: localTM, + concurrency: 2 + }) + + const deferredDial = pDefer() + sinon.stub(localTM, 'dial').callsFake(async () => { + await deferredDial.promise + }) + + // Add 3 dials + Promise.all([ + dialer.connectToMultiaddr(remoteAddr), + dialer.connectToMultiaddr(remoteAddr), + dialer.connectToMultiaddr(remoteAddr) + ]) + + // Let the call stack run + await delay(0) + + // We should have 2 in progress, and 1 waiting + expect(localTM.dial.callCount).to.equal(2) + expect(dialer.queue.pending).to.equal(2) + expect(dialer.queue.size).to.equal(1) + + deferredDial.resolve() + + // Let the call stack run + await delay(0) + // All dials should have executed + expect(localTM.dial.callCount).to.equal(3) + expect(dialer.queue.pending).to.equal(0) + expect(dialer.queue.size).to.equal(0) + }) +}) + +describe.skip('libp2p.dialer', () => { + let peerInfo + let libp2p + + before(async () => { + const peerId = await PeerId.createFromJSON(Peers[0]) + peerInfo = new PeerInfo(peerId) + }) + + afterEach(async () => { + sinon.restore() + libp2p && await libp2p.stop() + libp2p = null + }) + + it('should create a dialer', () => { + libp2p = new Libp2p({ + peerInfo, + modules: { + transport: [Transport], + streamMuxer: [Muxer], + connEncryption: [mockCrypto] + } + }) + + expect(libp2p.dialer).to.exist() + // Ensure the dialer also has the transport manager + expect(libp2p.transportManager).to.equal(libp2p.dialer.transportManager) + }) + + it('should use the dialer for connecting', async () => { + libp2p = new Libp2p({ + peerInfo, + modules: { + transport: [Transport], + streamMuxer: [Muxer], + connEncryption: [mockCrypto] + } + }) + + const connection = await libp2p.dialer.connectToMultiaddr(remoteAddr) + expect(connection).to.exist() + await connection.close() + }) +}) diff --git a/test/node.js b/test/node.js index fa27625f7c..61887dac95 100644 --- a/test/node.js +++ b/test/node.js @@ -1,3 +1,14 @@ 'use strict' -require('./transports/transport-manager.node') +const glob = require('glob') +const path = require('path') + +// Automatically require test files so we don't have to worry about adding new ones +glob('test/**/*.node.js', function (err, testPaths) { + if (err) throw err + if (testPaths.length < 1) throw new Error('Could not find any node test files') + + testPaths.forEach(file => { + require(path.resolve(file)) + }) +}) diff --git a/test/upgrading/upgrader.spec.js b/test/upgrading/upgrader.spec.js new file mode 100644 index 0000000000..d0583584fe --- /dev/null +++ b/test/upgrading/upgrader.spec.js @@ -0,0 +1,370 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +const { expect } = chai +const sinon = require('sinon') +const Muxer = require('libp2p-mplex') +const multiaddr = require('multiaddr') +const PeerId = require('peer-id') +const PeerInfo = require('peer-info') +const pipe = require('it-pipe') +const { collect } = require('streaming-iterables') +const pSettle = require('p-settle') +const Transport = require('libp2p-websockets') + +const Libp2p = require('../../src') +const Upgrader = require('../../src/upgrader') +const { codes } = require('../../src/errors') + +const mockCrypto = require('../utils/mockCrypto') +const mockMultiaddrConnPair = require('../utils/mockMultiaddrConn') +const Peers = require('../fixtures/peers') +const addrs = [ + multiaddr('/ip4/127.0.0.1/tcp/0'), + multiaddr('/ip4/127.0.0.1/tcp/0') +] + +describe('Upgrader', () => { + let localUpgrader + let remoteUpgrader + let localPeer + let remotePeer + + before(async () => { + ([ + localPeer, + remotePeer + ] = await Promise.all([ + PeerId.createFromJSON(Peers[0]), + PeerId.createFromJSON(Peers[1]) + ])) + + localUpgrader = new Upgrader({ + localPeer + }) + remoteUpgrader = new Upgrader({ + localPeer: remotePeer + }) + + localUpgrader.protocols.set('/echo/1.0.0', ({ stream }) => pipe(stream, stream)) + remoteUpgrader.protocols.set('/echo/1.0.0', ({ stream }) => pipe(stream, stream)) + }) + + afterEach(() => { + sinon.restore() + }) + + it('should ignore a missing remote peer id', async () => { + const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer }) + + const muxers = new Map([[Muxer.multicodec, Muxer]]) + sinon.stub(localUpgrader, 'muxers').value(muxers) + sinon.stub(remoteUpgrader, 'muxers').value(muxers) + + const cryptos = new Map([[mockCrypto.tag, mockCrypto]]) + sinon.stub(localUpgrader, 'cryptos').value(cryptos) + sinon.stub(remoteUpgrader, 'cryptos').value(cryptos) + + // Remove the peer id from the remote address + outbound.remoteAddr = outbound.remoteAddr.decapsulateCode(421) + + const connections = await Promise.all([ + localUpgrader.upgradeOutbound(outbound), + remoteUpgrader.upgradeInbound(inbound) + ]) + + expect(connections).to.have.length(2) + }) + + it('should upgrade with valid muxers and crypto', async () => { + const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer }) + + const muxers = new Map([[Muxer.multicodec, Muxer]]) + sinon.stub(localUpgrader, 'muxers').value(muxers) + sinon.stub(remoteUpgrader, 'muxers').value(muxers) + + const cryptos = new Map([[mockCrypto.tag, mockCrypto]]) + sinon.stub(localUpgrader, 'cryptos').value(cryptos) + sinon.stub(remoteUpgrader, 'cryptos').value(cryptos) + + const connections = await Promise.all([ + localUpgrader.upgradeOutbound(outbound), + remoteUpgrader.upgradeInbound(inbound) + ]) + + expect(connections).to.have.length(2) + + const { stream, protocol } = await connections[0].newStream('/echo/1.0.0') + expect(protocol).to.equal('/echo/1.0.0') + + const hello = Buffer.from('hello there!') + const result = await pipe( + [hello], + stream, + function toBuffer (source) { + return (async function * () { + for await (const val of source) yield val.slice() + })() + }, + collect + ) + + expect(result).to.eql([hello]) + }) + + it('should fail if crypto fails', async () => { + const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer }) + + const muxers = new Map([[Muxer.multicodec, Muxer]]) + sinon.stub(localUpgrader, 'muxers').value(muxers) + sinon.stub(remoteUpgrader, 'muxers').value(muxers) + + const crypto = { + tag: '/insecure', + secureInbound: () => { throw new Error('Boom') }, + secureOutbound: () => { throw new Error('Boom') } + } + + const cryptos = new Map([[crypto.tag, crypto]]) + sinon.stub(localUpgrader, 'cryptos').value(cryptos) + sinon.stub(remoteUpgrader, 'cryptos').value(cryptos) + + // Wait for the results of each side of the connection + const results = await pSettle([ + localUpgrader.upgradeOutbound(outbound), + remoteUpgrader.upgradeInbound(inbound) + ]) + + // Ensure both sides fail + expect(results).to.have.length(2) + results.forEach(result => { + expect(result.isRejected).to.equal(true) + expect(result.reason.code).to.equal(codes.ERR_ENCRYPTION_FAILED) + }) + }) + + it('should fail if muxers do not match', async () => { + const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer }) + + const muxersLocal = new Map([['/muxer-local', Muxer]]) + const muxersRemote = new Map([['/muxer-remote', Muxer]]) + sinon.stub(localUpgrader, 'muxers').value(muxersLocal) + sinon.stub(remoteUpgrader, 'muxers').value(muxersRemote) + + const cryptos = new Map([[mockCrypto.tag, mockCrypto]]) + sinon.stub(localUpgrader, 'cryptos').value(cryptos) + sinon.stub(remoteUpgrader, 'cryptos').value(cryptos) + + // Wait for the results of each side of the connection + const results = await pSettle([ + localUpgrader.upgradeOutbound(outbound), + remoteUpgrader.upgradeInbound(inbound) + ]) + + // Ensure both sides fail + expect(results).to.have.length(2) + results.forEach(result => { + expect(result.isRejected).to.equal(true) + expect(result.reason.code).to.equal(codes.ERR_MUXER_UNAVAILABLE) + }) + }) + + it('should map getStreams and close methods', async () => { + const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer }) + + const muxers = new Map([[Muxer.multicodec, Muxer]]) + sinon.stub(localUpgrader, 'muxers').value(muxers) + sinon.stub(remoteUpgrader, 'muxers').value(muxers) + + const cryptos = new Map([[mockCrypto.tag, mockCrypto]]) + sinon.stub(localUpgrader, 'cryptos').value(cryptos) + sinon.stub(remoteUpgrader, 'cryptos').value(cryptos) + + const connections = await Promise.all([ + localUpgrader.upgradeOutbound(outbound), + remoteUpgrader.upgradeInbound(inbound) + ]) + + expect(connections).to.have.length(2) + + // Create a few streams, at least 1 in each direction + await connections[0].newStream('/echo/1.0.0') + await connections[1].newStream('/echo/1.0.0') + await connections[0].newStream('/echo/1.0.0') + connections.forEach(conn => { + expect(conn.streams).to.have.length(3) + }) + + // Verify the MultiaddrConnection close method is called + sinon.spy(inbound, 'close') + sinon.spy(outbound, 'close') + await Promise.all(connections.map(conn => conn.close())) + expect(inbound.close.callCount).to.equal(1) + expect(outbound.close.callCount).to.equal(1) + }) + + it('should call connection handlers', async () => { + const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer }) + + const muxers = new Map([[Muxer.multicodec, Muxer]]) + sinon.stub(localUpgrader, 'muxers').value(muxers) + sinon.stub(remoteUpgrader, 'muxers').value(muxers) + + const cryptos = new Map([[mockCrypto.tag, mockCrypto]]) + sinon.stub(localUpgrader, 'cryptos').value(cryptos) + sinon.stub(remoteUpgrader, 'cryptos').value(cryptos) + + // Verify onConnection is called with the connection + sinon.spy(localUpgrader, 'onConnection') + sinon.spy(remoteUpgrader, 'onConnection') + const connections = await Promise.all([ + localUpgrader.upgradeOutbound(outbound), + remoteUpgrader.upgradeInbound(inbound) + ]) + expect(connections).to.have.length(2) + expect(localUpgrader.onConnection.callCount).to.equal(1) + expect(localUpgrader.onConnection.getCall(0).args).to.eql([connections[0]]) + expect(remoteUpgrader.onConnection.callCount).to.equal(1) + expect(remoteUpgrader.onConnection.getCall(0).args).to.eql([connections[1]]) + + // Verify onConnectionEnd is called with the connection + sinon.spy(localUpgrader, 'onConnectionEnd') + sinon.spy(remoteUpgrader, 'onConnectionEnd') + await Promise.all(connections.map(conn => conn.close())) + expect(localUpgrader.onConnectionEnd.callCount).to.equal(1) + expect(localUpgrader.onConnectionEnd.getCall(0).args).to.eql([connections[0]]) + expect(remoteUpgrader.onConnectionEnd.callCount).to.equal(1) + expect(remoteUpgrader.onConnectionEnd.getCall(0).args).to.eql([connections[1]]) + }) + + it('should fail to create a stream for an unsupported protocol', async () => { + const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer }) + + const muxers = new Map([[Muxer.multicodec, Muxer]]) + sinon.stub(localUpgrader, 'muxers').value(muxers) + sinon.stub(remoteUpgrader, 'muxers').value(muxers) + + const cryptos = new Map([[mockCrypto.tag, mockCrypto]]) + sinon.stub(localUpgrader, 'cryptos').value(cryptos) + sinon.stub(remoteUpgrader, 'cryptos').value(cryptos) + + const connections = await Promise.all([ + localUpgrader.upgradeOutbound(outbound), + remoteUpgrader.upgradeInbound(inbound) + ]) + + expect(connections).to.have.length(2) + + const results = await pSettle([ + connections[0].newStream('/unsupported/1.0.0'), + connections[1].newStream('/unsupported/1.0.0') + ]) + expect(results).to.have.length(2) + results.forEach(result => { + expect(result.isRejected).to.equal(true) + expect(result.reason.code).to.equal(codes.ERR_UNSUPPORTED_PROTOCOL) + }) + }) +}) + +describe('libp2p.upgrader', () => { + let peers + let libp2p + + before(async () => { + const ids = await Promise.all([ + PeerId.createFromJSON(Peers[0]), + PeerId.createFromJSON(Peers[1]) + ]) + peers = ids.map(peerId => new PeerInfo(peerId)) + }) + + afterEach(async () => { + sinon.restore() + libp2p && await libp2p.stop() + libp2p = null + }) + + it('should create an Upgrader', () => { + libp2p = new Libp2p({ + peerInfo: peers[0], + modules: { + transport: [Transport], + streamMuxer: [Muxer], + connEncryption: [mockCrypto] + } + }) + + expect(libp2p.upgrader).to.exist() + expect(libp2p.upgrader.muxers).to.eql(new Map([[Muxer.multicodec, Muxer]])) + expect(libp2p.upgrader.cryptos).to.eql(new Map([[mockCrypto.tag, mockCrypto]])) + // Ensure the transport manager also has the upgrader + expect(libp2p.upgrader).to.equal(libp2p.transportManager.upgrader) + }) + + it('should be able to register and unregister a handler', () => { + libp2p = new Libp2p({ + peerInfo: peers[0], + modules: { + transport: [Transport], + streamMuxer: [Muxer], + connEncryption: [mockCrypto] + } + }) + + expect(libp2p.upgrader.protocols.size).to.equal(0) + + const echoHandler = () => {} + libp2p.handle(['/echo/1.0.0', '/echo/1.0.1'], echoHandler) + expect(libp2p.upgrader.protocols.size).to.equal(2) + expect(libp2p.upgrader.protocols.get('/echo/1.0.0')).to.equal(echoHandler) + expect(libp2p.upgrader.protocols.get('/echo/1.0.1')).to.equal(echoHandler) + + libp2p.unhandle(['/echo/1.0.0']) + expect(libp2p.upgrader.protocols.size).to.equal(1) + expect(libp2p.upgrader.protocols.get('/echo/1.0.0')).to.equal(undefined) + expect(libp2p.upgrader.protocols.get('/echo/1.0.1')).to.equal(echoHandler) + }) + + it('should emit connect and disconnect events', async () => { + const remotePeer = peers[1] + libp2p = new Libp2p({ + peerInfo: peers[0], + modules: { + transport: [Transport], + streamMuxer: [Muxer], + connEncryption: [mockCrypto] + } + }) + + const remoteUpgrader = new Upgrader({ + localPeer: remotePeer.id, + muxers: new Map([[Muxer.multicodec, Muxer]]), + cryptos: new Map([[mockCrypto.tag, mockCrypto]]) + }) + + const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer: remotePeer.id }) + + // Spy on emit for easy verification + sinon.spy(libp2p, 'emit') + + // Upgrade and check the connect event + const connections = await Promise.all([ + libp2p.upgrader.upgradeOutbound(outbound), + remoteUpgrader.upgradeInbound(inbound) + ]) + expect(libp2p.emit.callCount).to.equal(1) + let [event, peerInfo] = libp2p.emit.getCall(0).args + expect(event).to.equal('peer:connect') + expect(peerInfo.id.isEqual(remotePeer.id)).to.equal(true) + + // Close and check the disconnect event + await Promise.all(connections.map(conn => conn.close())) + expect(libp2p.emit.callCount).to.equal(2) + ;([event, peerInfo] = libp2p.emit.getCall(1).args) + expect(event).to.equal('peer:disconnect') + expect(peerInfo.id.isEqual(remotePeer.id)).to.equal(true) + }) +}) diff --git a/test/utils/mockCrypto.js b/test/utils/mockCrypto.js new file mode 100644 index 0000000000..d013cec2cd --- /dev/null +++ b/test/utils/mockCrypto.js @@ -0,0 +1,24 @@ +'use strict' + +const PeerId = require('peer-id') +const Peers = require('../fixtures/peers') + +module.exports = { + tag: '/insecure', + secureInbound: (localPeer, stream) => { + return { + conn: stream, + remotePeer: localPeer + } + }, + secureOutbound: async (localPeer, stream, remotePeer) => { + // Crypto should always return a remotePeer + if (!remotePeer) { + remotePeer = await PeerId.createFromJSON(Peers[0]) + } + return { + conn: stream, + remotePeer: remotePeer + } + } +} diff --git a/test/utils/mockMultiaddrConn.js b/test/utils/mockMultiaddrConn.js new file mode 100644 index 0000000000..617f660422 --- /dev/null +++ b/test/utils/mockMultiaddrConn.js @@ -0,0 +1,43 @@ +'use strict' + +const duplexPair = require('it-pair/duplex') +const abortable = require('abortable-iterator') +const AbortController = require('abort-controller') + +/** + * Returns both sides of a mocked MultiaddrConnection + * @param {object} options + * @param {Multiaddr[]} options.addrs Should contain two addresses for the local and remote peer + * @param {PeerId} options.remotePeer The peer that is being "dialed" + * @returns {{inbound:MultiaddrConnection, outbound:MultiaddrConnection}} + */ +module.exports = function mockMultiaddrConnPair ({ addrs, remotePeer }) { + const controller = new AbortController() + + const [inbound, outbound] = duplexPair() + outbound.localAddr = addrs[0] + outbound.remoteAddr = addrs[1].encapsulate(`/p2p/${remotePeer.toB58String()}`) + outbound.timeline = { + open: Date.now() + } + outbound.close = () => { + outbound.timeline.close = Date.now() + controller.abort() + } + + inbound.localAddr = addrs[1] + inbound.remoteAddr = addrs[0] + inbound.timeline = { + open: Date.now() + } + inbound.close = () => { + inbound.timeline.close = Date.now() + controller.abort() + } + + // Make the sources abortable so we can close them easily + inbound.source = abortable(inbound.source, controller.signal) + outbound.source = abortable(outbound.source, controller.signal) + + return { inbound, outbound } +} From 138bb0bbaeaec77ffc470a1e5bf9d787417da46e Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Mon, 4 Nov 2019 14:05:58 +0100 Subject: [PATCH 05/92] refactor: crypto and pnet (#469) * feat: add initial plaintext 2 module * refactor: initial refactor of pnet * chore: fix lint * fix: update plaintext api usage * test: use plaintext for test crypto * chore: update deps test: update dialer suite scope * feat: add connection protection to the upgrader * refactor: cleanup and lint fix * chore: remove unncessary transforms * chore: temporarily disable bundlesize * chore: add missing dep * fix: use it-handshake to prevent overreading * chore(fix): PR feedback updates * chore: apply suggestions from code review Co-Authored-By: Vasco Santos --- .aegir.js | 35 +++++--- .travis.yml | 2 +- package.json | 6 +- src/index.js | 5 +- src/insecure/plaintext.js | 67 +++++++++++++++ src/insecure/proto.js | 22 +++++ src/pnet/crypto.js | 62 +++++--------- src/pnet/index.js | 61 +++++++------- src/pnet/state.js | 110 ------------------------ src/upgrader.js | 33 +++++++- test/dialing/direct.node.js | 140 ++++++++++++++++++------------- test/dialing/direct.spec.js | 87 ++++++++++--------- test/fixtures/browser.js | 2 +- test/fixtures/swarm.key.js | 5 ++ test/insecure/compliance.spec.js | 13 +++ test/insecure/plaintext.spec.js | 69 +++++++++++++++ test/pnet/index.spec.js | 94 +++++++++++++++++++++ test/upgrading/upgrader.spec.js | 71 +++++++++++++--- test/utils/mockCrypto.js | 2 +- 19 files changed, 577 insertions(+), 309 deletions(-) create mode 100644 src/insecure/plaintext.js create mode 100644 src/insecure/proto.js delete mode 100644 src/pnet/state.js create mode 100644 test/fixtures/swarm.key.js create mode 100644 test/insecure/compliance.spec.js create mode 100644 test/insecure/plaintext.spec.js create mode 100644 test/pnet/index.spec.js diff --git a/.aegir.js b/.aegir.js index 6895593bd0..891e4374c0 100644 --- a/.aegir.js +++ b/.aegir.js @@ -1,23 +1,38 @@ 'use strict' -const TransportManager = require('./src/transport-manager') -const mockUpgrader = require('./test/utils/mockUpgrader') +const Libp2p = require('./src') const { MULTIADDRS_WEBSOCKETS } = require('./test/fixtures/browser') -let tm - +const Peers = require('./test/fixtures/peers') +const PeerId = require('peer-id') +const PeerInfo = require('peer-info') const WebSockets = require('libp2p-websockets') +const Muxer = require('libp2p-mplex') +const Crypto = require('./src/insecure/plaintext') +const pipe = require('it-pipe') +let libp2p const before = async () => { - tm = new TransportManager({ - upgrader: mockUpgrader, - onConnection: () => {} + // Use the last peer + const peerId = await PeerId.createFromJSON(Peers[Peers.length - 1]) + const peerInfo = new PeerInfo(peerId) + peerInfo.multiaddrs.add(MULTIADDRS_WEBSOCKETS[0]) + + libp2p = new Libp2p({ + peerInfo, + modules: { + transport: [WebSockets], + streamMuxer: [Muxer], + connEncryption: [Crypto] + } }) - tm.add(WebSockets.prototype[Symbol.toStringTag], WebSockets) - await tm.listen(MULTIADDRS_WEBSOCKETS) + // Add the echo protocol + libp2p.handle('/echo/1.0.0', ({ stream }) => pipe(stream, stream)) + + await libp2p.start() } const after = async () => { - await tm.close() + await libp2p.stop() } module.exports = { diff --git a/.travis.yml b/.travis.yml index 5eb222c1ec..7a128bda2b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ jobs: include: - stage: check script: - - npx aegir build --bundlesize + # - npx aegir build --bundlesize - npx aegir dep-check -- -i wrtc -i electron-webrtc - npm run lint diff --git a/package.json b/package.json index 4abc96c890..6837318936 100644 --- a/package.json +++ b/package.json @@ -49,10 +49,12 @@ "err-code": "^1.1.2", "fsm-event": "^2.1.0", "hashlru": "^2.3.0", + "it-handshake": "^1.0.1", + "it-length-prefixed": "jacobheun/pull-length-prefixed#feat/fromReader", "it-pipe": "^1.0.1", "latency-monitor": "~0.2.1", "libp2p-crypto": "^0.16.2", - "libp2p-interfaces": "^0.1.1", + "libp2p-interfaces": "^0.1.3", "mafmt": "^7.0.0", "merge-options": "^1.0.1", "moving-average": "^1.0.0", @@ -99,7 +101,7 @@ "libp2p-secio": "^0.11.1", "libp2p-spdy": "^0.13.2", "libp2p-tcp": "^0.14.1", - "libp2p-websockets": "^0.13.0", + "libp2p-websockets": "^0.13.1", "lodash.times": "^4.3.2", "nock": "^10.0.6", "p-defer": "^3.0.0", diff --git a/src/index.js b/src/index.js index 3d5cf8c219..4d2f31b2c1 100644 --- a/src/index.js +++ b/src/index.js @@ -90,7 +90,7 @@ class Libp2p extends EventEmitter { if (this._modules.connEncryption) { const cryptos = this._modules.connEncryption cryptos.forEach((crypto) => { - this.upgrader.cryptos.set(crypto.tag, crypto) + this.upgrader.cryptos.set(crypto.protocol, crypto) }) } @@ -108,7 +108,7 @@ class Libp2p extends EventEmitter { // Attach private network protector if (this._modules.connProtector) { - this._switch.protector = this._modules.connProtector + this.upgrader.protector = this._modules.connProtector } else if (process.env.LIBP2P_FORCE_PNET) { throw new Error('Private network is enforced, but no protector was provided') } @@ -229,6 +229,7 @@ class Libp2p extends EventEmitter { try { await this.transportManager.close() + await this._switch.stop() } catch (err) { if (err) { log.error(err) diff --git a/src/insecure/plaintext.js b/src/insecure/plaintext.js new file mode 100644 index 0000000000..7b3981ebe3 --- /dev/null +++ b/src/insecure/plaintext.js @@ -0,0 +1,67 @@ +'use strict' + +const handshake = require('it-handshake') +const lp = require('it-length-prefixed') +const PeerId = require('peer-id') +const debug = require('debug') +const log = debug('libp2p:plaintext') +log.error = debug('libp2p:plaintext:error') +const { UnexpectedPeerError, InvalidCryptoExchangeError } = require('libp2p-interfaces/src/crypto/errors') + +const { Exchange, KeyType } = require('./proto') +const protocol = '/plaintext/2.0.0' + +function lpEncodeExchange (exchange) { + const pb = Exchange.encode(exchange) + return lp.encode.single(pb) +} + +async function encrypt (localId, conn, remoteId) { + const shake = handshake(conn) + + // Encode the public key and write it to the remote peer + shake.write(lpEncodeExchange({ + id: localId.toBytes(), + pubkey: { + Type: KeyType.RSA, // TODO: dont hard code + Data: localId.marshalPubKey() + } + })) + + log('write pubkey exchange to peer %j', remoteId) + + // Get the Exchange message + const response = (await lp.decodeFromReader(shake.reader).next()).value + const id = Exchange.decode(response.slice()) + log('read pubkey exchange from peer %j', remoteId) + + let peerId + try { + peerId = await PeerId.createFromPubKey(id.pubkey.Data) + } catch (err) { + log.error(err) + throw new InvalidCryptoExchangeError('Remote did not provide its public key') + } + + if (remoteId && !peerId.isEqual(remoteId)) { + throw new UnexpectedPeerError() + } + + log('plaintext key exchange completed successfully with peer %j', peerId) + + shake.rest() + return { + conn: shake.stream, + remotePeer: peerId + } +} + +module.exports = { + protocol, + secureInbound: (localId, conn, remoteId) => { + return encrypt(localId, conn, remoteId) + }, + secureOutbound: (localId, conn, remoteId) => { + return encrypt(localId, conn, remoteId) + } +} diff --git a/src/insecure/proto.js b/src/insecure/proto.js new file mode 100644 index 0000000000..2c7d7e89a6 --- /dev/null +++ b/src/insecure/proto.js @@ -0,0 +1,22 @@ +'use strict' + +const protobuf = require('protons') + +module.exports = protobuf(` +message Exchange { + optional bytes id = 1; + optional PublicKey pubkey = 2; +} + +enum KeyType { + RSA = 0; + Ed25519 = 1; + Secp256k1 = 2; + ECDSA = 3; +} + +message PublicKey { + required KeyType Type = 1; + required bytes Data = 2; +} +`) diff --git a/src/pnet/crypto.js b/src/pnet/crypto.js index 9c61f26e4f..8aea1b3261 100644 --- a/src/pnet/crypto.js +++ b/src/pnet/crypto.js @@ -1,6 +1,5 @@ 'use strict' -const pull = require('pull-stream') const debug = require('debug') const Errors = require('./errors') const xsalsa20 = require('xsalsa20') @@ -8,45 +7,40 @@ const KEY_LENGTH = require('./key-generator').KEY_LENGTH const log = debug('libp2p:pnet') log.trace = debug('libp2p:pnet:trace') -log.err = debug('libp2p:pnet:err') +log.error = debug('libp2p:pnet:err') /** - * Creates a pull stream to encrypt messages in a private network + * Creates a stream iterable to encrypt messages in a private network * * @param {Buffer} nonce The nonce to use in encryption * @param {Buffer} psk The private shared key to use in encryption - * @returns {PullStream} a through stream + * @returns {*} a through iterable */ module.exports.createBoxStream = (nonce, psk) => { const xor = xsalsa20(nonce, psk) - return pull( - ensureBuffer(), - pull.map((chunk) => { - return xor.update(chunk, chunk) - }) - ) + return (source) => (async function * () { + for await (const chunk of source) { + yield Buffer.from(xor.update(chunk.slice())) + } + })() } /** - * Creates a pull stream to decrypt messages in a private network + * Creates a stream iterable to decrypt messages in a private network * - * @param {Object} remote Holds the nonce of the peer + * @param {Buffer} nonce The nonce of the remote peer * @param {Buffer} psk The private shared key to use in decryption - * @returns {PullStream} a through stream + * @returns {*} a through iterable */ -module.exports.createUnboxStream = (remote, psk) => { - let xor - return pull( - ensureBuffer(), - pull.map((chunk) => { - if (!xor) { - xor = xsalsa20(remote.nonce, psk) - log.trace('Decryption enabled') - } +module.exports.createUnboxStream = (nonce, psk) => { + return (source) => (async function * () { + const xor = xsalsa20(nonce, psk) + log.trace('Decryption enabled') - return xor.update(chunk, chunk) - }) - ) + for await (const chunk of source) { + yield Buffer.from(xor.update(chunk.slice())) + } + })() } /** @@ -61,7 +55,7 @@ module.exports.decodeV1PSK = (pskBuffer) => { // This should pull from multibase/multicodec to allow for // more encoding flexibility. Ideally we'd consume the codecs // from the buffer line by line to evaluate the next line - // programatically instead of making assumptions about the + // programmatically instead of making assumptions about the // encodings of each line. const metadata = pskBuffer.toString().split(/(?:\r\n|\r|\n)/g) const pskTag = metadata.shift() @@ -78,21 +72,7 @@ module.exports.decodeV1PSK = (pskBuffer) => { psk: psk } } catch (err) { + log.error(err) throw new Error(Errors.INVALID_PSK) } } - -/** - * Returns a through pull-stream that ensures the passed chunks - * are buffers instead of strings - * @returns {PullStream} a through stream - */ -function ensureBuffer () { - return pull.map((chunk) => { - if (typeof chunk === 'string') { - return Buffer.from(chunk, 'utf-8') - } - - return chunk - }) -} diff --git a/src/pnet/index.js b/src/pnet/index.js index db79e3bc44..8cd8c334b0 100644 --- a/src/pnet/index.js +++ b/src/pnet/index.js @@ -1,12 +1,17 @@ 'use strict' -const pull = require('pull-stream') -const { Connection } = require('libp2p-interfaces/src/connection') +const pipe = require('it-pipe') const assert = require('assert') - +const duplexPair = require('it-pair/duplex') +const crypto = require('libp2p-crypto') const Errors = require('./errors') -const State = require('./state') -const decodeV1PSK = require('./crypto').decodeV1PSK +const { + createBoxStream, + createUnboxStream, + decodeV1PSK +} = require('./crypto') +const handshake = require('it-handshake') +const { NONCE_LENGTH } = require('./key-generator') const debug = require('debug') const log = debug('libp2p:pnet') log.err = debug('libp2p:pnet:err') @@ -27,41 +32,41 @@ class Protector { } /** - * Takes a given Connection and creates a privaste encryption stream + * Takes a given Connection and creates a private encryption stream * between its two peers from the PSK the Protector instance was * created with. * * @param {Connection} connection The connection to protect - * @param {function(Error)} callback - * @returns {Connection} The protected connection + * @returns {*} A protected duplex iterable */ - protect (connection, callback) { + async protect (connection) { assert(connection, Errors.NO_HANDSHAKE_CONNECTION) - const protectedConnection = new Connection(undefined, connection) - const state = new State(this.psk) - + // Exchange nonces log('protecting the connection') + const localNonce = crypto.randomBytes(NONCE_LENGTH) + + const shake = handshake(connection) + shake.write(localNonce) - // Run the connection through an encryptor - pull( - connection, - state.encrypt((err, encryptedOuterStream) => { - if (err) { - log.err('There was an error attempting to protect the connection', err) - return callback(err) - } + const result = await shake.reader.next(NONCE_LENGTH) + const remoteNonce = result.value.slice() + shake.rest() - connection.getPeerInfo(() => { - protectedConnection.setInnerConn(new Connection(encryptedOuterStream, connection)) - log('the connection has been successfully wrapped by the protector') - callback() - }) - }), - connection + // Create the boxing/unboxing pipe + log('exchanged nonces') + const [internal, external] = duplexPair() + pipe( + external, + // Encrypt all outbound traffic + createBoxStream(localNonce, this.psk), + shake.stream, + // Decrypt all inbound traffic + createUnboxStream(remoteNonce, this.psk), + external ) - return protectedConnection + return internal } } diff --git a/src/pnet/state.js b/src/pnet/state.js deleted file mode 100644 index b084e26e2a..0000000000 --- a/src/pnet/state.js +++ /dev/null @@ -1,110 +0,0 @@ -'use strict' - -const crypto = require('crypto') -const debug = require('debug') -const pair = require('pull-pair') -const Reader = require('pull-reader') -const cat = require('pull-cat') -const pull = require('pull-stream') -const deferred = require('pull-defer') - -const cryptoStreams = require('./crypto') -const NONCE_LENGTH = require('./key-generator').NONCE_LENGTH - -const log = debug('libp2p:pnet') -log.err = debug('libp2p:pnet:err') -log.trace = debug('libp2p:pnet:trace') - -/** - * Keeps track of the state of a given connection, such as the local psk - * and local and remote nonces for encryption/decryption - */ -class State { - /** - * @param {Buffer} psk The key buffer used for encryption - * @constructor - */ - constructor (psk) { - this.local = { - nonce: Buffer.from( - crypto.randomBytes(NONCE_LENGTH) - ), - psk: psk - } - this.remote = { nonce: null } - - this.rawReader = Reader(60e3) - this.encryptedReader = Reader(60e3) - - this.rawPairStream = pair() - this.encryptedPairStream = pair() - - // The raw, pair stream - this.innerRawStream = null - this.outerRawStream = { - sink: this.rawReader, - source: cat([ - pull.values([ - this.local.nonce - ]), - this.rawPairStream.source - ]) - } - - // The encrypted, pair stream - this.innerEncryptedStream = { - sink: this.encryptedReader, - source: this.encryptedPairStream.source - } - this.outerEncryptedStream = null - } - - /** - * Creates encryption streams for the given state - * - * @param {function(Error, Connection)} callback - * @returns {void} - */ - encrypt (callback) { - // The outer stream needs to be returned before we setup the - // rest of the streams, so we're delaying the execution - setTimeout(() => { - // Read the nonce first, once we have it resolve the - // deferred source, so we keep reading - const deferredSource = deferred.source() - this.rawReader.read(NONCE_LENGTH, (err, data) => { - if (err) { - log.err('There was an error attempting to read the nonce', err) - } - log.trace('remote nonce received') - this.remote.nonce = data - deferredSource.resolve(this.rawReader.read()) - }) - - this.innerRawStream = { - sink: this.rawPairStream.sink, - source: deferredSource - } - - // Create the pull exchange between the two inner streams - pull( - this.innerRawStream, - cryptoStreams.createUnboxStream(this.remote, this.local.psk), - this.innerEncryptedStream, - cryptoStreams.createBoxStream(this.local.nonce, this.local.psk), - this.innerRawStream - ) - - this.outerEncryptedStream = { - sink: this.encryptedPairStream.sink, - source: this.encryptedReader.read() - } - - callback(null, this.outerEncryptedStream) - }, 0) - - return this.outerRawStream - } -} - -module.exports = State diff --git a/src/upgrader.js b/src/upgrader.js index e470d81c1c..7947d6b89c 100644 --- a/src/upgrader.js +++ b/src/upgrader.js @@ -32,11 +32,20 @@ class Upgrader { * @param {PeerId} options.localPeer * @param {Map} options.cryptos * @param {Map} options.muxers + * @param {function(Connection)} options.onConnection Called when a connection is upgraded + * @param {function(Connection)} options.onConnectionEnd */ - constructor ({ localPeer, cryptos, muxers, onConnectionEnd = () => {}, onConnection = () => {} }) { + constructor ({ + localPeer, + cryptos, + muxers, + onConnectionEnd = () => {}, + onConnection = () => {} + }) { this.localPeer = localPeer this.cryptos = cryptos || new Map() this.muxers = muxers || new Map() + this.protector = null this.protocols = new Map() this.onConnection = onConnection this.onConnectionEnd = onConnectionEnd @@ -55,13 +64,21 @@ class Upgrader { let Muxer let cryptoProtocol + log('Starting the inbound connection upgrade') + + // Protect + let protectedConn = maConn + if (this.protector) { + protectedConn = await this.protector.protect(maConn) + } + try { // Encrypt the connection ({ conn: encryptedConn, remotePeer, protocol: cryptoProtocol - } = await this._encryptInbound(this.localPeer, maConn, this.cryptos)) + } = await this._encryptInbound(this.localPeer, protectedConn, this.cryptos)) // Multiplex the connection ;({ stream: muxedConnection, Muxer } = await this._multiplexInbound(encryptedConn, this.muxers)) @@ -104,13 +121,21 @@ class Upgrader { let cryptoProtocol let Muxer + log('Starting the outbound connection upgrade') + + // Protect + let protectedConn = maConn + if (this.protector) { + protectedConn = await this.protector.protect(maConn) + } + try { // Encrypt the connection ({ conn: encryptedConn, remotePeer, protocol: cryptoProtocol - } = await this._encryptOutbound(this.localPeer, maConn, remotePeerId, this.cryptos)) + } = await this._encryptOutbound(this.localPeer, protectedConn, remotePeerId, this.cryptos)) // Multiplex the connection ;({ stream: muxedConnection, Muxer } = await this._multiplexOutbound(encryptedConn, this.muxers)) @@ -241,7 +266,7 @@ class Upgrader { async _encryptInbound (localPeer, connection, cryptos) { const mss = new Multistream.Listener(connection) const protocols = Array.from(cryptos.keys()) - log('selecting inbound crypto protocol', protocols) + log('handling inbound crypto protocol selection', protocols) try { const { stream, protocol } = await mss.handle(protocols) diff --git a/test/dialing/direct.node.js b/test/dialing/direct.node.js index 071ad900aa..d8acbcac43 100644 --- a/test/dialing/direct.node.js +++ b/test/dialing/direct.node.js @@ -7,6 +7,7 @@ const { expect } = chai const sinon = require('sinon') const Transport = require('libp2p-tcp') const Muxer = require('libp2p-mplex') +const Crypto = require('../../src/insecure/plaintext') const multiaddr = require('multiaddr') const PeerId = require('peer-id') const PeerInfo = require('peer-info') @@ -18,9 +19,10 @@ const Libp2p = require('../../src') const Dialer = require('../../src/dialer') const TransportManager = require('../../src/transport-manager') const { codes: ErrorCodes } = require('../../src/errors') +const Protector = require('../../src/pnet') +const swarmKeyBuffer = Buffer.from(require('../fixtures/swarm.key')) const mockUpgrader = require('../utils/mockUpgrader') -const mockCrypto = require('../utils/mockCrypto') const Peers = require('../fixtures/peers') const listenAddr = multiaddr('/ip4/127.0.0.1/tcp/0') @@ -49,9 +51,7 @@ describe('Dialing (direct, TCP)', () => { remoteAddr = remoteTM.getAddrs()[0] }) - after(async () => { - await remoteTM.close() - }) + after(() => remoteTM.close()) afterEach(() => { sinon.restore() @@ -171,66 +171,88 @@ describe('Dialing (direct, TCP)', () => { expect(dialer.queue.pending).to.equal(0) expect(dialer.queue.size).to.equal(0) }) -}) - -describe('libp2p.dialer', () => { - let peerInfo - let remotePeerInfo - let libp2p - let remoteLibp2p - let remoteAddr - - before(async () => { - const [peerId, remotePeerId] = await Promise.all([ - PeerId.createFromJSON(Peers[0]), - PeerId.createFromJSON(Peers[1]) - ]) - - peerInfo = new PeerInfo(peerId) - remotePeerInfo = new PeerInfo(remotePeerId) - remoteLibp2p = new Libp2p({ - peerInfo: remotePeerInfo, - modules: { - transport: [Transport], - streamMuxer: [Muxer], - connEncryption: [mockCrypto] - } + describe('libp2p.dialer', () => { + let peerInfo + let remotePeerInfo + let libp2p + let remoteLibp2p + let remoteAddr + + before(async () => { + const [peerId, remotePeerId] = await Promise.all([ + PeerId.createFromJSON(Peers[0]), + PeerId.createFromJSON(Peers[1]) + ]) + + peerInfo = new PeerInfo(peerId) + remotePeerInfo = new PeerInfo(remotePeerId) + + remoteLibp2p = new Libp2p({ + peerInfo: remotePeerInfo, + modules: { + transport: [Transport], + streamMuxer: [Muxer], + connEncryption: [Crypto] + } + }) + remoteLibp2p.handle('/echo/1.0.0', ({ stream }) => pipe(stream, stream)) + + await remoteLibp2p.transportManager.listen([listenAddr]) + remoteAddr = remoteLibp2p.transportManager.getAddrs()[0] }) - remoteLibp2p.handle('/echo/1.0.0', ({ stream }) => pipe(stream, stream)) - - await remoteLibp2p.transportManager.listen([listenAddr]) - remoteAddr = remoteLibp2p.transportManager.getAddrs()[0] - }) - afterEach(async () => { - sinon.restore() - libp2p && await libp2p.stop() - libp2p = null - }) - - after(async () => { - await remoteLibp2p.stop() - }) - - it('should use the dialer for connecting', async () => { - libp2p = new Libp2p({ - peerInfo, - modules: { - transport: [Transport], - streamMuxer: [Muxer], - connEncryption: [mockCrypto] - } + afterEach(async () => { + sinon.restore() + libp2p && await libp2p.stop() + libp2p = null }) - sinon.spy(libp2p.dialer, 'connectToMultiaddr') + after(() => remoteLibp2p.stop()) + + it('should use the dialer for connecting', async () => { + libp2p = new Libp2p({ + peerInfo, + modules: { + transport: [Transport], + streamMuxer: [Muxer], + connEncryption: [Crypto] + } + }) + + sinon.spy(libp2p.dialer, 'connectToMultiaddr') + + const connection = await libp2p.dial(remoteAddr) + expect(connection).to.exist() + const { stream, protocol } = await connection.newStream('/echo/1.0.0') + expect(stream).to.exist() + expect(protocol).to.equal('/echo/1.0.0') + await connection.close() + expect(libp2p.dialer.connectToMultiaddr.callCount).to.equal(1) + }) - const connection = await libp2p.dial(remoteAddr) - expect(connection).to.exist() - const { stream, protocol } = await connection.newStream('/echo/1.0.0') - expect(stream).to.exist() - expect(protocol).to.equal('/echo/1.0.0') - await connection.close() - expect(libp2p.dialer.connectToMultiaddr.callCount).to.equal(1) + it('should use the protectors when provided for connecting', async () => { + const protector = new Protector(swarmKeyBuffer) + libp2p = new Libp2p({ + peerInfo, + modules: { + transport: [Transport], + streamMuxer: [Muxer], + connEncryption: [Crypto], + connProtector: protector + } + }) + + sinon.spy(libp2p.upgrader.protector, 'protect') + sinon.stub(remoteLibp2p.upgrader, 'protector').value(new Protector(swarmKeyBuffer)) + + const connection = await libp2p.dialer.connectToMultiaddr(remoteAddr) + expect(connection).to.exist() + const { stream, protocol } = await connection.newStream('/echo/1.0.0') + expect(stream).to.exist() + expect(protocol).to.equal('/echo/1.0.0') + await connection.close() + expect(libp2p.upgrader.protector.protect.callCount).to.equal(1) + }) }) }) diff --git a/test/dialing/direct.spec.js b/test/dialing/direct.spec.js index bfc77b4d5a..b23d4f13a8 100644 --- a/test/dialing/direct.spec.js +++ b/test/dialing/direct.spec.js @@ -9,6 +9,7 @@ const pDefer = require('p-defer') const delay = require('delay') const Transport = require('libp2p-websockets') const Muxer = require('libp2p-mplex') +const Crypto = require('../../src/insecure/plaintext') const multiaddr = require('multiaddr') const PeerId = require('peer-id') const PeerInfo = require('peer-info') @@ -22,7 +23,6 @@ const Libp2p = require('../../src') const Peers = require('../fixtures/peers') const { MULTIADDRS_WEBSOCKETS } = require('../fixtures/browser') const mockUpgrader = require('../utils/mockUpgrader') -const mockCrypto = require('../utils/mockCrypto') const unsupportedAddr = multiaddr('/ip4/127.0.0.1/tcp/9999/ws') const remoteAddr = MULTIADDRS_WEBSOCKETS[0] @@ -162,50 +162,61 @@ describe('Dialing (direct, WebSockets)', () => { expect(dialer.queue.pending).to.equal(0) expect(dialer.queue.size).to.equal(0) }) -}) - -describe.skip('libp2p.dialer', () => { - let peerInfo - let libp2p - before(async () => { - const peerId = await PeerId.createFromJSON(Peers[0]) - peerInfo = new PeerInfo(peerId) - }) + describe('libp2p.dialer', () => { + let peerInfo + let libp2p + let remoteLibp2p - afterEach(async () => { - sinon.restore() - libp2p && await libp2p.stop() - libp2p = null - }) + before(async () => { + const peerId = await PeerId.createFromJSON(Peers[0]) + peerInfo = new PeerInfo(peerId) + }) - it('should create a dialer', () => { - libp2p = new Libp2p({ - peerInfo, - modules: { - transport: [Transport], - streamMuxer: [Muxer], - connEncryption: [mockCrypto] - } + afterEach(async () => { + sinon.restore() + libp2p && await libp2p.stop() + libp2p = null }) - expect(libp2p.dialer).to.exist() - // Ensure the dialer also has the transport manager - expect(libp2p.transportManager).to.equal(libp2p.dialer.transportManager) - }) + after(async () => { + remoteLibp2p && await remoteLibp2p.stop() + }) - it('should use the dialer for connecting', async () => { - libp2p = new Libp2p({ - peerInfo, - modules: { - transport: [Transport], - streamMuxer: [Muxer], - connEncryption: [mockCrypto] - } + it('should create a dialer', () => { + libp2p = new Libp2p({ + peerInfo, + modules: { + transport: [Transport], + streamMuxer: [Muxer], + connEncryption: [Crypto] + } + }) + + expect(libp2p.dialer).to.exist() + // Ensure the dialer also has the transport manager + expect(libp2p.transportManager).to.equal(libp2p.dialer.transportManager) }) - const connection = await libp2p.dialer.connectToMultiaddr(remoteAddr) - expect(connection).to.exist() - await connection.close() + it('should use the dialer for connecting', async () => { + libp2p = new Libp2p({ + peerInfo, + modules: { + transport: [Transport], + streamMuxer: [Muxer], + connEncryption: [Crypto] + } + }) + + sinon.spy(libp2p.dialer, 'connectToMultiaddr') + + const connection = await libp2p.dialer.connectToMultiaddr(remoteAddr) + expect(connection).to.exist() + const { stream, protocol } = await connection.newStream('/echo/1.0.0') + expect(stream).to.exist() + expect(protocol).to.equal('/echo/1.0.0') + await connection.close() + expect(libp2p.dialer.connectToMultiaddr.callCount).to.equal(1) + }) }) }) diff --git a/test/fixtures/browser.js b/test/fixtures/browser.js index 57c9628a64..901c32e6c9 100644 --- a/test/fixtures/browser.js +++ b/test/fixtures/browser.js @@ -3,5 +3,5 @@ const multiaddr = require('multiaddr') module.exports.MULTIADDRS_WEBSOCKETS = [ - multiaddr('/ip4/127.0.0.1/tcp/15001/ws') + multiaddr('/ip4/127.0.0.1/tcp/15001/ws/p2p/QmckxVrJw1Yo8LqvmDJNUmdAsKtSbiKWmrXJFyKmUraBoN') ] diff --git a/test/fixtures/swarm.key.js b/test/fixtures/swarm.key.js new file mode 100644 index 0000000000..184f47b6cb --- /dev/null +++ b/test/fixtures/swarm.key.js @@ -0,0 +1,5 @@ +'use strict' + +module.exports = '/key/swarm/psk/1.0.0/\n' + + '/base16/\n' + + '411f0a244cbbc25ecbb2b070d00a1832516ded521eb3ee3aa13189efe2e2b9a2' diff --git a/test/insecure/compliance.spec.js b/test/insecure/compliance.spec.js new file mode 100644 index 0000000000..74fdf2071b --- /dev/null +++ b/test/insecure/compliance.spec.js @@ -0,0 +1,13 @@ +'use strict' +/* eslint-env mocha */ + +const tests = require('libp2p-interfaces/src/crypto/tests') +const plaintext = require('../../src/insecure/plaintext') + +describe('plaintext compliance', () => { + tests({ + setup () { + return plaintext + } + }) +}) diff --git a/test/insecure/plaintext.spec.js b/test/insecure/plaintext.spec.js new file mode 100644 index 0000000000..7ca3caf518 --- /dev/null +++ b/test/insecure/plaintext.spec.js @@ -0,0 +1,69 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +const { expect } = chai +const sinon = require('sinon') + +const PeerId = require('peer-id') +const duplexPair = require('it-pair/duplex') + +const peers = require('../fixtures/peers') +const plaintext = require('../../src/insecure/plaintext') +const { + InvalidCryptoExchangeError, + UnexpectedPeerError +} = require('libp2p-interfaces/src/crypto/errors') + +describe('plaintext', () => { + let localPeer + let remotePeer + let wrongPeer + + before(async () => { + [localPeer, remotePeer, wrongPeer] = await Promise.all([ + PeerId.createFromJSON(peers[0]), + PeerId.createFromJSON(peers[1]), + PeerId.createFromJSON(peers[2]) + ]) + }) + + afterEach(() => { + sinon.restore() + }) + + it('should verify the public key and id match', () => { + const [localConn, remoteConn] = duplexPair() + + // When we attempt to get the remote peer key, return the wrong peers pub key + sinon.stub(remotePeer, 'marshalPubKey').callsFake(() => { + return wrongPeer.marshalPubKey() + }) + + return Promise.all([ + plaintext.secureInbound(remotePeer, localConn), + plaintext.secureOutbound(localPeer, remoteConn, remotePeer) + ]).then(() => expect.fail('should have failed'), (err) => { + expect(err).to.exist() + expect(err).to.have.property('code', UnexpectedPeerError.code) + }) + }) + + it('should fail if the peer does not provide its public key', () => { + const [localConn, remoteConn] = duplexPair() + + // When we attempt to get the remote peer key, return the wrong peers pub key + sinon.stub(remotePeer, 'marshalPubKey').callsFake(() => { + return Buffer.alloc(0) + }) + + return Promise.all([ + plaintext.secureInbound(remotePeer, localConn), + plaintext.secureOutbound(localPeer, remoteConn, remotePeer) + ]).then(() => expect.fail('should have failed'), (err) => { + expect(err).to.exist() + expect(err).to.have.property('code', InvalidCryptoExchangeError.code) + }) + }) +}) diff --git a/test/pnet/index.spec.js b/test/pnet/index.spec.js new file mode 100644 index 0000000000..f479551884 --- /dev/null +++ b/test/pnet/index.spec.js @@ -0,0 +1,94 @@ +/* eslint-env mocha */ +'use strict' + +const chai = require('chai') +const dirtyChai = require('dirty-chai') +chai.use(dirtyChai) +const expect = chai.expect +const duplexPair = require('it-pair/duplex') +const pipe = require('it-pipe') +const { collect } = require('streaming-iterables') + +const Protector = require('../../src/pnet') +const Errors = Protector.errors +const generate = Protector.generate + +const swarmKeyBuffer = Buffer.alloc(95) +const wrongSwarmKeyBuffer = Buffer.alloc(95) + +// Write new psk files to the buffers +generate(swarmKeyBuffer) +generate(wrongSwarmKeyBuffer) + +describe('private network', () => { + it('should accept a valid psk buffer', () => { + const protector = new Protector(swarmKeyBuffer) + + expect(protector.tag).to.equal('/key/swarm/psk/1.0.0/') + expect(protector.psk.byteLength).to.equal(32) + }) + + it('should protect a simple connection', async () => { + const [inbound, outbound] = duplexPair() + const protector = new Protector(swarmKeyBuffer) + + const [aToB, bToA] = await Promise.all([ + protector.protect(inbound), + protector.protect(outbound) + ]) + + pipe( + [Buffer.from('hello world'), Buffer.from('doo dah')], + aToB + ) + + const output = await pipe( + bToA, + source => (async function * () { + for await (const chunk of source) { + yield chunk.slice() + } + })(), + collect + ) + + expect(output).to.eql([Buffer.from('hello world'), Buffer.from('doo dah')]) + }) + + it('should not be able to share correct data with different keys', async () => { + const [inbound, outbound] = duplexPair() + const protector = new Protector(swarmKeyBuffer) + const protectorB = new Protector(wrongSwarmKeyBuffer) + + const [aToB, bToA] = await Promise.all([ + protector.protect(inbound), + protectorB.protect(outbound) + ]) + + pipe( + [Buffer.from('hello world'), Buffer.from('doo dah')], + aToB + ) + + const output = await pipe( + bToA, + collect + ) + + expect(output).to.not.eql([Buffer.from('hello world'), Buffer.from('doo dah')]) + }) + + describe('invalid psks', () => { + it('should not accept a bad psk', () => { + expect(() => { + return new Protector(Buffer.from('not-a-key')) + }).to.throw(Errors.INVALID_PSK) + }) + + it('should not accept a psk of incorrect length', () => { + expect(() => { + return new Protector(Buffer.from('/key/swarm/psk/1.0.0/\n/base16/\ndffb7e')) + }).to.throw(Errors.INVALID_PSK) + }) + }) +}) diff --git a/test/upgrading/upgrader.spec.js b/test/upgrading/upgrader.spec.js index d0583584fe..ff30810b5d 100644 --- a/test/upgrading/upgrader.spec.js +++ b/test/upgrading/upgrader.spec.js @@ -13,12 +13,14 @@ const pipe = require('it-pipe') const { collect } = require('streaming-iterables') const pSettle = require('p-settle') const Transport = require('libp2p-websockets') +const Crypto = require('../../src/insecure/plaintext') +const Protector = require('../../src/pnet') +const swarmKeyBuffer = Buffer.from(require('../fixtures/swarm.key')) const Libp2p = require('../../src') const Upgrader = require('../../src/upgrader') const { codes } = require('../../src/errors') -const mockCrypto = require('../utils/mockCrypto') const mockMultiaddrConnPair = require('../utils/mockMultiaddrConn') const Peers = require('../fixtures/peers') const addrs = [ @@ -63,7 +65,7 @@ describe('Upgrader', () => { sinon.stub(localUpgrader, 'muxers').value(muxers) sinon.stub(remoteUpgrader, 'muxers').value(muxers) - const cryptos = new Map([[mockCrypto.tag, mockCrypto]]) + const cryptos = new Map([[Crypto.protocol, Crypto]]) sinon.stub(localUpgrader, 'cryptos').value(cryptos) sinon.stub(remoteUpgrader, 'cryptos').value(cryptos) @@ -85,7 +87,7 @@ describe('Upgrader', () => { sinon.stub(localUpgrader, 'muxers').value(muxers) sinon.stub(remoteUpgrader, 'muxers').value(muxers) - const cryptos = new Map([[mockCrypto.tag, mockCrypto]]) + const cryptos = new Map([[Crypto.protocol, Crypto]]) sinon.stub(localUpgrader, 'cryptos').value(cryptos) sinon.stub(remoteUpgrader, 'cryptos').value(cryptos) @@ -114,6 +116,48 @@ describe('Upgrader', () => { expect(result).to.eql([hello]) }) + it('should use a private connection protector when provided', async () => { + const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer }) + + const muxers = new Map([[Muxer.multicodec, Muxer]]) + sinon.stub(localUpgrader, 'muxers').value(muxers) + sinon.stub(remoteUpgrader, 'muxers').value(muxers) + + const cryptos = new Map([[Crypto.protocol, Crypto]]) + sinon.stub(localUpgrader, 'cryptos').value(cryptos) + sinon.stub(remoteUpgrader, 'cryptos').value(cryptos) + + const protector = new Protector(swarmKeyBuffer) + sinon.stub(localUpgrader, 'protector').value(protector) + sinon.stub(remoteUpgrader, 'protector').value(protector) + sinon.spy(protector, 'protect') + + const connections = await Promise.all([ + localUpgrader.upgradeOutbound(outbound), + remoteUpgrader.upgradeInbound(inbound) + ]) + + expect(connections).to.have.length(2) + + const { stream, protocol } = await connections[0].newStream('/echo/1.0.0') + expect(protocol).to.equal('/echo/1.0.0') + + const hello = Buffer.from('hello there!') + const result = await pipe( + [hello], + stream, + function toBuffer (source) { + return (async function * () { + for await (const val of source) yield val.slice() + })() + }, + collect + ) + + expect(result).to.eql([hello]) + expect(protector.protect.callCount).to.eql(2) + }) + it('should fail if crypto fails', async () => { const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer }) @@ -153,7 +197,7 @@ describe('Upgrader', () => { sinon.stub(localUpgrader, 'muxers').value(muxersLocal) sinon.stub(remoteUpgrader, 'muxers').value(muxersRemote) - const cryptos = new Map([[mockCrypto.tag, mockCrypto]]) + const cryptos = new Map([[Crypto.protocol, Crypto]]) sinon.stub(localUpgrader, 'cryptos').value(cryptos) sinon.stub(remoteUpgrader, 'cryptos').value(cryptos) @@ -178,7 +222,7 @@ describe('Upgrader', () => { sinon.stub(localUpgrader, 'muxers').value(muxers) sinon.stub(remoteUpgrader, 'muxers').value(muxers) - const cryptos = new Map([[mockCrypto.tag, mockCrypto]]) + const cryptos = new Map([[Crypto.protocol, Crypto]]) sinon.stub(localUpgrader, 'cryptos').value(cryptos) sinon.stub(remoteUpgrader, 'cryptos').value(cryptos) @@ -212,7 +256,7 @@ describe('Upgrader', () => { sinon.stub(localUpgrader, 'muxers').value(muxers) sinon.stub(remoteUpgrader, 'muxers').value(muxers) - const cryptos = new Map([[mockCrypto.tag, mockCrypto]]) + const cryptos = new Map([[Crypto.protocol, Crypto]]) sinon.stub(localUpgrader, 'cryptos').value(cryptos) sinon.stub(remoteUpgrader, 'cryptos').value(cryptos) @@ -246,7 +290,7 @@ describe('Upgrader', () => { sinon.stub(localUpgrader, 'muxers').value(muxers) sinon.stub(remoteUpgrader, 'muxers').value(muxers) - const cryptos = new Map([[mockCrypto.tag, mockCrypto]]) + const cryptos = new Map([[Crypto.protocol, Crypto]]) sinon.stub(localUpgrader, 'cryptos').value(cryptos) sinon.stub(remoteUpgrader, 'cryptos').value(cryptos) @@ -288,18 +332,21 @@ describe('libp2p.upgrader', () => { }) it('should create an Upgrader', () => { + const protector = new Protector(swarmKeyBuffer) libp2p = new Libp2p({ peerInfo: peers[0], modules: { transport: [Transport], streamMuxer: [Muxer], - connEncryption: [mockCrypto] + connEncryption: [Crypto], + connProtector: protector } }) expect(libp2p.upgrader).to.exist() expect(libp2p.upgrader.muxers).to.eql(new Map([[Muxer.multicodec, Muxer]])) - expect(libp2p.upgrader.cryptos).to.eql(new Map([[mockCrypto.tag, mockCrypto]])) + expect(libp2p.upgrader.cryptos).to.eql(new Map([[Crypto.protocol, Crypto]])) + expect(libp2p.upgrader.protector).to.equal(protector) // Ensure the transport manager also has the upgrader expect(libp2p.upgrader).to.equal(libp2p.transportManager.upgrader) }) @@ -310,7 +357,7 @@ describe('libp2p.upgrader', () => { modules: { transport: [Transport], streamMuxer: [Muxer], - connEncryption: [mockCrypto] + connEncryption: [Crypto] } }) @@ -335,14 +382,14 @@ describe('libp2p.upgrader', () => { modules: { transport: [Transport], streamMuxer: [Muxer], - connEncryption: [mockCrypto] + connEncryption: [Crypto] } }) const remoteUpgrader = new Upgrader({ localPeer: remotePeer.id, muxers: new Map([[Muxer.multicodec, Muxer]]), - cryptos: new Map([[mockCrypto.tag, mockCrypto]]) + cryptos: new Map([[Crypto.protocol, Crypto]]) }) const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer: remotePeer.id }) diff --git a/test/utils/mockCrypto.js b/test/utils/mockCrypto.js index d013cec2cd..6a5055c6ea 100644 --- a/test/utils/mockCrypto.js +++ b/test/utils/mockCrypto.js @@ -4,7 +4,7 @@ const PeerId = require('peer-id') const Peers = require('../fixtures/peers') module.exports = { - tag: '/insecure', + protocol: '/insecure', secureInbound: (localPeer, stream) => { return { conn: stream, From f3e276eb79295f18142ea792f64f27ed4ea86f82 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Wed, 6 Nov 2019 15:11:13 +0100 Subject: [PATCH 06/92] feat: peer store (#470) * feat: peer-store v0 * chore: apply suggestions from code review Co-Authored-By: Jacob Heun --- package.json | 1 - src/index.js | 34 +++-- src/peer-store/README.md | 3 + src/peer-store/index.js | 190 +++++++++++++++++++++++++ test/peer-store/peer-store.spec.js | 220 +++++++++++++++++++++++++++++ test/utils/base-options.js | 13 ++ test/utils/creators/peer.js | 24 ++++ test/utils/mockConnection.js | 50 +++++++ 8 files changed, 519 insertions(+), 16 deletions(-) create mode 100644 src/peer-store/README.md create mode 100644 src/peer-store/index.js create mode 100644 test/peer-store/peer-store.spec.js create mode 100644 test/utils/base-options.js create mode 100644 test/utils/creators/peer.js create mode 100644 test/utils/mockConnection.js diff --git a/package.json b/package.json index 6837318936..910325e8f2 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,6 @@ "once": "^1.4.0", "p-queue": "^6.1.1", "p-settle": "^3.1.0", - "peer-book": "^0.9.1", "peer-id": "^0.13.3", "peer-info": "^0.17.0", "promisify-es6": "^1.0.3", diff --git a/src/index.js b/src/index.js index 4d2f31b2c1..df830918ae 100644 --- a/src/index.js +++ b/src/index.js @@ -11,7 +11,6 @@ const promisify = require('promisify-es6') const each = require('async/each') const nextTick = require('async/nextTick') -const PeerBook = require('peer-book') const PeerInfo = require('peer-info') const multiaddr = require('multiaddr') const Switch = require('./switch') @@ -29,6 +28,7 @@ const { codes } = require('./errors') const Dialer = require('./dialer') const TransportManager = require('./transport-manager') const Upgrader = require('./upgrader') +const PeerStore = require('./peer-store') const notStarted = (action, state) => { return errCode( @@ -54,7 +54,7 @@ class Libp2p extends EventEmitter { this.datastore = this._options.datastore this.peerInfo = this._options.peerInfo - this.peerBook = this._options.peerBook || new PeerBook() + this.peerStore = new PeerStore() this._modules = this._options.modules this._config = this._options.config @@ -62,13 +62,15 @@ class Libp2p extends EventEmitter { this._discovery = [] // Discovery service instances/references // create the switch, and listen for errors - this._switch = new Switch(this.peerInfo, this.peerBook, this._options.switch) + this._switch = new Switch(this.peerInfo, this.peerStore, this._options.switch) // Setup the Upgrader this.upgrader = new Upgrader({ localPeer: this.peerInfo.id, onConnection: (connection) => { const peerInfo = getPeerInfo(connection.remotePeer) + + this.peerStore.put(peerInfo) this.emit('peer:connect', peerInfo) }, onConnectionEnd: (connection) => { @@ -179,10 +181,10 @@ class Libp2p extends EventEmitter { // Once we start, emit and dial any peers we may have already discovered this.state.on('STARTED', () => { - this.peerBook.getAllArray().forEach((peerInfo) => { + for (const peerInfo of this.peerStore.peers) { this.emit('peer:discovery', peerInfo) this._maybeConnect(peerInfo) - }) + } }) this._peerDiscovered = this._peerDiscovered.bind(this) @@ -245,7 +247,7 @@ class Libp2p extends EventEmitter { /** * Dials to the provided peer. If successful, the `PeerInfo` of the - * peer will be added to the nodes `PeerBook` + * peer will be added to the nodes `peerStore` * * @param {PeerInfo|PeerId|Multiaddr|string} peer The peer to dial * @param {object} options @@ -258,7 +260,7 @@ class Libp2p extends EventEmitter { /** * Dials to the provided peer and handshakes with the given protocol. - * If successful, the `PeerInfo` of the peer will be added to the nodes `PeerBook`, + * If successful, the `PeerInfo` of the peer will be added to the nodes `peerStore`, * and the `Connection` will be sent in the callback * * @async @@ -277,11 +279,19 @@ class Libp2p extends EventEmitter { connection = await this.dialer.connectToPeer(peer, options) } + const peerInfo = getPeerInfo(connection.remotePeer) + // If a protocol was provided, create a new stream if (protocols) { - return connection.newStream(protocols) + const stream = await connection.newStream(protocols) + + peerInfo.protocols.add(stream.protocol) + this.peerStore.put(peerInfo) + + return stream } + this.peerStore.put(peerInfo) return connection } @@ -369,12 +379,6 @@ class Libp2p extends EventEmitter { * the `peer:discovery` event. If auto dial is enabled for libp2p * and the current connection count is under the low watermark, the * peer will be dialed. - * - * TODO: If `peerBook.put` becomes centralized, https://github.com/libp2p/js-libp2p/issues/345, - * it would be ideal if only new peers were emitted. Currently, with - * other modules adding peers to the `PeerBook` we have no way of knowing - * if a peer is new or not, so it has to be emitted. - * * @private * @param {PeerInfo} peerInfo */ @@ -383,7 +387,7 @@ class Libp2p extends EventEmitter { log.error(new Error(codes.ERR_DISCOVERED_SELF)) return } - peerInfo = this.peerBook.put(peerInfo) + peerInfo = this.peerStore.put(peerInfo) if (!this.isStarted()) return diff --git a/src/peer-store/README.md b/src/peer-store/README.md new file mode 100644 index 0000000000..d9d79fe56a --- /dev/null +++ b/src/peer-store/README.md @@ -0,0 +1,3 @@ +# Peerstore + +WIP \ No newline at end of file diff --git a/src/peer-store/index.js b/src/peer-store/index.js new file mode 100644 index 0000000000..1e91b926f0 --- /dev/null +++ b/src/peer-store/index.js @@ -0,0 +1,190 @@ +'use strict' + +const assert = require('assert') +const debug = require('debug') +const log = debug('libp2p:peer-store') +log.error = debug('libp2p:peer-store:error') + +const { EventEmitter } = require('events') + +const PeerInfo = require('peer-info') + +/** + * Responsible for managing known peers, as well as their addresses and metadata + * @fires PeerStore#peer Emitted when a peer is connected to this node + * @fires PeerStore#change:protocols + * @fires PeerStore#change:multiaddrs + */ +class PeerStore extends EventEmitter { + constructor () { + super() + + /** + * Map of peers + * + * @type {Map} + */ + this.peers = new Map() + + // TODO: Track ourselves. We should split `peerInfo` up into its pieces so we get better + // control and observability. This will be the initial step for removing PeerInfo + // https://github.com/libp2p/go-libp2p-core/blob/master/peerstore/peerstore.go + // this.addressBook = new Map() + // this.protoBook = new Map() + } + + /** + * Stores the peerInfo of a new peer. + * If already exist, its info is updated. + * @param {PeerInfo} peerInfo + */ + put (peerInfo) { + assert(PeerInfo.isPeerInfo(peerInfo), 'peerInfo must be an instance of peer-info') + + // Already know the peer? + if (this.peers.has(peerInfo.id.toB58String())) { + this.update(peerInfo) + } else { + this.add(peerInfo) + + // Emit the new peer found + this.emit('peer', peerInfo) + } + } + + /** + * Add a new peer to the store. + * @param {PeerInfo} peerInfo + */ + add (peerInfo) { + assert(PeerInfo.isPeerInfo(peerInfo), 'peerInfo must be an instance of peer-info') + + // Create new instance and add values to it + const newPeerInfo = new PeerInfo(peerInfo.id) + + peerInfo.multiaddrs.forEach((ma) => newPeerInfo.multiaddrs.add(ma)) + peerInfo.protocols.forEach((p) => newPeerInfo.protocols.add(p)) + + const connectedMa = peerInfo.isConnected() + connectedMa && newPeerInfo.connect(connectedMa) + + const peerProxy = new Proxy(newPeerInfo, { + set: (obj, prop, value) => { + if (prop === 'multiaddrs') { + this.emit('change:multiaddrs', { + peerInfo: obj, + multiaddrs: value.toArray() + }) + } else if (prop === 'protocols') { + this.emit('change:protocols', { + peerInfo: obj, + protocols: Array.from(value) + }) + } + return Reflect.set(...arguments) + } + }) + + this.peers.set(peerInfo.id.toB58String(), peerProxy) + } + + /** + * Updates an already known peer. + * @param {PeerInfo} peerInfo + */ + update (peerInfo) { + assert(PeerInfo.isPeerInfo(peerInfo), 'peerInfo must be an instance of peer-info') + const id = peerInfo.id.toB58String() + const recorded = this.peers.get(id) + + // pass active connection state + const ma = peerInfo.isConnected() + if (ma) { + recorded.connect(ma) + } + + // Verify new multiaddrs + // TODO: better track added and removed multiaddrs + const multiaddrsIntersection = [ + ...recorded.multiaddrs.toArray() + ].filter((m) => peerInfo.multiaddrs.has(m)) + + if (multiaddrsIntersection.length !== peerInfo.multiaddrs.size || + multiaddrsIntersection.length !== recorded.multiaddrs.size) { + // recorded.multiaddrs = peerInfo.multiaddrs + recorded.multiaddrs.clear() + + for (const ma of peerInfo.multiaddrs.toArray()) { + recorded.multiaddrs.add(ma) + } + + this.emit('change:multiaddrs', { + peerInfo: peerInfo, + multiaddrs: recorded.multiaddrs.toArray() + }) + } + + // Update protocols + // TODO: better track added and removed protocols + const protocolsIntersection = new Set( + [...recorded.protocols].filter((p) => peerInfo.protocols.has(p)) + ) + + if (protocolsIntersection.size !== peerInfo.protocols.size || + protocolsIntersection.size !== recorded.protocols.size) { + recorded.protocols.clear() + + for (const protocol of peerInfo.protocols) { + recorded.protocols.add(protocol) + } + + this.emit('change:protocols', { + peerInfo: peerInfo, + protocols: Array.from(recorded.protocols) + }) + } + + // Add the public key if missing + if (!recorded.id.pubKey && peerInfo.id.pubKey) { + recorded.id.pubKey = peerInfo.id.pubKey + } + } + + /** + * Get the info to the given id. + * @param {string} peerId b58str id + * @returns {PeerInfo} + */ + get (peerId) { + const peerInfo = this.peers.get(peerId) + + if (peerInfo) { + return peerInfo + } + + return undefined + } + + /** + * Removes the Peer with the matching `peerId` from the PeerStore + * @param {string} peerId b58str id + * @returns {boolean} true if found and removed + */ + remove (peerId) { + return this.peers.delete(peerId) + } + + /** + * Completely replaces the existing peers metadata with the given `peerInfo` + * @param {PeerInfo} peerInfo + * @returns {void} + */ + replace (peerInfo) { + assert(PeerInfo.isPeerInfo(peerInfo), 'peerInfo must be an instance of peer-info') + + this.remove(peerInfo.id.toB58String()) + this.add(peerInfo) + } +} + +module.exports = PeerStore diff --git a/test/peer-store/peer-store.spec.js b/test/peer-store/peer-store.spec.js new file mode 100644 index 0000000000..8dfba557d3 --- /dev/null +++ b/test/peer-store/peer-store.spec.js @@ -0,0 +1,220 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +const { expect } = chai +const sinon = require('sinon') + +const pDefer = require('p-defer') +const mergeOptions = require('merge-options') + +const Libp2p = require('../../src') +const PeerStore = require('../../src/peer-store') +const multiaddr = require('multiaddr') + +const baseOptions = require('../utils/base-options') +const peerUtils = require('../utils/creators/peer') +const mockConnection = require('../utils/mockConnection') + +const addr = multiaddr('/ip4/127.0.0.1/tcp/8000') +const listenAddr = multiaddr('/ip4/127.0.0.1/tcp/0') + +describe('peer-store', () => { + let peerStore + + beforeEach(() => { + peerStore = new PeerStore() + }) + + it('should add a new peer and emit it when it does not exist', async () => { + const defer = pDefer() + + sinon.spy(peerStore, 'put') + sinon.spy(peerStore, 'add') + sinon.spy(peerStore, 'update') + + const [peerInfo] = await peerUtils.createPeerInfo(1) + + peerStore.on('peer', (peer) => { + expect(peer).to.exist() + defer.resolve() + }) + peerStore.put(peerInfo) + + // Wait for peerStore to emit the peer + await defer.promise + + expect(peerStore.put.callCount).to.equal(1) + expect(peerStore.add.callCount).to.equal(1) + expect(peerStore.update.callCount).to.equal(0) + }) + + it('should update peer when it is already in the store', async () => { + const [peerInfo] = await peerUtils.createPeerInfo(1) + + // Put the peer in the store + peerStore.put(peerInfo) + + sinon.spy(peerStore, 'put') + sinon.spy(peerStore, 'add') + sinon.spy(peerStore, 'update') + + // When updating, peer event must not be emitted + peerStore.on('peer', () => { + throw new Error('should not emit twice') + }) + // If no multiaddrs change, the event should not be emitted + peerStore.on('change:multiaddrs', () => { + throw new Error('should not emit change:multiaddrs') + }) + // If no protocols change, the event should not be emitted + peerStore.on('change:protocols', () => { + throw new Error('should not emit change:protocols') + }) + + peerStore.put(peerInfo) + + expect(peerStore.put.callCount).to.equal(1) + expect(peerStore.add.callCount).to.equal(0) + expect(peerStore.update.callCount).to.equal(1) + }) + + it('should emit the "change:multiaddrs" event when a peer has new multiaddrs', async () => { + const defer = pDefer() + const [createdPeerInfo] = await peerUtils.createPeerInfo(1) + + // Put the peer in the store + peerStore.put(createdPeerInfo) + + // When updating, "change:multiaddrs" event must not be emitted + peerStore.on('change:multiaddrs', ({ peerInfo, multiaddrs }) => { + expect(peerInfo).to.exist() + expect(peerInfo.id).to.eql(createdPeerInfo.id) + expect(peerInfo.protocols).to.eql(createdPeerInfo.protocols) + expect(multiaddrs).to.exist() + expect(multiaddrs).to.eql(createdPeerInfo.multiaddrs.toArray()) + defer.resolve() + }) + // If no protocols change, the event should not be emitted + peerStore.on('change:protocols', () => { + throw new Error('should not emit change:protocols') + }) + + createdPeerInfo.multiaddrs.add(addr) + peerStore.put(createdPeerInfo) + + // Wait for peerStore to emit the event + await defer.promise + }) + + it('should emit the "change:protocols" event when a peer has new protocols', async () => { + const defer = pDefer() + const [createdPeerInfo] = await peerUtils.createPeerInfo(1) + + // Put the peer in the store + peerStore.put(createdPeerInfo) + + // If no multiaddrs change, the event should not be emitted + peerStore.on('change:multiaddrs', () => { + throw new Error('should not emit change:multiaddrs') + }) + // When updating, "change:protocols" event must be emitted + peerStore.on('change:protocols', ({ peerInfo, protocols }) => { + expect(peerInfo).to.exist() + expect(peerInfo.id).to.eql(createdPeerInfo.id) + expect(peerInfo.multiaddrs).to.eql(createdPeerInfo.multiaddrs) + expect(protocols).to.exist() + expect(protocols).to.eql(Array.from(createdPeerInfo.protocols)) + defer.resolve() + }) + + createdPeerInfo.protocols.add('/new-protocol/1.0.0') + peerStore.put(createdPeerInfo) + + // Wait for peerStore to emit the event + await defer.promise + }) + + it('should be able to retrieve a peer from store through its b58str id', async () => { + const [peerInfo] = await peerUtils.createPeerInfo(1) + const id = peerInfo.id.toB58String() + + let retrievedPeer = peerStore.get(id) + expect(retrievedPeer).to.not.exist() + + // Put the peer in the store + peerStore.put(peerInfo) + + retrievedPeer = peerStore.get(id) + expect(retrievedPeer).to.exist() + expect(retrievedPeer.id).to.equal(peerInfo.id) + expect(retrievedPeer.multiaddrs).to.eql(peerInfo.multiaddrs) + expect(retrievedPeer.protocols).to.eql(peerInfo.protocols) + }) + + it('should be able to remove a peer from store through its b58str id', async () => { + const [peerInfo] = await peerUtils.createPeerInfo(1) + const id = peerInfo.id.toB58String() + + let removed = peerStore.remove(id) + expect(removed).to.eql(false) + + // Put the peer in the store + peerStore.put(peerInfo) + expect(peerStore.peers.size).to.equal(1) + + removed = peerStore.remove(id) + expect(removed).to.eql(true) + expect(peerStore.peers.size).to.equal(0) + }) +}) + +describe('peer-store on dial', () => { + let peerInfo + let remotePeerInfo + let libp2p + let remoteLibp2p + + before(async () => { + [peerInfo, remotePeerInfo] = await peerUtils.createPeerInfoFromFixture(2) + remoteLibp2p = new Libp2p(mergeOptions(baseOptions, { + peerInfo: remotePeerInfo + })) + }) + + after(async () => { + sinon.restore() + await remoteLibp2p.stop() + libp2p && await libp2p.stop() + }) + + it('should put the remote peerInfo after dial and emit event', async () => { + const remoteId = remotePeerInfo.id.toB58String() + + libp2p = new Libp2p(mergeOptions(baseOptions, { + peerInfo + })) + + sinon.spy(libp2p.peerStore, 'put') + sinon.spy(libp2p.peerStore, 'add') + sinon.spy(libp2p.peerStore, 'update') + sinon.stub(libp2p.dialer, 'connectToMultiaddr').returns(mockConnection({ + remotePeer: remotePeerInfo.id + })) + + const connection = await libp2p.dial(listenAddr) + await connection.close() + + expect(libp2p.peerStore.put.callCount).to.equal(1) + expect(libp2p.peerStore.add.callCount).to.equal(1) + expect(libp2p.peerStore.update.callCount).to.equal(0) + + const storedPeer = libp2p.peerStore.get(remoteId) + expect(storedPeer).to.exist() + }) +}) + +describe('peer-store on discovery', () => { + // TODO: implement with discovery +}) diff --git a/test/utils/base-options.js b/test/utils/base-options.js new file mode 100644 index 0000000000..038df59219 --- /dev/null +++ b/test/utils/base-options.js @@ -0,0 +1,13 @@ +'use strict' + +const Transport = require('libp2p-tcp') +const Muxer = require('libp2p-mplex') +const mockCrypto = require('../utils/mockCrypto') + +module.exports = { + modules: { + transport: [Transport], + streamMuxer: [Muxer], + connEncryption: [mockCrypto] + } +} diff --git a/test/utils/creators/peer.js b/test/utils/creators/peer.js new file mode 100644 index 0000000000..e39e52e46d --- /dev/null +++ b/test/utils/creators/peer.js @@ -0,0 +1,24 @@ +'use strict' + +const PeerId = require('peer-id') +const PeerInfo = require('peer-info') + +const Peers = require('../../fixtures/peers') + +module.exports.createPeerInfo = async (length) => { + const peers = await Promise.all( + Array.from({ length }) + .map((_, i) => PeerId.create()) + ) + + return peers.map((peer) => new PeerInfo(peer)) +} + +module.exports.createPeerInfoFromFixture = async (length) => { + const peers = await Promise.all( + Array.from({ length }) + .map((_, i) => PeerId.createFromJSON(Peers[i])) + ) + + return peers.map((peer) => new PeerInfo(peer)) +} diff --git a/test/utils/mockConnection.js b/test/utils/mockConnection.js new file mode 100644 index 0000000000..f3ce885884 --- /dev/null +++ b/test/utils/mockConnection.js @@ -0,0 +1,50 @@ +'use strict' + +const { Connection } = require('libp2p-interfaces/src/connection') +const multiaddr = require('multiaddr') + +const pair = require('it-pair') + +const peerUtils = require('./creators/peer') + +module.exports = async (properties = {}) => { + const localAddr = multiaddr('/ip4/127.0.0.1/tcp/8080') + const remoteAddr = multiaddr('/ip4/127.0.0.1/tcp/8081') + + const [localPeer, remotePeer] = await peerUtils.createPeerInfoFromFixture(2) + const openStreams = [] + let streamId = 0 + + return new Connection({ + localPeer: localPeer.id, + remotePeer: remotePeer.id, + localAddr, + remoteAddr, + stat: { + timeline: { + open: Date.now() - 10, + upgraded: Date.now() + }, + direction: 'outbound', + encryption: '/secio/1.0.0', + multiplexer: '/mplex/6.7.0' + }, + newStream: (protocols) => { + const id = streamId++ + const stream = pair() + + stream.close = () => stream.sink([]) + stream.id = id + + openStreams.push(stream) + + return { + stream, + protocol: protocols[0] + } + }, + close: () => { }, + getStreams: () => openStreams, + ...properties + }) +} From 797d8f0cf1620dd374cc2ea1fbbbc6c8e0542ad7 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Wed, 6 Nov 2019 15:47:11 +0100 Subject: [PATCH 07/92] feat: registrar (#471) * feat: peer-store v0 * feat: registrar * chore: apply suggestions from code review Co-Authored-By: Jacob Heun * chore: address review * chore: support multiple conns * chore: address review * fix: no remote peer from topology on disconnect --- src/connection-manager/topology.js | 108 ++++++++++++++ src/get-peer-info.js | 12 +- src/index.js | 8 ++ src/registrar.js | 139 ++++++++++++++++++ test/registrar/registrar.node.js | 57 ++++++++ test/registrar/registrar.spec.js | 224 +++++++++++++++++++++++++++++ test/registrar/utils.js | 50 +++++++ test/utils/base-options.js | 4 +- 8 files changed, 594 insertions(+), 8 deletions(-) create mode 100644 src/connection-manager/topology.js create mode 100644 src/registrar.js create mode 100644 test/registrar/registrar.node.js create mode 100644 test/registrar/registrar.spec.js create mode 100644 test/registrar/utils.js diff --git a/src/connection-manager/topology.js b/src/connection-manager/topology.js new file mode 100644 index 0000000000..2c2a877919 --- /dev/null +++ b/src/connection-manager/topology.js @@ -0,0 +1,108 @@ +'use strict' + +const assert = require('assert') + +class Topology { + /** + * @param {Object} props + * @param {number} props.min minimum needed connections (default: 0) + * @param {number} props.max maximum needed connections (default: Infinity) + * @param {Array} props.multicodecs protocol multicodecs + * @param {Object} props.handlers + * @param {function} props.handlers.onConnect protocol "onConnect" handler + * @param {function} props.handlers.onDisconnect protocol "onDisconnect" handler + * @constructor + */ + constructor ({ + min = 0, + max = Infinity, + multicodecs, + handlers + }) { + assert(multicodecs, 'one or more multicodec should be provided') + assert(handlers, 'the handlers should be provided') + assert(handlers.onConnect && typeof handlers.onConnect === 'function', + 'the \'onConnect\' handler must be provided') + assert(handlers.onDisconnect && typeof handlers.onDisconnect === 'function', + 'the \'onDisconnect\' handler must be provided') + + this.multicodecs = Array.isArray(multicodecs) ? multicodecs : [multicodecs] + this.min = min + this.max = max + + // Handlers + this._onConnect = handlers.onConnect + this._onDisconnect = handlers.onDisconnect + + this.peers = new Map() + this._registrar = undefined + + this._onProtocolChange = this._onProtocolChange.bind(this) + } + + set registrar (registrar) { + this._registrar = registrar + this._registrar.peerStore.on('change:protocols', this._onProtocolChange) + + // Update topology peers + this._updatePeers(this._registrar.peerStore.peers.values()) + } + + /** + * Update topology. + * @param {Array} peerInfoIterable + * @returns {void} + */ + _updatePeers (peerInfoIterable) { + for (const peerInfo of peerInfoIterable) { + if (this.multicodecs.filter(multicodec => peerInfo.protocols.has(multicodec))) { + // Add the peer regardless of whether or not there is currently a connection + this.peers.set(peerInfo.id.toB58String(), peerInfo) + // If there is a connection, call _onConnect + const connection = this._registrar.getConnection(peerInfo) + connection && this._onConnect(peerInfo, connection) + } else { + // Remove any peers we might be tracking that are no longer of value to us + this.peers.delete(peerInfo.id.toB58String()) + } + } + } + + /** + * Notify protocol of peer disconnected. + * @param {PeerInfo} peerInfo + * @param {Error} [error] + * @returns {void} + */ + disconnect (peerInfo, error) { + this._onDisconnect(peerInfo, error) + } + + /** + * Check if a new peer support the multicodecs for this topology. + * @param {Object} props + * @param {PeerInfo} props.peerInfo + * @param {Array} props.protocols + */ + _onProtocolChange ({ peerInfo, protocols }) { + const existingPeer = this.peers.get(peerInfo.id.toB58String()) + const hasProtocol = protocols.filter(protocol => this.multicodecs.includes(protocol)) + + // Not supporting the protocol anymore? + if (existingPeer && hasProtocol.length === 0) { + this._onDisconnect({ + peerInfo + }) + } + + // New to protocol support + for (const protocol of protocols) { + if (this.multicodecs.includes(protocol)) { + this._updatePeers([peerInfo]) + return + } + } + } +} + +module.exports = Topology diff --git a/src/get-peer-info.js b/src/get-peer-info.js index b75757f5e6..b74caa3ec1 100644 --- a/src/get-peer-info.js +++ b/src/get-peer-info.js @@ -7,14 +7,14 @@ const errCode = require('err-code') /** * Converts the given `peer` to a `PeerInfo` instance. - * The `PeerBook` will be checked for the resulting peer, and - * the peer will be updated in the `PeerBook`. + * The `PeerStore` will be checked for the resulting peer, and + * the peer will be updated in the `PeerStore`. * * @param {PeerInfo|PeerId|Multiaddr|string} peer - * @param {PeerBook} peerBook + * @param {PeerStore} peerStore * @returns {PeerInfo} */ -function getPeerInfo (peer, peerBook) { +function getPeerInfo (peer, peerStore) { if (typeof peer === 'string') { peer = multiaddr(peer) } @@ -38,7 +38,7 @@ function getPeerInfo (peer, peerBook) { addr && peer.multiaddrs.add(addr) - return peerBook ? peerBook.put(peer) : peer + return peerStore ? peerStore.put(peer) : peer } /** @@ -54,7 +54,7 @@ function getPeerInfoRemote (peer, libp2p) { let peerInfo try { - peerInfo = getPeerInfo(peer, libp2p.peerBook) + peerInfo = getPeerInfo(peer, libp2p.peerStore) } catch (err) { return Promise.reject(errCode( new Error(`${peer} is not a valid peer type`), diff --git a/src/index.js b/src/index.js index df830918ae..998d4a6bae 100644 --- a/src/index.js +++ b/src/index.js @@ -29,6 +29,7 @@ const Dialer = require('./dialer') const TransportManager = require('./transport-manager') const Upgrader = require('./upgrader') const PeerStore = require('./peer-store') +const Registrar = require('./registrar') const notStarted = (action, state) => { return errCode( @@ -71,10 +72,13 @@ class Libp2p extends EventEmitter { const peerInfo = getPeerInfo(connection.remotePeer) this.peerStore.put(peerInfo) + this.registrar.onConnect(peerInfo, connection) this.emit('peer:connect', peerInfo) }, onConnectionEnd: (connection) => { const peerInfo = getPeerInfo(connection.remotePeer) + + this.registrar.onDisconnect(peerInfo, connection) this.emit('peer:disconnect', peerInfo) } }) @@ -108,6 +112,10 @@ class Libp2p extends EventEmitter { transportManager: this.transportManager }) + this.registrar = new Registrar({ peerStore: this.peerStore }) + this.handle = this.handle.bind(this) + this.registrar.handle = this.handle + // Attach private network protector if (this._modules.connProtector) { this.upgrader.protector = this._modules.connProtector diff --git a/src/registrar.js b/src/registrar.js new file mode 100644 index 0000000000..c6e4439c00 --- /dev/null +++ b/src/registrar.js @@ -0,0 +1,139 @@ +'use strict' + +const assert = require('assert') +const debug = require('debug') +const log = debug('libp2p:peer-store') +log.error = debug('libp2p:peer-store:error') + +const { Connection } = require('libp2p-interfaces/src/connection') +const PeerInfo = require('peer-info') +const Toplogy = require('./connection-manager/topology') + +/** + * Responsible for notifying registered protocols of events in the network. + */ +class Registrar { + /** + * @param {Object} props + * @param {PeerStore} props.peerStore + * @constructor + */ + constructor ({ peerStore }) { + this.peerStore = peerStore + + /** + * Map of connections per peer + * TODO: this should be handled by connectionManager + * @type {Map>} + */ + this.connections = new Map() + + /** + * Map of topologies + * + * @type {Map} + */ + this.topologies = new Map() + + this._handle = undefined + } + + get handle () { + return this._handle + } + + set handle (handle) { + this._handle = handle + } + + /** + * Add a new connected peer to the record + * TODO: this should live in the ConnectionManager + * @param {PeerInfo} peerInfo + * @param {Connection} conn + * @returns {void} + */ + onConnect (peerInfo, conn) { + assert(PeerInfo.isPeerInfo(peerInfo), 'peerInfo must be an instance of peer-info') + assert(Connection.isConnection(conn), 'conn must be an instance of interface-connection') + + const id = peerInfo.id.toB58String() + const storedConn = this.connections.get(id) + + if (storedConn) { + storedConn.push(conn) + } else { + this.connections.set(id, [conn]) + } + } + + /** + * Remove a disconnected peer from the record + * TODO: this should live in the ConnectionManager + * @param {PeerInfo} peerInfo + * @param {Connection} connection + * @param {Error} [error] + * @returns {void} + */ + onDisconnect (peerInfo, connection, error) { + assert(PeerInfo.isPeerInfo(peerInfo), 'peerInfo must be an instance of peer-info') + + const id = peerInfo.id.toB58String() + let storedConn = this.connections.get(id) + + if (storedConn && storedConn.length > 1) { + storedConn = storedConn.filter((conn) => conn.id === connection.id) + } else if (storedConn) { + for (const [, topology] of this.topologies) { + topology.disconnect(peerInfo, error) + } + + this.connections.delete(peerInfo.id.toB58String()) + } + } + + /** + * Get a connection with a peer. + * @param {PeerInfo} peerInfo + * @returns {Connection} + */ + getConnection (peerInfo) { + assert(PeerInfo.isPeerInfo(peerInfo), 'peerInfo must be an instance of peer-info') + + // TODO: what should we return + return this.connections.get(peerInfo.id.toB58String())[0] + } + + /** + * Register handlers for a set of multicodecs given + * @param {Object} topologyProps properties for topology + * @param {Array|string} topologyProps.multicodecs + * @param {Object} topologyProps.handlers + * @param {function} topologyProps.handlers.onConnect + * @param {function} topologyProps.handlers.onDisconnect + * @return {string} registrar identifier + */ + register (topologyProps) { + // Create multicodec topology + const id = (parseInt(Math.random() * 1e9)).toString(36) + Date.now() + const topology = new Toplogy(topologyProps) + + this.topologies.set(id, topology) + + // Set registrar + topology.registrar = this + + return id + } + + /** + * Unregister topology. + * @param {string} id registrar identifier + * @return {boolean} unregistered successfully + */ + unregister (id) { + return this.topologies.delete(id) + } +} + +module.exports = Registrar diff --git a/test/registrar/registrar.node.js b/test/registrar/registrar.node.js new file mode 100644 index 0000000000..b2045004c4 --- /dev/null +++ b/test/registrar/registrar.node.js @@ -0,0 +1,57 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +const { expect } = chai +const sinon = require('sinon') + +const mergeOptions = require('merge-options') + +const multiaddr = require('multiaddr') +const Libp2p = require('../../src') + +const baseOptions = require('../utils/base-options') +const peerUtils = require('../utils/creators/peer') +const listenAddr = multiaddr('/ip4/127.0.0.1/tcp/0') + +describe('registrar on dial', () => { + let peerInfo + let remotePeerInfo + let libp2p + let remoteLibp2p + let remoteAddr + + before(async () => { + [peerInfo, remotePeerInfo] = await peerUtils.createPeerInfoFromFixture(2) + remoteLibp2p = new Libp2p(mergeOptions(baseOptions, { + peerInfo: remotePeerInfo + })) + + await remoteLibp2p.transportManager.listen([listenAddr]) + remoteAddr = remoteLibp2p.transportManager.getAddrs()[0] + }) + + after(async () => { + sinon.restore() + await remoteLibp2p.stop() + libp2p && await libp2p.stop() + }) + + it('should inform registrar of a new connection', async () => { + libp2p = new Libp2p(mergeOptions(baseOptions, { + peerInfo + })) + + sinon.spy(remoteLibp2p.registrar, 'onConnect') + + await libp2p.dial(remoteAddr) + expect(remoteLibp2p.registrar.onConnect.callCount).to.equal(1) + + const libp2pConn = libp2p.registrar.getConnection(remotePeerInfo) + expect(libp2pConn).to.exist() + + const remoteConn = remoteLibp2p.registrar.getConnection(peerInfo) + expect(remoteConn).to.exist() + }) +}) diff --git a/test/registrar/registrar.spec.js b/test/registrar/registrar.spec.js new file mode 100644 index 0000000000..ec7d1b6189 --- /dev/null +++ b/test/registrar/registrar.spec.js @@ -0,0 +1,224 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +const { expect } = chai +const pDefer = require('p-defer') + +const PeerInfo = require('peer-info') +const PeerStore = require('../../src/peer-store') +const Registrar = require('../../src/registrar') +const { createMockConnection } = require('./utils') + +const multicodec = '/test/1.0.0' + +describe('registrar', () => { + let peerStore, registrar + + describe('errors', () => { + beforeEach(() => { + peerStore = new PeerStore() + registrar = new Registrar({ peerStore }) + }) + + it('should fail to register a protocol if no multicodec is provided', () => { + try { + registrar.register() + } catch (err) { + expect(err).to.exist() + return + } + throw new Error('should fail to register a protocol if no multicodec is provided') + }) + + it('should fail to register a protocol if no handlers are provided', () => { + const topologyProps = { + multicodecs: multicodec + } + + try { + registrar.register(topologyProps) + } catch (err) { + expect(err).to.exist() + return + } + throw new Error('should fail to register a protocol if no handlers are provided') + }) + + it('should fail to register a protocol if the onConnect handler is not provided', () => { + const topologyProps = { + multicodecs: multicodec, + handlers: { + onDisconnect: () => { } + } + } + + try { + registrar.register(topologyProps) + } catch (err) { + expect(err).to.exist() + return + } + throw new Error('should fail to register a protocol if the onConnect handler is not provided') + }) + + it('should fail to register a protocol if the onDisconnect handler is not provided', () => { + const topologyProps = { + multicodecs: multicodec, + handlers: { + onConnect: () => { } + } + } + + try { + registrar.register(topologyProps) + } catch (err) { + expect(err).to.exist() + return + } + throw new Error('should fail to register a protocol if the onDisconnect handler is not provided') + }) + }) + + describe('registration', () => { + beforeEach(() => { + peerStore = new PeerStore() + registrar = new Registrar({ peerStore }) + }) + + it('should be able to register a protocol', () => { + const topologyProps = { + handlers: { + onConnect: () => { }, + onDisconnect: () => { } + }, + multicodecs: multicodec + } + + const identifier = registrar.register(topologyProps) + + expect(identifier).to.exist() + }) + + it('should be able to unregister a protocol', () => { + const topologyProps = { + handlers: { + onConnect: () => { }, + onDisconnect: () => { } + }, + multicodecs: multicodec + } + + const identifier = registrar.register(topologyProps) + const success = registrar.unregister(identifier) + + expect(success).to.eql(true) + }) + + it('should fail to unregister if no register was made', () => { + const success = registrar.unregister('bad-identifier') + + expect(success).to.eql(false) + }) + + it('should call onConnect handler for connected peers after register', async () => { + const onConnectDefer = pDefer() + const onDisconnectDefer = pDefer() + + // Setup connections before registrar + const conn = await createMockConnection() + const remotePeerInfo = await PeerInfo.create(conn.remotePeer) + + // Add protocol to peer + remotePeerInfo.protocols.add(multicodec) + + // Add connected peer to peerStore and registrar + peerStore.put(remotePeerInfo) + registrar.onConnect(remotePeerInfo, conn) + expect(registrar.connections.size).to.eql(1) + + const topologyProps = { + multicodecs: multicodec, + handlers: { + onConnect: (peerInfo, connection) => { + expect(peerInfo.id.toB58String()).to.eql(remotePeerInfo.id.toB58String()) + expect(connection.id).to.eql(conn.id) + + onConnectDefer.resolve() + }, + onDisconnect: (peerInfo) => { + expect(peerInfo.id.toB58String()).to.eql(remotePeerInfo.id.toB58String()) + + onDisconnectDefer.resolve() + } + } + } + + // Register protocol + const identifier = registrar.register(topologyProps) + const topology = registrar.topologies.get(identifier) + + // Topology created + expect(topology).to.exist() + expect(topology.peers.size).to.eql(1) + + registrar.onDisconnect(remotePeerInfo) + expect(registrar.connections.size).to.eql(0) + expect(topology.peers.size).to.eql(1) // topology should keep the peer + + // Wait for handlers to be called + return Promise.all([ + onConnectDefer.promise, + onDisconnectDefer.promise + ]) + }) + + it('should call onConnect handler after register, once a peer is connected and protocols are updated', async () => { + const onConnectDefer = pDefer() + const onDisconnectDefer = pDefer() + + const topologyProps = { + multicodecs: multicodec, + handlers: { + onConnect: () => { + onConnectDefer.resolve() + }, + onDisconnect: () => { + onDisconnectDefer.resolve() + } + } + } + + // Register protocol + const identifier = registrar.register(topologyProps) + const topology = registrar.topologies.get(identifier) + + // Topology created + expect(topology).to.exist() + expect(topology.peers.size).to.eql(0) + expect(registrar.connections.size).to.eql(0) + + // Setup connections before registrar + const conn = await createMockConnection() + const peerInfo = await PeerInfo.create(conn.remotePeer) + + // Add connected peer to peerStore and registrar + peerStore.put(peerInfo) + registrar.onConnect(peerInfo, conn) + + // Add protocol to peer and update it + peerInfo.protocols.add(multicodec) + peerStore.put(peerInfo) + + await onConnectDefer.promise + expect(topology.peers.size).to.eql(1) + + // Remove protocol to peer and update it + peerInfo.protocols.delete(multicodec) + peerStore.put(peerInfo) + + await onDisconnectDefer.promise + }) + }) +}) diff --git a/test/registrar/utils.js b/test/registrar/utils.js new file mode 100644 index 0000000000..4b4e04839f --- /dev/null +++ b/test/registrar/utils.js @@ -0,0 +1,50 @@ +'use strict' + +const { Connection } = require('libp2p-interfaces/src/connection') +const multiaddr = require('multiaddr') + +const pair = require('it-pair') + +const peerUtils = require('../utils/creators/peer') + +module.exports.createMockConnection = async (properties = {}) => { + const localAddr = multiaddr('/ip4/127.0.0.1/tcp/8080') + const remoteAddr = multiaddr('/ip4/127.0.0.1/tcp/8081') + + const [localPeer, remotePeer] = await peerUtils.createPeerInfoFromFixture(2) + const openStreams = [] + let streamId = 0 + + return new Connection({ + localPeer: localPeer.id, + remotePeer: remotePeer.id, + localAddr, + remoteAddr, + stat: { + timeline: { + open: Date.now() - 10, + upgraded: Date.now() + }, + direction: 'outbound', + encryption: '/secio/1.0.0', + multiplexer: '/mplex/6.7.0' + }, + newStream: (protocols) => { + const id = streamId++ + const stream = pair() + + stream.close = () => stream.sink([]) + stream.id = id + + openStreams.push(stream) + + return { + stream, + protocol: protocols[0] + } + }, + close: () => { }, + getStreams: () => openStreams, + ...properties + }) +} diff --git a/test/utils/base-options.js b/test/utils/base-options.js index 038df59219..68163eeed5 100644 --- a/test/utils/base-options.js +++ b/test/utils/base-options.js @@ -2,12 +2,12 @@ const Transport = require('libp2p-tcp') const Muxer = require('libp2p-mplex') -const mockCrypto = require('../utils/mockCrypto') +const Crypto = require('../../src/insecure/plaintext') module.exports = { modules: { transport: [Transport], streamMuxer: [Muxer], - connEncryption: [mockCrypto] + connEncryption: [Crypto] } } From 44d47087d14625cacc80e92ba7453e4f87ebc8db Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Thu, 7 Nov 2019 12:11:50 +0100 Subject: [PATCH 08/92] refactor: async identify and identify push (#473) * chore: add missing dep * feat: import from identify push branch https://github.com/libp2p/js-libp2p-identify/tree/feat/identify-push * feat: add the connection to stream handlers * refactor: identify to async/await * chore: fix lint * test: add identify tests * refactor: add identify to the dialer flow * feat: connect identify to the registrar * fix: resolve review feedback * fix: perform identify push when our protocols change --- package.json | 3 +- src/dialer.js | 29 +++ src/errors.js | 3 + src/identify/README.md | 28 +-- src/identify/consts.js | 6 + src/identify/dialer.js | 87 --------- src/identify/index.js | 302 ++++++++++++++++++++++++++++- src/identify/listener.js | 35 ---- src/index.js | 35 +++- src/upgrader.js | 19 +- src/util/index.js | 14 ++ test/dialing/direct.spec.js | 23 +++ test/identify/index.spec.js | 247 +++++++++++++++++++++++ test/upgrading/upgrader.spec.js | 4 +- test/utils/base-options.browser.js | 13 ++ 15 files changed, 674 insertions(+), 174 deletions(-) create mode 100644 src/identify/consts.js delete mode 100644 src/identify/dialer.js delete mode 100644 src/identify/listener.js create mode 100644 test/identify/index.spec.js create mode 100644 test/utils/base-options.browser.js diff --git a/package.json b/package.json index 910325e8f2..04970f958a 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "it-handshake": "^1.0.1", "it-length-prefixed": "jacobheun/pull-length-prefixed#feat/fromReader", "it-pipe": "^1.0.1", + "it-protocol-buffers": "^0.2.0", "latency-monitor": "~0.2.1", "libp2p-crypto": "^0.16.2", "libp2p-interfaces": "^0.1.3", @@ -68,9 +69,7 @@ "promisify-es6": "^1.0.3", "protons": "^1.0.1", "pull-cat": "^1.1.11", - "pull-defer": "~0.2.3", "pull-handshake": "^1.1.4", - "pull-reader": "^1.3.1", "pull-stream": "^3.6.9", "retimer": "^2.0.0", "xsalsa20": "^1.0.2" diff --git a/src/dialer.js b/src/dialer.js index 2177ccdad6..85ac6ae32d 100644 --- a/src/dialer.js +++ b/src/dialer.js @@ -1,5 +1,6 @@ 'use strict' +const nextTick = require('async/nextTick') const multiaddr = require('multiaddr') const errCode = require('err-code') const { default: PQueue } = require('p-queue') @@ -31,6 +32,22 @@ class Dialer { this.concurrency = concurrency this.timeout = timeout this.queue = new PQueue({ concurrency, timeout, throwOnTimeout: true }) + + /** + * @property {IdentifyService} + */ + this._identifyService = null + } + + set identifyService (service) { + this._identifyService = service + } + + /** + * @type {IdentifyService} + */ + get identifyService () { + return this._identifyService } /** @@ -64,6 +81,18 @@ class Dialer { throw err } + // Perform a delayed Identify handshake + if (this.identifyService) { + nextTick(async () => { + try { + await this.identifyService.identify(conn, conn.remotePeer) + // TODO: Update the PeerStore with the information from identify + } catch (err) { + log.error(err) + } + }) + } + return conn } diff --git a/src/errors.js b/src/errors.js index de75f094ff..2a103db2ed 100644 --- a/src/errors.js +++ b/src/errors.js @@ -8,6 +8,7 @@ exports.messages = { exports.codes = { DHT_DISABLED: 'ERR_DHT_DISABLED', PUBSUB_NOT_STARTED: 'ERR_PUBSUB_NOT_STARTED', + ERR_CONNECTION_ENDED: 'ERR_CONNECTION_ENDED', ERR_CONNECTION_FAILED: 'ERR_CONNECTION_FAILED', ERR_NODE_NOT_STARTED: 'ERR_NODE_NOT_STARTED', ERR_NO_VALID_ADDRESSES: 'ERR_NO_VALID_ADDRESSES', @@ -15,6 +16,8 @@ exports.codes = { ERR_DUPLICATE_TRANSPORT: 'ERR_DUPLICATE_TRANSPORT', ERR_ENCRYPTION_FAILED: 'ERR_ENCRYPTION_FAILED', ERR_INVALID_KEY: 'ERR_INVALID_KEY', + ERR_INVALID_MESSAGE: 'ERR_INVALID_MESSAGE', + ERR_INVALID_PEER: 'ERR_INVALID_PEER', ERR_MUXER_UNAVAILABLE: 'ERR_MUXER_UNAVAILABLE', ERR_TIMEOUT: 'ERR_TIMEOUT', ERR_TRANSPORT_UNAVAILABLE: 'ERR_TRANSPORT_UNAVAILABLE', diff --git a/src/identify/README.md b/src/identify/README.md index 865a392cbb..d356abe3bf 100644 --- a/src/identify/README.md +++ b/src/identify/README.md @@ -6,32 +6,8 @@ ## Description -Identify is a STUN protocol, used by libp2p-swarm in order to broadcast and learn about the `ip:port` pairs a specific peer is available through and to know when a new stream muxer is established, so a conn can be reused. +Identify is a STUN protocol, used by libp2p in order to broadcast and learn about the `ip:port` pairs a specific peer is available through and to know when a new stream muxer is established, so a conn can be reused. ## How does it work -Best way to understand the current design is through this issue: https://github.com/libp2p/js-libp2p-swarm/issues/78 - -### This module uses `pull-streams` - -We expose a streaming interface based on `pull-streams`, rather then on the Node.js core streams implementation (aka Node.js streams). `pull-streams` offers us a better mechanism for error handling and flow control guarantees. If you would like to know more about why we did this, see the discussion at this [issue](https://github.com/ipfs/js-ipfs/issues/362). - -You can learn more about pull-streams at: - -- [The history of Node.js streams, nodebp April 2014](https://www.youtube.com/watch?v=g5ewQEuXjsQ) -- [The history of streams, 2016](http://dominictarr.com/post/145135293917/history-of-streams) -- [pull-streams, the simple streaming primitive](http://dominictarr.com/post/149248845122/pull-streams-pull-streams-are-a-very-simple) -- [pull-streams documentation](https://pull-stream.github.io/) - -#### Converting `pull-streams` to Node.js Streams - -If you are a Node.js streams user, you can convert a pull-stream to a Node.js stream using the module [`pull-stream-to-stream`](https://github.com/pull-stream/pull-stream-to-stream), giving you an instance of a Node.js stream that is linked to the pull-stream. For example: - -```js -const pullToStream = require('pull-stream-to-stream') - -const nodeStreamInstance = pullToStream(pullStreamInstance) -// nodeStreamInstance is an instance of a Node.js Stream -``` - -To learn more about this utility, visit https://pull-stream.github.io/#pull-stream-to-stream. +The spec for Identify and Identify Push is at [libp2p/specs](https://github.com/libp2p/specs/tree/master/identify). diff --git a/src/identify/consts.js b/src/identify/consts.js new file mode 100644 index 0000000000..ae8d8432df --- /dev/null +++ b/src/identify/consts.js @@ -0,0 +1,6 @@ +'use strict' + +module.exports.PROTOCOL_VERSION = 'ipfs/0.1.0' +module.exports.AGENT_VERSION = 'js-libp2p/0.1.0' +module.exports.MULTICODEC_IDENTIFY = '/ipfs/id/1.0.0' +module.exports.MULTICODEC_IDENTIFY_PUSH = '/ipfs/id/push/1.0.0' diff --git a/src/identify/dialer.js b/src/identify/dialer.js deleted file mode 100644 index ba9a524fa0..0000000000 --- a/src/identify/dialer.js +++ /dev/null @@ -1,87 +0,0 @@ -'use strict' -const PeerInfo = require('peer-info') -const PeerId = require('peer-id') -const multiaddr = require('multiaddr') -const pull = require('pull-stream/pull') -const take = require('pull-stream/throughs/take') -const collect = require('pull-stream/sinks/collect') -const lp = require('pull-length-prefixed') - -const msg = require('./message') - -module.exports = (conn, expectedPeerInfo, callback) => { - if (typeof expectedPeerInfo === 'function') { - callback = expectedPeerInfo - expectedPeerInfo = null - // eslint-disable-next-line no-console - console.warn('WARNING: no expected peer info was given, identify will not be able to verify peer integrity') - } - - pull( - conn, - lp.decode(), - take(1), - collect((err, data) => { - if (err) { - return callback(err) - } - - // connection got closed graciously - if (data.length === 0) { - return callback(new Error('conn was closed, did not receive data')) - } - - const input = msg.decode(data[0]) - - PeerId.createFromPubKey(input.publicKey, (err, id) => { - if (err) { - return callback(err) - } - - const peerInfo = new PeerInfo(id) - if (expectedPeerInfo && expectedPeerInfo.id.toB58String() !== id.toB58String()) { - return callback(new Error('invalid peer')) - } - - try { - input.listenAddrs - .map(multiaddr) - .forEach((ma) => peerInfo.multiaddrs.add(ma)) - } catch (err) { - return callback(err) - } - - let observedAddr - - try { - observedAddr = getObservedAddrs(input) - } catch (err) { - return callback(err) - } - - // Copy the protocols - peerInfo.protocols = new Set(input.protocols) - - callback(null, peerInfo, observedAddr) - }) - }) - ) -} - -function getObservedAddrs (input) { - if (!hasObservedAddr(input)) { - return [] - } - - let addrs = input.observedAddr - - if (!Array.isArray(addrs)) { - addrs = [addrs] - } - - return addrs.map((oa) => multiaddr(oa)) -} - -function hasObservedAddr (input) { - return input.observedAddr && input.observedAddr.length > 0 -} diff --git a/src/identify/index.js b/src/identify/index.js index bcbdd641ff..ca1d70e47c 100644 --- a/src/identify/index.js +++ b/src/identify/index.js @@ -1,7 +1,299 @@ 'use strict' -exports = module.exports -exports.multicodec = '/ipfs/id/1.0.0' -exports.listener = require('./listener') -exports.dialer = require('./dialer') -exports.message = require('./message') +const debug = require('debug') +const pb = require('it-protocol-buffers') +const lp = require('it-length-prefixed') +const pipe = require('it-pipe') +const { collect, take } = require('streaming-iterables') + +const PeerInfo = require('peer-info') +const PeerId = require('peer-id') +const multiaddr = require('multiaddr') +const { toBuffer } = require('../util') + +const Message = require('./message') + +const log = debug('libp2p:identify') +log.error = debug('libp2p:identify:error') + +const { + MULTICODEC_IDENTIFY, + MULTICODEC_IDENTIFY_PUSH, + AGENT_VERSION, + PROTOCOL_VERSION +} = require('./consts') + +const errCode = require('err-code') +const { codes } = require('../errors') + +class IdentifyService { + /** + * Replaces the multiaddrs on the given `peerInfo`, + * with the provided `multiaddrs` + * @param {PeerInfo} peerInfo + * @param {Array|Array} multiaddrs + */ + static updatePeerAddresses (peerInfo, multiaddrs) { + if (multiaddrs && multiaddrs.length > 0) { + peerInfo.multiaddrs.clear() + multiaddrs.forEach(ma => { + try { + peerInfo.multiaddrs.add(ma) + } catch (err) { + log.error('could not add multiaddr', err) + } + }) + } + } + + /** + * Replaces the protocols on the given `peerInfo`, + * with the provided `protocols` + * @static + * @param {PeerInfo} peerInfo + * @param {Array} protocols + */ + static updatePeerProtocols (peerInfo, protocols) { + if (protocols && protocols.length > 0) { + peerInfo.protocols.clear() + protocols.forEach(proto => peerInfo.protocols.add(proto)) + } + } + + /** + * Takes the `addr` and converts it to a Multiaddr if possible + * @param {Buffer|String} addr + * @returns {Multiaddr|null} + */ + static getCleanMultiaddr (addr) { + if (addr && addr.length > 0) { + try { + return multiaddr(addr) + } catch (_) { + return null + } + } + return null + } + + /** + * @constructor + * @param {object} options + * @param {Registrar} options.registrar + * @param {Map} options.protocols A reference to the protocols we support + * @param {PeerInfo} options.peerInfo The peer running the identify service + */ + constructor (options) { + /** + * @property {Registrar} + */ + this.registrar = options.registrar + /** + * @property {PeerInfo} + */ + this.peerInfo = options.peerInfo + + this._protocols = options.protocols + + this.handleMessage = this.handleMessage.bind(this) + } + + /** + * Send an Identify Push update to the list of connections + * @param {Array} connections + * @returns {Promise} + */ + push (connections) { + const pushes = connections.map(async connection => { + try { + const { stream } = await connection.newStream(MULTICODEC_IDENTIFY_PUSH) + + await pipe( + [{ + listenAddrs: this.peerInfo.multiaddrs.toArray().map((ma) => ma.buffer), + protocols: Array.from(this._protocols.keys()) + }], + pb.encode(Message), + stream + ) + } catch (err) { + // Just log errors + log.error('could not push identify update to peer', err) + } + }) + + return Promise.all(pushes) + } + + /** + * Calls `push` for all peers in the `peerStore` that are connected + * @param {PeerStore} peerStore + */ + pushToPeerStore (peerStore) { + const connections = [] + let connection + for (const peer of peerStore.peers.values()) { + if (peer.protocols.has(MULTICODEC_IDENTIFY_PUSH) && (connection = this.registrar.getConnection(peer))) { + connections.push(connection) + } + } + + this.push(connections) + } + + /** + * Requests the `Identify` message from peer associated with the given `connection`. + * If the identified peer does not match the `PeerId` associated with the connection, + * an error will be thrown. + * + * @async + * @param {Connection} connection + * @param {PeerID} expectedPeer The PeerId the identify response should match + * @returns {Promise} + */ + async identify (connection, expectedPeer) { + const { stream } = await connection.newStream(MULTICODEC_IDENTIFY) + const [data] = await pipe( + stream, + lp.decode(), + take(1), + toBuffer, + collect + ) + + if (!data) { + throw errCode(new Error('No data could be retrieved'), codes.ERR_CONNECTION_ENDED) + } + + let message + try { + message = Message.decode(data) + } catch (err) { + throw errCode(err, codes.ERR_INVALID_MESSAGE) + } + + let { + publicKey, + listenAddrs, + protocols, + observedAddr + } = message + + const id = await PeerId.createFromPubKey(publicKey) + const peerInfo = new PeerInfo(id) + if (expectedPeer && expectedPeer.toB58String() !== id.toB58String()) { + throw errCode(new Error('identified peer does not match the expected peer'), codes.ERR_INVALID_PEER) + } + + // Get the observedAddr if there is one + observedAddr = IdentifyService.getCleanMultiaddr(observedAddr) + + // Copy the listenAddrs and protocols + IdentifyService.updatePeerAddresses(peerInfo, listenAddrs) + IdentifyService.updatePeerProtocols(peerInfo, protocols) + + this.registrar.peerStore.update(peerInfo) + // TODO: Track our observed address so that we can score it + log('received observed address of %s', observedAddr) + } + + /** + * A handler to register with Libp2p to process identify messages. + * + * @param {object} options + * @param {String} options.protocol + * @param {*} options.stream + * @param {Connection} options.connection + * @returns {Promise} + */ + handleMessage ({ connection, stream, protocol }) { + switch (protocol) { + case MULTICODEC_IDENTIFY: + return this._handleIdentify({ connection, stream }) + case MULTICODEC_IDENTIFY_PUSH: + return this._handlePush({ connection, stream }) + default: + log.error('cannot handle unknown protocol %s', protocol) + } + } + + /** + * Sends the `Identify` response to the requesting peer over the + * given `connection` + * @private + * @param {object} options + * @param {*} options.stream + * @param {Connection} options.connection + */ + _handleIdentify ({ connection, stream }) { + let publicKey = Buffer.alloc(0) + if (this.peerInfo.id.pubKey) { + publicKey = this.peerInfo.id.pubKey.bytes + } + + const message = Message.encode({ + protocolVersion: PROTOCOL_VERSION, + agentVersion: AGENT_VERSION, + publicKey, + listenAddrs: this.peerInfo.multiaddrs.toArray().map((ma) => ma.buffer), + observedAddr: connection.remoteAddr.buffer, + protocols: Array.from(this._protocols.keys()) + }) + + pipe( + [message], + lp.encode(), + stream + ) + } + + /** + * Reads the Identify Push message from the given `connection` + * @private + * @param {object} options + * @param {*} options.stream + * @param {Connection} options.connection + */ + async _handlePush ({ connection, stream }) { + const [data] = await pipe( + stream, + lp.decode(), + take(1), + toBuffer, + collect + ) + + let message + try { + message = Message.decode(data) + } catch (err) { + return log.error('received invalid message', err) + } + + // Update the listen addresses + const peerInfo = new PeerInfo(connection.remotePeer) + + try { + IdentifyService.updatePeerAddresses(peerInfo, message.listenAddrs) + } catch (err) { + return log.error('received invalid listen addrs', err) + } + + // Update the protocols + IdentifyService.updatePeerProtocols(peerInfo, message.protocols) + + // Update the peer in the PeerStore + this.registrar.peerStore.update(peerInfo) + } +} + +module.exports.IdentifyService = IdentifyService +/** + * The protocols the IdentifyService supports + * @property multicodecs + */ +module.exports.multicodecs = { + IDENTIFY: MULTICODEC_IDENTIFY, + IDENTIFY_PUSH: MULTICODEC_IDENTIFY_PUSH +} +module.exports.Message = Message diff --git a/src/identify/listener.js b/src/identify/listener.js deleted file mode 100644 index 8f1d47bfec..0000000000 --- a/src/identify/listener.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict' - -const pull = require('pull-stream/pull') -const values = require('pull-stream/sources/values') -const lp = require('pull-length-prefixed') - -const msg = require('./message') - -module.exports = (conn, pInfoSelf) => { - // send what I see from the other + my Info - conn.getObservedAddrs((err, observedAddrs) => { - if (err) { return } - observedAddrs = observedAddrs[0] - - let publicKey = Buffer.alloc(0) - if (pInfoSelf.id.pubKey) { - publicKey = pInfoSelf.id.pubKey.bytes - } - - const msgSend = msg.encode({ - protocolVersion: 'ipfs/0.1.0', - agentVersion: 'na', - publicKey: publicKey, - listenAddrs: pInfoSelf.multiaddrs.toArray().map((ma) => ma.buffer), - observedAddr: observedAddrs ? observedAddrs.buffer : Buffer.from(''), - protocols: Array.from(pInfoSelf.protocols) - }) - - pull( - values([msgSend]), - lp.encode(), - conn - ) - }) -} diff --git a/src/index.js b/src/index.js index 998d4a6bae..45648db0fa 100644 --- a/src/index.js +++ b/src/index.js @@ -30,6 +30,10 @@ const TransportManager = require('./transport-manager') const Upgrader = require('./upgrader') const PeerStore = require('./peer-store') const Registrar = require('./registrar') +const { + IdentifyService, + multicodecs: IDENTIFY_PROTOCOLS +} = require('./identify') const notStarted = (action, state) => { return errCode( @@ -83,6 +87,11 @@ class Libp2p extends EventEmitter { } }) + // Create the Registrar + this.registrar = new Registrar({ peerStore: this.peerStore }) + this.handle = this.handle.bind(this) + this.registrar.handle = this.handle + // Setup the transport manager this.transportManager = new TransportManager({ libp2p: this, @@ -100,21 +109,25 @@ class Libp2p extends EventEmitter { }) } + this.dialer = new Dialer({ + transportManager: this.transportManager + }) + // Attach stream multiplexers if (this._modules.streamMuxer) { const muxers = this._modules.streamMuxer muxers.forEach((muxer) => { this.upgrader.muxers.set(muxer.multicodec, muxer) }) - } - - this.dialer = new Dialer({ - transportManager: this.transportManager - }) - this.registrar = new Registrar({ peerStore: this.peerStore }) - this.handle = this.handle.bind(this) - this.registrar.handle = this.handle + // Add the identify service since we can multiplex + this.dialer.identifyService = new IdentifyService({ + registrar: this.registrar, + peerInfo: this.peerInfo, + protocols: this.upgrader.protocols + }) + this.handle(Object.values(IDENTIFY_PROTOCOLS), this.dialer.identifyService.handleMessage) + } // Attach private network protector if (this._modules.connProtector) { @@ -338,13 +351,15 @@ class Libp2p extends EventEmitter { /** * Registers the `handler` for each protocol * @param {string[]|string} protocols - * @param {function({ stream:*, protocol:string })} handler + * @param {function({ connection:*, stream:*, protocol:string })} handler */ handle (protocols, handler) { protocols = Array.isArray(protocols) ? protocols : [protocols] protocols.forEach(protocol => { this.upgrader.protocols.set(protocol, handler) }) + + this.dialer.identifyService.pushToPeerStore(this.peerStore) } /** @@ -357,6 +372,8 @@ class Libp2p extends EventEmitter { protocols.forEach(protocol => { this.upgrader.protocols.delete(protocol) }) + + this.dialer.identifyService.pushToPeerStore(this.peerStore) } async _onStarting () { diff --git a/src/upgrader.js b/src/upgrader.js index 7947d6b89c..1699451a6d 100644 --- a/src/upgrader.js +++ b/src/upgrader.js @@ -182,10 +182,14 @@ class Upgrader { // Run anytime a remote stream is created onStream: async muxedStream => { const mss = new Multistream.Listener(muxedStream) - const { stream, protocol } = await mss.handle(Array.from(this.protocols.keys())) - log('%s: incoming stream opened on %s', direction, protocol) - connection.addStream(stream, protocol) - this._onStream({ stream, protocol }) + try { + const { stream, protocol } = await mss.handle(Array.from(this.protocols.keys())) + log('%s: incoming stream opened on %s', direction, protocol) + connection.addStream(stream, protocol) + this._onStream({ connection, stream, protocol }) + } catch (err) { + log.error(err) + } }, // Run anytime a stream closes onStreamEnd: muxedStream => { @@ -246,12 +250,13 @@ class Upgrader { * Routes incoming streams to the correct handler * @private * @param {object} options + * @param {Connection} options.connection The connection the stream belongs to * @param {Stream} options.stream - * @param {string} protocol + * @param {string} options.protocol */ - _onStream ({ stream, protocol }) { + _onStream ({ connection, stream, protocol }) { const handler = this.protocols.get(protocol) - handler({ stream, protocol }) + handler({ connection, stream, protocol }) } /** diff --git a/src/util/index.js b/src/util/index.js index bfee1875a7..0fdd80cae6 100644 --- a/src/util/index.js +++ b/src/util/index.js @@ -30,4 +30,18 @@ function emitFirst (emitter, events, handler) { }) } +/** + * Converts BufferList messages to Buffers + * @param {*} source + * @returns {AsyncGenerator} + */ +function toBuffer (source) { + return (async function * () { + for await (const chunk of source) { + yield Buffer.isBuffer(chunk) ? chunk : chunk.slice() + } + })() +} + module.exports.emitFirst = emitFirst +module.exports.toBuffer = toBuffer diff --git a/test/dialing/direct.spec.js b/test/dialing/direct.spec.js index b23d4f13a8..1aca5987d9 100644 --- a/test/dialing/direct.spec.js +++ b/test/dialing/direct.spec.js @@ -218,5 +218,28 @@ describe('Dialing (direct, WebSockets)', () => { await connection.close() expect(libp2p.dialer.connectToMultiaddr.callCount).to.equal(1) }) + + it('should run identify automatically after connecting', async () => { + libp2p = new Libp2p({ + peerInfo, + modules: { + transport: [Transport], + streamMuxer: [Muxer], + connEncryption: [Crypto] + } + }) + + sinon.spy(libp2p.dialer.identifyService, 'identify') + sinon.spy(libp2p.peerStore, 'update') + + const connection = await libp2p.dialer.connectToMultiaddr(remoteAddr) + expect(connection).to.exist() + // Wait for setImmediate to trigger the identify call + await delay(1) + expect(libp2p.dialer.identifyService.identify.callCount).to.equal(1) + await libp2p.dialer.identifyService.identify.firstCall.returnValue + + expect(libp2p.peerStore.update.callCount).to.equal(1) + }) }) }) diff --git a/test/identify/index.spec.js b/test/identify/index.spec.js new file mode 100644 index 0000000000..114b5156e5 --- /dev/null +++ b/test/identify/index.spec.js @@ -0,0 +1,247 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +const { expect } = chai +const sinon = require('sinon') + +const delay = require('delay') +const PeerId = require('peer-id') +const PeerInfo = require('peer-info') +const duplexPair = require('it-pair/duplex') +const multiaddr = require('multiaddr') + +const { codes: Errors } = require('../../src/errors') +const { IdentifyService, multicodecs } = require('../../src/identify') +const Peers = require('../fixtures/peers') +const Libp2p = require('../../src') +const baseOptions = require('../utils/base-options.browser') + +const { MULTIADDRS_WEBSOCKETS } = require('../fixtures/browser') +const remoteAddr = MULTIADDRS_WEBSOCKETS[0] + +describe('Identify', () => { + let localPeer + let remotePeer + const protocols = new Map([ + [multicodecs.IDENTIFY, () => {}], + [multicodecs.IDENTIFY_PUSH, () => {}] + ]) + + before(async () => { + [localPeer, remotePeer] = (await Promise.all([ + PeerId.createFromJSON(Peers[0]), + PeerId.createFromJSON(Peers[1]) + ])).map(id => new PeerInfo(id)) + }) + + afterEach(() => { + sinon.restore() + }) + + it('should be able to identify another peer', async () => { + const localIdentify = new IdentifyService({ + peerInfo: localPeer, + protocols, + registrar: { + peerStore: { + update: () => {} + } + } + }) + const remoteIdentify = new IdentifyService({ + peerInfo: remotePeer, + protocols + }) + + const observedAddr = multiaddr('/ip4/127.0.0.1/tcp/1234') + const localConnectionMock = { newStream: () => {} } + const remoteConnectionMock = { remoteAddr: observedAddr } + + const [local, remote] = duplexPair() + sinon.stub(localConnectionMock, 'newStream').returns({ stream: local, protocol: multicodecs.IDENTIFY }) + + sinon.spy(localIdentify.registrar.peerStore, 'update') + + // Run identify + await Promise.all([ + localIdentify.identify(localConnectionMock, remotePeer.id), + remoteIdentify.handleMessage({ + connection: remoteConnectionMock, + stream: remote, + protocol: multicodecs.IDENTIFY + }) + ]) + + expect(localIdentify.registrar.peerStore.update.callCount).to.equal(1) + // Validate the remote peer gets updated in the peer store + const call = localIdentify.registrar.peerStore.update.firstCall + expect(call.args[0].id.bytes).to.equal(remotePeer.id.bytes) + }) + + it('should throw if identified peer is the wrong peer', async () => { + const localIdentify = new IdentifyService({ + peerInfo: localPeer, + protocols + }) + const remoteIdentify = new IdentifyService({ + peerInfo: remotePeer, + protocols + }) + + const observedAddr = multiaddr('/ip4/127.0.0.1/tcp/1234') + const localConnectionMock = { newStream: () => {} } + const remoteConnectionMock = { remoteAddr: observedAddr } + + const [local, remote] = duplexPair() + sinon.stub(localConnectionMock, 'newStream').returns({ stream: local, protocol: multicodecs.IDENTIFY }) + + // Run identify + try { + await Promise.all([ + localIdentify.identify(localConnectionMock, localPeer.id), + remoteIdentify.handleMessage({ + connection: remoteConnectionMock, + stream: remote, + protocol: multicodecs.IDENTIFY + }) + ]) + expect.fail('should have thrown') + } catch (err) { + expect(err).to.exist() + expect(err.code).to.eql(Errors.ERR_INVALID_PEER) + } + }) + + describe('push', () => { + it('should be able to push identify updates to another peer', async () => { + const localIdentify = new IdentifyService({ + peerInfo: localPeer, + registrar: { getConnection: () => {} }, + protocols: new Map([ + [multicodecs.IDENTIFY], + [multicodecs.IDENTIFY_PUSH], + ['/echo/1.0.0'] + ]) + }) + const remoteIdentify = new IdentifyService({ + peerInfo: remotePeer, + registrar: { + peerStore: { + update: () => {} + } + } + }) + + // Setup peer protocols and multiaddrs + const localProtocols = new Set([multicodecs.IDENTIFY, multicodecs.IDENTIFY_PUSH, '/echo/1.0.0']) + const listeningAddr = multiaddr('/ip4/127.0.0.1/tcp/1234') + sinon.stub(localPeer.multiaddrs, 'toArray').returns([listeningAddr]) + sinon.stub(localPeer, 'protocols').value(localProtocols) + sinon.stub(remotePeer, 'protocols').value(new Set([multicodecs.IDENTIFY, multicodecs.IDENTIFY_PUSH])) + + const localConnectionMock = { newStream: () => {} } + const remoteConnectionMock = { remotePeer: localPeer.id } + + const [local, remote] = duplexPair() + sinon.stub(localConnectionMock, 'newStream').returns({ stream: local, protocol: multicodecs.IDENTIFY_PUSH }) + + sinon.spy(IdentifyService, 'updatePeerAddresses') + sinon.spy(IdentifyService, 'updatePeerProtocols') + sinon.spy(remoteIdentify.registrar.peerStore, 'update') + + // Run identify + await Promise.all([ + localIdentify.push([localConnectionMock]), + remoteIdentify.handleMessage({ + connection: remoteConnectionMock, + stream: remote, + protocol: multicodecs.IDENTIFY_PUSH + }) + ]) + + expect(IdentifyService.updatePeerAddresses.callCount).to.equal(1) + expect(IdentifyService.updatePeerProtocols.callCount).to.equal(1) + + expect(remoteIdentify.registrar.peerStore.update.callCount).to.equal(1) + const [peerInfo] = remoteIdentify.registrar.peerStore.update.firstCall.args + expect(peerInfo.id.bytes).to.eql(localPeer.id.bytes) + expect(peerInfo.multiaddrs.toArray()).to.eql([listeningAddr]) + expect(peerInfo.protocols).to.eql(localProtocols) + }) + }) + + describe('libp2p.dialer.identifyService', () => { + let peerInfo + let libp2p + let remoteLibp2p + + before(async () => { + const peerId = await PeerId.createFromJSON(Peers[0]) + peerInfo = new PeerInfo(peerId) + }) + + afterEach(async () => { + sinon.restore() + libp2p && await libp2p.stop() + libp2p = null + }) + + after(async () => { + remoteLibp2p && await remoteLibp2p.stop() + }) + + it('should run identify automatically after connecting', async () => { + libp2p = new Libp2p({ + ...baseOptions, + peerInfo + }) + + sinon.spy(libp2p.dialer.identifyService, 'identify') + sinon.spy(libp2p.peerStore, 'update') + + const connection = await libp2p.dialer.connectToMultiaddr(remoteAddr) + expect(connection).to.exist() + // Wait for nextTick to trigger the identify call + await delay(1) + expect(libp2p.dialer.identifyService.identify.callCount).to.equal(1) + await libp2p.dialer.identifyService.identify.firstCall.returnValue + + expect(libp2p.peerStore.update.callCount).to.equal(1) + await connection.close() + }) + + it('should push protocol updates to an already connected peer', async () => { + libp2p = new Libp2p({ + ...baseOptions, + peerInfo + }) + + sinon.spy(libp2p.dialer.identifyService, 'identify') + sinon.spy(libp2p.dialer.identifyService, 'push') + sinon.spy(libp2p.peerStore, 'update') + + const connection = await libp2p.dialer.connectToMultiaddr(remoteAddr) + expect(connection).to.exist() + // Wait for nextTick to trigger the identify call + await delay(1) + + // Wait for identify to finish + await libp2p.dialer.identifyService.identify.firstCall.returnValue + + libp2p.handle('/echo/2.0.0', () => {}) + libp2p.unhandle('/echo/2.0.0') + + // Verify the remote peer is notified of both changes + expect(libp2p.dialer.identifyService.push.callCount).to.equal(2) + for (const call of libp2p.dialer.identifyService.push.getCalls()) { + const [connections] = call.args + expect(connections.length).to.equal(1) + expect(connections[0].remotePeer.toB58String()).to.equal(remoteAddr.getPeerId()) + const results = await call.returnValue + expect(results.length).to.equal(1) + } + }) + }) +}) diff --git a/test/upgrading/upgrader.spec.js b/test/upgrading/upgrader.spec.js index ff30810b5d..69d0686fde 100644 --- a/test/upgrading/upgrader.spec.js +++ b/test/upgrading/upgrader.spec.js @@ -361,16 +361,14 @@ describe('libp2p.upgrader', () => { } }) - expect(libp2p.upgrader.protocols.size).to.equal(0) + expect(libp2p.upgrader.protocols).to.not.have.any.keys(['/echo/1.0.0', '/echo/1.0.1']) const echoHandler = () => {} libp2p.handle(['/echo/1.0.0', '/echo/1.0.1'], echoHandler) - expect(libp2p.upgrader.protocols.size).to.equal(2) expect(libp2p.upgrader.protocols.get('/echo/1.0.0')).to.equal(echoHandler) expect(libp2p.upgrader.protocols.get('/echo/1.0.1')).to.equal(echoHandler) libp2p.unhandle(['/echo/1.0.0']) - expect(libp2p.upgrader.protocols.size).to.equal(1) expect(libp2p.upgrader.protocols.get('/echo/1.0.0')).to.equal(undefined) expect(libp2p.upgrader.protocols.get('/echo/1.0.1')).to.equal(echoHandler) }) diff --git a/test/utils/base-options.browser.js b/test/utils/base-options.browser.js new file mode 100644 index 0000000000..3a1337a0e4 --- /dev/null +++ b/test/utils/base-options.browser.js @@ -0,0 +1,13 @@ +'use strict' + +const Transport = require('libp2p-websockets') +const Muxer = require('libp2p-mplex') +const Crypto = require('../../src/insecure/plaintext') + +module.exports = { + modules: { + transport: [Transport], + streamMuxer: [Muxer], + connEncryption: [Crypto] + } +} From ced2dbf318fba47ce9e916d5b5f6ffec27741c7a Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Wed, 13 Nov 2019 16:38:07 +0100 Subject: [PATCH 09/92] chore: update it-length-prefixed (#476) fix: decode.fromReader usage --- package.json | 6 +++--- src/insecure/plaintext.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 04970f958a..16986a97ff 100644 --- a/package.json +++ b/package.json @@ -50,11 +50,11 @@ "fsm-event": "^2.1.0", "hashlru": "^2.3.0", "it-handshake": "^1.0.1", - "it-length-prefixed": "jacobheun/pull-length-prefixed#feat/fromReader", - "it-pipe": "^1.0.1", + "it-length-prefixed": "^3.0.0", + "it-pipe": "^1.1.0", "it-protocol-buffers": "^0.2.0", "latency-monitor": "~0.2.1", - "libp2p-crypto": "^0.16.2", + "libp2p-crypto": "^0.17.1", "libp2p-interfaces": "^0.1.3", "mafmt": "^7.0.0", "merge-options": "^1.0.1", diff --git a/src/insecure/plaintext.js b/src/insecure/plaintext.js index 7b3981ebe3..d941fa51a7 100644 --- a/src/insecure/plaintext.js +++ b/src/insecure/plaintext.js @@ -31,7 +31,7 @@ async function encrypt (localId, conn, remoteId) { log('write pubkey exchange to peer %j', remoteId) // Get the Exchange message - const response = (await lp.decodeFromReader(shake.reader).next()).value + const response = (await lp.decode.fromReader(shake.reader).next()).value const id = Exchange.decode(response.slice()) log('read pubkey exchange from peer %j', remoteId) From 34d57f8989eb961132aa092ec75aec636aebcefc Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Fri, 15 Nov 2019 16:48:32 +0100 Subject: [PATCH 10/92] refactor: pubsub (#467) * feat: peer-store v0 * chore: apply suggestions from code review Co-Authored-By: Jacob Heun * chore: address review * refactor: pubsub subsystem * chore: address review * chore: use topology interface * chore: address review * chore: address review * chore: simplify tests --- README.md | 14 +-- package.json | 8 +- src/connection-manager/topology.js | 108 ---------------- src/index.js | 39 +++--- src/pubsub.js | 141 ++++++--------------- src/registrar.js | 17 ++- test/pubsub/configuration.node.js | 92 ++++++++++++++ test/pubsub/implementations.node.js | 95 ++++++++++++++ test/pubsub/operation.node.js | 184 ++++++++++++++++++++++++++++ test/pubsub/utils.js | 29 +++++ test/registrar/registrar.spec.js | 76 +++--------- 11 files changed, 500 insertions(+), 303 deletions(-) delete mode 100644 src/connection-manager/topology.js create mode 100644 test/pubsub/configuration.node.js create mode 100644 test/pubsub/implementations.node.js create mode 100644 test/pubsub/operation.node.js create mode 100644 test/pubsub/utils.js diff --git a/README.md b/README.md index de08e1b0c0..1a28d9b9dd 100644 --- a/README.md +++ b/README.md @@ -211,22 +211,18 @@ class Node extends Libp2p { **IMPORTANT NOTE**: All the methods listed in the API section that take a callback are also now Promisified. Libp2p is migrating away from callbacks to async/await, and in a future release (that will be announced in advance), callback support will be removed entirely. You can follow progress of the async/await endeavor at https://github.com/ipfs/js-ipfs/issues/1670. -#### Create a Node - `Libp2p.createLibp2p(options, callback)` +#### Create a Node - `Libp2p.create(options)` > Behaves exactly like `new Libp2p(options)`, but doesn't require a PeerInfo. One will be generated instead ```js -const { createLibp2p } = require('libp2p') -createLibp2p(options, (err, libp2p) => { - if (err) throw err - libp2p.start((err) => { - if (err) throw err - }) -}) +const { create } = require('libp2p') +const libp2p = await create(options) + +await libp2p.start() ``` - `options`: Object of libp2p configuration options -- `callback`: Function with signature `function (Error, Libp2p) {}` #### Create a Node alternative - `new Libp2p(options)` diff --git a/package.json b/package.json index 16986a97ff..1ebd91a925 100644 --- a/package.json +++ b/package.json @@ -55,13 +55,14 @@ "it-protocol-buffers": "^0.2.0", "latency-monitor": "~0.2.1", "libp2p-crypto": "^0.17.1", - "libp2p-interfaces": "^0.1.3", + "libp2p-interfaces": "^0.1.5", "mafmt": "^7.0.0", "merge-options": "^1.0.1", "moving-average": "^1.0.0", "multiaddr": "^7.1.0", "multistream-select": "^0.15.0", "once": "^1.4.0", + "p-map": "^3.0.0", "p-queue": "^6.1.1", "p-settle": "^3.1.0", "peer-id": "^0.13.3", @@ -90,8 +91,8 @@ "libp2p-bootstrap": "^0.9.7", "libp2p-delegated-content-routing": "^0.2.2", "libp2p-delegated-peer-routing": "^0.2.2", - "libp2p-floodsub": "~0.17.0", - "libp2p-gossipsub": "~0.0.4", + "libp2p-floodsub": "^0.19.0", + "libp2p-gossipsub": "ChainSafe/gossipsub-js#beta/async", "libp2p-kad-dht": "^0.15.3", "libp2p-mdns": "^0.12.3", "libp2p-mplex": "^0.9.1", @@ -103,6 +104,7 @@ "lodash.times": "^4.3.2", "nock": "^10.0.6", "p-defer": "^3.0.0", + "p-wait-for": "^3.1.0", "portfinder": "^1.0.20", "pull-goodbye": "0.0.2", "pull-length-prefixed": "^1.3.3", diff --git a/src/connection-manager/topology.js b/src/connection-manager/topology.js deleted file mode 100644 index 2c2a877919..0000000000 --- a/src/connection-manager/topology.js +++ /dev/null @@ -1,108 +0,0 @@ -'use strict' - -const assert = require('assert') - -class Topology { - /** - * @param {Object} props - * @param {number} props.min minimum needed connections (default: 0) - * @param {number} props.max maximum needed connections (default: Infinity) - * @param {Array} props.multicodecs protocol multicodecs - * @param {Object} props.handlers - * @param {function} props.handlers.onConnect protocol "onConnect" handler - * @param {function} props.handlers.onDisconnect protocol "onDisconnect" handler - * @constructor - */ - constructor ({ - min = 0, - max = Infinity, - multicodecs, - handlers - }) { - assert(multicodecs, 'one or more multicodec should be provided') - assert(handlers, 'the handlers should be provided') - assert(handlers.onConnect && typeof handlers.onConnect === 'function', - 'the \'onConnect\' handler must be provided') - assert(handlers.onDisconnect && typeof handlers.onDisconnect === 'function', - 'the \'onDisconnect\' handler must be provided') - - this.multicodecs = Array.isArray(multicodecs) ? multicodecs : [multicodecs] - this.min = min - this.max = max - - // Handlers - this._onConnect = handlers.onConnect - this._onDisconnect = handlers.onDisconnect - - this.peers = new Map() - this._registrar = undefined - - this._onProtocolChange = this._onProtocolChange.bind(this) - } - - set registrar (registrar) { - this._registrar = registrar - this._registrar.peerStore.on('change:protocols', this._onProtocolChange) - - // Update topology peers - this._updatePeers(this._registrar.peerStore.peers.values()) - } - - /** - * Update topology. - * @param {Array} peerInfoIterable - * @returns {void} - */ - _updatePeers (peerInfoIterable) { - for (const peerInfo of peerInfoIterable) { - if (this.multicodecs.filter(multicodec => peerInfo.protocols.has(multicodec))) { - // Add the peer regardless of whether or not there is currently a connection - this.peers.set(peerInfo.id.toB58String(), peerInfo) - // If there is a connection, call _onConnect - const connection = this._registrar.getConnection(peerInfo) - connection && this._onConnect(peerInfo, connection) - } else { - // Remove any peers we might be tracking that are no longer of value to us - this.peers.delete(peerInfo.id.toB58String()) - } - } - } - - /** - * Notify protocol of peer disconnected. - * @param {PeerInfo} peerInfo - * @param {Error} [error] - * @returns {void} - */ - disconnect (peerInfo, error) { - this._onDisconnect(peerInfo, error) - } - - /** - * Check if a new peer support the multicodecs for this topology. - * @param {Object} props - * @param {PeerInfo} props.peerInfo - * @param {Array} props.protocols - */ - _onProtocolChange ({ peerInfo, protocols }) { - const existingPeer = this.peers.get(peerInfo.id.toB58String()) - const hasProtocol = protocols.filter(protocol => this.multicodecs.includes(protocol)) - - // Not supporting the protocol anymore? - if (existingPeer && hasProtocol.length === 0) { - this._onDisconnect({ - peerInfo - }) - } - - // New to protocol support - for (const protocol of protocols) { - if (this.multicodecs.includes(protocol)) { - this._updatePeers([peerInfo]) - return - } - } - } -} - -module.exports = Topology diff --git a/src/index.js b/src/index.js index 45648db0fa..5efcc8febd 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,7 @@ 'use strict' const FSM = require('fsm-event') -const EventEmitter = require('events').EventEmitter +const { EventEmitter } = require('events') const debug = require('debug') const log = debug('libp2p') log.error = debug('libp2p:error') @@ -9,7 +9,6 @@ const errCode = require('err-code') const promisify = require('promisify-es6') const each = require('async/each') -const nextTick = require('async/nextTick') const PeerInfo = require('peer-info') const multiaddr = require('multiaddr') @@ -66,6 +65,8 @@ class Libp2p extends EventEmitter { this._transport = [] // Transport instances/references this._discovery = [] // Discovery service instances/references + this.peerStore = new PeerStore() + // create the switch, and listen for errors this._switch = new Switch(this.peerInfo, this.peerStore, this._options.switch) @@ -147,7 +148,7 @@ class Libp2p extends EventEmitter { } // start pubsub - if (this._modules.pubsub && this._config.pubsub.enabled !== false) { + if (this._modules.pubsub) { this.pubsub = pubsub(this, this._modules.pubsub, this._config.pubsub) } @@ -251,6 +252,7 @@ class Libp2p extends EventEmitter { this.state('stop') try { + this.pubsub && await this.pubsub.stop() await this.transportManager.close() await this._switch.stop() } catch (err) { @@ -385,10 +387,16 @@ class Libp2p extends EventEmitter { const multiaddrs = this.peerInfo.multiaddrs.toArray() // Start parallel tasks + const tasks = [ + this.transportManager.listen(multiaddrs) + ] + + if (this._config.pubsub.enabled) { + this.pubsub && this.pubsub.start() + } + try { - await Promise.all([ - this.transportManager.listen(multiaddrs) - ]) + await Promise.all(tasks) } catch (err) { log.error(err) this.emit('error', err) @@ -483,16 +491,15 @@ module.exports = Libp2p * Like `new Libp2p(options)` except it will create a `PeerInfo` * instance if one is not provided in options. * @param {object} options Libp2p configuration options - * @param {function(Error, Libp2p)} callback - * @returns {void} + * @returns {Libp2p} */ -module.exports.createLibp2p = promisify((options, callback) => { +module.exports.create = async (options = {}) => { if (options.peerInfo) { - return nextTick(callback, null, new Libp2p(options)) + return new Libp2p(options) } - PeerInfo.create((err, peerInfo) => { - if (err) return callback(err) - options.peerInfo = peerInfo - callback(null, new Libp2p(options)) - }) -}) + + const peerInfo = await PeerInfo.create() + + options.peerInfo = peerInfo + return new Libp2p(options) +} diff --git a/src/pubsub.js b/src/pubsub.js index 2246a2d1e9..2c067d993d 100644 --- a/src/pubsub.js +++ b/src/pubsub.js @@ -1,52 +1,21 @@ 'use strict' -const nextTick = require('async/nextTick') -const { messages, codes } = require('./errors') -const promisify = require('promisify-es6') - const errCode = require('err-code') +const { messages, codes } = require('./errors') module.exports = (node, Pubsub, config) => { - const pubsub = new Pubsub(node, config) + const pubsub = new Pubsub(node.peerInfo, node.registrar, config) return { /** * Subscribe the given handler to a pubsub topic - * * @param {string} topic * @param {function} handler The handler to subscribe - * @param {object|null} [options] - * @param {function} [callback] An optional callback - * - * @returns {Promise|void} A promise is returned if no callback is provided - * - * @example Subscribe a handler to a topic - * - * // `null` must be passed for options until subscribe is no longer using promisify - * const handler = (message) => { } - * await libp2p.subscribe(topic, handler, null) - * - * @example Use a callback instead of the Promise api - * - * // `options` may be passed or omitted when supplying a callback - * const handler = (message) => { } - * libp2p.subscribe(topic, handler, callback) + * @returns {void} */ - subscribe: (topic, handler, options, callback) => { - // can't use promisify because it thinks the handler is a callback - if (typeof options === 'function') { - callback = options - options = {} - } - + subscribe: (topic, handler) => { if (!node.isStarted() && !pubsub.started) { - const err = errCode(new Error(messages.NOT_STARTED_YET), codes.PUBSUB_NOT_STARTED) - - if (callback) { - return nextTick(() => callback(err)) - } - - return Promise.reject(err) + throw errCode(new Error(messages.NOT_STARTED_YET), codes.PUBSUB_NOT_STARTED) } if (pubsub.listenerCount(topic) === 0) { @@ -54,46 +23,16 @@ module.exports = (node, Pubsub, config) => { } pubsub.on(topic, handler) - - if (callback) { - return nextTick(() => callback()) - } - - return Promise.resolve() }, /** * Unsubscribes from a pubsub topic - * * @param {string} topic - * @param {function|null} handler The handler to unsubscribe from - * @param {function} [callback] An optional callback - * - * @returns {Promise|void} A promise is returned if no callback is provided - * - * @example Unsubscribe a topic for all handlers - * - * // `null` must be passed until unsubscribe is no longer using promisify - * await libp2p.unsubscribe(topic, null) - * - * @example Unsubscribe a topic for 1 handler - * - * await libp2p.unsubscribe(topic, handler) - * - * @example Use a callback instead of the Promise api - * - * libp2p.unsubscribe(topic, handler, callback) + * @param {function} [handler] The handler to unsubscribe from */ - unsubscribe: (topic, handler, callback) => { - // can't use promisify because it thinks the handler is a callback + unsubscribe: (topic, handler) => { if (!node.isStarted() && !pubsub.started) { - const err = errCode(new Error(messages.NOT_STARTED_YET), codes.PUBSUB_NOT_STARTED) - - if (callback) { - return nextTick(() => callback(err)) - } - - return Promise.reject(err) + throw errCode(new Error(messages.NOT_STARTED_YET), codes.PUBSUB_NOT_STARTED) } if (!handler) { @@ -105,61 +44,61 @@ module.exports = (node, Pubsub, config) => { if (pubsub.listenerCount(topic) === 0) { pubsub.unsubscribe(topic) } - - if (callback) { - return nextTick(() => callback()) - } - - return Promise.resolve() }, - publish: promisify((topic, data, callback) => { + /** + * Publish messages to the given topics. + * @param {Array|string} topic + * @param {Buffer} data + * @returns {Promise} + */ + publish: (topic, data) => { if (!node.isStarted() && !pubsub.started) { - return nextTick(callback, errCode(new Error(messages.NOT_STARTED_YET), codes.PUBSUB_NOT_STARTED)) + throw errCode(new Error(messages.NOT_STARTED_YET), codes.PUBSUB_NOT_STARTED) } try { data = Buffer.from(data) } catch (err) { - return nextTick(callback, errCode(new Error('data must be convertible to a Buffer'), 'ERR_DATA_IS_NOT_VALID')) + throw errCode(new Error('data must be convertible to a Buffer'), 'ERR_DATA_IS_NOT_VALID') } - pubsub.publish(topic, data, callback) - }), + return pubsub.publish(topic, data) + }, - ls: promisify((callback) => { + /** + * Get a list of topics the node is subscribed to. + * @returns {Array} topics + */ + getTopics: () => { if (!node.isStarted() && !pubsub.started) { - return nextTick(callback, errCode(new Error(messages.NOT_STARTED_YET), codes.PUBSUB_NOT_STARTED)) + throw errCode(new Error(messages.NOT_STARTED_YET), codes.PUBSUB_NOT_STARTED) } - const subscriptions = Array.from(pubsub.subscriptions) - - nextTick(() => callback(null, subscriptions)) - }), + return pubsub.getTopics() + }, - peers: promisify((topic, callback) => { + /** + * Get a list of the peer-ids that are subscribed to one topic. + * @param {string} topic + * @returns {Array} + */ + getPeersSubscribed: (topic) => { if (!node.isStarted() && !pubsub.started) { - return nextTick(callback, errCode(new Error(messages.NOT_STARTED_YET), codes.PUBSUB_NOT_STARTED)) - } - - if (typeof topic === 'function') { - callback = topic - topic = null + throw errCode(new Error(messages.NOT_STARTED_YET), codes.PUBSUB_NOT_STARTED) } - const peers = Array.from(pubsub.peers.values()) - .filter((peer) => topic ? peer.topics.has(topic) : true) - .map((peer) => peer.info.id.toB58String()) - - nextTick(() => callback(null, peers)) - }), + return pubsub.getPeersSubscribed(topic) + }, setMaxListeners (n) { return pubsub.setMaxListeners(n) }, - start: promisify((cb) => pubsub.start(cb)), + _pubsub: pubsub, + + start: () => pubsub.start(), - stop: promisify((cb) => pubsub.stop(cb)) + stop: () => pubsub.stop() } } diff --git a/src/registrar.js b/src/registrar.js index c6e4439c00..c4a116806b 100644 --- a/src/registrar.js +++ b/src/registrar.js @@ -5,9 +5,9 @@ const debug = require('debug') const log = debug('libp2p:peer-store') log.error = debug('libp2p:peer-store:error') +const Topology = require('libp2p-interfaces/src/topology') const { Connection } = require('libp2p-interfaces/src/connection') const PeerInfo = require('peer-info') -const Toplogy = require('./connection-manager/topology') /** * Responsible for notifying registered protocols of events in the network. @@ -106,17 +106,16 @@ class Registrar { /** * Register handlers for a set of multicodecs given - * @param {Object} topologyProps properties for topology - * @param {Array|string} topologyProps.multicodecs - * @param {Object} topologyProps.handlers - * @param {function} topologyProps.handlers.onConnect - * @param {function} topologyProps.handlers.onDisconnect + * @param {Topology} topology protocol topology * @return {string} registrar identifier */ - register (topologyProps) { - // Create multicodec topology + register (topology) { + assert( + Topology.isTopology(topology), + 'topology must be an instance of interfaces/topology') + + // Create topology const id = (parseInt(Math.random() * 1e9)).toString(36) + Date.now() - const topology = new Toplogy(topologyProps) this.topologies.set(id, topology) diff --git a/test/pubsub/configuration.node.js b/test/pubsub/configuration.node.js new file mode 100644 index 0000000000..829e303f3d --- /dev/null +++ b/test/pubsub/configuration.node.js @@ -0,0 +1,92 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +const { expect } = chai + +const mergeOptions = require('merge-options') +const multiaddr = require('multiaddr') + +const { create } = require('../../src') +const { baseOptions, subsystemOptions } = require('./utils') +const peerUtils = require('../utils/creators/peer') + +const listenAddr = multiaddr('/ip4/127.0.0.1/tcp/0') + +describe('Pubsub subsystem is configurable', () => { + let libp2p + + afterEach(async () => { + libp2p && await libp2p.stop() + }) + + it('should not exist if no module is provided', async () => { + libp2p = await create(baseOptions) + expect(libp2p.pubsub).to.not.exist() + }) + + it('should exist if the module is provided', async () => { + libp2p = await create(subsystemOptions) + expect(libp2p.pubsub).to.exist() + }) + + it('should start and stop by default once libp2p starts', async () => { + const [peerInfo] = await peerUtils.createPeerInfoFromFixture(1) + peerInfo.multiaddrs.add(listenAddr) + + const customOptions = mergeOptions(subsystemOptions, { + peerInfo + }) + + libp2p = await create(customOptions) + expect(libp2p.pubsub._pubsub.started).to.equal(false) + + await libp2p.start() + expect(libp2p.pubsub._pubsub.started).to.equal(true) + + await libp2p.stop() + expect(libp2p.pubsub._pubsub.started).to.equal(false) + }) + + it('should not start if disabled once libp2p starts', async () => { + const [peerInfo] = await peerUtils.createPeerInfoFromFixture(1) + peerInfo.multiaddrs.add(listenAddr) + + const customOptions = mergeOptions(subsystemOptions, { + peerInfo, + config: { + pubsub: { + enabled: false + } + } + }) + + libp2p = await create(customOptions) + expect(libp2p.pubsub._pubsub.started).to.equal(false) + + await libp2p.start() + expect(libp2p.pubsub._pubsub.started).to.equal(false) + }) + + it('should allow a manual start', async () => { + const [peerInfo] = await peerUtils.createPeerInfoFromFixture(1) + peerInfo.multiaddrs.add(listenAddr) + + const customOptions = mergeOptions(subsystemOptions, { + peerInfo, + config: { + pubsub: { + enabled: false + } + } + }) + + libp2p = await create(customOptions) + await libp2p.start() + expect(libp2p.pubsub._pubsub.started).to.equal(false) + + await libp2p.pubsub.start() + expect(libp2p.pubsub._pubsub.started).to.equal(true) + }) +}) diff --git a/test/pubsub/implementations.node.js b/test/pubsub/implementations.node.js new file mode 100644 index 0000000000..cb2ee9f217 --- /dev/null +++ b/test/pubsub/implementations.node.js @@ -0,0 +1,95 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +const { expect } = chai + +const pWaitFor = require('p-wait-for') +const pDefer = require('p-defer') +const mergeOptions = require('merge-options') + +const Floodsub = require('libp2p-floodsub') +const Gossipsub = require('libp2p-gossipsub') +const { multicodec: floodsubMulticodec } = require('libp2p-floodsub') +const { multicodec: gossipsubMulticodec } = require('libp2p-gossipsub') + +const multiaddr = require('multiaddr') + +const { create } = require('../../src') +const { baseOptions } = require('./utils') +const peerUtils = require('../utils/creators/peer') + +const listenAddr = multiaddr('/ip4/127.0.0.1/tcp/0') +const remoteListenAddr = multiaddr('/ip4/127.0.0.1/tcp/0') + +describe('Pubsub subsystem is able to use different implementations', () => { + let peerInfo, remotePeerInfo + let libp2p, remoteLibp2p + let remAddr + + beforeEach(async () => { + [peerInfo, remotePeerInfo] = await peerUtils.createPeerInfoFromFixture(2) + + peerInfo.multiaddrs.add(listenAddr) + remotePeerInfo.multiaddrs.add(remoteListenAddr) + }) + + afterEach(() => Promise.all([ + libp2p && libp2p.stop(), + remoteLibp2p && remoteLibp2p.stop() + ])) + + it('Floodsub nodes', () => { + return pubsubTest(floodsubMulticodec, Floodsub) + }) + + it('Gossipsub nodes', () => { + return pubsubTest(gossipsubMulticodec, Gossipsub) + }) + + const pubsubTest = async (multicodec, pubsub) => { + const defer = pDefer() + const topic = 'test-topic' + const data = 'hey!' + + libp2p = await create(mergeOptions(baseOptions, { + peerInfo, + modules: { + pubsub: pubsub + } + })) + + remoteLibp2p = await create(mergeOptions(baseOptions, { + peerInfo: remotePeerInfo, + modules: { + pubsub: pubsub + } + })) + + await Promise.all([ + libp2p.start(), + remoteLibp2p.start() + ]) + + const libp2pId = libp2p.peerInfo.id.toB58String() + remAddr = remoteLibp2p.transportManager.getAddrs()[0] + + const connection = await libp2p.dialProtocol(remAddr, multicodec) + expect(connection).to.exist() + + libp2p.pubsub.subscribe(topic, (msg) => { + expect(msg.data.toString()).to.equal(data) + defer.resolve() + }) + + // wait for remoteLibp2p to know about libp2p subscription + await pWaitFor(() => { + const subscribedPeers = remoteLibp2p.pubsub.getPeersSubscribed(topic) + return subscribedPeers.includes(libp2pId) + }) + + remoteLibp2p.pubsub.publish(topic, data) + await defer.promise + } +}) diff --git a/test/pubsub/operation.node.js b/test/pubsub/operation.node.js new file mode 100644 index 0000000000..5ffe3f34ab --- /dev/null +++ b/test/pubsub/operation.node.js @@ -0,0 +1,184 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +const { expect } = chai +const sinon = require('sinon') + +const pWaitFor = require('p-wait-for') +const pDefer = require('p-defer') +const mergeOptions = require('merge-options') +const multiaddr = require('multiaddr') + +const { create } = require('../../src') +const { subsystemOptions, subsystemMulticodecs } = require('./utils') +const peerUtils = require('../utils/creators/peer') + +const listenAddr = multiaddr('/ip4/127.0.0.1/tcp/0') +const remoteListenAddr = multiaddr('/ip4/127.0.0.1/tcp/0') + +describe('Pubsub subsystem operates correctly', () => { + let peerInfo, remotePeerInfo + let libp2p, remoteLibp2p + let remAddr + + beforeEach(async () => { + [peerInfo, remotePeerInfo] = await peerUtils.createPeerInfoFromFixture(2) + + peerInfo.multiaddrs.add(listenAddr) + remotePeerInfo.multiaddrs.add(remoteListenAddr) + }) + + describe('pubsub started before connect', () => { + beforeEach(async () => { + libp2p = await create(mergeOptions(subsystemOptions, { + peerInfo + })) + + remoteLibp2p = await create(mergeOptions(subsystemOptions, { + peerInfo: remotePeerInfo + })) + + await Promise.all([ + libp2p.start(), + remoteLibp2p.start() + ]) + + remAddr = remoteLibp2p.transportManager.getAddrs()[0] + }) + + afterEach(() => Promise.all([ + libp2p && libp2p.stop(), + remoteLibp2p && remoteLibp2p.stop() + ])) + + afterEach(() => { + sinon.restore() + }) + + it('should get notified of connected peers on dial', async () => { + const connection = await libp2p.dialProtocol(remAddr, subsystemMulticodecs) + + expect(connection).to.exist() + + return Promise.all([ + pWaitFor(() => libp2p.pubsub._pubsub.peers.size === 1), + pWaitFor(() => remoteLibp2p.pubsub._pubsub.peers.size === 1) + ]) + }) + + it('should receive pubsub messages', async () => { + const defer = pDefer() + const topic = 'test-topic' + const data = 'hey!' + const libp2pId = libp2p.peerInfo.id.toB58String() + + await libp2p.dialProtocol(remAddr, subsystemMulticodecs) + + let subscribedTopics = libp2p.pubsub.getTopics() + expect(subscribedTopics).to.not.include(topic) + + libp2p.pubsub.subscribe(topic, (msg) => { + expect(msg.data.toString()).to.equal(data) + defer.resolve() + }) + + subscribedTopics = libp2p.pubsub.getTopics() + expect(subscribedTopics).to.include(topic) + + // wait for remoteLibp2p to know about libp2p subscription + await pWaitFor(() => { + const subscribedPeers = remoteLibp2p.pubsub.getPeersSubscribed(topic) + return subscribedPeers.includes(libp2pId) + }) + remoteLibp2p.pubsub.publish(topic, data) + + await defer.promise + }) + }) + + describe('pubsub started after connect', () => { + beforeEach(async () => { + libp2p = await create(mergeOptions(subsystemOptions, { + peerInfo + })) + + remoteLibp2p = await create(mergeOptions(subsystemOptions, { + peerInfo: remotePeerInfo, + config: { + pubsub: { + enabled: false + } + } + })) + + await libp2p.start() + await remoteLibp2p.start() + + remAddr = remoteLibp2p.transportManager.getAddrs()[0] + }) + + afterEach(() => Promise.all([ + libp2p && libp2p.stop(), + remoteLibp2p && remoteLibp2p.stop() + ])) + + afterEach(() => { + sinon.restore() + }) + + it('should get notified of connected peers after starting', async () => { + const connection = await libp2p.dial(remAddr) + + expect(connection).to.exist() + expect(libp2p.pubsub._pubsub.peers.size).to.be.eql(0) + expect(remoteLibp2p.pubsub._pubsub.peers.size).to.be.eql(0) + + remoteLibp2p.pubsub.start() + + return Promise.all([ + pWaitFor(() => libp2p.pubsub._pubsub.peers.size === 1), + pWaitFor(() => remoteLibp2p.pubsub._pubsub.peers.size === 1) + ]) + }) + + it('should receive pubsub messages', async function () { + this.timeout(10e3) + const defer = pDefer() + const libp2pId = libp2p.peerInfo.id.toB58String() + const topic = 'test-topic' + const data = 'hey!' + + await libp2p.dial(remAddr) + + remoteLibp2p.pubsub.start() + + await Promise.all([ + pWaitFor(() => libp2p.pubsub._pubsub.peers.size === 1), + pWaitFor(() => remoteLibp2p.pubsub._pubsub.peers.size === 1) + ]) + + let subscribedTopics = libp2p.pubsub.getTopics() + expect(subscribedTopics).to.not.include(topic) + + libp2p.pubsub.subscribe(topic, (msg) => { + expect(msg.data.toString()).to.equal(data) + defer.resolve() + }) + + subscribedTopics = libp2p.pubsub.getTopics() + expect(subscribedTopics).to.include(topic) + + // wait for remoteLibp2p to know about libp2p subscription + await pWaitFor(() => { + const subscribedPeers = remoteLibp2p.pubsub.getPeersSubscribed(topic) + return subscribedPeers.includes(libp2pId) + }) + + remoteLibp2p.pubsub.publish(topic, data) + + await defer.promise + }) + }) +}) diff --git a/test/pubsub/utils.js b/test/pubsub/utils.js new file mode 100644 index 0000000000..11495c5df8 --- /dev/null +++ b/test/pubsub/utils.js @@ -0,0 +1,29 @@ +'use strict' + +const Gossipsub = require('libp2p-gossipsub') +const { multicodec } = require('libp2p-gossipsub') +const Crypto = require('../../src/insecure/plaintext') +const Muxer = require('libp2p-mplex') +const Transport = require('libp2p-tcp') + +const mergeOptions = require('merge-options') + +const baseOptions = { + modules: { + transport: [Transport], + streamMuxer: [Muxer], + connEncryption: [Crypto] + } +} + +module.exports.baseOptions = baseOptions + +const subsystemOptions = mergeOptions(baseOptions, { + modules: { + pubsub: Gossipsub + } +}) + +module.exports.subsystemOptions = subsystemOptions + +module.exports.subsystemMulticodecs = [multicodec] diff --git a/test/registrar/registrar.spec.js b/test/registrar/registrar.spec.js index ec7d1b6189..9114e03590 100644 --- a/test/registrar/registrar.spec.js +++ b/test/registrar/registrar.spec.js @@ -7,6 +7,7 @@ const { expect } = chai const pDefer = require('p-defer') const PeerInfo = require('peer-info') +const Topology = require('libp2p-interfaces/src/topology/multicodec-topology') const PeerStore = require('../../src/peer-store') const Registrar = require('../../src/registrar') const { createMockConnection } = require('./utils') @@ -32,52 +33,17 @@ describe('registrar', () => { throw new Error('should fail to register a protocol if no multicodec is provided') }) - it('should fail to register a protocol if no handlers are provided', () => { - const topologyProps = { - multicodecs: multicodec + it('should fail to register a protocol if an invalid topology is provided', () => { + const fakeTopology = { + random: 1 } - - try { - registrar.register(topologyProps) - } catch (err) { - expect(err).to.exist() - return - } - throw new Error('should fail to register a protocol if no handlers are provided') - }) - - it('should fail to register a protocol if the onConnect handler is not provided', () => { - const topologyProps = { - multicodecs: multicodec, - handlers: { - onDisconnect: () => { } - } - } - - try { - registrar.register(topologyProps) - } catch (err) { - expect(err).to.exist() - return - } - throw new Error('should fail to register a protocol if the onConnect handler is not provided') - }) - - it('should fail to register a protocol if the onDisconnect handler is not provided', () => { - const topologyProps = { - multicodecs: multicodec, - handlers: { - onConnect: () => { } - } - } - try { - registrar.register(topologyProps) + registrar.register() } catch (err) { - expect(err).to.exist() + expect(err).to.exist(fakeTopology) return } - throw new Error('should fail to register a protocol if the onDisconnect handler is not provided') + throw new Error('should fail to register a protocol if an invalid topology is provided') }) }) @@ -88,13 +54,13 @@ describe('registrar', () => { }) it('should be able to register a protocol', () => { - const topologyProps = { + const topologyProps = new Topology({ + multicodecs: multicodec, handlers: { onConnect: () => { }, onDisconnect: () => { } - }, - multicodecs: multicodec - } + } + }) const identifier = registrar.register(topologyProps) @@ -102,13 +68,13 @@ describe('registrar', () => { }) it('should be able to unregister a protocol', () => { - const topologyProps = { + const topologyProps = new Topology({ + multicodecs: multicodec, handlers: { onConnect: () => { }, onDisconnect: () => { } - }, - multicodecs: multicodec - } + } + }) const identifier = registrar.register(topologyProps) const success = registrar.unregister(identifier) @@ -138,7 +104,7 @@ describe('registrar', () => { registrar.onConnect(remotePeerInfo, conn) expect(registrar.connections.size).to.eql(1) - const topologyProps = { + const topologyProps = new Topology({ multicodecs: multicodec, handlers: { onConnect: (peerInfo, connection) => { @@ -153,7 +119,7 @@ describe('registrar', () => { onDisconnectDefer.resolve() } } - } + }) // Register protocol const identifier = registrar.register(topologyProps) @@ -161,11 +127,9 @@ describe('registrar', () => { // Topology created expect(topology).to.exist() - expect(topology.peers.size).to.eql(1) registrar.onDisconnect(remotePeerInfo) expect(registrar.connections.size).to.eql(0) - expect(topology.peers.size).to.eql(1) // topology should keep the peer // Wait for handlers to be called return Promise.all([ @@ -178,7 +142,7 @@ describe('registrar', () => { const onConnectDefer = pDefer() const onDisconnectDefer = pDefer() - const topologyProps = { + const topologyProps = new Topology({ multicodecs: multicodec, handlers: { onConnect: () => { @@ -188,7 +152,7 @@ describe('registrar', () => { onDisconnectDefer.resolve() } } - } + }) // Register protocol const identifier = registrar.register(topologyProps) @@ -196,7 +160,6 @@ describe('registrar', () => { // Topology created expect(topology).to.exist() - expect(topology.peers.size).to.eql(0) expect(registrar.connections.size).to.eql(0) // Setup connections before registrar @@ -212,7 +175,6 @@ describe('registrar', () => { peerStore.put(peerInfo) await onConnectDefer.promise - expect(topology.peers.size).to.eql(1) // Remove protocol to peer and update it peerInfo.protocols.delete(multicodec) From 3c79d33db9d23c72e635e95f772ea2613e657adf Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Tue, 19 Nov 2019 17:29:28 -0600 Subject: [PATCH 11/92] chore: use gossipsub release (#479) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1ebd91a925..14d4e7b00b 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "libp2p-delegated-content-routing": "^0.2.2", "libp2p-delegated-peer-routing": "^0.2.2", "libp2p-floodsub": "^0.19.0", - "libp2p-gossipsub": "ChainSafe/gossipsub-js#beta/async", + "libp2p-gossipsub": "^0.1.0", "libp2p-kad-dht": "^0.15.3", "libp2p-mdns": "^0.12.3", "libp2p-mplex": "^0.9.1", From 86b275a0d33554b5f8a4a91d071653253679b972 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Tue, 19 Nov 2019 17:44:45 -0600 Subject: [PATCH 12/92] refactor: core async (#478) * refactor: cleanup core test: auto dial on startup * fix: make hangup work properly * chore: fix lint * chore: apply suggestions from code review Co-Authored-By: Vasco Santos --- src/get-peer-info.js | 7 +- src/index.js | 204 +++++++++--------------------- src/transport-manager.js | 18 +++ src/util/index.js | 31 ----- test/dialing/direct.node.js | 17 +++ test/dialing/direct.spec.js | 19 ++- test/peer-discovery/index.spec.js | 45 +++++++ 7 files changed, 162 insertions(+), 179 deletions(-) create mode 100644 test/peer-discovery/index.spec.js diff --git a/src/get-peer-info.js b/src/get-peer-info.js index b74caa3ec1..01a6bc49f2 100644 --- a/src/get-peer-info.js +++ b/src/get-peer-info.js @@ -56,10 +56,7 @@ function getPeerInfoRemote (peer, libp2p) { try { peerInfo = getPeerInfo(peer, libp2p.peerStore) } catch (err) { - return Promise.reject(errCode( - new Error(`${peer} is not a valid peer type`), - 'ERR_INVALID_PEER_TYPE' - )) + throw errCode(err, 'ERR_INVALID_PEER_TYPE') } // If we don't have an address for the peer, attempt to find it @@ -67,7 +64,7 @@ function getPeerInfoRemote (peer, libp2p) { return libp2p.peerRouting.findPeer(peerInfo.id) } - return Promise.resolve(peerInfo) + return peerInfo } module.exports = { diff --git a/src/index.js b/src/index.js index 5efcc8febd..ed1d979494 100644 --- a/src/index.js +++ b/src/index.js @@ -1,21 +1,13 @@ 'use strict' -const FSM = require('fsm-event') const { EventEmitter } = require('events') const debug = require('debug') const log = debug('libp2p') log.error = debug('libp2p:error') -const errCode = require('err-code') -const promisify = require('promisify-es6') - -const each = require('async/each') const PeerInfo = require('peer-info') const multiaddr = require('multiaddr') -const Switch = require('./switch') -const Ping = require('./ping') -const { emitFirst } = require('./util') const peerRouting = require('./peer-routing') const contentRouting = require('./content-routing') const dht = require('./dht') @@ -34,20 +26,11 @@ const { multicodecs: IDENTIFY_PROTOCOLS } = require('./identify') -const notStarted = (action, state) => { - return errCode( - new Error(`libp2p cannot ${action} when not started; state is ${state}`), - codes.ERR_NODE_NOT_STARTED - ) -} - /** * @fires Libp2p#error Emitted when an error occurs * @fires Libp2p#peer:connect Emitted when a peer is connected to this node * @fires Libp2p#peer:disconnect Emitted when a peer disconnects from this node * @fires Libp2p#peer:discovery Emitted when a peer is discovered - * @fires Libp2p#start Emitted when the node and its services has started - * @fires Libp2p#stop Emitted when the node and its services has stopped */ class Libp2p extends EventEmitter { constructor (_options) { @@ -67,9 +50,6 @@ class Libp2p extends EventEmitter { this.peerStore = new PeerStore() - // create the switch, and listen for errors - this._switch = new Switch(this.peerInfo, this.peerStore, this._options.switch) - // Setup the Upgrader this.upgrader = new Upgrader({ localPeer: this.peerInfo.id, @@ -158,63 +138,7 @@ class Libp2p extends EventEmitter { this.contentRouting = contentRouting(this) this.dht = dht(this) - // Mount default protocols - Ping.mount(this._switch) - - this.state = new FSM('STOPPED', { - STOPPED: { - start: 'STARTING', - stop: 'STOPPED', - done: 'STOPPED' - }, - STARTING: { - done: 'STARTED', - abort: 'STOPPED', - stop: 'STOPPING' - }, - STARTED: { - stop: 'STOPPING', - start: 'STARTED' - }, - STOPPING: { - stop: 'STOPPING', - done: 'STOPPED' - } - }) - this.state.on('STARTING', () => { - log('libp2p is starting') - this._onStarting() - }) - this.state.on('STOPPING', () => { - log('libp2p is stopping') - }) - this.state.on('STARTED', () => { - log('libp2p has started') - this.emit('start') - }) - this.state.on('STOPPED', () => { - log('libp2p has stopped') - this.emit('stop') - }) - this.state.on('error', (err) => { - log.error(err) - this.emit('error', err) - }) - - // Once we start, emit and dial any peers we may have already discovered - this.state.on('STARTED', () => { - for (const peerInfo of this.peerStore.peers) { - this.emit('peer:discovery', peerInfo) - this._maybeConnect(peerInfo) - } - }) - this._peerDiscovered = this._peerDiscovered.bind(this) - - // promisify all instance methods - ;['start', 'hangUp', 'ping'].forEach(method => { - this[method] = promisify(this[method], { context: this }) - }) } /** @@ -233,14 +157,23 @@ class Libp2p extends EventEmitter { } /** - * Starts the libp2p node and all sub services + * Starts the libp2p node and all its subsystems * - * @param {function(Error)} callback - * @returns {void} + * @returns {Promise} */ - start (callback = () => {}) { - emitFirst(this, ['error', 'start'], callback) - this.state('start') + async start () { + log('libp2p is starting') + try { + await this._onStarting() + await this._onDidStart() + log('libp2p has started') + } catch (err) { + this.emit('error', err) + log.error('An error occurred starting libp2p', err) + await this.stop() + throw err + } + this._isStarted = true } /** @@ -249,23 +182,22 @@ class Libp2p extends EventEmitter { * @returns {void} */ async stop () { - this.state('stop') + log('libp2p is stopping') try { this.pubsub && await this.pubsub.stop() await this.transportManager.close() - await this._switch.stop() } catch (err) { if (err) { log.error(err) this.emit('error', err) } } - this.state('done') + log('libp2p has stopped') } isStarted () { - return this.state ? this.state._state === 'STARTED' : false + return this._isStarted } /** @@ -319,36 +251,30 @@ class Libp2p extends EventEmitter { } /** - * Disconnects from the given peer + * Disconnects all connections to the given `peer` * - * @param {PeerInfo|PeerId|Multiaddr|string} peer The peer to ping - * @param {function(Error)} callback - * @returns {void} + * @param {PeerId} peer The PeerId to close connections to + * @returns {Promise} */ - hangUp (peer, callback) { - getPeerInfoRemote(peer, this) - .then(peerInfo => { - this._switch.hangUp(peerInfo, callback) - }, callback) + hangUp (peer) { + return Promise.all( + this.registrar.connections.get(peer.toB58String()).map(connection => { + return connection.close() + }) + ) } - /** - * Pings the provided peer - * - * @param {PeerInfo|PeerId|Multiaddr|string} peer The peer to ping - * @param {function(Error, Ping)} callback - * @returns {void} - */ - ping (peer, callback) { - if (!this.isStarted()) { - return callback(notStarted('ping', this.state._state)) - } - - getPeerInfoRemote(peer, this) - .then(peerInfo => { - callback(null, new Ping(this._switch, peerInfo)) - }, callback) - } + // TODO: Update ping + // /** + // * Pings the provided peer + // * + // * @param {PeerInfo|PeerId|Multiaddr|string} peer The peer to ping + // * @returns {Promise} + // */ + // ping (peer) { + // const peerInfo = await getPeerInfoRemote(peer, this) + // return new Ping(this._switch, peerInfo) + // } /** * Registers the `handler` for each protocol @@ -379,32 +305,25 @@ class Libp2p extends EventEmitter { } async _onStarting () { - if (!this._modules.transport) { - this.emit('error', new Error('no transports were present')) - return this.state('abort') - } - const multiaddrs = this.peerInfo.multiaddrs.toArray() - // Start parallel tasks - const tasks = [ - this.transportManager.listen(multiaddrs) - ] + await this.transportManager.listen(multiaddrs) if (this._config.pubsub.enabled) { this.pubsub && this.pubsub.start() } + } - try { - await Promise.all(tasks) - } catch (err) { - log.error(err) - this.emit('error', err) - return this.state('stop') + /** + * Called when libp2p has started and before it returns + * @private + */ + _onDidStart () { + // Once we start, emit and dial any peers we may have already discovered + for (const peerInfo of this.peerStore.peers.values()) { + this.emit('peer:discovery', peerInfo) + this._maybeConnect(peerInfo) } - - // libp2p has started - this.state('done') } /** @@ -435,15 +354,18 @@ class Libp2p extends EventEmitter { * @private * @param {PeerInfo} peerInfo */ - _maybeConnect (peerInfo) { - // If auto dialing is on, check if we should dial - if (this._config.peerDiscovery.autoDial === true && !peerInfo.isConnected()) { + async _maybeConnect (peerInfo) { + // If auto dialing is on and we have no connection to the peer, check if we should dial + if (this._config.peerDiscovery.autoDial === true && !this.registrar.connections.get(peerInfo)) { const minPeers = this._options.connectionManager.minPeers || 0 - if (minPeers > Object.keys(this._switch.connection.connections).length) { + // TODO: This does not account for multiple connections to a peer + if (minPeers > this.registrar.connections.size) { log('connecting to discovered peer') - this._switch.dialer.connect(peerInfo, (err) => { - err && log.error('could not connect to discovered peer', err) - }) + try { + await this.dialer.connectToPeer(peerInfo) + } catch (err) { + log.error('could not connect to discovered peer', err) + } } } } @@ -452,9 +374,9 @@ class Libp2p extends EventEmitter { * Initializes and starts peer discovery services * * @private - * @param {function(Error)} callback + * @returns {Promise} */ - _setupPeerDiscovery (callback) { + _setupPeerDiscovery () { for (const DiscoveryService of this._modules.peerDiscovery) { let config = { enabled: true // on by default @@ -480,9 +402,7 @@ class Libp2p extends EventEmitter { } } - each(this._discovery, (d, cb) => { - d.start(cb) - }, callback) + return this._discovery.map(d => d.start()) } } diff --git a/src/transport-manager.js b/src/transport-manager.js index 136dc8c8a2..9fe86349f6 100644 --- a/src/transport-manager.js +++ b/src/transport-manager.js @@ -119,6 +119,12 @@ class TransportManager { * @param {Multiaddr[]} addrs */ async listen (addrs) { + if (addrs.length === 0) { + log('no addresses were provided for listening, this node is dial only') + return + } + + const couldNotListen = [] for (const [key, transport] of this._transports.entries()) { const supportedAddrs = transport.filter(addrs) const tasks = [] @@ -133,6 +139,12 @@ class TransportManager { tasks.push(listener.listen(addr)) } + // Keep track of transports we had no addresses for + if (tasks.length === 0) { + couldNotListen.push(key) + continue + } + const results = await pSettle(tasks) // If we are listening on at least 1 address, succeed. // TODO: we should look at adding a retry (`p-retry`) here to better support @@ -143,6 +155,12 @@ class TransportManager { throw errCode(new Error(`Transport (${key}) could not listen on any available address`), codes.ERR_NO_VALID_ADDRESSES) } } + + // If no transports were able to listen, throw an error. This likely + // means we were given addresses we do not have transports for + if (couldNotListen.length === this._transports.size) { + throw errCode(new Error(`no valid addresses were provided for transports [${couldNotListen}]`), codes.ERR_NO_VALID_ADDRESSES) + } } /** diff --git a/src/util/index.js b/src/util/index.js index 0fdd80cae6..bca13a4530 100644 --- a/src/util/index.js +++ b/src/util/index.js @@ -1,34 +1,4 @@ 'use strict' -const once = require('once') - -/** - * Registers `handler` to each event in `events`. The `handler` - * will only be called for the first event fired, at which point - * the `handler` will be removed as a listener. - * - * Ensures `handler` is only called once. - * - * @example - * // will call `callback` when `start` or `error` is emitted by `this` - * emitFirst(this, ['error', 'start'], callback) - * - * @private - * @param {EventEmitter} emitter The emitter to listen on - * @param {Array} events The events to listen for - * @param {function(*)} handler The handler to call when an event is triggered - * @returns {void} - */ -function emitFirst (emitter, events, handler) { - handler = once(handler) - events.forEach((e) => { - emitter.once(e, (...args) => { - events.forEach((ev) => { - emitter.removeListener(ev, handler) - }) - handler.apply(emitter, args) - }) - }) -} /** * Converts BufferList messages to Buffers @@ -43,5 +13,4 @@ function toBuffer (source) { })() } -module.exports.emitFirst = emitFirst module.exports.toBuffer = toBuffer diff --git a/test/dialing/direct.node.js b/test/dialing/direct.node.js index d8acbcac43..73eee1bab9 100644 --- a/test/dialing/direct.node.js +++ b/test/dialing/direct.node.js @@ -231,6 +231,23 @@ describe('Dialing (direct, TCP)', () => { expect(libp2p.dialer.connectToMultiaddr.callCount).to.equal(1) }) + it('should be able to use hangup to close connections', async () => { + libp2p = new Libp2p({ + peerInfo, + modules: { + transport: [Transport], + streamMuxer: [Muxer], + connEncryption: [Crypto] + } + }) + + const connection = await libp2p.dial(remoteAddr) + expect(connection).to.exist() + expect(connection.stat.timeline.close).to.not.exist() + await libp2p.hangUp(connection.remotePeer) + expect(connection.stat.timeline.close).to.exist() + }) + it('should use the protectors when provided for connecting', async () => { const protector = new Protector(swarmKeyBuffer) libp2p = new Libp2p({ diff --git a/test/dialing/direct.spec.js b/test/dialing/direct.spec.js index 1aca5987d9..8a16fbab51 100644 --- a/test/dialing/direct.spec.js +++ b/test/dialing/direct.spec.js @@ -210,7 +210,7 @@ describe('Dialing (direct, WebSockets)', () => { sinon.spy(libp2p.dialer, 'connectToMultiaddr') - const connection = await libp2p.dialer.connectToMultiaddr(remoteAddr) + const connection = await libp2p.dial(remoteAddr) expect(connection).to.exist() const { stream, protocol } = await connection.newStream('/echo/1.0.0') expect(stream).to.exist() @@ -241,5 +241,22 @@ describe('Dialing (direct, WebSockets)', () => { expect(libp2p.peerStore.update.callCount).to.equal(1) }) + + it('should be able to use hangup to close connections', async () => { + libp2p = new Libp2p({ + peerInfo, + modules: { + transport: [Transport], + streamMuxer: [Muxer], + connEncryption: [Crypto] + } + }) + + const connection = await libp2p.dial(remoteAddr) + expect(connection).to.exist() + expect(connection.stat.timeline.close).to.not.exist() + await libp2p.hangUp(connection.remotePeer) + expect(connection.stat.timeline.close).to.exist() + }) }) }) diff --git a/test/peer-discovery/index.spec.js b/test/peer-discovery/index.spec.js new file mode 100644 index 0000000000..2b64ad305b --- /dev/null +++ b/test/peer-discovery/index.spec.js @@ -0,0 +1,45 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +const { expect } = chai +const sinon = require('sinon') +const defer = require('p-defer') + +const Libp2p = require('../../src') +const baseOptions = require('../utils/base-options.browser') +const { createPeerInfoFromFixture } = require('../utils/creators/peer') + +describe('peer discovery', () => { + let peerInfo + let remotePeerInfo + let libp2p + + before(async () => { + [peerInfo, remotePeerInfo] = await createPeerInfoFromFixture(2) + }) + + afterEach(async () => { + libp2p && await libp2p.stop() + }) + + it('should dial know peers on startup', async () => { + libp2p = new Libp2p({ + ...baseOptions, + peerInfo + }) + libp2p.peerStore.add(remotePeerInfo) + const deferred = defer() + sinon.stub(libp2p.dialer, 'connectToPeer').callsFake((remotePeerInfo) => { + expect(remotePeerInfo).to.equal(remotePeerInfo) + deferred.resolve() + }) + const spy = sinon.spy() + libp2p.on('peer:discovery', spy) + + libp2p.start() + await deferred.promise + expect(spy.getCall(0).args).to.eql([remotePeerInfo]) + }) +}) From c37703dc17bcafe064d5d0833b1416a2c84060ee Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Tue, 26 Nov 2019 07:42:37 -0600 Subject: [PATCH 13/92] refactor: update secio and tests to use it (#484) * refactor: use async secio * test: add secio to most test suites * chore: update secio version --- .aegir.js | 2 +- package.json | 2 +- src/transport-manager.js | 2 +- test/dialing/direct.node.js | 2 +- test/dialing/direct.spec.js | 2 +- test/upgrading/upgrader.spec.js | 2 +- test/utils/base-options.browser.js | 2 +- test/utils/base-options.js | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.aegir.js b/.aegir.js index 891e4374c0..51e31c896b 100644 --- a/.aegir.js +++ b/.aegir.js @@ -7,7 +7,7 @@ const PeerId = require('peer-id') const PeerInfo = require('peer-info') const WebSockets = require('libp2p-websockets') const Muxer = require('libp2p-mplex') -const Crypto = require('./src/insecure/plaintext') +const Crypto = require('libp2p-secio') const pipe = require('it-pipe') let libp2p diff --git a/package.json b/package.json index 14d4e7b00b..0487022db1 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "libp2p-mdns": "^0.12.3", "libp2p-mplex": "^0.9.1", "libp2p-pnet": "~0.1.0", - "libp2p-secio": "^0.11.1", + "libp2p-secio": "^0.12.1", "libp2p-spdy": "^0.13.2", "libp2p-tcp": "^0.14.1", "libp2p-websockets": "^0.13.1", diff --git a/src/transport-manager.js b/src/transport-manager.js index 9fe86349f6..722049a450 100644 --- a/src/transport-manager.js +++ b/src/transport-manager.js @@ -82,7 +82,7 @@ class TransportManager { try { return await transport.dial(ma, options) } catch (err) { - throw errCode(new Error('Transport dial failed'), codes.ERR_TRANSPORT_DIAL_FAILED, err) + throw errCode(err, codes.ERR_TRANSPORT_DIAL_FAILED) } } diff --git a/test/dialing/direct.node.js b/test/dialing/direct.node.js index 73eee1bab9..6734fb5f0e 100644 --- a/test/dialing/direct.node.js +++ b/test/dialing/direct.node.js @@ -7,7 +7,7 @@ const { expect } = chai const sinon = require('sinon') const Transport = require('libp2p-tcp') const Muxer = require('libp2p-mplex') -const Crypto = require('../../src/insecure/plaintext') +const Crypto = require('libp2p-secio') const multiaddr = require('multiaddr') const PeerId = require('peer-id') const PeerInfo = require('peer-info') diff --git a/test/dialing/direct.spec.js b/test/dialing/direct.spec.js index 8a16fbab51..eb1b158b5e 100644 --- a/test/dialing/direct.spec.js +++ b/test/dialing/direct.spec.js @@ -9,7 +9,7 @@ const pDefer = require('p-defer') const delay = require('delay') const Transport = require('libp2p-websockets') const Muxer = require('libp2p-mplex') -const Crypto = require('../../src/insecure/plaintext') +const Crypto = require('libp2p-secio') const multiaddr = require('multiaddr') const PeerId = require('peer-id') const PeerInfo = require('peer-info') diff --git a/test/upgrading/upgrader.spec.js b/test/upgrading/upgrader.spec.js index 69d0686fde..35d870069b 100644 --- a/test/upgrading/upgrader.spec.js +++ b/test/upgrading/upgrader.spec.js @@ -13,7 +13,7 @@ const pipe = require('it-pipe') const { collect } = require('streaming-iterables') const pSettle = require('p-settle') const Transport = require('libp2p-websockets') -const Crypto = require('../../src/insecure/plaintext') +const Crypto = require('libp2p-secio') const Protector = require('../../src/pnet') const swarmKeyBuffer = Buffer.from(require('../fixtures/swarm.key')) diff --git a/test/utils/base-options.browser.js b/test/utils/base-options.browser.js index 3a1337a0e4..1c1a425505 100644 --- a/test/utils/base-options.browser.js +++ b/test/utils/base-options.browser.js @@ -2,7 +2,7 @@ const Transport = require('libp2p-websockets') const Muxer = require('libp2p-mplex') -const Crypto = require('../../src/insecure/plaintext') +const Crypto = require('libp2p-secio') module.exports = { modules: { diff --git a/test/utils/base-options.js b/test/utils/base-options.js index 68163eeed5..ca7a037ce5 100644 --- a/test/utils/base-options.js +++ b/test/utils/base-options.js @@ -2,7 +2,7 @@ const Transport = require('libp2p-tcp') const Muxer = require('libp2p-mplex') -const Crypto = require('../../src/insecure/plaintext') +const Crypto = require('libp2p-secio') module.exports = { modules: { From 1ea945ad24027c1c9f82a668da724cba98bcee46 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Tue, 26 Nov 2019 16:40:04 +0100 Subject: [PATCH 14/92] refactor: dht async/await (#480) * refactor: core async (#478) * refactor: cleanup core test: auto dial on startup * fix: make hangup work properly * chore: fix lint * chore: apply suggestions from code review Co-Authored-By: Vasco Santos * fix: provide libp2p dialer to the dht * chore: use dht release --- package.json | 2 +- src/dht.js | 81 +++++++++++++------- src/dialer.js | 16 +++- src/errors.js | 1 + src/index.js | 18 ++--- src/peer-store/index.js | 21 ++++- test/dht/configuration.node.js | 92 ++++++++++++++++++++++ test/dht/operation.node.js | 135 +++++++++++++++++++++++++++++++++ test/dht/utils.js | 37 +++++++++ test/dialing/direct.node.js | 20 ++++- 10 files changed, 380 insertions(+), 43 deletions(-) create mode 100644 test/dht/configuration.node.js create mode 100644 test/dht/operation.node.js create mode 100644 test/dht/utils.js diff --git a/package.json b/package.json index 0487022db1..a04d9db977 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "libp2p-delegated-peer-routing": "^0.2.2", "libp2p-floodsub": "^0.19.0", "libp2p-gossipsub": "^0.1.0", - "libp2p-kad-dht": "^0.15.3", + "libp2p-kad-dht": "~0.17.0", "libp2p-mdns": "^0.12.3", "libp2p-mplex": "^0.9.1", "libp2p-pnet": "~0.1.0", diff --git a/src/dht.js b/src/dht.js index f53c09b9cd..f2c801243e 100644 --- a/src/dht.js +++ b/src/dht.js @@ -1,43 +1,72 @@ 'use strict' -const nextTick = require('async/nextTick') const errCode = require('err-code') -const promisify = require('promisify-es6') const { messages, codes } = require('./errors') -module.exports = (node) => { +module.exports = (node, DHT, config) => { + const dht = new DHT({ + dialer: node.dialer, + peerInfo: node.peerInfo, + peerStore: node.peerStore, + registrar: node.registrar, + datastore: this.datastore, + ...config + }) + return { - put: promisify((key, value, callback) => { - if (!node._dht) { - return nextTick(callback, errCode(new Error(messages.DHT_DISABLED), codes.DHT_DISABLED)) + /** + * Store the given key/value pair in the DHT. + * @param {Buffer} key + * @param {Buffer} value + * @param {Object} [options] - put options + * @param {number} [options.minPeers] - minimum number of peers required to successfully put + * @returns {Promise} + */ + put: (key, value, options) => { + if (!node.isStarted() || !dht.isStarted) { + throw errCode(new Error(messages.NOT_STARTED_YET), codes.DHT_NOT_STARTED) } - node._dht.put(key, value, callback) - }), - get: promisify((key, options, callback) => { - if (typeof options === 'function') { - callback = options - options = {} - } + return dht.put(key, value, options) + }, - if (!node._dht) { - return nextTick(callback, errCode(new Error(messages.DHT_DISABLED), codes.DHT_DISABLED)) + /** + * Get the value to the given key. + * Times out after 1 minute by default. + * @param {Buffer} key + * @param {Object} [options] - get options + * @param {number} [options.timeout] - optional timeout (default: 60000) + * @returns {Promise<{from: PeerId, val: Buffer}>} + */ + get: (key, options) => { + if (!node.isStarted() || !dht.isStarted) { + throw errCode(new Error(messages.NOT_STARTED_YET), codes.DHT_NOT_STARTED) } - node._dht.get(key, options, callback) - }), - getMany: promisify((key, nVals, options, callback) => { - if (typeof options === 'function') { - callback = options - options = {} - } + return dht.get(key, options) + }, - if (!node._dht) { - return nextTick(callback, errCode(new Error(messages.DHT_DISABLED), codes.DHT_DISABLED)) + /** + * Get the `n` values to the given key without sorting. + * @param {Buffer} key + * @param {number} nVals + * @param {Object} [options] - get options + * @param {number} [options.timeout] - optional timeout (default: 60000) + * @returns {Promise>} + */ + getMany: (key, nVals, options) => { + if (!node.isStarted() || !dht.isStarted) { + throw errCode(new Error(messages.NOT_STARTED_YET), codes.DHT_NOT_STARTED) } - node._dht.getMany(key, nVals, options, callback) - }) + return dht.getMany(key, nVals, options) + }, + + _dht: dht, + + start: () => dht.start(), + + stop: () => dht.stop() } } diff --git a/src/dialer.js b/src/dialer.js index 85ac6ae32d..2d4071d13e 100644 --- a/src/dialer.js +++ b/src/dialer.js @@ -8,6 +8,7 @@ const AbortController = require('abort-controller') const debug = require('debug') const log = debug('libp2p:dialer') log.error = debug('libp2p:dialer:error') +const PeerId = require('peer-id') const { codes } = require('./errors') const { @@ -20,15 +21,18 @@ class Dialer { * @constructor * @param {object} options * @param {TransportManager} options.transportManager + * @param {Peerstore} peerStore * @param {number} options.concurrency Number of max concurrent dials. Defaults to `MAX_PARALLEL_DIALS` * @param {number} options.timeout How long a dial attempt is allowed to take. Defaults to `DIAL_TIMEOUT` */ constructor ({ transportManager, + peerStore, concurrency = MAX_PARALLEL_DIALS, timeout = DIAL_TIMEOUT }) { this.transportManager = transportManager + this.peerStore = peerStore this.concurrency = concurrency this.timeout = timeout this.queue = new PQueue({ concurrency, timeout, throwOnTimeout: true }) @@ -97,18 +101,22 @@ class Dialer { } /** - * Connects to a given `PeerInfo` by dialing all of its known addresses. + * Connects to a given `PeerInfo` or `PeerId` by dialing all of its known addresses. * The dial to the first address that is successfully able to upgrade a connection * will be used. * * @async - * @param {PeerInfo} peerInfo The remote peer to dial + * @param {PeerInfo|PeerId} peer The remote peer to dial * @param {object} [options] * @param {AbortSignal} [options.signal] An AbortController signal * @returns {Promise} */ - async connectToPeer (peerInfo, options = {}) { - const addrs = peerInfo.multiaddrs.toArray() + async connectToPeer (peer, options = {}) { + if (PeerId.isPeerId(peer)) { + peer = this.peerStore.get(peer.toB58String()) + } + + const addrs = peer.multiaddrs.toArray() for (const addr of addrs) { try { return await this.connectToMultiaddr(addr, options) diff --git a/src/errors.js b/src/errors.js index 2a103db2ed..9a4f6e75c1 100644 --- a/src/errors.js +++ b/src/errors.js @@ -8,6 +8,7 @@ exports.messages = { exports.codes = { DHT_DISABLED: 'ERR_DHT_DISABLED', PUBSUB_NOT_STARTED: 'ERR_PUBSUB_NOT_STARTED', + DHT_NOT_STARTED: 'ERR_DHT_NOT_STARTED', ERR_CONNECTION_ENDED: 'ERR_CONNECTION_ENDED', ERR_CONNECTION_FAILED: 'ERR_CONNECTION_FAILED', ERR_NODE_NOT_STARTED: 'ERR_NODE_NOT_STARTED', diff --git a/src/index.js b/src/index.js index ed1d979494..a043046569 100644 --- a/src/index.js +++ b/src/index.js @@ -91,7 +91,8 @@ class Libp2p extends EventEmitter { } this.dialer = new Dialer({ - transportManager: this.transportManager + transportManager: this.transportManager, + peerStore: this.peerStore }) // Attach stream multiplexers @@ -118,13 +119,8 @@ class Libp2p extends EventEmitter { } // dht provided components (peerRouting, contentRouting, dht) - if (this._config.dht.enabled) { - const DHT = this._modules.dht - - this._dht = new DHT(this._switch, { - datastore: this.datastore, - ...this._config.dht - }) + if (this._modules.dht) { + this._dht = dht(this, this._modules.dht, this._config.dht) } // start pubsub @@ -136,7 +132,6 @@ class Libp2p extends EventEmitter { // peer and content routing will automatically get modules from _modules and _dht this.peerRouting = peerRouting(this) this.contentRouting = contentRouting(this) - this.dht = dht(this) this._peerDiscovered = this._peerDiscovered.bind(this) } @@ -186,6 +181,7 @@ class Libp2p extends EventEmitter { try { this.pubsub && await this.pubsub.stop() + this._dht && await this._dht.stop() await this.transportManager.close() } catch (err) { if (err) { @@ -312,6 +308,10 @@ class Libp2p extends EventEmitter { if (this._config.pubsub.enabled) { this.pubsub && this.pubsub.start() } + + if (this._config.dht.enabled) { + this._dht && this._dht.start() + } } /** diff --git a/src/peer-store/index.js b/src/peer-store/index.js index 1e91b926f0..5f52f279dc 100644 --- a/src/peer-store/index.js +++ b/src/peer-store/index.js @@ -37,24 +37,28 @@ class PeerStore extends EventEmitter { * Stores the peerInfo of a new peer. * If already exist, its info is updated. * @param {PeerInfo} peerInfo + * @return {PeerInfo} */ put (peerInfo) { assert(PeerInfo.isPeerInfo(peerInfo), 'peerInfo must be an instance of peer-info') + let peer // Already know the peer? if (this.peers.has(peerInfo.id.toB58String())) { - this.update(peerInfo) + peer = this.update(peerInfo) } else { - this.add(peerInfo) + peer = this.add(peerInfo) // Emit the new peer found this.emit('peer', peerInfo) } + return peer } /** * Add a new peer to the store. * @param {PeerInfo} peerInfo + * @return {PeerInfo} */ add (peerInfo) { assert(PeerInfo.isPeerInfo(peerInfo), 'peerInfo must be an instance of peer-info') @@ -86,11 +90,13 @@ class PeerStore extends EventEmitter { }) this.peers.set(peerInfo.id.toB58String(), peerProxy) + return peerProxy } /** * Updates an already known peer. * @param {PeerInfo} peerInfo + * @return {PeerInfo} */ update (peerInfo) { assert(PeerInfo.isPeerInfo(peerInfo), 'peerInfo must be an instance of peer-info') @@ -148,6 +154,8 @@ class PeerStore extends EventEmitter { if (!recorded.id.pubKey && peerInfo.id.pubKey) { recorded.id.pubKey = peerInfo.id.pubKey } + + return recorded } /** @@ -165,6 +173,15 @@ class PeerStore extends EventEmitter { return undefined } + /** + * Has the info to the given id. + * @param {string} peerId b58str id + * @returns {boolean} + */ + has (peerId) { + return this.peers.has(peerId) + } + /** * Removes the Peer with the matching `peerId` from the PeerStore * @param {string} peerId b58str id diff --git a/test/dht/configuration.node.js b/test/dht/configuration.node.js new file mode 100644 index 0000000000..c003c124fc --- /dev/null +++ b/test/dht/configuration.node.js @@ -0,0 +1,92 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +const { expect } = chai + +const mergeOptions = require('merge-options') +const multiaddr = require('multiaddr') + +const { create } = require('../../src') +const { baseOptions, subsystemOptions } = require('./utils') +const peerUtils = require('../utils/creators/peer') + +const listenAddr = multiaddr('/ip4/127.0.0.1/tcp/0') + +describe('DHT subsystem is configurable', () => { + let libp2p + + afterEach(async () => { + libp2p && await libp2p.stop() + }) + + it('should not exist if no module is provided', async () => { + libp2p = await create(baseOptions) + expect(libp2p._dht).to.not.exist() + }) + + it('should exist if the module is provided', async () => { + libp2p = await create(subsystemOptions) + expect(libp2p._dht).to.exist() + }) + + it('should start and stop by default once libp2p starts', async () => { + const [peerInfo] = await peerUtils.createPeerInfoFromFixture(1) + peerInfo.multiaddrs.add(listenAddr) + + const customOptions = mergeOptions(subsystemOptions, { + peerInfo + }) + + libp2p = await create(customOptions) + expect(libp2p._dht._dht.isStarted).to.equal(false) + + await libp2p.start() + expect(libp2p._dht._dht.isStarted).to.equal(true) + + await libp2p.stop() + expect(libp2p._dht._dht.isStarted).to.equal(false) + }) + + it('should not start if disabled once libp2p starts', async () => { + const [peerInfo] = await peerUtils.createPeerInfoFromFixture(1) + peerInfo.multiaddrs.add(listenAddr) + + const customOptions = mergeOptions(subsystemOptions, { + peerInfo, + config: { + dht: { + enabled: false + } + } + }) + + libp2p = await create(customOptions) + expect(libp2p._dht._dht.isStarted).to.equal(false) + + await libp2p.start() + expect(libp2p._dht._dht.isStarted).to.equal(false) + }) + + it('should allow a manual start', async () => { + const [peerInfo] = await peerUtils.createPeerInfoFromFixture(1) + peerInfo.multiaddrs.add(listenAddr) + + const customOptions = mergeOptions(subsystemOptions, { + peerInfo, + config: { + dht: { + enabled: false + } + } + }) + + libp2p = await create(customOptions) + await libp2p.start() + expect(libp2p._dht._dht.isStarted).to.equal(false) + + await libp2p._dht.start() + expect(libp2p._dht._dht.isStarted).to.equal(true) + }) +}) diff --git a/test/dht/operation.node.js b/test/dht/operation.node.js new file mode 100644 index 0000000000..6e707db3dd --- /dev/null +++ b/test/dht/operation.node.js @@ -0,0 +1,135 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +const { expect } = chai + +const pWaitFor = require('p-wait-for') +const mergeOptions = require('merge-options') +const multiaddr = require('multiaddr') + +const { create } = require('../../src') +const { subsystemOptions, subsystemMulticodecs } = require('./utils') +const peerUtils = require('../utils/creators/peer') + +const listenAddr = multiaddr('/ip4/127.0.0.1/tcp/8000') +const remoteListenAddr = multiaddr('/ip4/127.0.0.1/tcp/8001') + +describe('DHT subsystem operates correctly', () => { + let peerInfo, remotePeerInfo + let libp2p, remoteLibp2p + let remAddr + + beforeEach(async () => { + [peerInfo, remotePeerInfo] = await peerUtils.createPeerInfoFromFixture(2) + + peerInfo.multiaddrs.add(listenAddr) + remotePeerInfo.multiaddrs.add(remoteListenAddr) + }) + + describe('dht started before connect', () => { + beforeEach(async () => { + libp2p = await create(mergeOptions(subsystemOptions, { + peerInfo + })) + + remoteLibp2p = await create(mergeOptions(subsystemOptions, { + peerInfo: remotePeerInfo + })) + + await Promise.all([ + libp2p.start(), + remoteLibp2p.start() + ]) + + remAddr = remoteLibp2p.transportManager.getAddrs()[0] + }) + + afterEach(() => Promise.all([ + libp2p && libp2p.stop(), + remoteLibp2p && remoteLibp2p.stop() + ])) + + it('should get notified of connected peers on dial', async () => { + const connection = await libp2p.dialProtocol(remAddr, subsystemMulticodecs) + + expect(connection).to.exist() + + return Promise.all([ + pWaitFor(() => libp2p._dht._dht.routingTable.size === 1), + pWaitFor(() => remoteLibp2p._dht._dht.routingTable.size === 1) + ]) + }) + + it('should put on a peer and get from the other', async () => { + const key = Buffer.from('hello') + const value = Buffer.from('world') + + await libp2p.dialProtocol(remAddr, subsystemMulticodecs) + + await Promise.all([ + pWaitFor(() => libp2p._dht._dht.routingTable.size === 1), + pWaitFor(() => remoteLibp2p._dht._dht.routingTable.size === 1) + ]) + + await libp2p._dht.put(key, value) + + const fetchedValue = await remoteLibp2p._dht.get(key) + expect(fetchedValue).to.eql(value) + }) + }) + + describe('dht started after connect', () => { + beforeEach(async () => { + libp2p = await create(mergeOptions(subsystemOptions, { + peerInfo + })) + + remoteLibp2p = await create(mergeOptions(subsystemOptions, { + peerInfo: remotePeerInfo, + config: { + dht: { + enabled: false + } + } + })) + + await libp2p.start() + await remoteLibp2p.start() + + remAddr = remoteLibp2p.transportManager.getAddrs()[0] + }) + + afterEach(() => Promise.all([ + libp2p && libp2p.stop(), + remoteLibp2p && remoteLibp2p.stop() + ])) + + it('should get notified of connected peers after starting', async () => { + const connection = await libp2p.dial(remAddr) + + expect(connection).to.exist() + expect(libp2p._dht._dht.routingTable.size).to.be.eql(0) + expect(remoteLibp2p._dht._dht.routingTable.size).to.be.eql(0) + + await remoteLibp2p._dht.start() + return pWaitFor(() => libp2p._dht._dht.routingTable.size === 1) + }) + + it('should put on a peer and get from the other', async () => { + await libp2p.dial(remAddr) + + const key = Buffer.from('hello') + const value = Buffer.from('world') + + await remoteLibp2p._dht.start() + await pWaitFor(() => libp2p._dht._dht.routingTable.size === 1) + + await libp2p._dht.put(key, value) + + const fetchedValue = await remoteLibp2p._dht.get(key) + expect(fetchedValue).to.eql(value) + }) + }) +}) diff --git a/test/dht/utils.js b/test/dht/utils.js new file mode 100644 index 0000000000..b5249ea9e3 --- /dev/null +++ b/test/dht/utils.js @@ -0,0 +1,37 @@ +'use strict' + +const KadDht = require('libp2p-kad-dht') +const { multicodec } = require('libp2p-kad-dht') +const Crypto = require('../../src/insecure/plaintext') +const Muxer = require('libp2p-mplex') +const Transport = require('libp2p-tcp') + +const mergeOptions = require('merge-options') + +const baseOptions = { + modules: { + transport: [Transport], + streamMuxer: [Muxer], + connEncryption: [Crypto] + } +} + +module.exports.baseOptions = baseOptions + +const subsystemOptions = mergeOptions(baseOptions, { + modules: { + dht: KadDht + }, + config: { + dht: { + kBucketSize: 20, + randomWalk: { + enabled: true + }, + enabled: true + } + } +}) + +module.exports.subsystemOptions = subsystemOptions +module.exports.subsystemMulticodecs = [multicodec] diff --git a/test/dialing/direct.node.js b/test/dialing/direct.node.js index 6734fb5f0e..04d63b40e4 100644 --- a/test/dialing/direct.node.js +++ b/test/dialing/direct.node.js @@ -17,6 +17,7 @@ const pipe = require('it-pipe') const Libp2p = require('../../src') const Dialer = require('../../src/dialer') +const PeerStore = require('../../src/peer-store') const TransportManager = require('../../src/transport-manager') const { codes: ErrorCodes } = require('../../src/errors') const Protector = require('../../src/pnet') @@ -86,7 +87,7 @@ describe('Dialing (direct, TCP)', () => { expect.fail('Dial should have failed') }) - it('should be able to connect to a given peer', async () => { + it('should be able to connect to a given peer info', async () => { const dialer = new Dialer({ transportManager: localTM }) const peerId = await PeerId.createFromJSON(Peers[0]) const peerInfo = new PeerInfo(peerId) @@ -97,6 +98,23 @@ describe('Dialing (direct, TCP)', () => { await connection.close() }) + it('should be able to connect to a given peer id', async () => { + const peerStore = new PeerStore() + const dialer = new Dialer({ + transportManager: localTM, + peerStore + }) + + const peerId = await PeerId.createFromJSON(Peers[0]) + const peerInfo = new PeerInfo(peerId) + peerInfo.multiaddrs.add(remoteAddr) + peerStore.put(peerInfo) + + const connection = await dialer.connectToPeer(peerId) + expect(connection).to.exist() + await connection.close() + }) + it('should fail to connect to a given peer with unsupported addresses', async () => { const dialer = new Dialer({ transportManager: localTM }) const peerId = await PeerId.createFromJSON(Peers[0]) From b316cdd19b73441581e6d3c5ebcb86f3f0d627e8 Mon Sep 17 00:00:00 2001 From: dirkmc Date: Wed, 27 Nov 2019 06:24:22 -0500 Subject: [PATCH 15/92] refactor(docs): async await version of examples/echo (#483) * fix: performance bottleneck in stat.js (#463) Array.shift seems to be very slow, perhaps linear, on some engines, resulting in _update consuming a lot of CPU. * docs(fix): correct docs and example for pnet (#464) * docs(fix): correct docs and example for pnet * docs(fix): correct pnet docs * docs(fix): update README.md language (#468) * docs: reciprocate (#474) * docs(example): fix ipfs cat (#475) `ipfs.files.cat` is incorrect. the correct function is `ipfs.cat` * fix: async await examples/echo * fix: examples readme typos (#481) * fix: simplify libp2p bundle for echo example --- examples/echo/src/dialer.js | 73 ++++++++++++++++-------------- examples/echo/src/libp2p-bundle.js | 54 +--------------------- examples/echo/src/listener.js | 57 +++++++++++------------ 3 files changed, 66 insertions(+), 118 deletions(-) diff --git a/examples/echo/src/dialer.js b/examples/echo/src/dialer.js index e703501126..d2b7cfe732 100644 --- a/examples/echo/src/dialer.js +++ b/examples/echo/src/dialer.js @@ -8,51 +8,54 @@ const PeerId = require('peer-id') const PeerInfo = require('peer-info') const Node = require('./libp2p-bundle') -const pull = require('pull-stream') -const async = require('async') +const pipe = require('it-pipe') -async.parallel([ - (cb) => PeerId.createFromJSON(require('./id-d'), cb), - (cb) => PeerId.createFromJSON(require('./id-l'), cb) -], (err, ids) => { - if (err) { throw err } +async function run() { + const [dialerId, listenerId] = await Promise.all([ + PeerId.createFromJSON(require('./id-d')), + PeerId.createFromJSON(require('./id-l')) + ]) // Dialer - const dialerId = ids[0] const dialerPeerInfo = new PeerInfo(dialerId) dialerPeerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') const dialerNode = new Node({ peerInfo: dialerPeerInfo }) - // Peer to Dial - const listenerPeerInfo = new PeerInfo(ids[1]) - const listenerId = ids[1] + // Peer to Dial (the listener) + const listenerPeerInfo = new PeerInfo(listenerId) const listenerMultiaddr = '/ip4/127.0.0.1/tcp/10333/p2p/' + listenerId.toB58String() listenerPeerInfo.multiaddrs.add(listenerMultiaddr) - dialerNode.start((err) => { - if (err) { throw err } - - console.log('Dialer ready, listening on:') - dialerPeerInfo.multiaddrs.forEach((ma) => console.log(ma.toString() + - '/p2p/' + dialerId.toB58String())) - - console.log('Dialing to peer:', listenerMultiaddr.toString()) - dialerNode.dialProtocol(listenerPeerInfo, '/echo/1.0.0', (err, conn) => { - if (err) { throw err } - - console.log('nodeA dialed to nodeB on protocol: /echo/1.0.0') - - pull( - pull.values(['hey']), - conn, - pull.collect((err, data) => { - if (err) { throw err } - console.log('received echo:', data.toString()) - }) - ) - }) - }) -}) + // Start the dialer libp2p node + await dialerNode.start() + + console.log('Dialer ready, listening on:') + dialerPeerInfo.multiaddrs.forEach((ma) => console.log(ma.toString() + + '/p2p/' + dialerId.toB58String())) + + // Dial the listener node + console.log('Dialing to peer:', listenerMultiaddr.toString()) + const { stream } = await dialerNode.dialProtocol(listenerPeerInfo, '/echo/1.0.0') + + console.log('nodeA dialed to nodeB on protocol: /echo/1.0.0') + + pipe( + // Source data + ['hey'], + // Write to the stream, and pass its output to the next function + stream, + // Sink function + async function (source) { + // For each chunk of data + for await (const data of source) { + // Output the data + console.log('received echo:', data.toString()) + } + } + ) +} + +run() diff --git a/examples/echo/src/libp2p-bundle.js b/examples/echo/src/libp2p-bundle.js index c45b4cfaf5..cca65f8f95 100644 --- a/examples/echo/src/libp2p-bundle.js +++ b/examples/echo/src/libp2p-bundle.js @@ -1,41 +1,12 @@ 'use strict' const TCP = require('libp2p-tcp') -const MulticastDNS = require('libp2p-mdns') const WS = require('libp2p-websockets') -const Bootstrap = require('libp2p-bootstrap') -const spdy = require('libp2p-spdy') -const KadDHT = require('libp2p-kad-dht') const mplex = require('libp2p-mplex') const secio = require('libp2p-secio') const defaultsDeep = require('@nodeutils/defaults-deep') const libp2p = require('../../..') -function mapMuxers (list) { - return list.map((pref) => { - if (typeof pref !== 'string') { - return pref - } - switch (pref.trim().toLowerCase()) { - case 'spdy': return spdy - case 'mplex': return mplex - default: - throw new Error(pref + ' muxer not available') - } - }) -} - -function getMuxers (muxers) { - const muxerPrefs = process.env.LIBP2P_MUXER - if (muxerPrefs && !muxers) { - return mapMuxers(muxerPrefs.split(',')) - } else if (muxers) { - return mapMuxers(muxers) - } else { - return [mplex, spdy] - } -} - class Node extends libp2p { constructor (_options) { const defaults = { @@ -44,29 +15,8 @@ class Node extends libp2p { TCP, WS ], - streamMuxer: getMuxers(_options.muxer), - connEncryption: [ secio ], - peerDiscovery: [ - MulticastDNS, - Bootstrap - ], - dht: KadDHT - }, - config: { - peerDiscovery: { - mdns: { - interval: 10000, - enabled: false - }, - bootstrap: { - interval: 10000, - enabled: false, - list: _options.bootstrapList - } - }, - dht: { - kBucketSize: 20 - } + streamMuxer: [ mplex ], + connEncryption: [ secio ] } } diff --git a/examples/echo/src/listener.js b/examples/echo/src/listener.js index 10546ec38f..476ca7e20f 100644 --- a/examples/echo/src/listener.js +++ b/examples/echo/src/listener.js @@ -8,39 +8,34 @@ const PeerId = require('peer-id') const PeerInfo = require('peer-info') const Node = require('./libp2p-bundle') -const pull = require('pull-stream') -const series = require('async/series') - -let listenerId -let listenerNode - -series([ - (cb) => { - PeerId.createFromJSON(require('./id-l'), (err, id) => { - if (err) { return cb(err) } - listenerId = id - cb() - }) - }, - (cb) => { - const listenerPeerInfo = new PeerInfo(listenerId) - listenerPeerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/10333') - listenerNode = new Node({ - peerInfo: listenerPeerInfo - }) - - listenerNode.on('peer:connect', (peerInfo) => { - console.log('received dial to me from:', peerInfo.id.toB58String()) - }) - - listenerNode.handle('/echo/1.0.0', (protocol, conn) => pull(conn, conn)) - listenerNode.start(cb) - } -], (err) => { - if (err) { throw err } +const pipe = require('it-pipe') + +async function run() { + const listenerId = await PeerId.createFromJSON(require('./id-l')) + + // Listener libp2p node + const listenerPeerInfo = new PeerInfo(listenerId) + listenerPeerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/10333') + const listenerNode = new Node({ + peerInfo: listenerPeerInfo + }) + + // Log a message when we receive a connection + listenerNode.on('peer:connect', (peerInfo) => { + console.log('received dial to me from:', peerInfo.id.toB58String()) + }) + + // Handle incoming connections for the protocol by piping from the stream + // back to itself (an echo) + await listenerNode.handle('/echo/1.0.0', ({ stream }) => pipe(stream.source, stream.sink)) + + // Start listening + await listenerNode.start() console.log('Listener ready, listening on:') listenerNode.peerInfo.multiaddrs.forEach((ma) => { console.log(ma.toString() + '/p2p/' + listenerId.toB58String()) }) -}) +} + +run() From 995640ee2f8a7891d58b2d07ea4118a773e68155 Mon Sep 17 00:00:00 2001 From: dirkmc Date: Wed, 27 Nov 2019 06:43:17 -0500 Subject: [PATCH 16/92] refactor(docs): async await version of examples/chat (#482) * fix: performance bottleneck in stat.js (#463) Array.shift seems to be very slow, perhaps linear, on some engines, resulting in _update consuming a lot of CPU. * docs(fix): correct docs and example for pnet (#464) * docs(fix): correct docs and example for pnet * docs(fix): correct pnet docs * docs(fix): update README.md language (#468) * docs: reciprocate (#474) * docs(example): fix ipfs cat (#475) `ipfs.files.cat` is incorrect. the correct function is `ipfs.cat` * fix: async-await example chat * fix: move handler before start * fix: examples readme typos (#481) * fix: simplify libp2p bundle for echo example * chore: remove unused vars --- examples/chat/src/dialer.js | 92 ++++++++++-------------------- examples/chat/src/libp2p-bundle.js | 54 +----------------- examples/chat/src/listener.js | 63 ++++++++------------ examples/chat/src/stream.js | 40 +++++++++++++ 4 files changed, 96 insertions(+), 153 deletions(-) create mode 100644 examples/chat/src/stream.js diff --git a/examples/chat/src/dialer.js b/examples/chat/src/dialer.js index d4e9ca7aee..ae6d8d05b5 100644 --- a/examples/chat/src/dialer.js +++ b/examples/chat/src/dialer.js @@ -4,76 +4,44 @@ const PeerId = require('peer-id') const PeerInfo = require('peer-info') const Node = require('./libp2p-bundle') -const pull = require('pull-stream') -const async = require('async') -const Pushable = require('pull-pushable') -const p = Pushable() -let idListener +const { stdinToStream, streamToConsole } = require('./stream') -async.parallel([ - (callback) => { - PeerId.createFromJSON(require('./peer-id-dialer'), (err, idDialer) => { - if (err) { - throw err - } - callback(null, idDialer) - }) - }, - (callback) => { - PeerId.createFromJSON(require('./peer-id-listener'), (err, idListener) => { - if (err) { - throw err - } - callback(null, idListener) - }) - } -], (err, ids) => { - if (err) throw err - const peerDialer = new PeerInfo(ids[0]) +async function run() { + const [idDialer, idListener] = await Promise.all([ + PeerId.createFromJSON(require('./peer-id-dialer')), + PeerId.createFromJSON(require('./peer-id-listener')) + ]) + + // Create a new libp2p node on localhost with a randomly chosen port + const peerDialer = new PeerInfo(idDialer) peerDialer.multiaddrs.add('/ip4/0.0.0.0/tcp/0') const nodeDialer = new Node({ peerInfo: peerDialer }) - const peerListener = new PeerInfo(ids[1]) - idListener = ids[1] + // Create a PeerInfo with the listening peer's address + const peerListener = new PeerInfo(idListener) peerListener.multiaddrs.add('/ip4/127.0.0.1/tcp/10333') - nodeDialer.start((err) => { - if (err) { - throw err - } - console.log('Dialer ready, listening on:') + // Start the libp2p host + await nodeDialer.start() - peerListener.multiaddrs.forEach((ma) => { - console.log(ma.toString() + '/p2p/' + idListener.toB58String()) - }) + // Output this node's address + console.log('Dialer ready, listening on:') + peerListener.multiaddrs.forEach((ma) => { + console.log(ma.toString() + '/p2p/' + idListener.toB58String()) + }) - nodeDialer.dialProtocol(peerListener, '/chat/1.0.0', (err, conn) => { - if (err) { - throw err - } - console.log('nodeA dialed to nodeB on protocol: /chat/1.0.0') - console.log('Type a message and see what happens') - // Write operation. Data sent as a buffer - pull( - p, - conn - ) - // Sink, data converted from buffer to utf8 string - pull( - conn, - pull.map((data) => { - return data.toString('utf8').replace('\n', '') - }), - pull.drain(console.log) - ) + // Dial to the remote peer (the "listener") + const { stream } = await nodeDialer.dialProtocol(peerListener, '/chat/1.0.0') - process.stdin.setEncoding('utf8') - process.openStdin().on('data', (chunk) => { - var data = chunk.toString() - p.push(data) - }) - }) - }) -}) + console.log('Dialer dialed to listener on protocol: /chat/1.0.0') + console.log('Type a message and see what happens') + + // Send stdin to the stream + stdinToStream(stream) + // Read the stream and output to console + streamToConsole(stream) +} + +run() diff --git a/examples/chat/src/libp2p-bundle.js b/examples/chat/src/libp2p-bundle.js index c45b4cfaf5..cca65f8f95 100644 --- a/examples/chat/src/libp2p-bundle.js +++ b/examples/chat/src/libp2p-bundle.js @@ -1,41 +1,12 @@ 'use strict' const TCP = require('libp2p-tcp') -const MulticastDNS = require('libp2p-mdns') const WS = require('libp2p-websockets') -const Bootstrap = require('libp2p-bootstrap') -const spdy = require('libp2p-spdy') -const KadDHT = require('libp2p-kad-dht') const mplex = require('libp2p-mplex') const secio = require('libp2p-secio') const defaultsDeep = require('@nodeutils/defaults-deep') const libp2p = require('../../..') -function mapMuxers (list) { - return list.map((pref) => { - if (typeof pref !== 'string') { - return pref - } - switch (pref.trim().toLowerCase()) { - case 'spdy': return spdy - case 'mplex': return mplex - default: - throw new Error(pref + ' muxer not available') - } - }) -} - -function getMuxers (muxers) { - const muxerPrefs = process.env.LIBP2P_MUXER - if (muxerPrefs && !muxers) { - return mapMuxers(muxerPrefs.split(',')) - } else if (muxers) { - return mapMuxers(muxers) - } else { - return [mplex, spdy] - } -} - class Node extends libp2p { constructor (_options) { const defaults = { @@ -44,29 +15,8 @@ class Node extends libp2p { TCP, WS ], - streamMuxer: getMuxers(_options.muxer), - connEncryption: [ secio ], - peerDiscovery: [ - MulticastDNS, - Bootstrap - ], - dht: KadDHT - }, - config: { - peerDiscovery: { - mdns: { - interval: 10000, - enabled: false - }, - bootstrap: { - interval: 10000, - enabled: false, - list: _options.bootstrapList - } - }, - dht: { - kBucketSize: 20 - } + streamMuxer: [ mplex ], + connEncryption: [ secio ] } } diff --git a/examples/chat/src/listener.js b/examples/chat/src/listener.js index 1dc3233d73..52c69edeb2 100644 --- a/examples/chat/src/listener.js +++ b/examples/chat/src/listener.js @@ -4,53 +4,38 @@ const PeerId = require('peer-id') const PeerInfo = require('peer-info') const Node = require('./libp2p-bundle.js') -const pull = require('pull-stream') -const Pushable = require('pull-pushable') -const p = Pushable() +const { stdinToStream, streamToConsole } = require('./stream') -PeerId.createFromJSON(require('./peer-id-listener'), (err, idListener) => { - if (err) { - throw err - } +async function run() { + // Create a new libp2p node with the given multi-address + const idListener = await PeerId.createFromJSON(require('./peer-id-listener')) const peerListener = new PeerInfo(idListener) peerListener.multiaddrs.add('/ip4/0.0.0.0/tcp/10333') const nodeListener = new Node({ peerInfo: peerListener }) - nodeListener.start((err) => { - if (err) { - throw err - } - - nodeListener.on('peer:connect', (peerInfo) => { - console.log(peerInfo.id.toB58String()) - }) - - nodeListener.handle('/chat/1.0.0', (protocol, conn) => { - pull( - p, - conn - ) + // Log a message when a remote peer connects to us + nodeListener.on('peer:connect', (peerInfo) => { + console.log(peerInfo.id.toB58String()) + }) - pull( - conn, - pull.map((data) => { - return data.toString('utf8').replace('\n', '') - }), - pull.drain(console.log) - ) + // Handle messages for the protocol + await nodeListener.handle('/chat/1.0.0', async ({ stream }) => { + // Send stdin to the stream + stdinToStream(stream) + // Read the stream and output to console + streamToConsole(stream) + }) - process.stdin.setEncoding('utf8') - process.openStdin().on('data', (chunk) => { - var data = chunk.toString() - p.push(data) - }) - }) + // Start listening + await nodeListener.start() - console.log('Listener ready, listening on:') - peerListener.multiaddrs.forEach((ma) => { - console.log(ma.toString() + '/p2p/' + idListener.toB58String()) - }) + // Output listen addresses to the console + console.log('Listener ready, listening on:') + peerListener.multiaddrs.forEach((ma) => { + console.log(ma.toString() + '/p2p/' + idListener.toB58String()) }) -}) +} + +run() diff --git a/examples/chat/src/stream.js b/examples/chat/src/stream.js new file mode 100644 index 0000000000..30385b9718 --- /dev/null +++ b/examples/chat/src/stream.js @@ -0,0 +1,40 @@ +'use strict' +/* eslint-disable no-console */ + +const pipe = require('it-pipe') +const lp = require('it-length-prefixed') + +function stdinToStream(stream) { + // Read utf-8 from stdin + process.stdin.setEncoding('utf8') + pipe( + // Read from stdin (the source) + process.stdin, + // Encode with length prefix (so receiving side knows how much data is coming) + lp.encode(), + // Write to the stream (the sink) + stream.sink + ) +} + +function streamToConsole(stream) { + pipe( + // Read from the stream (the source) + stream.source, + // Decode length-prefixed data + lp.decode(), + // Sink function + async function (source) { + // For each chunk of data + for await (const msg of source) { + // Output the data as a utf8 string + console.log('> ' + msg.toString('utf8').replace('\n', '')) + } + } + ) +} + +module.exports = { + stdinToStream, + streamToConsole +} From acbbc0f84e2f0400ebba76ff2587330a1877e763 Mon Sep 17 00:00:00 2001 From: dirkmc Date: Thu, 28 Nov 2019 10:45:04 -0500 Subject: [PATCH 17/92] fix: replace peerInfo addresses with listen addresses (#485) * feat: replace peer info addresses with listen addresses * test: add listening test * chore: fix linting --- src/index.js | 8 ++++++ test/core/listening.node.js | 56 +++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 test/core/listening.node.js diff --git a/src/index.js b/src/index.js index a043046569..b0358ba2bd 100644 --- a/src/index.js +++ b/src/index.js @@ -301,10 +301,18 @@ class Libp2p extends EventEmitter { } async _onStarting () { + // Listen on the addresses supplied in the peerInfo const multiaddrs = this.peerInfo.multiaddrs.toArray() await this.transportManager.listen(multiaddrs) + // The addresses may change once the listener starts + // eg /ip4/0.0.0.0/tcp/0 => /ip4/192.168.1.0/tcp/58751 + this.peerInfo.multiaddrs.clear() + for (const ma of this.transportManager.getAddrs()) { + this.peerInfo.multiaddrs.add(ma) + } + if (this._config.pubsub.enabled) { this.pubsub && this.pubsub.start() } diff --git a/test/core/listening.node.js b/test/core/listening.node.js new file mode 100644 index 0000000000..dd67c74f8b --- /dev/null +++ b/test/core/listening.node.js @@ -0,0 +1,56 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +const { expect } = chai + +const multiaddr = require('multiaddr') +const Transport = require('libp2p-tcp') + +const { create } = require('../../src') +const peerUtils = require('../utils/creators/peer') + +const listenAddr = multiaddr('/ip4/0.0.0.0/tcp/0') + +describe('Listening', () => { + let peerInfo + let libp2p + + before(async () => { + [peerInfo] = await peerUtils.createPeerInfoFromFixture(1) + peerInfo.multiaddrs.add(listenAddr) + }) + + after(async () => { + await libp2p.stop() + }) + + it('should replace wildcard host and port with actual host and port on startup', async () => { + libp2p = await create({ + peerInfo, + modules: { + transport: [Transport] + } + }) + + await libp2p.start() + + const addrs = libp2p.peerInfo.multiaddrs.toArray() + + // Should get something like: + // /ip4/127.0.0.1/tcp/50866 + // /ip4/192.168.1.2/tcp/50866 + expect(addrs.length).to.equal(2) + + const opts = [addrs[0].toOptions(), addrs[1].toOptions()] + expect(opts[0].family).to.equal('ipv4') + expect(opts[1].family).to.equal('ipv4') + expect(opts[0].transport).to.equal('tcp') + expect(opts[1].transport).to.equal('tcp') + expect(opts[0].host).to.match(/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/) + expect(opts[1].host).to.match(/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/) + expect(opts[0].port).to.be.gt(0) + expect(opts[1].port).to.be.gt(0) + }) +}) From 997ee166b06156ee0e8f55f713a341347319b22b Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Thu, 28 Nov 2019 22:50:14 +0100 Subject: [PATCH 18/92] feat: discovery modules (#486) * feat: discovery modules * chore: address review --- package.json | 4 +- src/index.js | 13 ++- test/peer-discovery/index.node.js | 172 ++++++++++++++++++++++++++++++ test/peer-discovery/index.spec.js | 21 ++++ 4 files changed, 206 insertions(+), 4 deletions(-) create mode 100644 test/peer-discovery/index.node.js diff --git a/package.json b/package.json index a04d9db977..b923e288c2 100644 --- a/package.json +++ b/package.json @@ -88,13 +88,13 @@ "glob": "^7.1.4", "interface-datastore": "^0.6.0", "it-pair": "^1.0.0", - "libp2p-bootstrap": "^0.9.7", + "libp2p-bootstrap": "^0.10.3", "libp2p-delegated-content-routing": "^0.2.2", "libp2p-delegated-peer-routing": "^0.2.2", "libp2p-floodsub": "^0.19.0", "libp2p-gossipsub": "^0.1.0", "libp2p-kad-dht": "~0.17.0", - "libp2p-mdns": "^0.12.3", + "libp2p-mdns": "^0.13.0", "libp2p-mplex": "^0.9.1", "libp2p-pnet": "~0.1.0", "libp2p-secio": "^0.12.1", diff --git a/src/index.js b/src/index.js index b0358ba2bd..8d88cec132 100644 --- a/src/index.js +++ b/src/index.js @@ -168,7 +168,6 @@ class Libp2p extends EventEmitter { await this.stop() throw err } - this._isStarted = true } /** @@ -317,8 +316,13 @@ class Libp2p extends EventEmitter { this.pubsub && this.pubsub.start() } + // DHT subsystem if (this._config.dht.enabled) { this._dht && this._dht.start() + + // TODO: this should be modified once random-walk is used as + // the other discovery modules + this._dht._dht.on('peer', this._peerDiscovered) } } @@ -327,6 +331,11 @@ class Libp2p extends EventEmitter { * @private */ _onDidStart () { + this._isStarted = true + + // Peer discovery + this._setupPeerDiscovery() + // Once we start, emit and dial any peers we may have already discovered for (const peerInfo of this.peerStore.peers.values()) { this.emit('peer:discovery', peerInfo) @@ -385,7 +394,7 @@ class Libp2p extends EventEmitter { * @returns {Promise} */ _setupPeerDiscovery () { - for (const DiscoveryService of this._modules.peerDiscovery) { + for (const DiscoveryService of this._modules.peerDiscovery || []) { let config = { enabled: true // on by default } diff --git a/test/peer-discovery/index.node.js b/test/peer-discovery/index.node.js new file mode 100644 index 0000000000..bea051f6a2 --- /dev/null +++ b/test/peer-discovery/index.node.js @@ -0,0 +1,172 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) + +const defer = require('p-defer') +const mergeOptions = require('merge-options') + +const Bootstrap = require('libp2p-bootstrap') +const crypto = require('libp2p-crypto') +const KadDht = require('libp2p-kad-dht') +const MulticastDNS = require('libp2p-mdns') +const multiaddr = require('multiaddr') + +const Libp2p = require('../../src') +const baseOptions = require('../utils/base-options') +const { createPeerInfoFromFixture } = require('../utils/creators/peer') + +describe('peer discovery scenarios', () => { + let peerInfo, remotePeerInfo1, remotePeerInfo2 + let libp2p + + before(async () => { + [peerInfo, remotePeerInfo1, remotePeerInfo2] = await createPeerInfoFromFixture(3) + + peerInfo.multiaddrs.add(multiaddr('/ip4/127.0.0.1/tcp/0')) + remotePeerInfo1.multiaddrs.add(multiaddr('/ip4/127.0.0.1/tcp/0')) + remotePeerInfo2.multiaddrs.add(multiaddr('/ip4/127.0.0.1/tcp/0')) + }) + + afterEach(async () => { + libp2p && await libp2p.stop() + }) + + it('bootstrap should discover all peers in the list', async () => { + const deferred = defer() + + const bootstrappers = [ + ...remotePeerInfo1.multiaddrs.toArray().map((ma) => `${ma}/p2p/${remotePeerInfo1.id.toB58String()}`), + ...remotePeerInfo2.multiaddrs.toArray().map((ma) => `${ma}/p2p/${remotePeerInfo2.id.toB58String()}`) + ] + + libp2p = new Libp2p(mergeOptions(baseOptions, { + peerInfo, + modules: { + peerDiscovery: [Bootstrap] + }, + config: { + peerDiscovery: { + bootstrap: { + enabled: true, + list: bootstrappers + } + } + } + })) + + const expectedPeers = new Set([ + remotePeerInfo1.id.toB58String(), + remotePeerInfo2.id.toB58String() + ]) + + libp2p.on('peer:discovery', (peerInfo) => { + expectedPeers.delete(peerInfo.id.toB58String()) + if (expectedPeers.size === 0) { + libp2p.removeAllListeners('peer:discovery') + deferred.resolve() + } + }) + + await libp2p.start() + + return deferred.promise + }) + + it('MulticastDNS should discover all peers on the local network', async () => { + const deferred = defer() + + const getConfig = (peerInfo) => mergeOptions(baseOptions, { + peerInfo, + modules: { + peerDiscovery: [MulticastDNS] + }, + config: { + peerDiscovery: { + mdns: { + enabled: true, + interval: 200, // discover quickly + // use a random tag to prevent CI collision + serviceTag: crypto.randomBytes(10).toString('hex') + } + } + } + }) + + libp2p = new Libp2p(getConfig(peerInfo)) + const remoteLibp2p1 = new Libp2p(getConfig(remotePeerInfo1)) + const remoteLibp2p2 = new Libp2p(getConfig(remotePeerInfo2)) + + const expectedPeers = new Set([ + remotePeerInfo1.id.toB58String(), + remotePeerInfo2.id.toB58String() + ]) + + libp2p.on('peer:discovery', (peerInfo) => { + expectedPeers.delete(peerInfo.id.toB58String()) + if (expectedPeers.size === 0) { + libp2p.removeAllListeners('peer:discovery') + deferred.resolve() + } + }) + + await remoteLibp2p1.start() + await remoteLibp2p2.start() + await libp2p.start() + + await deferred.promise + + await remoteLibp2p1.stop() + await remoteLibp2p2.stop() + }) + + it('kad-dht should discover other peers', async () => { + const deferred = defer() + + const getConfig = (peerInfo) => mergeOptions(baseOptions, { + peerInfo, + modules: { + dht: KadDht + }, + config: { + dht: { + randomWalk: { + enabled: true, + delay: 100, + interval: 200, // start the query sooner + timeout: 3000 + }, + enabled: true + } + } + }) + + libp2p = new Libp2p(getConfig(peerInfo)) + const remoteLibp2p1 = new Libp2p(getConfig(remotePeerInfo1)) + const remoteLibp2p2 = new Libp2p(getConfig(remotePeerInfo2)) + + libp2p.on('peer:discovery', (peerInfo) => { + if (peerInfo.id.toB58String() === remotePeerInfo2.id.toB58String()) { + libp2p.removeAllListeners('peer:discovery') + deferred.resolve() + } + }) + + await remoteLibp2p1.start() + await remoteLibp2p2.start() + await libp2p.start() + + // Topology: + // A -> B + // C -> B + await Promise.all([ + libp2p.dial(remotePeerInfo1), + remoteLibp2p2.dial(remotePeerInfo1) + ]) + + await deferred.promise + await remoteLibp2p1.stop() + await remoteLibp2p2.stop() + }) +}) diff --git a/test/peer-discovery/index.spec.js b/test/peer-discovery/index.spec.js index 2b64ad305b..b3cee86bdb 100644 --- a/test/peer-discovery/index.spec.js +++ b/test/peer-discovery/index.spec.js @@ -5,7 +5,11 @@ const chai = require('chai') chai.use(require('dirty-chai')) const { expect } = chai const sinon = require('sinon') + const defer = require('p-defer') +const mergeOptions = require('merge-options') + +const MulticastDNS = require('libp2p-mdns') const Libp2p = require('../../src') const baseOptions = require('../utils/base-options.browser') @@ -22,6 +26,7 @@ describe('peer discovery', () => { afterEach(async () => { libp2p && await libp2p.stop() + sinon.reset() }) it('should dial know peers on startup', async () => { @@ -42,4 +47,20 @@ describe('peer discovery', () => { await deferred.promise expect(spy.getCall(0).args).to.eql([remotePeerInfo]) }) + + it('should ignore self on discovery', async () => { + libp2p = new Libp2p(mergeOptions(baseOptions, { + peerInfo, + modules: { + peerDiscovery: [MulticastDNS] + } + })) + + await libp2p.start() + const discoverySpy = sinon.spy() + libp2p.on('peer:discovery', discoverySpy) + libp2p._discovery[0].emit('peer', libp2p.peerInfo) + + expect(discoverySpy.called).to.eql(false) + }) }) From b518391a47e38b2c26b30b07ffa1a2fec0cb5594 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Fri, 29 Nov 2019 16:41:08 +0100 Subject: [PATCH 19/92] refactor: circuit relay to async (#477) * refactor: add dialing over relay support * chore: fix lint * fix: dont clear listeners on close * fix: if dial errors already have codes, just rethrow them * fix: clear the registrar when libp2p stops * fix: improve connection maintenance with circuit * chore: correct feedback * test: use chai as promised * test(fix): reset multiaddrs on dial test --- .aegir.js | 9 + package.json | 5 +- src/circuit/README.md | 30 +- src/circuit/circuit.js | 126 -------- src/circuit/circuit/dialer.js | 275 ---------------- src/circuit/circuit/hop.js | 365 +++++++--------------- src/circuit/circuit/stop.js | 105 ++++--- src/circuit/circuit/stream-handler.js | 132 +++----- src/circuit/circuit/utils.js | 151 +++------ src/circuit/index.js | 185 ++++++++++- src/circuit/listener.js | 129 ++------ src/circuit/stream-to-conn.js | 49 +++ src/dialer.js | 1 - src/errors.js | 1 + src/index.js | 21 +- src/registrar.js | 20 +- src/transport-manager.js | 5 +- test/dialing/direct.node.js | 34 +- test/dialing/direct.spec.js | 34 +- test/dialing/relay.node.js | 162 ++++++++++ test/identify/index.spec.js | 28 +- test/registrar/registrar.node.js | 15 + test/registrar/registrar.spec.js | 16 +- test/transports/transport-manager.node.js | 5 +- test/transports/transport-manager.spec.js | 51 ++- test/utils/base-options.browser.js | 8 + test/utils/base-options.js | 8 + test/utils/creators/peer.js | 14 +- test/utils/mockConnection.js | 107 ++++++- test/utils/mockMultiaddrConn.js | 9 +- 30 files changed, 934 insertions(+), 1166 deletions(-) delete mode 100644 src/circuit/circuit.js delete mode 100644 src/circuit/circuit/dialer.js create mode 100644 src/circuit/stream-to-conn.js create mode 100644 test/dialing/relay.node.js diff --git a/.aegir.js b/.aegir.js index 51e31c896b..f5eb73392c 100644 --- a/.aegir.js +++ b/.aegir.js @@ -23,6 +23,15 @@ const before = async () => { transport: [WebSockets], streamMuxer: [Muxer], connEncryption: [Crypto] + }, + config: { + relay: { + enabled: true, + hop: { + enabled: true, + active: false + } + } } }) // Add the echo protocol diff --git a/package.json b/package.json index b923e288c2..e80409f8c9 100644 --- a/package.json +++ b/package.json @@ -59,13 +59,13 @@ "mafmt": "^7.0.0", "merge-options": "^1.0.1", "moving-average": "^1.0.0", - "multiaddr": "^7.1.0", + "multiaddr": "^7.2.1", "multistream-select": "^0.15.0", "once": "^1.4.0", "p-map": "^3.0.0", "p-queue": "^6.1.1", "p-settle": "^3.1.0", - "peer-id": "^0.13.3", + "peer-id": "^0.13.4", "peer-info": "^0.17.0", "promisify-es6": "^1.0.3", "protons": "^1.0.1", @@ -80,6 +80,7 @@ "abortable-iterator": "^2.1.0", "aegir": "^20.0.0", "chai": "^4.2.0", + "chai-as-promised": "^7.1.1", "chai-checkmark": "^1.0.1", "cids": "^0.7.1", "delay": "^4.3.0", diff --git a/src/circuit/README.md b/src/circuit/README.md index 36b659f7ff..b9f89fcfc7 100644 --- a/src/circuit/README.md +++ b/src/circuit/README.md @@ -32,8 +32,6 @@ Prior to `libp2p-circuit` there was a rift in the IPFS network, were IPFS nodes - [Example](#example) - [Create dialer/listener](#create-dialerlistener) - [Create `relay`](#create-relay) - - [This module uses `pull-streams`](#this-module-uses-pull-streams) - - [Converting `pull-streams` to Node.js Streams](#converting-pull-streams-to-nodejs-streams) - [API](#api) - [Implementation rational](#implementation-rational) @@ -48,7 +46,7 @@ const Circuit = require('libp2p-circuit') const multiaddr = require('multiaddr') const pull = require('pull-stream') -const mh1 = multiaddr('/p2p-circuit/ipfs/QmHash') // dial /ipfs/QmHash over any circuit +const mh1 = multiaddr('/p2p-circuit/p2p/QmHash') // dial /p2p/QmHash over any circuit const circuit = new Circuit(swarmInstance, options) // pass swarm instance and options @@ -91,37 +89,13 @@ const relay = new Relay(options) relay.mount(swarmInstance) // start relaying traffic ``` -### This module uses `pull-streams` - -We expose a streaming interface based on `pull-streams`, rather then on the Node.js core streams implementation (aka Node.js streams). `pull-streams` offers us a better mechanism for error handling and flow control guarantees. If you would like to know more about why we did this, see the discussion at this [issue](https://github.com/ipfs/js-ipfs/issues/362). - -You can learn more about pull-streams at: - -- [The history of Node.js streams, nodebp April 2014](https://www.youtube.com/watch?v=g5ewQEuXjsQ) -- [The history of streams, 2016](http://dominictarr.com/post/145135293917/history-of-streams) -- [pull-streams, the simple streaming primitive](http://dominictarr.com/post/149248845122/pull-streams-pull-streams-are-a-very-simple) -- [pull-streams documentation](https://pull-stream.github.io/) - -#### Converting `pull-streams` to Node.js Streams - -If you are a Node.js streams user, you can convert a pull-stream to a Node.js stream using the module [`pull-stream-to-stream`](https://github.com/dominictarr/pull-stream-to-stream), giving you an instance of a Node.js stream that is linked to the pull-stream. For example: - -```js -const pullToStream = require('pull-stream-to-stream') - -const nodeStreamInstance = pullToStream(pullStreamInstance) -// nodeStreamInstance is an instance of a Node.js Stream -``` - -To learn more about this utility, visit https://pull-stream.github.io/#pull-stream-to-stream. - ## API [![](https://raw.githubusercontent.com/libp2p/interface-transport/master/img/badge.png)](https://github.com/libp2p/interface-transport) `libp2p-circuit` accepts Circuit addresses for both IPFS and non IPFS encapsulated addresses, i.e: -`/p2p-circuit/ip4/127.0.0.1/tcp/4001/ipfs/QmHash` +`/p2p-circuit/ip4/127.0.0.1/tcp/4001/p2p/QmHash` Both for dialing and listening. diff --git a/src/circuit/circuit.js b/src/circuit/circuit.js deleted file mode 100644 index 17597ab4ee..0000000000 --- a/src/circuit/circuit.js +++ /dev/null @@ -1,126 +0,0 @@ -'use strict' - -const mafmt = require('mafmt') -const multiaddr = require('multiaddr') - -const CircuitDialer = require('./circuit/dialer') -const utilsFactory = require('./circuit/utils') - -const debug = require('debug') -const log = debug('libp2p:circuit:transportdialer') -log.err = debug('libp2p:circuit:error:transportdialer') - -const createListener = require('./listener') - -class Circuit { - static get tag () { - return 'Circuit' - } - - /** - * Creates an instance of Dialer. - * - * @param {Swarm} swarm - the swarm - * @param {any} options - config options - * - * @memberOf Dialer - */ - constructor (swarm, options) { - this.options = options || {} - - this.swarm = swarm - this.dialer = null - this.utils = utilsFactory(swarm) - this.peerInfo = this.swarm._peerInfo - this.relays = this.filter(this.peerInfo.multiaddrs.toArray()) - - // if no explicit relays, add a default relay addr - if (this.relays.length === 0) { - this.peerInfo - .multiaddrs - .add(`/p2p-circuit/ipfs/${this.peerInfo.id.toB58String()}`) - } - - this.dialer = new CircuitDialer(swarm, options) - - this.swarm.on('peer-mux-established', (peerInfo) => { - this.dialer.canHop(peerInfo) - }) - this.swarm.on('peer-mux-closed', (peerInfo) => { - this.dialer.relayPeers.delete(peerInfo.id.toB58String()) - }) - } - - /** - * Dial the relays in the Addresses.Swarm config - * - * @param {Array} relays - * @return {void} - */ - _dialSwarmRelays () { - // if we have relay addresses in swarm config, then dial those relays - this.relays.forEach((relay) => { - const relaySegments = relay - .toString() - .split('/p2p-circuit') - .filter(segment => segment.length) - - relaySegments.forEach((relaySegment) => { - const ma = this.utils.peerInfoFromMa(multiaddr(relaySegment)) - this.dialer._dialRelay(ma) - }) - }) - } - - /** - * Dial a peer over a relay - * - * @param {multiaddr} ma - the multiaddr of the peer to dial - * @param {Object} options - dial options - * @param {Function} cb - a callback called once dialed - * @returns {Connection} - the connection - * - * @memberOf Dialer - */ - dial (ma, options, cb) { - return this.dialer.dial(ma, options, cb) - } - - /** - * Create a listener - * - * @param {any} options - * @param {Function} handler - * @return {listener} - */ - createListener (options, handler) { - if (typeof options === 'function') { - handler = options - options = this.options || {} - } - - const listener = createListener(this.swarm, options, handler) - listener.on('listen', this._dialSwarmRelays.bind(this)) - return listener - } - - /** - * Filter check for all multiaddresses - * that this transport can dial on - * - * @param {any} multiaddrs - * @returns {Array} - * - * @memberOf Dialer - */ - filter (multiaddrs) { - if (!Array.isArray(multiaddrs)) { - multiaddrs = [multiaddrs] - } - return multiaddrs.filter((ma) => { - return mafmt.Circuit.matches(ma) - }) - } -} - -module.exports = Circuit diff --git a/src/circuit/circuit/dialer.js b/src/circuit/circuit/dialer.js deleted file mode 100644 index 2c250e2c22..0000000000 --- a/src/circuit/circuit/dialer.js +++ /dev/null @@ -1,275 +0,0 @@ -'use strict' - -const once = require('once') -const PeerId = require('peer-id') -const waterfall = require('async/waterfall') -const setImmediate = require('async/setImmediate') -const multiaddr = require('multiaddr') - -const { Connection } = require('libp2p-interfaces/src/connection') - -const utilsFactory = require('./utils') -const StreamHandler = require('./stream-handler') - -const debug = require('debug') -const log = debug('libp2p:circuit:dialer') -log.err = debug('libp2p:circuit:error:dialer') - -const multicodec = require('../multicodec') -const proto = require('../protocol') - -class Dialer { - /** - * Creates an instance of Dialer. - * @param {Swarm} swarm - the swarm - * @param {any} options - config options - * - * @memberOf Dialer - */ - constructor (swarm, options) { - this.swarm = swarm - this.relayPeers = new Map() - this.relayConns = new Map() - this.options = options - this.utils = utilsFactory(swarm) - } - - /** - * Helper that returns a relay connection - * - * @param {*} relay - * @param {*} callback - * @returns {Function} - callback - */ - _dialRelayHelper (relay, callback) { - if (this.relayConns.has(relay.id.toB58String())) { - return callback(null, this.relayConns.get(relay.id.toB58String())) - } - - return this._dialRelay(relay, callback) - } - - /** - * Dial a peer over a relay - * - * @param {multiaddr} ma - the multiaddr of the peer to dial - * @param {Function} cb - a callback called once dialed - * @returns {Connection} - the connection - * - */ - dial (ma, cb) { - cb = cb || (() => { }) - const strMa = ma.toString() - if (!strMa.includes('/p2p-circuit')) { - log.err('invalid circuit address') - return cb(new Error('invalid circuit address')) - } - - const addr = strMa.split('p2p-circuit') // extract relay address if any - const relay = addr[0] === '/' ? null : multiaddr(addr[0]) - const peer = multiaddr(addr[1] || addr[0]) - - const dstConn = new Connection() - setImmediate( - this._dialPeer.bind(this), - peer, - relay, - (err, conn) => { - if (err) { - log.err(err) - return cb(err) - } - - dstConn.setInnerConn(conn) - cb(null, dstConn) - }) - - return dstConn - } - - /** - * Does the peer support the HOP protocol - * - * @param {PeerInfo} peer - * @param {Function} callback - * @returns {void} - */ - canHop (peer, callback) { - callback = once(callback || (() => { })) - - this._dialRelayHelper(peer, (err, conn) => { - if (err) { - return callback(err) - } - - const sh = new StreamHandler(conn) - waterfall([ - (cb) => sh.write(proto.CircuitRelay.encode({ - type: proto.CircuitRelay.Type.CAN_HOP - }), cb), - (cb) => sh.read(cb) - ], (err, msg) => { - if (err) { - return callback(err) - } - const response = proto.CircuitRelay.decode(msg) - - if (response.code !== proto.CircuitRelay.Status.SUCCESS) { - const err = new Error(`HOP not supported, skipping - ${this.utils.getB58String(peer)}`) - log(err) - return callback(err) - } - - log('HOP supported adding as relay - %s', this.utils.getB58String(peer)) - this.relayPeers.set(this.utils.getB58String(peer), peer) - sh.close() - callback() - }) - }) - } - - /** - * Dial the destination peer over a relay - * - * @param {multiaddr} dstMa - * @param {Connection|PeerInfo} relay - * @param {Function} cb - * @return {Function|void} - * @private - */ - _dialPeer (dstMa, relay, cb) { - if (typeof relay === 'function') { - cb = relay - relay = null - } - - if (!cb) { - cb = () => {} - } - - dstMa = multiaddr(dstMa) - // if no relay provided, dial on all available relays until one succeeds - if (!relay) { - const relays = Array.from(this.relayPeers.values()) - const next = (nextRelay) => { - if (!nextRelay) { - const err = 'no relay peers were found or all relays failed to dial' - log.err(err) - return cb(err) - } - - return this._negotiateRelay( - nextRelay, - dstMa, - (err, conn) => { - if (err) { - log.err(err) - return next(relays.shift()) - } - cb(null, conn) - }) - } - next(relays.shift()) - } else { - return this._negotiateRelay( - relay, - dstMa, - (err, conn) => { - if (err) { - log.err('An error has occurred negotiating the relay connection', err) - return cb(err) - } - - return cb(null, conn) - }) - } - } - - /** - * Negotiate the relay connection - * - * @param {Multiaddr|PeerInfo|Connection} relay - the Connection or PeerInfo of the relay - * @param {multiaddr} dstMa - the multiaddr of the peer to relay the connection for - * @param {Function} callback - a callback which gets the negotiated relay connection - * @returns {void} - * @private - * - * @memberOf Dialer - */ - _negotiateRelay (relay, dstMa, callback) { - dstMa = multiaddr(dstMa) - relay = this.utils.peerInfoFromMa(relay) - const srcMas = this.swarm._peerInfo.multiaddrs.toArray() - this._dialRelayHelper(relay, (err, conn) => { - if (err) { - log.err(err) - return callback(err) - } - const sh = new StreamHandler(conn) - waterfall([ - (cb) => { - log('negotiating relay for peer %s', dstMa.getPeerId()) - let dstPeerId - try { - dstPeerId = PeerId.createFromB58String(dstMa.getPeerId()).id - } catch (err) { - return cb(err) - } - sh.write( - proto.CircuitRelay.encode({ - type: proto.CircuitRelay.Type.HOP, - srcPeer: { - id: this.swarm._peerInfo.id.id, - addrs: srcMas.map((addr) => addr.buffer) - }, - dstPeer: { - id: dstPeerId, - addrs: [dstMa.buffer] - } - }), cb) - }, - (cb) => sh.read(cb) - ], (err, msg) => { - if (err) { - return callback(err) - } - const message = proto.CircuitRelay.decode(msg) - if (message.type !== proto.CircuitRelay.Type.STATUS) { - return callback(new Error('Got invalid message type - ' + - `expected ${proto.CircuitRelay.Type.STATUS} got ${message.type}`)) - } - - if (message.code !== proto.CircuitRelay.Status.SUCCESS) { - return callback(new Error(`Got ${message.code} error code trying to dial over relay`)) - } - - callback(null, new Connection(sh.rest())) - }) - }) - } - - /** - * Dial a relay peer by its PeerInfo - * - * @param {PeerInfo} peer - the PeerInfo of the relay peer - * @param {Function} cb - a callback with the connection to the relay peer - * @returns {void} - * @private - */ - _dialRelay (peer, cb) { - cb = once(cb || (() => { })) - - this.swarm.dial( - peer, - multicodec.relay, - once((err, conn) => { - if (err) { - log.err(err) - return cb(err) - } - cb(null, conn) - })) - } -} - -module.exports = Dialer diff --git a/src/circuit/circuit/hop.js b/src/circuit/circuit/hop.js index 33448655f5..0db27c0d53 100644 --- a/src/circuit/circuit/hop.js +++ b/src/circuit/circuit/hop.js @@ -1,283 +1,136 @@ 'use strict' -const pull = require('pull-stream/pull') const debug = require('debug') const PeerInfo = require('peer-info') const PeerId = require('peer-id') -const EE = require('events').EventEmitter -const once = require('once') -const utilsFactory = require('./utils') +const { validateAddrs } = require('./utils') const StreamHandler = require('./stream-handler') -const proto = require('../protocol').CircuitRelay -const multiaddr = require('multiaddr') -const series = require('async/series') -const waterfall = require('async/waterfall') -const setImmediate = require('async/setImmediate') +const { CircuitRelay: CircuitPB } = require('../protocol') +const pipe = require('it-pipe') +const errCode = require('err-code') +const { codes: Errors } = require('../../errors') -const multicodec = require('./../multicodec') +const { stop } = require('./stop') -const log = debug('libp2p:circuit:relay') -log.err = debug('libp2p:circuit:error:relay') +const multicodec = require('./../multicodec') -class Hop extends EE { - /** - * Construct a Circuit object - * - * This class will handle incoming circuit connections and - * either start a relay or hand the relayed connection to - * the swarm - * - * @param {Swarm} swarm - * @param {Object} options - */ - constructor (swarm, options) { - super() - this.swarm = swarm - this.peerInfo = this.swarm._peerInfo - this.utils = utilsFactory(swarm) - this.config = options || { active: false, enabled: false } - this.active = this.config.active +const log = debug('libp2p:circuit:hop') +log.error = debug('libp2p:circuit:hop:error') + +module.exports.handleHop = async function handleHop ({ + connection, + request, + streamHandler, + circuit +}) { + // Ensure hop is enabled + if (!circuit._options.hop.enabled) { + log('HOP request received but we are not acting as a relay') + return streamHandler.end({ + type: CircuitPB.Type.STATUS, + code: CircuitPB.Status.HOP_CANT_SPEAK_RELAY + }) } - /** - * Handle the relay message - * - * @param {CircuitRelay} message - * @param {StreamHandler} sh - * @returns {*} - */ - handle (message, sh) { - if (!this.config.enabled) { - this.utils.writeResponse( - sh, - proto.Status.HOP_CANT_SPEAK_RELAY) - return sh.close() - } - - // check if message is `CAN_HOP` - if (message.type === proto.Type.CAN_HOP) { - this.utils.writeResponse( - sh, - proto.Status.SUCCESS) - return sh.close() - } - - // This is a relay request - validate and create a circuit - let srcPeerId = null - let dstPeerId = null - try { - srcPeerId = PeerId.createFromBytes(message.srcPeer.id).toB58String() - dstPeerId = PeerId.createFromBytes(message.dstPeer.id).toB58String() - } catch (err) { - log.err(err) - - if (!srcPeerId) { - this.utils.writeResponse( - sh, - proto.Status.HOP_SRC_MULTIADDR_INVALID) - return sh.close() - } - - if (!dstPeerId) { - this.utils.writeResponse( - sh, - proto.Status.HOP_DST_MULTIADDR_INVALID) - return sh.close() - } - } - - if (srcPeerId === dstPeerId) { - this.utils.writeResponse( - sh, - proto.Status.HOP_CANT_RELAY_TO_SELF) - return sh.close() - } - - if (!message.dstPeer.addrs.length) { - // TODO: use encapsulate here - const addr = multiaddr(`/p2p-circuit/ipfs/${dstPeerId}`).buffer - message.dstPeer.addrs.push(addr) - } - - log('trying to establish a circuit: %s <-> %s', srcPeerId, dstPeerId) - const noPeer = () => { - // log.err(err) - this.utils.writeResponse( - sh, - proto.Status.HOP_NO_CONN_TO_DST) - return sh.close() - } - - const isConnected = (cb) => { - let dstPeer - try { - dstPeer = this.swarm._peerBook.get(dstPeerId) - if (!dstPeer.isConnected() && !this.active) { - const err = new Error(`No Connection to peer ${dstPeerId}`) - noPeer(err) - return cb(err) - } - } catch (err) { - if (!this.active) { - noPeer(err) - return cb(err) - } - } - cb() - } - - series([ - (cb) => this.utils.validateAddrs(message, sh, proto.Type.HOP, cb), - (cb) => isConnected(cb), - (cb) => this._circuit(sh, message, cb) - ], (err) => { - if (err) { - log.err(err) - sh.close() - return setImmediate(() => this.emit('circuit:error', err)) - } - setImmediate(() => this.emit('circuit:success')) - }) + // Validate the HOP request has the required input + try { + validateAddrs(request, streamHandler) + } catch (err) { + return log.error('invalid hop request via peer %s', connection.remotePeer.toB58String(), err) } - /** - * Connect to STOP - * - * @param {PeerInfo} peer - * @param {StreamHandler} srcSh - * @param {function} callback - * @returns {void} - */ - _connectToStop (peer, srcSh, callback) { - this._dialPeer(peer, (err, dstConn) => { - if (err) { - this.utils.writeResponse( - srcSh, - proto.Status.HOP_CANT_DIAL_DST) - log.err(err) - return callback(err) - } + // Get the connection to the destination (stop) peer + const destinationPeer = new PeerId(request.dstPeer.id) - return this.utils.writeResponse( - srcSh, - proto.Status.SUCCESS, - (err) => { - if (err) { - log.err(err) - return callback(err) - } - return callback(null, dstConn) - }) + const destinationConnection = circuit._registrar.getConnection(new PeerInfo(destinationPeer)) + if (!destinationConnection && !circuit._options.hop.active) { + log('HOP request received but we are not connected to the destination peer') + return streamHandler.end({ + type: CircuitPB.Type.STATUS, + code: CircuitPB.Status.HOP_NO_CONN_TO_DST }) } - /** - * Negotiate STOP - * - * @param {StreamHandler} dstSh - * @param {StreamHandler} srcSh - * @param {CircuitRelay} message - * @param {function} callback - * @returns {void} - */ - _negotiateStop (dstSh, srcSh, message, callback) { - const stopMsg = Object.assign({}, message, { - type: proto.Type.STOP // change the message type - }) - dstSh.write(proto.encode(stopMsg), - (err) => { - if (err) { - this.utils.writeResponse( - srcSh, - proto.Status.HOP_CANT_OPEN_DST_STREAM) - log.err(err) - return callback(err) - } - - // read response from STOP - dstSh.read((err, msg) => { - if (err) { - log.err(err) - return callback(err) - } + // TODO: Handle being an active relay - const message = proto.decode(msg) - if (message.code !== proto.Status.SUCCESS) { - return callback(new Error('Unable to create circuit!')) - } - - return callback(null, msg) - }) - }) + // Handle the incoming HOP request by performing a STOP request + const stopRequest = { + type: CircuitPB.Type.STOP, + dstPeer: request.dstPeer, + srcPeer: request.srcPeer } - /** - * Attempt to make a circuit from A <-> R <-> B where R is this relay - * - * @param {StreamHandler} srcSh - the source stream handler - * @param {CircuitRelay} message - the message with the src and dst entries - * @param {Function} callback - callback to signal success or failure - * @returns {void} - * @private - */ - _circuit (srcSh, message, callback) { - let dstSh = null - waterfall([ - (cb) => this._connectToStop(message.dstPeer, srcSh, cb), - (_dstConn, cb) => { - dstSh = new StreamHandler(_dstConn) - this._negotiateStop(dstSh, srcSh, message, cb) - } - ], (err) => { - if (err) { - // close/end the source stream if there was an error - if (srcSh) { - srcSh.close() - } - - if (dstSh) { - dstSh.close() - } - return callback(err) - } - - const src = srcSh.rest() - const dst = dstSh.rest() - - const srcIdStr = PeerId.createFromBytes(message.srcPeer.id).toB58String() - const dstIdStr = PeerId.createFromBytes(message.dstPeer.id).toB58String() - - // circuit the src and dst streams - pull( - src, - dst, - src - ) - log('circuit %s <-> %s established', srcIdStr, dstIdStr) - callback() + let destinationStream + try { + destinationStream = await stop({ + connection: destinationConnection, + request: stopRequest, + circuit }) + } catch (err) { + return log.error(err) } - /** - * Dial the dest peer and create a circuit - * - * @param {Multiaddr} dstPeer - * @param {Function} callback - * @returns {void} - * @private - */ - _dialPeer (dstPeer, callback) { - const peerInfo = new PeerInfo(PeerId.createFromBytes(dstPeer.id)) - dstPeer.addrs.forEach((a) => peerInfo.multiaddrs.add(a)) - this.swarm.dial(peerInfo, multicodec.relay, once((err, conn) => { - if (err) { - log.err(err) - return callback(err) - } + log('hop request from %s is valid', connection.remotePeer.toB58String()) + streamHandler.write({ + type: CircuitPB.Type.STATUS, + code: CircuitPB.Status.SUCCESS + }) + const sourceStream = streamHandler.rest() + + // Short circuit the two streams to create the relayed connection + return pipe( + sourceStream, + destinationStream, + sourceStream + ) +} - callback(null, conn) - })) +/** + * Performs a HOP request to a relay peer, to request a connection to another + * peer. A new, virtual, connection will be created between the two via the relay. + * + * @param {object} options + * @param {Connection} options.connection Connection to the relay + * @param {*} options.request + * @param {Circuit} options.circuit + * @returns {Promise} + */ +module.exports.hop = async function hop ({ + connection, + request +}) { + // Create a new stream to the relay + const { stream } = await connection.newStream([multicodec.relay]) + // Send the HOP request + const streamHandler = new StreamHandler({ stream }) + streamHandler.write(request) + + const response = await streamHandler.read() + + if (response.code === CircuitPB.Status.SUCCESS) { + log('hop request was successful') + return streamHandler.rest() } + + log('hop request failed with code %d, closing stream', response.code) + streamHandler.close() + throw errCode(new Error(`HOP request failed with code ${response.code}`), Errors.ERR_HOP_REQUEST_FAILED) } -module.exports = Hop +/** + * Creates an unencoded CAN_HOP response based on the Circuits configuration + * @private + */ +module.exports.handleCanHop = function handleCanHop ({ + connection, + streamHandler, + circuit +}) { + const canHop = circuit._options.hop.enabled + log('can hop (%s) request from %s', canHop, connection.remotePeer.toB58String()) + streamHandler.end({ + type: CircuitPB.Type.STATUS, + code: canHop ? CircuitPB.Status.SUCCESS : CircuitPB.Status.HOP_CANT_SPEAK_RELAY + }) +} diff --git a/src/circuit/circuit/stop.js b/src/circuit/circuit/stop.js index e18ef028f8..ef4ca3d1dd 100644 --- a/src/circuit/circuit/stop.js +++ b/src/circuit/circuit/stop.js @@ -1,56 +1,69 @@ 'use strict' -const setImmediate = require('async/setImmediate') - -const EE = require('events').EventEmitter -const { Connection } = require('libp2p-interfaces/src/connection') -const utilsFactory = require('./utils') -const PeerInfo = require('peer-info') -const proto = require('../protocol').CircuitRelay -const series = require('async/series') +const { CircuitRelay: CircuitPB } = require('../protocol') +const multicodec = require('../multicodec') +const StreamHandler = require('./stream-handler') +const { validateAddrs } = require('./utils') const debug = require('debug') - const log = debug('libp2p:circuit:stop') -log.err = debug('libp2p:circuit:error:stop') +log.error = debug('libp2p:circuit:stop:error') -class Stop extends EE { - constructor (swarm) { - super() - this.swarm = swarm - this.utils = utilsFactory(swarm) +/** + * Handles incoming STOP requests + * + * @private + * @param {*} options + * @param {Connection} options.connection + * @param {*} options.request The CircuitRelay protobuf request (unencoded) + * @param {StreamHandler} options.streamHandler + * @returns {Promise<*>} Resolves a duplex iterable + */ +module.exports.handleStop = function handleStop ({ + connection, + request, + streamHandler +}) { + // Validate the STOP request has the required input + try { + validateAddrs(request, streamHandler) + } catch (err) { + return log.error('invalid stop request via peer %s', connection.remotePeer.toB58String(), err) } - /** - * Handle the incoming STOP message - * - * @param {{}} msg - the parsed protobuf message - * @param {StreamHandler} sh - the stream handler wrapped connection - * @param {Function} callback - callback - * @returns {undefined} - */ - handle (msg, sh, callback) { - callback = callback || (() => {}) - - series([ - (cb) => this.utils.validateAddrs(msg, sh, proto.Type.STOP, cb), - (cb) => this.utils.writeResponse(sh, proto.Status.Success, cb) - ], (err) => { - if (err) { - // we don't return the error here, - // since multistream select don't expect one - callback() - return log(err) - } - - const peerInfo = new PeerInfo(this.utils.peerIdFromId(msg.srcPeer.id)) - msg.srcPeer.addrs.forEach((addr) => peerInfo.multiaddrs.add(addr)) - const newConn = new Connection(sh.rest()) - newConn.setPeerInfo(peerInfo) - setImmediate(() => this.emit('connection', newConn)) - callback(newConn) - }) - } + // The request is valid + log('stop request is valid') + streamHandler.write({ + type: CircuitPB.Type.STATUS, + code: CircuitPB.Status.SUCCESS + }) + return streamHandler.rest() } -module.exports = Stop +/** + * Creates a STOP request + * @private + * @param {*} options + * @param {Connection} options.connection + * @param {*} options.request The CircuitRelay protobuf request (unencoded) + * @returns {Promise<*>} Resolves a duplex iterable + */ +module.exports.stop = async function stop ({ + connection, + request +}) { + const { stream } = await connection.newStream([multicodec.relay]) + log('starting stop request to %s', connection.remotePeer.toB58String()) + const streamHandler = new StreamHandler({ stream }) + + streamHandler.write(request) + const response = await streamHandler.read() + + if (response.code === CircuitPB.Status.SUCCESS) { + log('stop request to %s was successful', connection.remotePeer.toB58String()) + return streamHandler.rest() + } + + log('stop request failed with code %d', response.code) + streamHandler.close() +} diff --git a/src/circuit/circuit/stream-handler.js b/src/circuit/circuit/stream-handler.js index 0d0081712b..5fd8b6389a 100644 --- a/src/circuit/circuit/stream-handler.js +++ b/src/circuit/circuit/stream-handler.js @@ -1,139 +1,79 @@ 'use strict' -const values = require('pull-stream/sources/values') -const collect = require('pull-stream/sinks/collect') -const empty = require('pull-stream/sources/empty') -const pull = require('pull-stream/pull') -const lp = require('pull-length-prefixed') -const handshake = require('pull-handshake') +const lp = require('it-length-prefixed') +const handshake = require('it-handshake') +const { CircuitRelay: CircuitPB } = require('../protocol') const debug = require('debug') const log = debug('libp2p:circuit:stream-handler') -log.err = debug('libp2p:circuit:error:stream-handler') +log.error = debug('libp2p:circuit:stream-handler:error') class StreamHandler { /** * Create a stream handler for connection * - * @param {Connection} conn - connection to read/write - * @param {Function|undefined} cb - handshake callback called on error - * @param {Number} timeout - handshake timeout - * @param {Number} maxLength - max bytes length of message + * @param {object} options + * @param {*} options.stream - A duplex iterable + * @param {Number} options.maxLength - max bytes length of message */ - constructor (conn, cb, timeout, maxLength) { - this.conn = conn - this.stream = null - this.shake = null - this.timeout = cb || 1000 * 60 - this.maxLength = maxLength || 4096 + constructor ({ stream, maxLength = 4096 }) { + this.stream = stream - if (typeof cb === 'function') { - this.timeout = timeout || 1000 * 60 - } - - this.stream = handshake({ timeout: this.timeout }, cb) - this.shake = this.stream.handshake - - pull(this.stream, conn, this.stream) - } - - isValid () { - return this.conn && this.shake && this.stream + this.shake = handshake(this.stream) + this.decoder = lp.decode.fromReader(this.shake.reader, { maxDataLength: maxLength }) } /** * Read and decode message - * - * @param {Function} cb - * @returns {void|Function} + * @async + * @returns {void} */ - read (cb) { - if (!this.isValid()) { - return cb(new Error('handler is not in a valid state')) + async read () { + const msg = await this.decoder.next() + if (msg.value) { + const value = CircuitPB.decode(msg.value.slice()) + log('read message type', value.type) + return value } - lp.decodeFromReader( - this.shake, - { maxLength: this.maxLength }, - (err, msg) => { - if (err) { - log.err(err) - // this.shake.abort(err) - return cb(err) - } - - return cb(null, msg) - }) + log('read received no value, closing stream') + // End the stream, we didn't get data + this.close() } /** * Encode and write array of buffers * - * @param {Buffer[]} msg - * @param {Function} [cb] - * @returns {Function} + * @param {*} msg An unencoded CircuitRelay protobuf message */ - write (msg, cb) { - cb = cb || (() => {}) - - if (!this.isValid()) { - return cb(new Error('handler is not in a valid state')) - } - - pull( - values([msg]), - lp.encode(), - collect((err, encoded) => { - if (err) { - log.err(err) - this.shake.abort(err) - return cb(err) - } - - encoded.forEach((e) => this.shake.write(e)) - cb() - }) - ) - } - - /** - * Get the raw Connection - * - * @returns {null|Connection|*} - */ - getRawConn () { - return this.conn + write (msg) { + log('write message type %s', msg.type) + this.shake.write(lp.encode.single(CircuitPB.encode(msg))) } /** * Return the handshake rest stream and invalidate handler * - * @return {*|{source, sink}} + * @return {*} A duplex iterable */ rest () { - const rest = this.shake.rest() + this.shake.rest() + return this.shake.stream + } - this.conn = null - this.stream = null - this.shake = null - return rest + end (msg) { + this.write(msg) + this.close() } /** * Close the stream * - * @returns {undefined} + * @returns {void} */ close () { - if (!this.isValid()) { - return - } - - // close stream - pull( - empty(), - this.rest() - ) + log('closing the stream') + this.rest().sink([]) } } diff --git a/src/circuit/circuit/utils.js b/src/circuit/circuit/utils.js index c3aa87a5dc..8e87851d21 100644 --- a/src/circuit/circuit/utils.js +++ b/src/circuit/circuit/utils.js @@ -1,118 +1,51 @@ 'use strict' const multiaddr = require('multiaddr') -const PeerInfo = require('peer-info') -const PeerId = require('peer-id') -const proto = require('../protocol') -const { getPeerInfo } = require('../../get-peer-info') - -module.exports = function (swarm) { - /** - * Get b58 string from multiaddr or peerinfo - * - * @param {Multiaddr|PeerInfo} peer - * @return {*} - */ - function getB58String (peer) { - let b58Id = null - if (multiaddr.isMultiaddr(peer)) { - const relayMa = multiaddr(peer) - b58Id = relayMa.getPeerId() - } else if (PeerInfo.isPeerInfo(peer)) { - b58Id = peer.id.toB58String() - } - - return b58Id - } - - /** - * Helper to make a peer info from a multiaddrs - * - * @param {Multiaddr|PeerInfo|PeerId} peer - * @return {PeerInfo} - * @private - */ - function peerInfoFromMa (peer) { - return getPeerInfo(peer, swarm._peerBook) - } - - /** - * Checks if peer has an existing connection - * - * @param {String} peerId - * @param {Swarm} swarm - * @return {Boolean} - */ - function isPeerConnected (peerId) { - return swarm.muxedConns[peerId] || swarm.conns[peerId] - } - - /** - * Write a response - * - * @param {StreamHandler} streamHandler - * @param {CircuitRelay.Status} status - * @param {Function} cb - * @returns {*} - */ - function writeResponse (streamHandler, status, cb) { - cb = cb || (() => {}) - streamHandler.write(proto.CircuitRelay.encode({ - type: proto.CircuitRelay.Type.STATUS, - code: status - })) - return cb() - } - - /** - * Validate incomming HOP/STOP message - * - * @param {CircuitRelay} msg - * @param {StreamHandler} streamHandler - * @param {CircuitRelay.Type} type - * @returns {*} - * @param {Function} cb - */ - function validateAddrs (msg, streamHandler, type, cb) { - try { - msg.dstPeer.addrs.forEach((addr) => { - return multiaddr(addr) - }) - } catch (err) { - writeResponse(streamHandler, type === proto.CircuitRelay.Type.HOP - ? proto.CircuitRelay.Status.HOP_DST_MULTIADDR_INVALID - : proto.CircuitRelay.Status.STOP_DST_MULTIADDR_INVALID) - return cb(err) - } - - try { - msg.srcPeer.addrs.forEach((addr) => { - return multiaddr(addr) - }) - } catch (err) { - writeResponse(streamHandler, type === proto.CircuitRelay.Type.HOP - ? proto.CircuitRelay.Status.HOP_SRC_MULTIADDR_INVALID - : proto.CircuitRelay.Status.STOP_SRC_MULTIADDR_INVALID) - return cb(err) - } +const { CircuitRelay } = require('../protocol') + +/** + * Write a response + * + * @param {StreamHandler} streamHandler + * @param {CircuitRelay.Status} status + */ +function writeResponse (streamHandler, status) { + streamHandler.write({ + type: CircuitRelay.Type.STATUS, + code: status + }) +} - return cb(null) +/** + * Validate incomming HOP/STOP message + * + * @param {*} msg A CircuitRelay unencoded protobuf message + * @param {StreamHandler} streamHandler + */ +function validateAddrs (msg, streamHandler) { + try { + msg.dstPeer.addrs.forEach((addr) => { + return multiaddr(addr) + }) + } catch (err) { + writeResponse(streamHandler, msg.type === CircuitRelay.Type.HOP + ? CircuitRelay.Status.HOP_DST_MULTIADDR_INVALID + : CircuitRelay.Status.STOP_DST_MULTIADDR_INVALID) + throw err } - function peerIdFromId (id) { - if (typeof id === 'string') { - return PeerId.createFromB58String(id) - } - - return PeerId.createFromBytes(id) + try { + msg.srcPeer.addrs.forEach((addr) => { + return multiaddr(addr) + }) + } catch (err) { + writeResponse(streamHandler, msg.type === CircuitRelay.Type.HOP + ? CircuitRelay.Status.HOP_SRC_MULTIADDR_INVALID + : CircuitRelay.Status.STOP_SRC_MULTIADDR_INVALID) + throw err } +} - return { - getB58String, - peerInfoFromMa, - isPeerConnected, - validateAddrs, - writeResponse, - peerIdFromId - } +module.exports = { + validateAddrs } diff --git a/src/circuit/index.js b/src/circuit/index.js index e4c7fed7d7..d5d293bfef 100644 --- a/src/circuit/index.js +++ b/src/circuit/index.js @@ -1,3 +1,186 @@ 'use strict' -module.exports = require('./circuit') +const mafmt = require('mafmt') +const multiaddr = require('multiaddr') +const PeerId = require('peer-id') +const PeerInfo = require('peer-info') +const withIs = require('class-is') +const { CircuitRelay: CircuitPB } = require('./protocol') + +const debug = require('debug') +const log = debug('libp2p:circuit') +log.error = debug('libp2p:circuit:error') + +const { relay: multicodec } = require('./multicodec') +const createListener = require('./listener') +const { handleCanHop, handleHop, hop } = require('./circuit/hop') +const { handleStop } = require('./circuit/stop') +const StreamHandler = require('./circuit/stream-handler') +const toConnection = require('./stream-to-conn') + +class Circuit { + /** + * Creates an instance of Circuit. + * + * @constructor + * @param {object} options + * @param {Libp2p} options.libp2p + * @param {Upgrader} options.upgrader + */ + constructor ({ libp2p, upgrader }) { + this._dialer = libp2p.dialer + this._registrar = libp2p.registrar + this._upgrader = upgrader + this._options = libp2p._config.relay + this.peerInfo = libp2p.peerInfo + this._registrar.handle(multicodec, this._onProtocol.bind(this)) + } + + async _onProtocol ({ connection, stream, protocol }) { + const streamHandler = new StreamHandler({ stream }) + const request = await streamHandler.read() + const circuit = this + let virtualConnection + + switch (request.type) { + case CircuitPB.Type.CAN_HOP: { + log('received CAN_HOP request from %s', connection.remotePeer.toB58String()) + await handleCanHop({ circuit, connection, streamHandler }) + break + } + case CircuitPB.Type.HOP: { + log('received HOP request from %s', connection.remotePeer.toB58String()) + virtualConnection = await handleHop({ + connection, + request, + streamHandler, + circuit + }) + break + } + case CircuitPB.Type.STOP: { + log('received STOP request from %s', connection.remotePeer.toB58String()) + virtualConnection = await handleStop({ + connection, + request, + streamHandler, + circuit + }) + break + } + default: { + log('Request of type %s not supported', request.type) + } + } + + if (virtualConnection) { + const remoteAddr = multiaddr(request.dstPeer.addrs[0]) + const localAddr = multiaddr(request.srcPeer.addrs[0]) + const maConn = toConnection({ + stream: virtualConnection, + remoteAddr, + localAddr + }) + const type = CircuitPB.Type === CircuitPB.Type.HOP ? 'relay' : 'inbound' + log('new %s connection %s', type, maConn.remoteAddr) + + const conn = await this._upgrader.upgradeInbound(maConn) + log('%s connection %s upgraded', type, maConn.remoteAddr) + this.handler && this.handler(conn) + } + } + + /** + * Dial a peer over a relay + * + * @param {multiaddr} ma - the multiaddr of the peer to dial + * @param {Object} options - dial options + * @param {AbortSignal} [options.signal] - An optional abort signal + * @returns {Connection} - the connection + */ + async dial (ma, options) { + // Check the multiaddr to see if it contains a relay and a destination peer + const addrs = ma.toString().split('/p2p-circuit') + const relayAddr = multiaddr(addrs[0]) + const destinationAddr = multiaddr(addrs[addrs.length - 1]) + const relayPeer = PeerId.createFromCID(relayAddr.getPeerId()) + const destinationPeer = PeerId.createFromCID(destinationAddr.getPeerId()) + + let disconnectOnFailure = false + let relayConnection = this._registrar.getConnection(new PeerInfo(relayPeer)) + if (!relayConnection) { + relayConnection = await this._dialer.connectToMultiaddr(relayAddr, options) + disconnectOnFailure = true + } + + try { + const virtualConnection = await hop({ + connection: relayConnection, + circuit: this, + request: { + type: CircuitPB.Type.HOP, + srcPeer: { + id: this.peerInfo.id.toBytes(), + addrs: this.peerInfo.multiaddrs.toArray().map(addr => addr.buffer) + }, + dstPeer: { + id: destinationPeer.toBytes(), + addrs: [multiaddr(destinationAddr).buffer] + } + } + }) + + const localAddr = relayAddr.encapsulate(`/p2p-circuit/p2p/${this.peerInfo.id.toB58String()}`) + const maConn = toConnection({ + stream: virtualConnection, + remoteAddr: ma, + localAddr + }) + log('new outbound connection %s', maConn.remoteAddr) + + return this._upgrader.upgradeOutbound(maConn) + } catch (err) { + log.error('Circuit relay dial failed', err) + disconnectOnFailure && await relayConnection.close() + throw err + } + } + + /** + * Create a listener + * + * @param {any} options + * @param {Function} handler + * @return {listener} + */ + createListener (options, handler) { + if (typeof options === 'function') { + handler = options + options = {} + } + + // Called on successful HOP and STOP requests + this.handler = handler + + return createListener(this, options) + } + + /** + * Filter check for all Multiaddrs that this transport can dial on + * + * @param {Array} multiaddrs + * @returns {Array} + */ + filter (multiaddrs) { + multiaddrs = Array.isArray(multiaddrs) ? multiaddrs : [multiaddrs] + + return multiaddrs.filter((ma) => { + return mafmt.Circuit.matches(ma) + }) + } +} + +/** + * @type {Circuit} + */ +module.exports = withIs(Circuit, { className: 'Circuit', symbolName: '@libp2p/js-libp2p-circuit/circuit' }) diff --git a/src/circuit/listener.js b/src/circuit/listener.js index ff55056e8d..2569c50039 100644 --- a/src/circuit/listener.js +++ b/src/circuit/listener.js @@ -1,94 +1,42 @@ 'use strict' -const setImmediate = require('async/setImmediate') - -const multicodec = require('./multicodec') -const EE = require('events').EventEmitter +const EventEmitter = require('events') const multiaddr = require('multiaddr') -const mafmt = require('mafmt') -const Stop = require('./circuit/stop') -const Hop = require('./circuit/hop') -const proto = require('./protocol') -const utilsFactory = require('./circuit/utils') - -const StreamHandler = require('./circuit/stream-handler') const debug = require('debug') - const log = debug('libp2p:circuit:listener') log.err = debug('libp2p:circuit:error:listener') -module.exports = (swarm, options, connHandler) => { - const listener = new EE() - const utils = utilsFactory(swarm) - - listener.stopHandler = new Stop(swarm) - listener.stopHandler.on('connection', (conn) => listener.emit('connection', conn)) - listener.hopHandler = new Hop(swarm, options.hop) +/** + * @param {*} circuit + * @returns {Listener} a transport listener + */ +module.exports = (circuit) => { + const listener = new EventEmitter() + const listeningAddrs = new Map() /** * Add swarm handler and listen for incoming connections * - * @param {Multiaddr} ma - * @param {Function} callback + * @param {Multiaddr} addr * @return {void} */ - listener.listen = (ma, callback) => { - callback = callback || (() => {}) - - swarm.handle(multicodec.relay, (_, conn) => { - const sh = new StreamHandler(conn) - - sh.read((err, msg) => { - if (err) { - log.err(err) - return - } - - let request = null - try { - request = proto.CircuitRelay.decode(msg) - } catch (err) { - return utils.writeResponse( - sh, - proto.CircuitRelay.Status.MALFORMED_MESSAGE) - } - - switch (request.type) { - case proto.CircuitRelay.Type.CAN_HOP: - case proto.CircuitRelay.Type.HOP: { - return listener.hopHandler.handle(request, sh) - } + listener.listen = async (addr) => { + const [addrString] = String(addr).split('/p2p-circuit').slice(-1) - case proto.CircuitRelay.Type.STOP: { - return listener.stopHandler.handle(request, sh, connHandler) - } + const relayConn = await circuit._dialer.connectToMultiaddr(multiaddr(addrString)) + const relayedAddr = relayConn.remoteAddr.encapsulate('/p2p-circuit') - default: { - utils.writeResponse( - sh, - proto.CircuitRelay.Status.INVALID_MSG_TYPE) - return sh.close() - } - } - }) - }) - - setImmediate(() => listener.emit('listen')) - callback() + listeningAddrs.set(relayConn.remotePeer.toB58String(), relayedAddr) + listener.emit('listening') } /** - * Remove swarm listener + * TODO: Remove the peers from our topology * - * @param {Function} cb * @return {void} */ - listener.close = (cb) => { - swarm.unhandle(multicodec.relay) - setImmediate(() => listener.emit('close')) - cb() - } + listener.close = () => {} /** * Get fixed up multiaddrs @@ -104,45 +52,14 @@ module.exports = (swarm, options, connHandler) => { * the encapsulated transport address. This is useful when for example, a peer should only * be dialed over TCP rather than any other transport * - * @param {Function} callback - * @return {void} + * @return {Multiaddr[]} */ - listener.getAddrs = (callback) => { - let addrs = swarm._peerInfo.multiaddrs.toArray() - - // get all the explicit relay addrs excluding self - const p2pAddrs = addrs.filter((addr) => { - return mafmt.Circuit.matches(addr) && - !addr.toString().includes(swarm._peerInfo.id.toB58String()) - }) - - // use the explicit relays instead of any relay - if (p2pAddrs.length) { - addrs = p2pAddrs + listener.getAddrs = () => { + const addrs = [] + for (const addr of listeningAddrs.values()) { + addrs.push(addr) } - - const listenAddrs = [] - addrs.forEach((addr) => { - const peerMa = `/p2p-circuit/ipfs/${swarm._peerInfo.id.toB58String()}` - if (addr.toString() === peerMa) { - listenAddrs.push(multiaddr(peerMa)) - return - } - - if (!mafmt.Circuit.matches(addr)) { - if (addr.getPeerId()) { - // by default we're reachable over any relay - listenAddrs.push(multiaddr('/p2p-circuit').encapsulate(addr)) - } else { - const ma = `${addr}/ipfs/${swarm._peerInfo.id.toB58String()}` - listenAddrs.push(multiaddr('/p2p-circuit').encapsulate(ma)) - } - } else { - listenAddrs.push(addr.encapsulate(`/ipfs/${swarm._peerInfo.id.toB58String()}`)) - } - }) - - callback(null, listenAddrs) + return addrs } return listener diff --git a/src/circuit/stream-to-conn.js b/src/circuit/stream-to-conn.js new file mode 100644 index 0000000000..d90407163e --- /dev/null +++ b/src/circuit/stream-to-conn.js @@ -0,0 +1,49 @@ +'use strict' + +const abortable = require('abortable-iterator') +const log = require('debug')('libp2p:circuit:stream') + +// Convert a duplex iterable into a MultiaddrConnection +// https://github.com/libp2p/interface-transport#multiaddrconnection +module.exports = ({ stream, remoteAddr, localAddr }, options = {}) => { + const { sink, source } = stream + const maConn = { + async sink (source) { + if (options.signal) { + source = abortable(source, options.signal) + } + + try { + await sink(source) + } catch (err) { + // If aborted we can safely ignore + if (err.type !== 'aborted') { + // If the source errored the socket will already have been destroyed by + // toIterable.duplex(). If the socket errored it will already be + // destroyed. There's nothing to do here except log the error & return. + log(err) + } + } + close() + }, + + source: options.signal ? abortable(source, options.signal) : source, + conn: stream, + localAddr, + remoteAddr, + timeline: { open: Date.now() }, + + close () { + sink([]) + close() + } + } + + function close () { + if (!maConn.timeline.close) { + maConn.timeline.close = Date.now() + } + } + + return maConn +} diff --git a/src/dialer.js b/src/dialer.js index 2d4071d13e..2154554fa6 100644 --- a/src/dialer.js +++ b/src/dialer.js @@ -90,7 +90,6 @@ class Dialer { nextTick(async () => { try { await this.identifyService.identify(conn, conn.remotePeer) - // TODO: Update the PeerStore with the information from identify } catch (err) { log.error(err) } diff --git a/src/errors.js b/src/errors.js index 9a4f6e75c1..4c26c941ba 100644 --- a/src/errors.js +++ b/src/errors.js @@ -16,6 +16,7 @@ exports.codes = { ERR_DISCOVERED_SELF: 'ERR_DISCOVERED_SELF', ERR_DUPLICATE_TRANSPORT: 'ERR_DUPLICATE_TRANSPORT', ERR_ENCRYPTION_FAILED: 'ERR_ENCRYPTION_FAILED', + ERR_HOP_REQUEST_FAILED: 'ERR_HOP_REQUEST_FAILED', ERR_INVALID_KEY: 'ERR_INVALID_KEY', ERR_INVALID_MESSAGE: 'ERR_INVALID_MESSAGE', ERR_INVALID_PEER: 'ERR_INVALID_PEER', diff --git a/src/index.js b/src/index.js index 8d88cec132..de655d184e 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,7 @@ const { getPeerInfo, getPeerInfoRemote } = require('./get-peer-info') const { validate: validateConfig } = require('./config') const { codes } = require('./errors') +const Circuit = require('./circuit') const Dialer = require('./dialer') const TransportManager = require('./transport-manager') const Upgrader = require('./upgrader') @@ -78,9 +79,6 @@ class Libp2p extends EventEmitter { libp2p: this, upgrader: this.upgrader }) - this._modules.transport.forEach((Transport) => { - this.transportManager.add(Transport.prototype[Symbol.toStringTag], Transport) - }) // Attach crypto channels if (this._modules.connEncryption) { @@ -95,6 +93,12 @@ class Libp2p extends EventEmitter { peerStore: this.peerStore }) + this._modules.transport.forEach((Transport) => { + this.transportManager.add(Transport.prototype[Symbol.toStringTag], Transport) + }) + // TODO: enable relay if enabled + this.transportManager.add(Circuit.prototype[Symbol.toStringTag], Circuit) + // Attach stream multiplexers if (this._modules.streamMuxer) { const muxers = this._modules.streamMuxer @@ -182,6 +186,7 @@ class Libp2p extends EventEmitter { this.pubsub && await this.pubsub.stop() this._dht && await this._dht.stop() await this.transportManager.close() + await this.registrar.close() } catch (err) { if (err) { log.error(err) @@ -282,7 +287,10 @@ class Libp2p extends EventEmitter { this.upgrader.protocols.set(protocol, handler) }) - this.dialer.identifyService.pushToPeerStore(this.peerStore) + // Only push if libp2p is running + if (this.isStarted()) { + this.dialer.identifyService.pushToPeerStore(this.peerStore) + } } /** @@ -296,7 +304,10 @@ class Libp2p extends EventEmitter { this.upgrader.protocols.delete(protocol) }) - this.dialer.identifyService.pushToPeerStore(this.peerStore) + // Only push if libp2p is running + if (this.isStarted()) { + this.dialer.identifyService.pushToPeerStore(this.peerStore) + } } async _onStarting () { diff --git a/src/registrar.js b/src/registrar.js index c4a116806b..666336ffe8 100644 --- a/src/registrar.js +++ b/src/registrar.js @@ -46,6 +46,23 @@ class Registrar { this._handle = handle } + /** + * Cleans up the registrar + * @async + */ + async close () { + // Close all connections we're tracking + const tasks = [] + for (const connectionList of this.connections.values()) { + for (const connection of connectionList) { + tasks.push(connection.close()) + } + } + + await tasks + this.connections.clear() + } + /** * Add a new connected peer to the record * TODO: this should live in the ConnectionManager @@ -100,8 +117,9 @@ class Registrar { getConnection (peerInfo) { assert(PeerInfo.isPeerInfo(peerInfo), 'peerInfo must be an instance of peer-info') + const connections = this.connections.get(peerInfo.id.toB58String()) // TODO: what should we return - return this.connections.get(peerInfo.id.toB58String())[0] + return connections ? connections[0] : null } /** diff --git a/src/transport-manager.js b/src/transport-manager.js index 722049a450..5e4a9201a7 100644 --- a/src/transport-manager.js +++ b/src/transport-manager.js @@ -64,7 +64,9 @@ class TransportManager { await Promise.all(tasks) log('all listeners closed') - this._listeners.clear() + for (const key of this._listeners.keys()) { + this._listeners.set(key, []) + } } /** @@ -82,6 +84,7 @@ class TransportManager { try { return await transport.dial(ma, options) } catch (err) { + if (err.code) throw err throw errCode(err, codes.ERR_TRANSPORT_DIAL_FAILED) } } diff --git a/test/dialing/direct.node.js b/test/dialing/direct.node.js index 04d63b40e4..8fbe5d7944 100644 --- a/test/dialing/direct.node.js +++ b/test/dialing/direct.node.js @@ -3,6 +3,7 @@ const chai = require('chai') chai.use(require('dirty-chai')) +chai.use(require('chai-as-promised')) const { expect } = chai const sinon = require('sinon') const Transport = require('libp2p-tcp') @@ -77,14 +78,9 @@ describe('Dialing (direct, TCP)', () => { it('should fail to connect to an unsupported multiaddr', async () => { const dialer = new Dialer({ transportManager: localTM }) - try { - await dialer.connectToMultiaddr(unsupportedAddr) - } catch (err) { - expect(err).to.satisfy((err) => err.code === ErrorCodes.ERR_TRANSPORT_UNAVAILABLE) - return - } - - expect.fail('Dial should have failed') + await expect(dialer.connectToMultiaddr(unsupportedAddr)) + .to.eventually.be.rejected() + .and.to.have.property('code', ErrorCodes.ERR_TRANSPORT_UNAVAILABLE) }) it('should be able to connect to a given peer info', async () => { @@ -121,14 +117,9 @@ describe('Dialing (direct, TCP)', () => { const peerInfo = new PeerInfo(peerId) peerInfo.multiaddrs.add(unsupportedAddr) - try { - await dialer.connectToPeer(peerInfo) - } catch (err) { - expect(err).to.satisfy((err) => err.code === ErrorCodes.ERR_CONNECTION_FAILED) - return - } - - expect.fail('Dial should have failed') + await expect(dialer.connectToPeer(peerInfo)) + .to.eventually.be.rejected() + .and.to.have.property('code', ErrorCodes.ERR_CONNECTION_FAILED) }) it('should abort dials on queue task timeout', async () => { @@ -144,14 +135,9 @@ describe('Dialing (direct, TCP)', () => { expect(options.signal.aborted).to.equal(true) }) - try { - await dialer.connectToMultiaddr(remoteAddr) - } catch (err) { - expect(err).to.satisfy((err) => err.code === ErrorCodes.ERR_TIMEOUT) - return - } - - expect.fail('Dial should have failed') + await expect(dialer.connectToMultiaddr(remoteAddr)) + .to.eventually.be.rejected() + .and.to.have.property('code', ErrorCodes.ERR_TIMEOUT) }) it('should dial to the max concurrency', async () => { diff --git a/test/dialing/direct.spec.js b/test/dialing/direct.spec.js index eb1b158b5e..f5eaff9aa8 100644 --- a/test/dialing/direct.spec.js +++ b/test/dialing/direct.spec.js @@ -3,6 +3,7 @@ const chai = require('chai') chai.use(require('dirty-chai')) +chai.use(require('chai-as-promised')) const { expect } = chai const sinon = require('sinon') const pDefer = require('p-defer') @@ -67,14 +68,9 @@ describe('Dialing (direct, WebSockets)', () => { it('should fail to connect to an unsupported multiaddr', async () => { const dialer = new Dialer({ transportManager: localTM }) - try { - await dialer.connectToMultiaddr(unsupportedAddr) - } catch (err) { - expect(err).to.satisfy((err) => err.code === ErrorCodes.ERR_TRANSPORT_DIAL_FAILED) - return - } - - expect.fail('Dial should have failed') + await expect(dialer.connectToMultiaddr(unsupportedAddr)) + .to.eventually.be.rejected() + .and.to.have.property('code', ErrorCodes.ERR_TRANSPORT_DIAL_FAILED) }) it('should be able to connect to a given peer', async () => { @@ -94,14 +90,9 @@ describe('Dialing (direct, WebSockets)', () => { const peerInfo = new PeerInfo(peerId) peerInfo.multiaddrs.add(unsupportedAddr) - try { - await dialer.connectToPeer(peerInfo) - } catch (err) { - expect(err).to.satisfy((err) => err.code === ErrorCodes.ERR_CONNECTION_FAILED) - return - } - - expect.fail('Dial should have failed') + await expect(dialer.connectToPeer(peerInfo)) + .to.eventually.be.rejected() + .and.to.have.property('code', ErrorCodes.ERR_CONNECTION_FAILED) }) it('should abort dials on queue task timeout', async () => { @@ -117,14 +108,9 @@ describe('Dialing (direct, WebSockets)', () => { expect(options.signal.aborted).to.equal(true) }) - try { - await dialer.connectToMultiaddr(remoteAddr) - } catch (err) { - expect(err).to.satisfy((err) => err.code === ErrorCodes.ERR_TIMEOUT) - return - } - - expect.fail('Dial should have failed') + await expect(dialer.connectToMultiaddr(remoteAddr)) + .to.eventually.be.rejected() + .and.to.have.property('code', ErrorCodes.ERR_TIMEOUT) }) it('should dial to the max concurrency', async () => { diff --git a/test/dialing/relay.node.js b/test/dialing/relay.node.js new file mode 100644 index 0000000000..597bfee183 --- /dev/null +++ b/test/dialing/relay.node.js @@ -0,0 +1,162 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +chai.use(require('chai-as-promised')) +const { expect } = chai +const sinon = require('sinon') + +const multiaddr = require('multiaddr') +const { collect } = require('streaming-iterables') +const pipe = require('it-pipe') +const { createPeerInfoFromFixture } = require('../utils/creators/peer') +const baseOptions = require('../utils/base-options') +const Libp2p = require('../../src') +const { codes: Errors } = require('../../src/errors') + +describe('Dialing (via relay, TCP)', () => { + let srcLibp2p + let relayLibp2p + let dstLibp2p + + before(async () => { + const peerInfos = await createPeerInfoFromFixture(3) + // Create 3 nodes, and turn HOP on for the relay + ;[srcLibp2p, relayLibp2p, dstLibp2p] = peerInfos.map((peerInfo, index) => { + const opts = baseOptions + index === 1 && (opts.config.relay.hop.enabled = true) + return new Libp2p({ + ...opts, + peerInfo + }) + }) + + dstLibp2p.handle('/echo/1.0.0', ({ stream }) => pipe(stream, stream)) + }) + + beforeEach(() => { + // Start each node + return Promise.all([srcLibp2p, relayLibp2p, dstLibp2p].map(libp2p => { + // Reset multiaddrs and start + libp2p.peerInfo.multiaddrs.clear() + libp2p.peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') + libp2p.start() + })) + }) + + afterEach(() => { + // Stop each node + return Promise.all([srcLibp2p, relayLibp2p, dstLibp2p].map(libp2p => libp2p.stop())) + }) + + it('should be able to connect to a peer over a relay with active connections', async () => { + const relayAddr = relayLibp2p.transportManager.getAddrs()[0] + const relayIdString = relayLibp2p.peerInfo.id.toString() + + const dialAddr = relayAddr + .encapsulate(`/p2p/${relayIdString}`) + .encapsulate(`/p2p-circuit/p2p/${dstLibp2p.peerInfo.id.toString()}`) + + const tcpAddrs = dstLibp2p.transportManager.getAddrs() + await dstLibp2p.transportManager.listen([multiaddr(`/p2p-circuit${relayAddr}/p2p/${relayIdString}`)]) + expect(dstLibp2p.transportManager.getAddrs()).to.have.deep.members([...tcpAddrs, dialAddr.decapsulate('p2p')]) + + const connection = await srcLibp2p.dial(dialAddr) + expect(connection).to.exist() + expect(connection.remotePeer.toBytes()).to.eql(dstLibp2p.peerInfo.id.toBytes()) + expect(connection.localPeer.toBytes()).to.eql(srcLibp2p.peerInfo.id.toBytes()) + expect(connection.remoteAddr).to.eql(dialAddr) + expect(connection.localAddr).to.eql( + relayAddr // the relay address + .encapsulate(`/p2p/${relayIdString}`) // with its peer id + .encapsulate('/p2p-circuit') // the local peer is connected over the relay + .encapsulate(`/p2p/${srcLibp2p.peerInfo.id.toB58String()}`) // and the local peer id + ) + + const { stream: echoStream } = await connection.newStream('/echo/1.0.0') + const input = Buffer.from('hello') + const [output] = await pipe( + [input], + echoStream, + collect + ) + expect(output.slice()).to.eql(input) + }) + + it('should fail to connect to a peer over a relay with inactive connections', async () => { + const relayAddr = relayLibp2p.transportManager.getAddrs()[0] + const relayIdString = relayLibp2p.peerInfo.id.toString() + + const dialAddr = relayAddr + .encapsulate(`/p2p/${relayIdString}`) + .encapsulate(`/p2p-circuit/p2p/${dstLibp2p.peerInfo.id.toString()}`) + + await expect(srcLibp2p.dial(dialAddr)) + .to.eventually.be.rejected() + .and.to.have.property('code', Errors.ERR_HOP_REQUEST_FAILED) + }) + + it('should not stay connected to a relay when not already connected and HOP fails', async () => { + const relayAddr = relayLibp2p.transportManager.getAddrs()[0] + const relayIdString = relayLibp2p.peerInfo.id.toString() + + const dialAddr = relayAddr + .encapsulate(`/p2p/${relayIdString}`) + .encapsulate(`/p2p-circuit/p2p/${dstLibp2p.peerInfo.id.toString()}`) + + await expect(srcLibp2p.dial(dialAddr)) + .to.eventually.be.rejected() + .and.to.have.property('code', Errors.ERR_HOP_REQUEST_FAILED) + + // We should not be connected to the relay, because we weren't before the dial + const srcToRelayConn = srcLibp2p.registrar.getConnection(relayLibp2p.peerInfo) + expect(srcToRelayConn).to.not.exist() + }) + + it('dialer should stay connected to an already connected relay on hop failure', async () => { + const relayAddr = relayLibp2p.transportManager.getAddrs()[0] + const relayIdString = relayLibp2p.peerInfo.id.toString() + + const dialAddr = relayAddr + .encapsulate(`/p2p/${relayIdString}`) + .encapsulate(`/p2p-circuit/p2p/${dstLibp2p.peerInfo.id.toString()}`) + + await srcLibp2p.dial(relayAddr) + + await expect(srcLibp2p.dial(dialAddr)) + .to.eventually.be.rejected() + .and.to.have.property('code', Errors.ERR_HOP_REQUEST_FAILED) + + const srcToRelayConn = srcLibp2p.registrar.getConnection(relayLibp2p.peerInfo) + expect(srcToRelayConn).to.exist() + expect(srcToRelayConn.stat.status).to.equal('open') + }) + + it('destination peer should stay connected to an already connected relay on hop failure', async () => { + const relayAddr = relayLibp2p.transportManager.getAddrs()[0] + const relayIdString = relayLibp2p.peerInfo.id.toString() + + const dialAddr = relayAddr + .encapsulate(`/p2p/${relayIdString}`) + .encapsulate(`/p2p-circuit/p2p/${dstLibp2p.peerInfo.id.toString()}`) + + // Connect the destination peer and the relay + const tcpAddrs = dstLibp2p.transportManager.getAddrs() + await dstLibp2p.transportManager.listen([multiaddr(`/p2p-circuit${relayAddr}/p2p/${relayIdString}`)]) + expect(dstLibp2p.transportManager.getAddrs()).to.have.deep.members([...tcpAddrs, dialAddr.decapsulate('p2p')]) + + // Tamper with the our multiaddrs for the circuit message + sinon.stub(srcLibp2p.peerInfo.multiaddrs, 'toArray').returns([{ + buffer: Buffer.from('an invalid multiaddr') + }]) + + await expect(srcLibp2p.dial(dialAddr)) + .to.eventually.be.rejected() + .and.to.have.property('code', Errors.ERR_HOP_REQUEST_FAILED) + + const dstToRelayConn = dstLibp2p.registrar.getConnection(relayLibp2p.peerInfo) + expect(dstToRelayConn).to.exist() + expect(dstToRelayConn.stat.status).to.equal('open') + }) +}) diff --git a/test/identify/index.spec.js b/test/identify/index.spec.js index 114b5156e5..8cc372c60d 100644 --- a/test/identify/index.spec.js +++ b/test/identify/index.spec.js @@ -3,6 +3,7 @@ const chai = require('chai') chai.use(require('dirty-chai')) +chai.use(require('chai-as-promised')) const { expect } = chai const sinon = require('sinon') @@ -98,20 +99,18 @@ describe('Identify', () => { sinon.stub(localConnectionMock, 'newStream').returns({ stream: local, protocol: multicodecs.IDENTIFY }) // Run identify - try { - await Promise.all([ - localIdentify.identify(localConnectionMock, localPeer.id), - remoteIdentify.handleMessage({ - connection: remoteConnectionMock, - stream: remote, - protocol: multicodecs.IDENTIFY - }) - ]) - expect.fail('should have thrown') - } catch (err) { - expect(err).to.exist() - expect(err.code).to.eql(Errors.ERR_INVALID_PEER) - } + const identifyPromise = Promise.all([ + localIdentify.identify(localConnectionMock, localPeer.id), + remoteIdentify.handleMessage({ + connection: remoteConnectionMock, + stream: remote, + protocol: multicodecs.IDENTIFY + }) + ]) + + await expect(identifyPromise) + .to.eventually.be.rejected() + .and.to.have.property('code', Errors.ERR_INVALID_PEER) }) describe('push', () => { @@ -229,6 +228,7 @@ describe('Identify', () => { // Wait for identify to finish await libp2p.dialer.identifyService.identify.firstCall.returnValue + sinon.stub(libp2p, 'isStarted').returns(true) libp2p.handle('/echo/2.0.0', () => {}) libp2p.unhandle('/echo/2.0.0') diff --git a/test/registrar/registrar.node.js b/test/registrar/registrar.node.js index b2045004c4..c2357cdb88 100644 --- a/test/registrar/registrar.node.js +++ b/test/registrar/registrar.node.js @@ -54,4 +54,19 @@ describe('registrar on dial', () => { const remoteConn = remoteLibp2p.registrar.getConnection(peerInfo) expect(remoteConn).to.exist() }) + + it('should be closed on libp2p stop', async () => { + libp2p = new Libp2p(mergeOptions(baseOptions, { + peerInfo + })) + + await libp2p.dial(remoteAddr) + expect(libp2p.registrar.connections.size).to.equal(1) + + sinon.spy(libp2p.registrar, 'close') + + await libp2p.stop() + expect(libp2p.registrar.close.callCount).to.equal(1) + expect(libp2p.registrar.connections.size).to.equal(0) + }) }) diff --git a/test/registrar/registrar.spec.js b/test/registrar/registrar.spec.js index 9114e03590..a56854d42b 100644 --- a/test/registrar/registrar.spec.js +++ b/test/registrar/registrar.spec.js @@ -24,26 +24,14 @@ describe('registrar', () => { }) it('should fail to register a protocol if no multicodec is provided', () => { - try { - registrar.register() - } catch (err) { - expect(err).to.exist() - return - } - throw new Error('should fail to register a protocol if no multicodec is provided') + expect(() => registrar.register()).to.throw() }) it('should fail to register a protocol if an invalid topology is provided', () => { const fakeTopology = { random: 1 } - try { - registrar.register() - } catch (err) { - expect(err).to.exist(fakeTopology) - return - } - throw new Error('should fail to register a protocol if an invalid topology is provided') + expect(() => registrar.register(fakeTopology)).to.throw() }) }) diff --git a/test/transports/transport-manager.node.js b/test/transports/transport-manager.node.js index 71d776fc2c..4fd57e9cd1 100644 --- a/test/transports/transport-manager.node.js +++ b/test/transports/transport-manager.node.js @@ -38,11 +38,12 @@ describe('Transport Manager (TCP)', () => { it('should be able to listen', async () => { tm.add(Transport.prototype[Symbol.toStringTag], Transport) await tm.listen(addrs) - expect(tm._listeners.size).to.equal(1) + expect(tm._listeners).to.have.key(Transport.prototype[Symbol.toStringTag]) + expect(tm._listeners.get(Transport.prototype[Symbol.toStringTag])).to.have.length(addrs.length) // Ephemeral ip addresses may result in multiple listeners expect(tm.getAddrs().length).to.equal(addrs.length) await tm.close() - expect(tm._listeners.size).to.equal(0) + expect(tm._listeners.get(Transport.prototype[Symbol.toStringTag])).to.have.length(0) }) it('should be able to dial', async () => { diff --git a/test/transports/transport-manager.spec.js b/test/transports/transport-manager.spec.js index 91a976eb3b..ab08fc1759 100644 --- a/test/transports/transport-manager.spec.js +++ b/test/transports/transport-manager.spec.js @@ -3,6 +3,7 @@ const chai = require('chai') chai.use(require('dirty-chai')) +chai.use(require('chai-as-promised')) const { expect } = chai const sinon = require('sinon') @@ -39,21 +40,25 @@ describe('Transport Manager (WebSockets)', () => { await tm.remove(Transport.prototype[Symbol.toStringTag]) }) - it('should not be able to add a transport without a key', () => { - expect(() => { + it('should not be able to add a transport without a key', async () => { + // Chai as promised conflicts with normal `throws` validation, + // so wrap the call in an async function + await expect((async () => { // eslint-disable-line tm.add(undefined, Transport) - }).to.throw().that.satisfies((err) => { - return err.code === ErrorCodes.ERR_INVALID_KEY - }) + })()) + .to.eventually.be.rejected() + .and.to.have.property('code', ErrorCodes.ERR_INVALID_KEY) }) - it('should not be able to add a transport twice', () => { + it('should not be able to add a transport twice', async () => { tm.add(Transport.prototype[Symbol.toStringTag], Transport) - expect(() => { + // Chai as promised conflicts with normal `throws` validation, + // so wrap the call in an async function + await expect((async () => { // eslint-disable-line tm.add(Transport.prototype[Symbol.toStringTag], Transport) - }).to.throw().that.satisfies((err) => { - return err.code === ErrorCodes.ERR_DUPLICATE_TRANSPORT - }) + })()) + .to.eventually.be.rejected() + .and.to.have.property('code', ErrorCodes.ERR_DUPLICATE_TRANSPORT) }) it('should be able to dial', async () => { @@ -67,27 +72,18 @@ describe('Transport Manager (WebSockets)', () => { it('should fail to dial an unsupported address', async () => { tm.add(Transport.prototype[Symbol.toStringTag], Transport) const addr = multiaddr('/ip4/127.0.0.1/tcp/0') - try { - await tm.dial(addr) - } catch (err) { - expect(err).to.satisfy((err) => err.code === ErrorCodes.ERR_TRANSPORT_UNAVAILABLE) - return - } - - expect.fail('Dial attempt should have failed') + await expect(tm.dial(addr)) + .to.eventually.be.rejected() + .and.to.have.property('code', ErrorCodes.ERR_TRANSPORT_UNAVAILABLE) }) it('should fail to listen with no valid address', async () => { tm.add(Transport.prototype[Symbol.toStringTag], Transport) const addrs = [multiaddr('/ip4/127.0.0.1/tcp/0')] - try { - await tm.listen(addrs) - } catch (err) { - expect(err).to.satisfy((err) => err.code === ErrorCodes.ERR_NO_VALID_ADDRESSES) - return - } - - expect.fail('should have failed') + + await expect(tm.listen(addrs)) + .to.eventually.be.rejected() + .and.to.have.property('code', ErrorCodes.ERR_NO_VALID_ADDRESSES) }) }) @@ -115,7 +111,8 @@ describe('libp2p.transportManager', () => { }) expect(libp2p.transportManager).to.exist() - expect(libp2p.transportManager._transports.size).to.equal(1) + // Our transport and circuit relay + expect(libp2p.transportManager._transports.size).to.equal(2) }) it('starting and stopping libp2p should start and stop TransportManager', async () => { diff --git a/test/utils/base-options.browser.js b/test/utils/base-options.browser.js index 1c1a425505..d437b58023 100644 --- a/test/utils/base-options.browser.js +++ b/test/utils/base-options.browser.js @@ -9,5 +9,13 @@ module.exports = { transport: [Transport], streamMuxer: [Muxer], connEncryption: [Crypto] + }, + config: { + relay: { + enabled: true, + hop: { + enabled: false + } + } } } diff --git a/test/utils/base-options.js b/test/utils/base-options.js index ca7a037ce5..8b7fb5350d 100644 --- a/test/utils/base-options.js +++ b/test/utils/base-options.js @@ -9,5 +9,13 @@ module.exports = { transport: [Transport], streamMuxer: [Muxer], connEncryption: [Crypto] + }, + config: { + relay: { + enabled: true, + hop: { + enabled: false + } + } } } diff --git a/test/utils/creators/peer.js b/test/utils/creators/peer.js index e39e52e46d..88750bea1b 100644 --- a/test/utils/creators/peer.js +++ b/test/utils/creators/peer.js @@ -5,7 +5,7 @@ const PeerInfo = require('peer-info') const Peers = require('../../fixtures/peers') -module.exports.createPeerInfo = async (length) => { +async function createPeerInfo (length) { const peers = await Promise.all( Array.from({ length }) .map((_, i) => PeerId.create()) @@ -14,11 +14,19 @@ module.exports.createPeerInfo = async (length) => { return peers.map((peer) => new PeerInfo(peer)) } -module.exports.createPeerInfoFromFixture = async (length) => { - const peers = await Promise.all( +function createPeerIdsFromFixture (length) { + return Promise.all( Array.from({ length }) .map((_, i) => PeerId.createFromJSON(Peers[i])) ) +} + +async function createPeerInfoFromFixture (length) { + const peers = await createPeerIdsFromFixture(length) return peers.map((peer) => new PeerInfo(peer)) } + +module.exports.createPeerInfo = createPeerInfo +module.exports.createPeerIdsFromFixture = createPeerIdsFromFixture +module.exports.createPeerInfoFromFixture = createPeerInfoFromFixture diff --git a/test/utils/mockConnection.js b/test/utils/mockConnection.js index f3ce885884..5e6219ad90 100644 --- a/test/utils/mockConnection.js +++ b/test/utils/mockConnection.js @@ -1,10 +1,15 @@ 'use strict' +const pipe = require('it-pipe') const { Connection } = require('libp2p-interfaces/src/connection') const multiaddr = require('multiaddr') - +const Muxer = require('libp2p-mplex') +const Multistream = require('multistream-select') const pair = require('it-pair') +const errCode = require('err-code') +const { codes } = require('../../src/errors') +const mockMultiaddrConnPair = require('./mockMultiaddrConn') const peerUtils = require('./creators/peer') module.exports = async (properties = {}) => { @@ -48,3 +53,103 @@ module.exports = async (properties = {}) => { ...properties }) } + +/** + * Creates a full connection pair, without the transport or encryption + * + * @param {object} options + * @param {Multiaddr[]} options.addrs Should contain two addresses for the local and remote peer respectively + * @param {PeerId[]} options.remotePeer Should contain two peer ids, for the local and remote peer respectively + * @param {Map} options.protocols The protocols the connections should support + * @returns {{inbound:Connection, outbound:Connection}} + */ +module.exports.pair = function connectionPair ({ addrs, peers, protocols }) { + const [localPeer, remotePeer] = peers + + const { + inbound: inboundMaConn, + outbound: outboundMaConn + } = mockMultiaddrConnPair({ addrs, remotePeer }) + + const inbound = createConnection({ + direction: 'inbound', + maConn: inboundMaConn, + protocols, + // Inbound connection peers are reversed + localPeer: remotePeer, + remotePeer: localPeer + }) + const outbound = createConnection({ + direction: 'outbound', + maConn: outboundMaConn, + protocols, + localPeer, + remotePeer + }) + + return { inbound, outbound } +} + +function createConnection ({ + direction, + maConn, + localPeer, + remotePeer, + protocols +}) { + // Create the muxer + const muxer = new Muxer({ + // Run anytime a remote stream is created + onStream: async muxedStream => { + const mss = new Multistream.Listener(muxedStream) + try { + const { stream, protocol } = await mss.handle(Array.from(protocols.keys())) + connection.addStream(stream, protocol) + // Need to be able to notify a peer of this this._onStream({ connection, stream, protocol }) + const handler = protocols.get(protocol) + handler({ connection, stream, protocol }) + } catch (err) { + // Do nothing + } + }, + // Run anytime a stream closes + onStreamEnd: muxedStream => { + connection.removeStream(muxedStream.id) + } + }) + + const newStream = async protocols => { + const muxedStream = muxer.newStream() + const mss = new Multistream.Dialer(muxedStream) + try { + const { stream, protocol } = await mss.select(protocols) + return { stream: { ...muxedStream, ...stream }, protocol } + } catch (err) { + throw errCode(err, codes.ERR_UNSUPPORTED_PROTOCOL) + } + } + + // Pipe all data through the muxer + pipe(maConn, muxer, maConn) + + maConn.timeline.upgraded = Date.now() + + // Create the connection + const connection = new Connection({ + localAddr: maConn.localAddr, + remoteAddr: maConn.remoteAddr, + localPeer: localPeer, + remotePeer: remotePeer, + stat: { + direction, + timeline: maConn.timeline, + multiplexer: Muxer.multicodec, + encryption: 'N/A' + }, + newStream, + getStreams: () => muxer.streams, + close: err => maConn.close(err) + }) + + return connection +} diff --git a/test/utils/mockMultiaddrConn.js b/test/utils/mockMultiaddrConn.js index 617f660422..9fd9c9b597 100644 --- a/test/utils/mockMultiaddrConn.js +++ b/test/utils/mockMultiaddrConn.js @@ -13,10 +13,11 @@ const AbortController = require('abort-controller') */ module.exports = function mockMultiaddrConnPair ({ addrs, remotePeer }) { const controller = new AbortController() + const [localAddr, remoteAddr] = addrs const [inbound, outbound] = duplexPair() - outbound.localAddr = addrs[0] - outbound.remoteAddr = addrs[1].encapsulate(`/p2p/${remotePeer.toB58String()}`) + outbound.localAddr = localAddr + outbound.remoteAddr = remoteAddr.encapsulate(`/p2p/${remotePeer.toB58String()}`) outbound.timeline = { open: Date.now() } @@ -25,8 +26,8 @@ module.exports = function mockMultiaddrConnPair ({ addrs, remotePeer }) { controller.abort() } - inbound.localAddr = addrs[1] - inbound.remoteAddr = addrs[0] + inbound.localAddr = remoteAddr + inbound.remoteAddr = localAddr inbound.timeline = { open: Date.now() } From fc22c36ba75c2d138490d75a2bd6ff4aab7b7881 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Sun, 1 Dec 2019 22:54:59 +0100 Subject: [PATCH 20/92] refactor: async routing (#489) * feat: async routing * chore: put dht extra api commands under content routing * chore: add default option to createPeerInfo Co-Authored-By: Jacob Heun * chore: address review * chore: rm dlv --- package.json | 9 +- src/content-routing.js | 125 ++++--- src/dht.js | 72 ---- src/index.js | 13 +- src/peer-routing.js | 49 +-- test/content-routing/content-routing.node.js | 323 ++++++++++++++++++ .../dht/configuration.node.js | 24 +- .../dht/operation.node.js | 30 +- test/{ => content-routing}/dht/utils.js | 2 +- test/content-routing/utils.js | 24 ++ test/core/listening.node.js | 2 +- test/dialing/relay.node.js | 4 +- test/peer-discovery/index.node.js | 4 +- test/peer-discovery/index.spec.js | 4 +- test/peer-routing/peer-routing.node.js | 219 ++++++++++++ test/peer-routing/utils.js | 24 ++ test/peer-store/peer-store.spec.js | 14 +- test/pubsub/configuration.node.js | 6 +- test/pubsub/implementations.node.js | 2 +- test/pubsub/operation.node.js | 2 +- test/registrar/registrar.node.js | 2 +- test/registrar/utils.js | 2 +- test/utils/creators/peer.js | 73 +++- test/utils/mockConnection.js | 2 +- 24 files changed, 804 insertions(+), 227 deletions(-) delete mode 100644 src/dht.js create mode 100644 test/content-routing/content-routing.node.js rename test/{ => content-routing}/dht/configuration.node.js (71%) rename test/{ => content-routing}/dht/operation.node.js (75%) rename test/{ => content-routing}/dht/utils.js (92%) create mode 100644 test/content-routing/utils.js create mode 100644 test/peer-routing/peer-routing.node.js create mode 100644 test/peer-routing/utils.js diff --git a/package.json b/package.json index e80409f8c9..be3948ca35 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "dependencies": { "abort-controller": "^3.0.0", "async": "^2.6.2", + "async-iterator-all": "^1.0.0", "bignumber.js": "^9.0.0", "class-is": "^1.1.0", "debug": "^4.1.1", @@ -62,6 +63,7 @@ "multiaddr": "^7.2.1", "multistream-select": "^0.15.0", "once": "^1.4.0", + "p-any": "^2.1.0", "p-map": "^3.0.0", "p-queue": "^6.1.1", "p-settle": "^3.1.0", @@ -90,11 +92,11 @@ "interface-datastore": "^0.6.0", "it-pair": "^1.0.0", "libp2p-bootstrap": "^0.10.3", - "libp2p-delegated-content-routing": "^0.2.2", - "libp2p-delegated-peer-routing": "^0.2.2", + "libp2p-delegated-content-routing": "^0.4.1", + "libp2p-delegated-peer-routing": "^0.4.0", "libp2p-floodsub": "^0.19.0", "libp2p-gossipsub": "^0.1.0", - "libp2p-kad-dht": "~0.17.0", + "libp2p-kad-dht": "^0.18.0", "libp2p-mdns": "^0.13.0", "libp2p-mplex": "^0.9.1", "libp2p-pnet": "~0.1.0", @@ -105,6 +107,7 @@ "lodash.times": "^4.3.2", "nock": "^10.0.6", "p-defer": "^3.0.0", + "p-times": "^2.1.0", "p-wait-for": "^3.1.0", "portfinder": "^1.0.20", "pull-goodbye": "0.0.2", diff --git a/src/content-routing.js b/src/content-routing.js index 099aa6e44e..022b8b2bac 100644 --- a/src/content-routing.js +++ b/src/content-routing.js @@ -1,16 +1,18 @@ 'use strict' -const tryEach = require('async/tryEach') -const parallel = require('async/parallel') const errCode = require('err-code') -const promisify = require('promisify-es6') +const { messages, codes } = require('./errors') + +const all = require('async-iterator-all') +const pAny = require('p-any') module.exports = (node) => { const routers = node._modules.contentRouting || [] + const dht = node._dht // If we have the dht, make it first - if (node._dht) { - routers.unshift(node._dht) + if (dht) { + routers.unshift(dht) } return { @@ -19,66 +21,93 @@ module.exports = (node) => { * Once a content router succeeds, iteration will stop. * * @param {CID} key The CID key of the content to find - * @param {object} options - * @param {number} options.maxTimeout How long the query should run - * @param {number} options.maxNumProviders - maximum number of providers to find - * @param {function(Error, Result)} callback - * @returns {void} + * @param {object} [options] + * @param {number} [options.timeout] How long the query should run + * @param {number} [options.maxNumProviders] - maximum number of providers to find + * @returns {AsyncIterable} */ - findProviders: promisify((key, options, callback) => { - if (typeof options === 'function') { - callback = options - options = {} - } else if (typeof options === 'number') { // This can be deprecated in a future release - options = { - maxTimeout: options - } - } - + async * findProviders (key, options) { if (!routers.length) { - return callback(errCode(new Error('No content routers available'), 'NO_ROUTERS_AVAILABLE')) + throw errCode(new Error('No content routers available'), 'NO_ROUTERS_AVAILABLE') } - const tasks = routers.map((router) => { - return (cb) => router.findProviders(key, options, (err, results) => { - if (err) { - return cb(err) - } + const result = await pAny( + routers.map(async (router) => { + const provs = await all(router.findProviders(key, options)) - // If we don't have any results, we need to provide an error to keep trying - if (!results || Object.keys(results).length === 0) { - return cb(errCode(new Error('not found'), 'NOT_FOUND'), null) + if (!provs || !provs.length) { + throw errCode(new Error('not found'), 'NOT_FOUND') } - - cb(null, results) + return provs }) - }) + ) - tryEach(tasks, (err, results) => { - if (err && err.code !== 'NOT_FOUND') { - return callback(err) - } - results = results || [] - callback(null, results) - }) - }), + for (const pInfo of result) { + yield pInfo + } + }, /** * Iterates over all content routers in parallel to notify it is * a provider of the given key. * * @param {CID} key The CID key of the content to find - * @param {function(Error)} callback - * @returns {void} + * @returns {Promise} */ - provide: promisify((key, callback) => { + async provide (key) { // eslint-disable-line require-await if (!routers.length) { - return callback(errCode(new Error('No content routers available'), 'NO_ROUTERS_AVAILABLE')) + throw errCode(new Error('No content routers available'), 'NO_ROUTERS_AVAILABLE') + } + + return Promise.all(routers.map((router) => router.provide(key))) + }, + + /** + * Store the given key/value pair in the DHT. + * @param {Buffer} key + * @param {Buffer} value + * @param {Object} [options] - put options + * @param {number} [options.minPeers] - minimum number of peers required to successfully put + * @returns {Promise} + */ + async put (key, value, options) { // eslint-disable-line require-await + if (!node.isStarted() || !dht.isStarted) { + throw errCode(new Error(messages.NOT_STARTED_YET), codes.DHT_NOT_STARTED) + } + + return dht.put(key, value, options) + }, + + /** + * Get the value to the given key. + * Times out after 1 minute by default. + * @param {Buffer} key + * @param {Object} [options] - get options + * @param {number} [options.timeout] - optional timeout (default: 60000) + * @returns {Promise<{from: PeerId, val: Buffer}>} + */ + async get (key, options) { // eslint-disable-line require-await + if (!node.isStarted() || !dht.isStarted) { + throw errCode(new Error(messages.NOT_STARTED_YET), codes.DHT_NOT_STARTED) + } + + return dht.get(key, options) + }, + + /** + * Get the `n` values to the given key without sorting. + * @param {Buffer} key + * @param {number} nVals + * @param {Object} [options] - get options + * @param {number} [options.timeout] - optional timeout (default: 60000) + * @returns {Promise>} + */ + async getMany (key, nVals, options) { // eslint-disable-line require-await + if (!node.isStarted() || !dht.isStarted) { + throw errCode(new Error(messages.NOT_STARTED_YET), codes.DHT_NOT_STARTED) } - parallel(routers.map((router) => { - return (cb) => router.provide(key, cb) - }), callback) - }) + return dht.getMany(key, nVals, options) + } } } diff --git a/src/dht.js b/src/dht.js deleted file mode 100644 index f2c801243e..0000000000 --- a/src/dht.js +++ /dev/null @@ -1,72 +0,0 @@ -'use strict' - -const errCode = require('err-code') - -const { messages, codes } = require('./errors') - -module.exports = (node, DHT, config) => { - const dht = new DHT({ - dialer: node.dialer, - peerInfo: node.peerInfo, - peerStore: node.peerStore, - registrar: node.registrar, - datastore: this.datastore, - ...config - }) - - return { - /** - * Store the given key/value pair in the DHT. - * @param {Buffer} key - * @param {Buffer} value - * @param {Object} [options] - put options - * @param {number} [options.minPeers] - minimum number of peers required to successfully put - * @returns {Promise} - */ - put: (key, value, options) => { - if (!node.isStarted() || !dht.isStarted) { - throw errCode(new Error(messages.NOT_STARTED_YET), codes.DHT_NOT_STARTED) - } - - return dht.put(key, value, options) - }, - - /** - * Get the value to the given key. - * Times out after 1 minute by default. - * @param {Buffer} key - * @param {Object} [options] - get options - * @param {number} [options.timeout] - optional timeout (default: 60000) - * @returns {Promise<{from: PeerId, val: Buffer}>} - */ - get: (key, options) => { - if (!node.isStarted() || !dht.isStarted) { - throw errCode(new Error(messages.NOT_STARTED_YET), codes.DHT_NOT_STARTED) - } - - return dht.get(key, options) - }, - - /** - * Get the `n` values to the given key without sorting. - * @param {Buffer} key - * @param {number} nVals - * @param {Object} [options] - get options - * @param {number} [options.timeout] - optional timeout (default: 60000) - * @returns {Promise>} - */ - getMany: (key, nVals, options) => { - if (!node.isStarted() || !dht.isStarted) { - throw errCode(new Error(messages.NOT_STARTED_YET), codes.DHT_NOT_STARTED) - } - - return dht.getMany(key, nVals, options) - }, - - _dht: dht, - - start: () => dht.start(), - - stop: () => dht.stop() - } -} diff --git a/src/index.js b/src/index.js index de655d184e..245babd5e5 100644 --- a/src/index.js +++ b/src/index.js @@ -10,7 +10,6 @@ const multiaddr = require('multiaddr') const peerRouting = require('./peer-routing') const contentRouting = require('./content-routing') -const dht = require('./dht') const pubsub = require('./pubsub') const { getPeerInfo, getPeerInfoRemote } = require('./get-peer-info') const { validate: validateConfig } = require('./config') @@ -124,7 +123,15 @@ class Libp2p extends EventEmitter { // dht provided components (peerRouting, contentRouting, dht) if (this._modules.dht) { - this._dht = dht(this, this._modules.dht, this._config.dht) + const DHT = this._modules.dht + this._dht = new DHT({ + dialer: this.dialer, + peerInfo: this.peerInfo, + peerStore: this.peerStore, + registrar: this.registrar, + datastore: this.datastore, + ...this._config.dht + }) } // start pubsub @@ -333,7 +340,7 @@ class Libp2p extends EventEmitter { // TODO: this should be modified once random-walk is used as // the other discovery modules - this._dht._dht.on('peer', this._peerDiscovered) + this._dht.on('peer', this._peerDiscovered) } } diff --git a/src/peer-routing.js b/src/peer-routing.js index d1b768d2f4..a3eac01d7e 100644 --- a/src/peer-routing.js +++ b/src/peer-routing.js @@ -1,8 +1,7 @@ 'use strict' -const tryEach = require('async/tryEach') const errCode = require('err-code') -const promisify = require('promisify-es6') +const pAny = require('p-any') module.exports = (node) => { const routers = node._modules.peerRouting || [] @@ -17,43 +16,25 @@ module.exports = (node) => { * Iterates over all peer routers in series to find the given peer. * * @param {String} id The id of the peer to find - * @param {object} options - * @param {number} options.maxTimeout How long the query should run - * @param {function(Error, Result)} callback - * @returns {void} + * @param {object} [options] + * @param {number} [options.timeout] How long the query should run + * @returns {Promise} */ - findPeer: promisify((id, options, callback) => { - if (typeof options === 'function') { - callback = options - options = {} - } - + findPeer: async (id, options) => { // eslint-disable-line require-await if (!routers.length) { - callback(errCode(new Error('No peer routers available'), 'NO_ROUTERS_AVAILABLE')) + throw errCode(new Error('No peer routers available'), 'NO_ROUTERS_AVAILABLE') } - const tasks = routers.map((router) => { - return (cb) => router.findPeer(id, options, (err, result) => { - if (err) { - return cb(err) - } - - // If we don't have a result, we need to provide an error to keep trying - if (!result || Object.keys(result).length === 0) { - return cb(errCode(new Error('not found'), 'NOT_FOUND'), null) - } + return pAny(routers.map(async (router) => { + const result = await router.findPeer(id, options) - cb(null, result) - }) - }) - - tryEach(tasks, (err, results) => { - if (err) { - return callback(err) + // If we don't have a result, we need to provide an error to keep trying + if (!result || Object.keys(result).length === 0) { + throw errCode(new Error('not found'), 'NOT_FOUND') } - results = results || [] - callback(null, results) - }) - }) + + return result + })) + } } } diff --git a/test/content-routing/content-routing.node.js b/test/content-routing/content-routing.node.js new file mode 100644 index 0000000000..8dd0265811 --- /dev/null +++ b/test/content-routing/content-routing.node.js @@ -0,0 +1,323 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +const { expect } = chai +const nock = require('nock') +const sinon = require('sinon') + +const pDefer = require('p-defer') +const mergeOptions = require('merge-options') + +const CID = require('cids') +const DelegatedContentRouter = require('libp2p-delegated-content-routing') +const multiaddr = require('multiaddr') + +const peerUtils = require('../utils/creators/peer') +const { baseOptions, routingOptions } = require('./utils') + +describe('content-routing', () => { + describe('no routers', () => { + let node + + before(async () => { + [node] = await peerUtils.createPeer({ + config: baseOptions + }) + }) + + it('.findProviders should return an error', async () => { + try { + for await (const _ of node.contentRouting.findProviders('a cid')) {} // eslint-disable-line no-unused-vars + throw new Error('.findProviders should return an error') + } catch (err) { + expect(err).to.exist() + expect(err.code).to.equal('NO_ROUTERS_AVAILABLE') + } + }) + + it('.provide should return an error', async () => { + await expect(node.contentRouting.provide('a cid')) + .to.eventually.be.rejected() + .and.to.have.property('code', 'NO_ROUTERS_AVAILABLE') + }) + }) + + describe('via dht router', () => { + const number = 5 + let nodes + + before(async () => { + nodes = await peerUtils.createPeer({ + number, + config: routingOptions + }) + + // Ring dial + await Promise.all( + nodes.map((peer, i) => peer.dial(nodes[(i + 1) % number].peerInfo)) + ) + }) + + afterEach(() => { + sinon.restore() + }) + + after(() => Promise.all(nodes.map((n) => n.stop()))) + + it('should use the nodes dht to provide', () => { + const deferred = pDefer() + + sinon.stub(nodes[0]._dht, 'provide').callsFake(() => { + deferred.resolve() + }) + + nodes[0].contentRouting.provide() + return deferred.promise + }) + + it('should use the nodes dht to find providers', async () => { + const deferred = pDefer() + + sinon.stub(nodes[0]._dht, 'findProviders').callsFake(function * () { + deferred.resolve() + yield + }) + + await nodes[0].contentRouting.findProviders().next() + + return deferred.promise + }) + }) + + describe('via delegate router', () => { + let node + let delegate + + beforeEach(async () => { + const [peerInfo] = await peerUtils.createPeerInfo({ fixture: false }) + + delegate = new DelegatedContentRouter(peerInfo.id, { + host: '0.0.0.0', + protocol: 'http', + port: 60197 + }, [ + multiaddr('/ip4/0.0.0.0/tcp/60197') + ]) + + ;[node] = await peerUtils.createPeer({ + config: mergeOptions(baseOptions, { + modules: { + contentRouting: [delegate] + }, + config: { + dht: { + enabled: false + } + } + }) + }) + }) + + afterEach(() => { + sinon.restore() + }) + + afterEach(() => node.stop()) + + it('should use the delegate router to provide', () => { + const deferred = pDefer() + + sinon.stub(delegate, 'provide').callsFake(() => { + deferred.resolve() + }) + + node.contentRouting.provide() + return deferred.promise + }) + + it('should use the delegate router to find providers', async () => { + const deferred = pDefer() + + sinon.stub(delegate, 'findProviders').callsFake(function * () { + deferred.resolve() + yield + }) + + await node.contentRouting.findProviders().next() + + return deferred.promise + }) + + it('should be able to register as a provider', async () => { + const cid = new CID('QmU621oD8AhHw6t25vVyfYKmL9VV3PTgc52FngEhTGACFB') + const mockApi = nock('http://0.0.0.0:60197') + // mock the refs call + .post('/api/v0/refs') + .query({ + recursive: false, + arg: cid.toBaseEncodedString() + }) + .reply(200, null, [ + 'Content-Type', 'application/json', + 'X-Chunked-Output', '1' + ]) + + await node.contentRouting.provide(cid) + + expect(mockApi.isDone()).to.equal(true) + }) + + it('should handle errors when registering as a provider', async () => { + const cid = new CID('QmU621oD8AhHw6t25vVyfYKmL9VV3PTgc52FngEhTGACFB') + const mockApi = nock('http://0.0.0.0:60197') + // mock the refs call + .post('/api/v0/refs') + .query({ + recursive: false, + arg: cid.toBaseEncodedString() + }) + .reply(502, 'Bad Gateway', ['Content-Type', 'application/json']) + + await expect(node.contentRouting.provide(cid)) + .to.eventually.be.rejected() + + expect(mockApi.isDone()).to.equal(true) + }) + + it('should be able to find providers', async () => { + const cid = new CID('QmU621oD8AhHw6t25vVyfYKmL9VV3PTgc52FngEhTGACFB') + const provider = 'QmZNgCqZCvTsi3B4Vt7gsSqpkqDpE7M2Y9TDmEhbDb4ceF' + + const mockApi = nock('http://0.0.0.0:60197') + .post('/api/v0/dht/findprovs') + .query({ + arg: cid.toBaseEncodedString() + }) + .reply(200, `{"Extra":"","ID":"QmWKqWXCtRXEeCQTo3FoZ7g4AfnGiauYYiczvNxFCHicbB","Responses":[{"Addrs":["/ip4/0.0.0.0/tcp/0"],"ID":"${provider}"}],"Type":4}\n`, [ + 'Content-Type', 'application/json', + 'X-Chunked-Output', '1' + ]) + + const providers = [] + for await (const provider of node.contentRouting.findProviders(cid, { timeout: 1000 })) { + providers.push(provider) + } + + expect(providers).to.have.length(1) + expect(providers[0].id.toB58String()).to.equal(provider) + expect(mockApi.isDone()).to.equal(true) + }) + + it('should handle errors when finding providers', async () => { + const cid = new CID('QmU621oD8AhHw6t25vVyfYKmL9VV3PTgc52FngEhTGACFB') + const mockApi = nock('http://0.0.0.0:60197') + .post('/api/v0/dht/findprovs') + .query({ + arg: cid.toBaseEncodedString() + }) + .reply(502, 'Bad Gateway', [ + 'X-Chunked-Output', '1' + ]) + + try { + for await (const _ of node.contentRouting.findProviders(cid)) { } // eslint-disable-line no-unused-vars + throw new Error('should handle errors when finding providers') + } catch (err) { + expect(err).to.exist() + } + + expect(mockApi.isDone()).to.equal(true) + }) + }) + + describe('via dht and delegate routers', () => { + let node + let delegate + + beforeEach(async () => { + const [peerInfo] = await peerUtils.createPeerInfo({ fixture: false }) + + delegate = new DelegatedContentRouter(peerInfo.id, { + host: '0.0.0.0', + protocol: 'http', + port: 60197 + }, [ + multiaddr('/ip4/0.0.0.0/tcp/60197') + ]) + + ;[node] = await peerUtils.createPeer({ + config: mergeOptions(routingOptions, { + modules: { + contentRouting: [delegate] + } + }) + }) + }) + + afterEach(() => { + sinon.restore() + }) + + afterEach(() => node.stop()) + + it('should use both the dht and delegate router to provide', async () => { + const dhtDeferred = pDefer() + const delegatedDeferred = pDefer() + + sinon.stub(node._dht, 'provide').callsFake(() => { + dhtDeferred.resolve() + }) + + sinon.stub(delegate, 'provide').callsFake(() => { + delegatedDeferred.resolve() + }) + + await node.contentRouting.provide() + + await Promise.all([ + dhtDeferred.promise, + delegatedDeferred.promise + ]) + }) + + it('should only use the dht if it finds providers', async () => { + const results = [true] + + sinon.stub(node._dht, 'findProviders').callsFake(function * () { + yield results[0] + }) + + sinon.stub(delegate, 'findProviders').callsFake(function * () { // eslint-disable-line require-yield + throw new Error('the delegate should not have been called') + }) + + const providers = [] + for await (const prov of node.contentRouting.findProviders('a cid')) { + providers.push(prov) + } + + expect(providers).to.have.length.above(0) + expect(providers).to.eql(results) + }) + + it('should use the delegate if the dht fails to find providers', async () => { + const results = [true] + + sinon.stub(node._dht, 'findProviders').callsFake(function * () {}) + + sinon.stub(delegate, 'findProviders').callsFake(function * () { + yield results[0] + }) + + const providers = [] + for await (const prov of node.contentRouting.findProviders('a cid')) { + providers.push(prov) + } + + expect(providers).to.have.length.above(0) + expect(providers).to.eql(results) + }) + }) +}) diff --git a/test/dht/configuration.node.js b/test/content-routing/dht/configuration.node.js similarity index 71% rename from test/dht/configuration.node.js rename to test/content-routing/dht/configuration.node.js index c003c124fc..c0165cdd9f 100644 --- a/test/dht/configuration.node.js +++ b/test/content-routing/dht/configuration.node.js @@ -8,9 +8,9 @@ const { expect } = chai const mergeOptions = require('merge-options') const multiaddr = require('multiaddr') -const { create } = require('../../src') +const { create } = require('../../../src') const { baseOptions, subsystemOptions } = require('./utils') -const peerUtils = require('../utils/creators/peer') +const peerUtils = require('../../utils/creators/peer') const listenAddr = multiaddr('/ip4/127.0.0.1/tcp/0') @@ -32,7 +32,7 @@ describe('DHT subsystem is configurable', () => { }) it('should start and stop by default once libp2p starts', async () => { - const [peerInfo] = await peerUtils.createPeerInfoFromFixture(1) + const [peerInfo] = await peerUtils.createPeerInfo(1) peerInfo.multiaddrs.add(listenAddr) const customOptions = mergeOptions(subsystemOptions, { @@ -40,17 +40,17 @@ describe('DHT subsystem is configurable', () => { }) libp2p = await create(customOptions) - expect(libp2p._dht._dht.isStarted).to.equal(false) + expect(libp2p._dht.isStarted).to.equal(false) await libp2p.start() - expect(libp2p._dht._dht.isStarted).to.equal(true) + expect(libp2p._dht.isStarted).to.equal(true) await libp2p.stop() - expect(libp2p._dht._dht.isStarted).to.equal(false) + expect(libp2p._dht.isStarted).to.equal(false) }) it('should not start if disabled once libp2p starts', async () => { - const [peerInfo] = await peerUtils.createPeerInfoFromFixture(1) + const [peerInfo] = await peerUtils.createPeerInfo(1) peerInfo.multiaddrs.add(listenAddr) const customOptions = mergeOptions(subsystemOptions, { @@ -63,14 +63,14 @@ describe('DHT subsystem is configurable', () => { }) libp2p = await create(customOptions) - expect(libp2p._dht._dht.isStarted).to.equal(false) + expect(libp2p._dht.isStarted).to.equal(false) await libp2p.start() - expect(libp2p._dht._dht.isStarted).to.equal(false) + expect(libp2p._dht.isStarted).to.equal(false) }) it('should allow a manual start', async () => { - const [peerInfo] = await peerUtils.createPeerInfoFromFixture(1) + const [peerInfo] = await peerUtils.createPeerInfo(1) peerInfo.multiaddrs.add(listenAddr) const customOptions = mergeOptions(subsystemOptions, { @@ -84,9 +84,9 @@ describe('DHT subsystem is configurable', () => { libp2p = await create(customOptions) await libp2p.start() - expect(libp2p._dht._dht.isStarted).to.equal(false) + expect(libp2p._dht.isStarted).to.equal(false) await libp2p._dht.start() - expect(libp2p._dht._dht.isStarted).to.equal(true) + expect(libp2p._dht.isStarted).to.equal(true) }) }) diff --git a/test/dht/operation.node.js b/test/content-routing/dht/operation.node.js similarity index 75% rename from test/dht/operation.node.js rename to test/content-routing/dht/operation.node.js index 6e707db3dd..8ca57579ba 100644 --- a/test/dht/operation.node.js +++ b/test/content-routing/dht/operation.node.js @@ -9,9 +9,9 @@ const pWaitFor = require('p-wait-for') const mergeOptions = require('merge-options') const multiaddr = require('multiaddr') -const { create } = require('../../src') +const { create } = require('../../../src') const { subsystemOptions, subsystemMulticodecs } = require('./utils') -const peerUtils = require('../utils/creators/peer') +const peerUtils = require('../../utils/creators/peer') const listenAddr = multiaddr('/ip4/127.0.0.1/tcp/8000') const remoteListenAddr = multiaddr('/ip4/127.0.0.1/tcp/8001') @@ -22,7 +22,7 @@ describe('DHT subsystem operates correctly', () => { let remAddr beforeEach(async () => { - [peerInfo, remotePeerInfo] = await peerUtils.createPeerInfoFromFixture(2) + [peerInfo, remotePeerInfo] = await peerUtils.createPeerInfo({ number: 2 }) peerInfo.multiaddrs.add(listenAddr) remotePeerInfo.multiaddrs.add(remoteListenAddr) @@ -57,8 +57,8 @@ describe('DHT subsystem operates correctly', () => { expect(connection).to.exist() return Promise.all([ - pWaitFor(() => libp2p._dht._dht.routingTable.size === 1), - pWaitFor(() => remoteLibp2p._dht._dht.routingTable.size === 1) + pWaitFor(() => libp2p._dht.routingTable.size === 1), + pWaitFor(() => remoteLibp2p._dht.routingTable.size === 1) ]) }) @@ -69,13 +69,13 @@ describe('DHT subsystem operates correctly', () => { await libp2p.dialProtocol(remAddr, subsystemMulticodecs) await Promise.all([ - pWaitFor(() => libp2p._dht._dht.routingTable.size === 1), - pWaitFor(() => remoteLibp2p._dht._dht.routingTable.size === 1) + pWaitFor(() => libp2p._dht.routingTable.size === 1), + pWaitFor(() => remoteLibp2p._dht.routingTable.size === 1) ]) - await libp2p._dht.put(key, value) + await libp2p.contentRouting.put(key, value) - const fetchedValue = await remoteLibp2p._dht.get(key) + const fetchedValue = await remoteLibp2p.contentRouting.get(key) expect(fetchedValue).to.eql(value) }) }) @@ -110,11 +110,11 @@ describe('DHT subsystem operates correctly', () => { const connection = await libp2p.dial(remAddr) expect(connection).to.exist() - expect(libp2p._dht._dht.routingTable.size).to.be.eql(0) - expect(remoteLibp2p._dht._dht.routingTable.size).to.be.eql(0) + expect(libp2p._dht.routingTable.size).to.be.eql(0) + expect(remoteLibp2p._dht.routingTable.size).to.be.eql(0) await remoteLibp2p._dht.start() - return pWaitFor(() => libp2p._dht._dht.routingTable.size === 1) + return pWaitFor(() => libp2p._dht.routingTable.size === 1) }) it('should put on a peer and get from the other', async () => { @@ -124,11 +124,11 @@ describe('DHT subsystem operates correctly', () => { const value = Buffer.from('world') await remoteLibp2p._dht.start() - await pWaitFor(() => libp2p._dht._dht.routingTable.size === 1) + await pWaitFor(() => libp2p._dht.routingTable.size === 1) - await libp2p._dht.put(key, value) + await libp2p.contentRouting.put(key, value) - const fetchedValue = await remoteLibp2p._dht.get(key) + const fetchedValue = await remoteLibp2p.contentRouting.get(key) expect(fetchedValue).to.eql(value) }) }) diff --git a/test/dht/utils.js b/test/content-routing/dht/utils.js similarity index 92% rename from test/dht/utils.js rename to test/content-routing/dht/utils.js index b5249ea9e3..c879f8d068 100644 --- a/test/dht/utils.js +++ b/test/content-routing/dht/utils.js @@ -2,7 +2,7 @@ const KadDht = require('libp2p-kad-dht') const { multicodec } = require('libp2p-kad-dht') -const Crypto = require('../../src/insecure/plaintext') +const Crypto = require('../../../src/insecure/plaintext') const Muxer = require('libp2p-mplex') const Transport = require('libp2p-tcp') diff --git a/test/content-routing/utils.js b/test/content-routing/utils.js new file mode 100644 index 0000000000..120f7281df --- /dev/null +++ b/test/content-routing/utils.js @@ -0,0 +1,24 @@ +'use strict' + +const KadDht = require('libp2p-kad-dht') +const mergeOptions = require('merge-options') +const baseOptions = require('../utils/base-options') + +module.exports.baseOptions = baseOptions + +const routingOptions = mergeOptions(baseOptions, { + modules: { + dht: KadDht + }, + config: { + dht: { + kBucketSize: 20, + randomWalk: { + enabled: true + }, + enabled: true + } + } +}) + +module.exports.routingOptions = routingOptions diff --git a/test/core/listening.node.js b/test/core/listening.node.js index dd67c74f8b..0178add3ac 100644 --- a/test/core/listening.node.js +++ b/test/core/listening.node.js @@ -18,7 +18,7 @@ describe('Listening', () => { let libp2p before(async () => { - [peerInfo] = await peerUtils.createPeerInfoFromFixture(1) + [peerInfo] = await peerUtils.createPeerInfo() peerInfo.multiaddrs.add(listenAddr) }) diff --git a/test/dialing/relay.node.js b/test/dialing/relay.node.js index 597bfee183..8eb0be2dfd 100644 --- a/test/dialing/relay.node.js +++ b/test/dialing/relay.node.js @@ -10,7 +10,7 @@ const sinon = require('sinon') const multiaddr = require('multiaddr') const { collect } = require('streaming-iterables') const pipe = require('it-pipe') -const { createPeerInfoFromFixture } = require('../utils/creators/peer') +const { createPeerInfo } = require('../utils/creators/peer') const baseOptions = require('../utils/base-options') const Libp2p = require('../../src') const { codes: Errors } = require('../../src/errors') @@ -21,7 +21,7 @@ describe('Dialing (via relay, TCP)', () => { let dstLibp2p before(async () => { - const peerInfos = await createPeerInfoFromFixture(3) + const peerInfos = await createPeerInfo({ number: 3 }) // Create 3 nodes, and turn HOP on for the relay ;[srcLibp2p, relayLibp2p, dstLibp2p] = peerInfos.map((peerInfo, index) => { const opts = baseOptions diff --git a/test/peer-discovery/index.node.js b/test/peer-discovery/index.node.js index bea051f6a2..5f8b13a348 100644 --- a/test/peer-discovery/index.node.js +++ b/test/peer-discovery/index.node.js @@ -15,14 +15,14 @@ const multiaddr = require('multiaddr') const Libp2p = require('../../src') const baseOptions = require('../utils/base-options') -const { createPeerInfoFromFixture } = require('../utils/creators/peer') +const { createPeerInfo } = require('../utils/creators/peer') describe('peer discovery scenarios', () => { let peerInfo, remotePeerInfo1, remotePeerInfo2 let libp2p before(async () => { - [peerInfo, remotePeerInfo1, remotePeerInfo2] = await createPeerInfoFromFixture(3) + [peerInfo, remotePeerInfo1, remotePeerInfo2] = await createPeerInfo({ number: 3 }) peerInfo.multiaddrs.add(multiaddr('/ip4/127.0.0.1/tcp/0')) remotePeerInfo1.multiaddrs.add(multiaddr('/ip4/127.0.0.1/tcp/0')) diff --git a/test/peer-discovery/index.spec.js b/test/peer-discovery/index.spec.js index b3cee86bdb..3f220e47fa 100644 --- a/test/peer-discovery/index.spec.js +++ b/test/peer-discovery/index.spec.js @@ -13,7 +13,7 @@ const MulticastDNS = require('libp2p-mdns') const Libp2p = require('../../src') const baseOptions = require('../utils/base-options.browser') -const { createPeerInfoFromFixture } = require('../utils/creators/peer') +const { createPeerInfo } = require('../utils/creators/peer') describe('peer discovery', () => { let peerInfo @@ -21,7 +21,7 @@ describe('peer discovery', () => { let libp2p before(async () => { - [peerInfo, remotePeerInfo] = await createPeerInfoFromFixture(2) + [peerInfo, remotePeerInfo] = await createPeerInfo({ number: 2 }) }) afterEach(async () => { diff --git a/test/peer-routing/peer-routing.node.js b/test/peer-routing/peer-routing.node.js new file mode 100644 index 0000000000..ade33628d6 --- /dev/null +++ b/test/peer-routing/peer-routing.node.js @@ -0,0 +1,219 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +const { expect } = chai +const nock = require('nock') +const sinon = require('sinon') + +const pDefer = require('p-defer') +const mergeOptions = require('merge-options') + +const DelegatedPeerRouter = require('libp2p-delegated-peer-routing') + +const peerUtils = require('../utils/creators/peer') +const { baseOptions, routingOptions } = require('./utils') + +describe('peer-routing', () => { + describe('no routers', () => { + let node + + before(async () => { + [node] = await peerUtils.createPeer({ + config: baseOptions + }) + }) + + it('.findPeer should return an error', async () => { + await expect(node.peerRouting.findPeer('a cid')) + .to.eventually.be.rejected() + .and.to.have.property('code', 'NO_ROUTERS_AVAILABLE') + }) + }) + + describe('via dht router', () => { + const number = 5 + let nodes + + before(async () => { + nodes = await peerUtils.createPeer({ + number, + config: routingOptions + }) + + // Ring dial + await Promise.all( + nodes.map((peer, i) => peer.dial(nodes[(i + 1) % number].peerInfo)) + ) + }) + + after(() => { + sinon.restore() + }) + + after(() => Promise.all(nodes.map((n) => n.stop()))) + + it('should use the nodes dht', () => { + const deferred = pDefer() + + sinon.stub(nodes[0]._dht, 'findPeer').callsFake(() => { + deferred.resolve() + return nodes[1].peerInfo + }) + + nodes[0].peerRouting.findPeer() + return deferred.promise + }) + }) + + describe('via delegate router', () => { + let node + let delegate + + beforeEach(async () => { + delegate = new DelegatedPeerRouter({ + host: '0.0.0.0', + protocol: 'http', + port: 60197 + }) + + ;[node] = await peerUtils.createPeer({ + config: mergeOptions(baseOptions, { + modules: { + peerRouting: [delegate] + }, + config: { + dht: { + enabled: false + } + } + }) + }) + }) + + afterEach(() => { + nock.cleanAll() + sinon.restore() + }) + + afterEach(() => node.stop()) + + it('should use the delegate router to find peers', async () => { + const deferred = pDefer() + + sinon.stub(delegate, 'findPeer').callsFake(() => { + deferred.resolve() + return 'fake peer-info' + }) + + await node.peerRouting.findPeer() + return deferred.promise + }) + + it('should be able to find a peer', async () => { + const peerKey = 'QmTp9VkYvnHyrqKQuFPiuZkiX9gPcqj6x5LJ1rmWuSySnL' + const mockApi = nock('http://0.0.0.0:60197') + .post('/api/v0/dht/findpeer') + .query({ + arg: peerKey + }) + .reply(200, `{"Extra":"","ID":"some other id","Responses":null,"Type":0}\n{"Extra":"","ID":"","Responses":[{"Addrs":["/ip4/127.0.0.1/tcp/4001"],"ID":"${peerKey}"}],"Type":2}\n`, [ + 'Content-Type', 'application/json', + 'X-Chunked-Output', '1' + ]) + + const peerInfo = await node.peerRouting.findPeer(peerKey) + + expect(peerInfo.id.toB58String()).to.equal(peerKey) + expect(mockApi.isDone()).to.equal(true) + }) + + it('should error when a peer cannot be found', async () => { + const peerKey = 'key of a peer not on the network' + const mockApi = nock('http://0.0.0.0:60197') + .post('/api/v0/dht/findpeer') + .query({ + arg: peerKey + }) + .reply(200, '{"Extra":"","ID":"some other id","Responses":null,"Type":6}\n{"Extra":"","ID":"yet another id","Responses":null,"Type":0}\n{"Extra":"routing:not found","ID":"","Responses":null,"Type":3}\n', [ + 'Content-Type', 'application/json', + 'X-Chunked-Output', '1' + ]) + + await expect(node.peerRouting.findPeer(peerKey)) + .to.eventually.be.rejected() + + expect(mockApi.isDone()).to.equal(true) + }) + + it('should handle errors from the api', async () => { + const peerKey = 'key of a peer not on the network' + const mockApi = nock('http://0.0.0.0:60197') + .post('/api/v0/dht/findpeer') + .query({ + arg: peerKey + }) + .reply(502) + + await expect(node.peerRouting.findPeer(peerKey)) + .to.eventually.be.rejected() + + expect(mockApi.isDone()).to.equal(true) + }) + }) + + describe('via dht and delegate routers', () => { + let node + let delegate + + beforeEach(async () => { + delegate = new DelegatedPeerRouter({ + host: '0.0.0.0', + protocol: 'http', + port: 60197 + }) + + ;[node] = await peerUtils.createPeer({ + config: mergeOptions(routingOptions, { + modules: { + peerRouting: [delegate] + } + }) + }) + }) + + afterEach(() => { + sinon.restore() + }) + + afterEach(() => node.stop()) + + it('should only use the dht if it finds the peer', async () => { + const dhtDeferred = pDefer() + + sinon.stub(node._dht, 'findPeer').callsFake(() => { + dhtDeferred.resolve() + return node.peerInfo + }) + sinon.stub(delegate, 'findPeer').callsFake(() => { + throw new Error('the delegate should not have been called') + }) + + await node.peerRouting.findPeer('a peer id') + await dhtDeferred.promise + }) + + it('should use the delegate if the dht fails to find the peer', async () => { + const results = [true] + + sinon.stub(node._dht, 'findPeer').callsFake(() => {}) + sinon.stub(delegate, 'findPeer').callsFake(() => { + return results + }) + + const peer = await node.peerRouting.findPeer('a peer id') + expect(peer).to.eql(results) + }) + }) +}) diff --git a/test/peer-routing/utils.js b/test/peer-routing/utils.js new file mode 100644 index 0000000000..120f7281df --- /dev/null +++ b/test/peer-routing/utils.js @@ -0,0 +1,24 @@ +'use strict' + +const KadDht = require('libp2p-kad-dht') +const mergeOptions = require('merge-options') +const baseOptions = require('../utils/base-options') + +module.exports.baseOptions = baseOptions + +const routingOptions = mergeOptions(baseOptions, { + modules: { + dht: KadDht + }, + config: { + dht: { + kBucketSize: 20, + randomWalk: { + enabled: true + }, + enabled: true + } + } +}) + +module.exports.routingOptions = routingOptions diff --git a/test/peer-store/peer-store.spec.js b/test/peer-store/peer-store.spec.js index 8dfba557d3..816773e8a3 100644 --- a/test/peer-store/peer-store.spec.js +++ b/test/peer-store/peer-store.spec.js @@ -34,7 +34,7 @@ describe('peer-store', () => { sinon.spy(peerStore, 'add') sinon.spy(peerStore, 'update') - const [peerInfo] = await peerUtils.createPeerInfo(1) + const [peerInfo] = await peerUtils.createPeerInfo() peerStore.on('peer', (peer) => { expect(peer).to.exist() @@ -51,7 +51,7 @@ describe('peer-store', () => { }) it('should update peer when it is already in the store', async () => { - const [peerInfo] = await peerUtils.createPeerInfo(1) + const [peerInfo] = await peerUtils.createPeerInfo() // Put the peer in the store peerStore.put(peerInfo) @@ -82,7 +82,7 @@ describe('peer-store', () => { it('should emit the "change:multiaddrs" event when a peer has new multiaddrs', async () => { const defer = pDefer() - const [createdPeerInfo] = await peerUtils.createPeerInfo(1) + const [createdPeerInfo] = await peerUtils.createPeerInfo() // Put the peer in the store peerStore.put(createdPeerInfo) @@ -110,7 +110,7 @@ describe('peer-store', () => { it('should emit the "change:protocols" event when a peer has new protocols', async () => { const defer = pDefer() - const [createdPeerInfo] = await peerUtils.createPeerInfo(1) + const [createdPeerInfo] = await peerUtils.createPeerInfo() // Put the peer in the store peerStore.put(createdPeerInfo) @@ -137,7 +137,7 @@ describe('peer-store', () => { }) it('should be able to retrieve a peer from store through its b58str id', async () => { - const [peerInfo] = await peerUtils.createPeerInfo(1) + const [peerInfo] = await peerUtils.createPeerInfo() const id = peerInfo.id.toB58String() let retrievedPeer = peerStore.get(id) @@ -154,7 +154,7 @@ describe('peer-store', () => { }) it('should be able to remove a peer from store through its b58str id', async () => { - const [peerInfo] = await peerUtils.createPeerInfo(1) + const [peerInfo] = await peerUtils.createPeerInfo() const id = peerInfo.id.toB58String() let removed = peerStore.remove(id) @@ -177,7 +177,7 @@ describe('peer-store on dial', () => { let remoteLibp2p before(async () => { - [peerInfo, remotePeerInfo] = await peerUtils.createPeerInfoFromFixture(2) + [peerInfo, remotePeerInfo] = await peerUtils.createPeerInfo({ number: 2 }) remoteLibp2p = new Libp2p(mergeOptions(baseOptions, { peerInfo: remotePeerInfo })) diff --git a/test/pubsub/configuration.node.js b/test/pubsub/configuration.node.js index 829e303f3d..b66c3f82d6 100644 --- a/test/pubsub/configuration.node.js +++ b/test/pubsub/configuration.node.js @@ -32,7 +32,7 @@ describe('Pubsub subsystem is configurable', () => { }) it('should start and stop by default once libp2p starts', async () => { - const [peerInfo] = await peerUtils.createPeerInfoFromFixture(1) + const [peerInfo] = await peerUtils.createPeerInfo() peerInfo.multiaddrs.add(listenAddr) const customOptions = mergeOptions(subsystemOptions, { @@ -50,7 +50,7 @@ describe('Pubsub subsystem is configurable', () => { }) it('should not start if disabled once libp2p starts', async () => { - const [peerInfo] = await peerUtils.createPeerInfoFromFixture(1) + const [peerInfo] = await peerUtils.createPeerInfo() peerInfo.multiaddrs.add(listenAddr) const customOptions = mergeOptions(subsystemOptions, { @@ -70,7 +70,7 @@ describe('Pubsub subsystem is configurable', () => { }) it('should allow a manual start', async () => { - const [peerInfo] = await peerUtils.createPeerInfoFromFixture(1) + const [peerInfo] = await peerUtils.createPeerInfo() peerInfo.multiaddrs.add(listenAddr) const customOptions = mergeOptions(subsystemOptions, { diff --git a/test/pubsub/implementations.node.js b/test/pubsub/implementations.node.js index cb2ee9f217..ef2e56fff1 100644 --- a/test/pubsub/implementations.node.js +++ b/test/pubsub/implementations.node.js @@ -29,7 +29,7 @@ describe('Pubsub subsystem is able to use different implementations', () => { let remAddr beforeEach(async () => { - [peerInfo, remotePeerInfo] = await peerUtils.createPeerInfoFromFixture(2) + [peerInfo, remotePeerInfo] = await peerUtils.createPeerInfo({ number: 2 }) peerInfo.multiaddrs.add(listenAddr) remotePeerInfo.multiaddrs.add(remoteListenAddr) diff --git a/test/pubsub/operation.node.js b/test/pubsub/operation.node.js index 5ffe3f34ab..3b41aa6dd8 100644 --- a/test/pubsub/operation.node.js +++ b/test/pubsub/operation.node.js @@ -24,7 +24,7 @@ describe('Pubsub subsystem operates correctly', () => { let remAddr beforeEach(async () => { - [peerInfo, remotePeerInfo] = await peerUtils.createPeerInfoFromFixture(2) + [peerInfo, remotePeerInfo] = await peerUtils.createPeerInfo({ number: 2 }) peerInfo.multiaddrs.add(listenAddr) remotePeerInfo.multiaddrs.add(remoteListenAddr) diff --git a/test/registrar/registrar.node.js b/test/registrar/registrar.node.js index c2357cdb88..1f914a55ab 100644 --- a/test/registrar/registrar.node.js +++ b/test/registrar/registrar.node.js @@ -23,7 +23,7 @@ describe('registrar on dial', () => { let remoteAddr before(async () => { - [peerInfo, remotePeerInfo] = await peerUtils.createPeerInfoFromFixture(2) + [peerInfo, remotePeerInfo] = await peerUtils.createPeerInfo({ number: 2 }) remoteLibp2p = new Libp2p(mergeOptions(baseOptions, { peerInfo: remotePeerInfo })) diff --git a/test/registrar/utils.js b/test/registrar/utils.js index 4b4e04839f..4f18684fc2 100644 --- a/test/registrar/utils.js +++ b/test/registrar/utils.js @@ -11,7 +11,7 @@ module.exports.createMockConnection = async (properties = {}) => { const localAddr = multiaddr('/ip4/127.0.0.1/tcp/8080') const remoteAddr = multiaddr('/ip4/127.0.0.1/tcp/8081') - const [localPeer, remotePeer] = await peerUtils.createPeerInfoFromFixture(2) + const [localPeer, remotePeer] = await peerUtils.createPeerInfo({ number: 2 }) const openStreams = [] let streamId = 0 diff --git a/test/utils/creators/peer.js b/test/utils/creators/peer.js index 88750bea1b..9fb1c3746e 100644 --- a/test/utils/creators/peer.js +++ b/test/utils/creators/peer.js @@ -1,32 +1,71 @@ 'use strict' +const pTimes = require('p-times') + +const multiaddr = require('multiaddr') const PeerId = require('peer-id') const PeerInfo = require('peer-info') +const Libp2p = require('../../../src') const Peers = require('../../fixtures/peers') +const defaultOptions = require('../base-options.browser') -async function createPeerInfo (length) { - const peers = await Promise.all( - Array.from({ length }) - .map((_, i) => PeerId.create()) - ) +const listenAddr = multiaddr('/ip4/127.0.0.1/tcp/0') - return peers.map((peer) => new PeerInfo(peer)) -} +/** + * Create libp2p nodes. + * @param {Object} [properties] + * @param {Object} [properties.config] + * @param {number} [properties.number] number of peers (default: 1). + * @param {boolean} [properties.fixture] use fixture for peer-id generation (default: true) + * @param {boolean} [properties.started] nodes should start (defaul: true) + * @return {Promise>} + */ +async function createPeer ({ number = 1, fixture = true, started = true, config = defaultOptions } = {}) { + const peerInfos = await createPeerInfo({ number, fixture }) -function createPeerIdsFromFixture (length) { - return Promise.all( - Array.from({ length }) - .map((_, i) => PeerId.createFromJSON(Peers[i])) - ) + const peers = await pTimes(number, (i) => Libp2p.create({ + peerInfo: peerInfos[i], + ...config + })) + + if (started) { + await Promise.all(peers.map((p) => { + p.peerInfo.multiaddrs.add(listenAddr) + return p.start() + })) + } + + return peers } -async function createPeerInfoFromFixture (length) { - const peers = await createPeerIdsFromFixture(length) +/** + * Create Peer-ids. + * @param {Object} [properties] + * @param {number} [properties.number] number of peers (default: 1). + * @param {boolean} [properties.fixture] use fixture for peer-id generation (default: true) + * @return {Promise>} + */ +async function createPeerInfo ({ number = 1, fixture = true } = {}) { + const peerIds = await createPeerId({ number, fixture }) + + return pTimes(number, (i) => PeerInfo.create(peerIds[i])) +} - return peers.map((peer) => new PeerInfo(peer)) +/** + * Create Peer-ids. + * @param {Object} [properties] + * @param {number} [properties.number] number of peers (default: 1). + * @param {boolean} [properties.fixture] use fixture for peer-id generation (default: true) + * @return {Promise>} + */ +function createPeerId ({ number = 1, fixture = true } = {}) { + return pTimes(number, (i) => fixture + ? PeerId.createFromJSON(Peers[i]) + : PeerId.create() + ) } +module.exports.createPeer = createPeer module.exports.createPeerInfo = createPeerInfo -module.exports.createPeerIdsFromFixture = createPeerIdsFromFixture -module.exports.createPeerInfoFromFixture = createPeerInfoFromFixture +module.exports.createPeerId = createPeerId diff --git a/test/utils/mockConnection.js b/test/utils/mockConnection.js index 5e6219ad90..022f5efce0 100644 --- a/test/utils/mockConnection.js +++ b/test/utils/mockConnection.js @@ -16,7 +16,7 @@ module.exports = async (properties = {}) => { const localAddr = multiaddr('/ip4/127.0.0.1/tcp/8080') const remoteAddr = multiaddr('/ip4/127.0.0.1/tcp/8081') - const [localPeer, remotePeer] = await peerUtils.createPeerInfoFromFixture(2) + const [localPeer, remotePeer] = await peerUtils.createPeerInfo({ number: 2 }) const openStreams = [] let streamId = 0 From 11ed6bd14c10d6e582c688d822fa72178c20e621 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Mon, 2 Dec 2019 18:13:14 +0100 Subject: [PATCH 21/92] feat: support peer-id instances in peer store operations (#491) --- src/peer-store/index.js | 22 +++++++++++++++++++--- test/peer-store/peer-store.spec.js | 4 ++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/peer-store/index.js b/src/peer-store/index.js index 5f52f279dc..559335a018 100644 --- a/src/peer-store/index.js +++ b/src/peer-store/index.js @@ -7,6 +7,7 @@ log.error = debug('libp2p:peer-store:error') const { EventEmitter } = require('events') +const PeerId = require('peer-id') const PeerInfo = require('peer-info') /** @@ -160,10 +161,15 @@ class PeerStore extends EventEmitter { /** * Get the info to the given id. - * @param {string} peerId b58str id + * @param {PeerId|string} peerId b58str id * @returns {PeerInfo} */ get (peerId) { + // TODO: deprecate this and just accept `PeerId` instances + if (PeerId.isPeerId(peerId)) { + peerId = peerId.toB58String() + } + const peerInfo = this.peers.get(peerId) if (peerInfo) { @@ -175,19 +181,29 @@ class PeerStore extends EventEmitter { /** * Has the info to the given id. - * @param {string} peerId b58str id + * @param {PeerId|string} peerId b58str id * @returns {boolean} */ has (peerId) { + // TODO: deprecate this and just accept `PeerId` instances + if (PeerId.isPeerId(peerId)) { + peerId = peerId.toB58String() + } + return this.peers.has(peerId) } /** * Removes the Peer with the matching `peerId` from the PeerStore - * @param {string} peerId b58str id + * @param {PeerId|string} peerId b58str id * @returns {boolean} true if found and removed */ remove (peerId) { + // TODO: deprecate this and just accept `PeerId` instances + if (PeerId.isPeerId(peerId)) { + peerId = peerId.toB58String() + } + return this.peers.delete(peerId) } diff --git a/test/peer-store/peer-store.spec.js b/test/peer-store/peer-store.spec.js index 816773e8a3..08da6e12bf 100644 --- a/test/peer-store/peer-store.spec.js +++ b/test/peer-store/peer-store.spec.js @@ -138,7 +138,7 @@ describe('peer-store', () => { it('should be able to retrieve a peer from store through its b58str id', async () => { const [peerInfo] = await peerUtils.createPeerInfo() - const id = peerInfo.id.toB58String() + const id = peerInfo.id let retrievedPeer = peerStore.get(id) expect(retrievedPeer).to.not.exist() @@ -155,7 +155,7 @@ describe('peer-store', () => { it('should be able to remove a peer from store through its b58str id', async () => { const [peerInfo] = await peerUtils.createPeerInfo() - const id = peerInfo.id.toB58String() + const id = peerInfo.id let removed = peerStore.remove(id) expect(removed).to.eql(false) From dbb9e573114de0c6a9b7b5a694d2e9f478d3aab1 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Tue, 3 Dec 2019 11:29:20 +0100 Subject: [PATCH 22/92] chore: update pubsub implementations (#493) --- package.json | 4 ++-- src/pubsub.js | 4 ++-- test/pubsub/implementations.node.js | 2 +- test/pubsub/operation.node.js | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index be3948ca35..755088762d 100644 --- a/package.json +++ b/package.json @@ -94,8 +94,8 @@ "libp2p-bootstrap": "^0.10.3", "libp2p-delegated-content-routing": "^0.4.1", "libp2p-delegated-peer-routing": "^0.4.0", - "libp2p-floodsub": "^0.19.0", - "libp2p-gossipsub": "^0.1.0", + "libp2p-floodsub": "^0.20.0", + "libp2p-gossipsub": "^0.2.0", "libp2p-kad-dht": "^0.18.0", "libp2p-mdns": "^0.13.0", "libp2p-mplex": "^0.9.1", diff --git a/src/pubsub.js b/src/pubsub.js index 2c067d993d..3f62358672 100644 --- a/src/pubsub.js +++ b/src/pubsub.js @@ -83,12 +83,12 @@ module.exports = (node, Pubsub, config) => { * @param {string} topic * @returns {Array} */ - getPeersSubscribed: (topic) => { + getSubscribers: (topic) => { if (!node.isStarted() && !pubsub.started) { throw errCode(new Error(messages.NOT_STARTED_YET), codes.PUBSUB_NOT_STARTED) } - return pubsub.getPeersSubscribed(topic) + return pubsub.getSubscribers(topic) }, setMaxListeners (n) { diff --git a/test/pubsub/implementations.node.js b/test/pubsub/implementations.node.js index ef2e56fff1..d0edfff6d7 100644 --- a/test/pubsub/implementations.node.js +++ b/test/pubsub/implementations.node.js @@ -85,7 +85,7 @@ describe('Pubsub subsystem is able to use different implementations', () => { // wait for remoteLibp2p to know about libp2p subscription await pWaitFor(() => { - const subscribedPeers = remoteLibp2p.pubsub.getPeersSubscribed(topic) + const subscribedPeers = remoteLibp2p.pubsub.getSubscribers(topic) return subscribedPeers.includes(libp2pId) }) diff --git a/test/pubsub/operation.node.js b/test/pubsub/operation.node.js index 3b41aa6dd8..6344868e01 100644 --- a/test/pubsub/operation.node.js +++ b/test/pubsub/operation.node.js @@ -89,7 +89,7 @@ describe('Pubsub subsystem operates correctly', () => { // wait for remoteLibp2p to know about libp2p subscription await pWaitFor(() => { - const subscribedPeers = remoteLibp2p.pubsub.getPeersSubscribed(topic) + const subscribedPeers = remoteLibp2p.pubsub.getSubscribers(topic) return subscribedPeers.includes(libp2pId) }) remoteLibp2p.pubsub.publish(topic, data) @@ -172,7 +172,7 @@ describe('Pubsub subsystem operates correctly', () => { // wait for remoteLibp2p to know about libp2p subscription await pWaitFor(() => { - const subscribedPeers = remoteLibp2p.pubsub.getPeersSubscribed(topic) + const subscribedPeers = remoteLibp2p.pubsub.getSubscribers(topic) return subscribedPeers.includes(libp2pId) }) From f3eb1f120192ccb57bd673c6366e06b1fba613b6 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Tue, 3 Dec 2019 20:14:15 +0100 Subject: [PATCH 23/92] fix: clean up peer discovery flow (#494) * fix: clean up peer discovery flow * test(fix): let libp2p start after connecting * test(fix): dont auto dial in disco tests --- src/identify/index.js | 9 ++++----- src/index.js | 32 +++++++++++++----------------- src/peer-store/index.js | 29 +++++++++++++-------------- test/dialing/direct.spec.js | 5 +++-- test/identify/index.spec.js | 26 ++++++++++++------------ test/peer-discovery/index.node.js | 32 ++++++++++++++++++++---------- test/peer-store/peer-store.spec.js | 2 -- test/registrar/registrar.spec.js | 2 +- 8 files changed, 71 insertions(+), 66 deletions(-) diff --git a/src/identify/index.js b/src/identify/index.js index ca1d70e47c..6f3490379a 100644 --- a/src/identify/index.js +++ b/src/identify/index.js @@ -148,10 +148,9 @@ class IdentifyService { * * @async * @param {Connection} connection - * @param {PeerID} expectedPeer The PeerId the identify response should match * @returns {Promise} */ - async identify (connection, expectedPeer) { + async identify (connection) { const { stream } = await connection.newStream(MULTICODEC_IDENTIFY) const [data] = await pipe( stream, @@ -181,7 +180,7 @@ class IdentifyService { const id = await PeerId.createFromPubKey(publicKey) const peerInfo = new PeerInfo(id) - if (expectedPeer && expectedPeer.toB58String() !== id.toB58String()) { + if (connection.remotePeer.toString() !== id.toString()) { throw errCode(new Error('identified peer does not match the expected peer'), codes.ERR_INVALID_PEER) } @@ -192,7 +191,7 @@ class IdentifyService { IdentifyService.updatePeerAddresses(peerInfo, listenAddrs) IdentifyService.updatePeerProtocols(peerInfo, protocols) - this.registrar.peerStore.update(peerInfo) + this.registrar.peerStore.replace(peerInfo) // TODO: Track our observed address so that we can score it log('received observed address of %s', observedAddr) } @@ -283,7 +282,7 @@ class IdentifyService { IdentifyService.updatePeerProtocols(peerInfo, message.protocols) // Update the peer in the PeerStore - this.registrar.peerStore.update(peerInfo) + this.registrar.peerStore.replace(peerInfo) } } diff --git a/src/index.js b/src/index.js index 245babd5e5..9d99228c55 100644 --- a/src/index.js +++ b/src/index.js @@ -54,9 +54,7 @@ class Libp2p extends EventEmitter { this.upgrader = new Upgrader({ localPeer: this.peerInfo.id, onConnection: (connection) => { - const peerInfo = getPeerInfo(connection.remotePeer) - - this.peerStore.put(peerInfo) + const peerInfo = this.peerStore.put(new PeerInfo(connection.remotePeer)) this.registrar.onConnect(peerInfo, connection) this.emit('peer:connect', peerInfo) }, @@ -144,7 +142,7 @@ class Libp2p extends EventEmitter { this.peerRouting = peerRouting(this) this.contentRouting = contentRouting(this) - this._peerDiscovered = this._peerDiscovered.bind(this) + this._onDiscoveryPeer = this._onDiscoveryPeer.bind(this) } /** @@ -340,7 +338,7 @@ class Libp2p extends EventEmitter { // TODO: this should be modified once random-walk is used as // the other discovery modules - this._dht.on('peer', this._peerDiscovered) + this._dht.on('peer', this._onDiscoveryPeer) } } @@ -351,6 +349,11 @@ class Libp2p extends EventEmitter { _onDidStart () { this._isStarted = true + this.peerStore.on('peer', peerInfo => { + this.emit('peer:discovery', peerInfo) + this._maybeConnect(peerInfo) + }) + // Peer discovery this._setupPeerDiscovery() @@ -362,24 +365,17 @@ class Libp2p extends EventEmitter { } /** - * Handles discovered peers. Each discovered peer will be emitted via - * the `peer:discovery` event. If auto dial is enabled for libp2p - * and the current connection count is under the low watermark, the - * peer will be dialed. + * Called whenever peer discovery services emit `peer` events. + * Known peers may be emitted. * @private * @param {PeerInfo} peerInfo */ - _peerDiscovered (peerInfo) { - if (peerInfo.id.toB58String() === this.peerInfo.id.toB58String()) { + _onDiscoveryPeer (peerInfo) { + if (peerInfo.id.toString() === this.peerInfo.id.toString()) { log.error(new Error(codes.ERR_DISCOVERED_SELF)) return } - peerInfo = this.peerStore.put(peerInfo) - - if (!this.isStarted()) return - - this.emit('peer:discovery', peerInfo) - this._maybeConnect(peerInfo) + this.peerStore.put(peerInfo) } /** @@ -432,7 +428,7 @@ class Libp2p extends EventEmitter { discoveryService = DiscoveryService } - discoveryService.on('peer', this._peerDiscovered) + discoveryService.on('peer', this._onDiscoveryPeer) this._discovery.push(discoveryService) } } diff --git a/src/peer-store/index.js b/src/peer-store/index.js index 559335a018..27f6d5929e 100644 --- a/src/peer-store/index.js +++ b/src/peer-store/index.js @@ -45,7 +45,7 @@ class PeerStore extends EventEmitter { let peer // Already know the peer? - if (this.peers.has(peerInfo.id.toB58String())) { + if (this.has(peerInfo.id)) { peer = this.update(peerInfo) } else { peer = this.add(peerInfo) @@ -118,15 +118,12 @@ class PeerStore extends EventEmitter { if (multiaddrsIntersection.length !== peerInfo.multiaddrs.size || multiaddrsIntersection.length !== recorded.multiaddrs.size) { - // recorded.multiaddrs = peerInfo.multiaddrs - recorded.multiaddrs.clear() - for (const ma of peerInfo.multiaddrs.toArray()) { recorded.multiaddrs.add(ma) } this.emit('change:multiaddrs', { - peerInfo: peerInfo, + peerInfo: recorded, multiaddrs: recorded.multiaddrs.toArray() }) } @@ -139,14 +136,12 @@ class PeerStore extends EventEmitter { if (protocolsIntersection.size !== peerInfo.protocols.size || protocolsIntersection.size !== recorded.protocols.size) { - recorded.protocols.clear() - for (const protocol of peerInfo.protocols) { recorded.protocols.add(protocol) } this.emit('change:protocols', { - peerInfo: peerInfo, + peerInfo: recorded, protocols: Array.from(recorded.protocols) }) } @@ -170,13 +165,7 @@ class PeerStore extends EventEmitter { peerId = peerId.toB58String() } - const peerInfo = this.peers.get(peerId) - - if (peerInfo) { - return peerInfo - } - - return undefined + return this.peers.get(peerId) } /** @@ -217,6 +206,16 @@ class PeerStore extends EventEmitter { this.remove(peerInfo.id.toB58String()) this.add(peerInfo) + + // This should be cleaned up in PeerStore v2 + this.emit('change:multiaddrs', { + peerInfo, + multiaddrs: peerInfo.multiaddrs.toArray() + }) + this.emit('change:protocols', { + peerInfo, + protocols: Array.from(peerInfo.protocols) + }) } } diff --git a/test/dialing/direct.spec.js b/test/dialing/direct.spec.js index f5eaff9aa8..57ff1d0048 100644 --- a/test/dialing/direct.spec.js +++ b/test/dialing/direct.spec.js @@ -216,7 +216,8 @@ describe('Dialing (direct, WebSockets)', () => { }) sinon.spy(libp2p.dialer.identifyService, 'identify') - sinon.spy(libp2p.peerStore, 'update') + sinon.spy(libp2p.peerStore, 'replace') + sinon.spy(libp2p.upgrader, 'onConnection') const connection = await libp2p.dialer.connectToMultiaddr(remoteAddr) expect(connection).to.exist() @@ -225,7 +226,7 @@ describe('Dialing (direct, WebSockets)', () => { expect(libp2p.dialer.identifyService.identify.callCount).to.equal(1) await libp2p.dialer.identifyService.identify.firstCall.returnValue - expect(libp2p.peerStore.update.callCount).to.equal(1) + expect(libp2p.peerStore.replace.callCount).to.equal(1) }) it('should be able to use hangup to close connections', async () => { diff --git a/test/identify/index.spec.js b/test/identify/index.spec.js index 8cc372c60d..d39ac0e295 100644 --- a/test/identify/index.spec.js +++ b/test/identify/index.spec.js @@ -47,7 +47,7 @@ describe('Identify', () => { protocols, registrar: { peerStore: { - update: () => {} + replace: () => {} } } }) @@ -57,17 +57,17 @@ describe('Identify', () => { }) const observedAddr = multiaddr('/ip4/127.0.0.1/tcp/1234') - const localConnectionMock = { newStream: () => {} } + const localConnectionMock = { newStream: () => {}, remotePeer: remotePeer.id } const remoteConnectionMock = { remoteAddr: observedAddr } const [local, remote] = duplexPair() sinon.stub(localConnectionMock, 'newStream').returns({ stream: local, protocol: multicodecs.IDENTIFY }) - sinon.spy(localIdentify.registrar.peerStore, 'update') + sinon.spy(localIdentify.registrar.peerStore, 'replace') // Run identify await Promise.all([ - localIdentify.identify(localConnectionMock, remotePeer.id), + localIdentify.identify(localConnectionMock), remoteIdentify.handleMessage({ connection: remoteConnectionMock, stream: remote, @@ -75,9 +75,9 @@ describe('Identify', () => { }) ]) - expect(localIdentify.registrar.peerStore.update.callCount).to.equal(1) + expect(localIdentify.registrar.peerStore.replace.callCount).to.equal(1) // Validate the remote peer gets updated in the peer store - const call = localIdentify.registrar.peerStore.update.firstCall + const call = localIdentify.registrar.peerStore.replace.firstCall expect(call.args[0].id.bytes).to.equal(remotePeer.id.bytes) }) @@ -92,7 +92,7 @@ describe('Identify', () => { }) const observedAddr = multiaddr('/ip4/127.0.0.1/tcp/1234') - const localConnectionMock = { newStream: () => {} } + const localConnectionMock = { newStream: () => {}, remotePeer } const remoteConnectionMock = { remoteAddr: observedAddr } const [local, remote] = duplexPair() @@ -128,7 +128,7 @@ describe('Identify', () => { peerInfo: remotePeer, registrar: { peerStore: { - update: () => {} + replace: () => {} } } }) @@ -148,7 +148,7 @@ describe('Identify', () => { sinon.spy(IdentifyService, 'updatePeerAddresses') sinon.spy(IdentifyService, 'updatePeerProtocols') - sinon.spy(remoteIdentify.registrar.peerStore, 'update') + sinon.spy(remoteIdentify.registrar.peerStore, 'replace') // Run identify await Promise.all([ @@ -163,8 +163,8 @@ describe('Identify', () => { expect(IdentifyService.updatePeerAddresses.callCount).to.equal(1) expect(IdentifyService.updatePeerProtocols.callCount).to.equal(1) - expect(remoteIdentify.registrar.peerStore.update.callCount).to.equal(1) - const [peerInfo] = remoteIdentify.registrar.peerStore.update.firstCall.args + expect(remoteIdentify.registrar.peerStore.replace.callCount).to.equal(1) + const [peerInfo] = remoteIdentify.registrar.peerStore.replace.firstCall.args expect(peerInfo.id.bytes).to.eql(localPeer.id.bytes) expect(peerInfo.multiaddrs.toArray()).to.eql([listeningAddr]) expect(peerInfo.protocols).to.eql(localProtocols) @@ -198,7 +198,7 @@ describe('Identify', () => { }) sinon.spy(libp2p.dialer.identifyService, 'identify') - sinon.spy(libp2p.peerStore, 'update') + sinon.spy(libp2p.peerStore, 'replace') const connection = await libp2p.dialer.connectToMultiaddr(remoteAddr) expect(connection).to.exist() @@ -207,7 +207,7 @@ describe('Identify', () => { expect(libp2p.dialer.identifyService.identify.callCount).to.equal(1) await libp2p.dialer.identifyService.identify.firstCall.returnValue - expect(libp2p.peerStore.update.callCount).to.equal(1) + expect(libp2p.peerStore.replace.callCount).to.equal(1) await connection.close() }) diff --git a/test/peer-discovery/index.node.js b/test/peer-discovery/index.node.js index 5f8b13a348..72babbf00a 100644 --- a/test/peer-discovery/index.node.js +++ b/test/peer-discovery/index.node.js @@ -48,6 +48,7 @@ describe('peer discovery scenarios', () => { }, config: { peerDiscovery: { + autoDial: false, bootstrap: { enabled: true, list: bootstrappers @@ -84,6 +85,7 @@ describe('peer discovery scenarios', () => { }, config: { peerDiscovery: { + autoDial: false, mdns: { enabled: true, interval: 200, // discover quickly @@ -111,9 +113,11 @@ describe('peer discovery scenarios', () => { } }) - await remoteLibp2p1.start() - await remoteLibp2p2.start() - await libp2p.start() + await Promise.all([ + remoteLibp2p1.start(), + remoteLibp2p2.start(), + libp2p.start() + ]) await deferred.promise @@ -130,11 +134,14 @@ describe('peer discovery scenarios', () => { dht: KadDht }, config: { + peerDiscovery: { + autoDial: false + }, dht: { randomWalk: { enabled: true, - delay: 100, - interval: 200, // start the query sooner + delay: 100, // start the first query quickly + interval: 1000, timeout: 3000 }, enabled: true @@ -153,9 +160,10 @@ describe('peer discovery scenarios', () => { } }) - await remoteLibp2p1.start() - await remoteLibp2p2.start() - await libp2p.start() + await Promise.all([ + remoteLibp2p1.start(), + remoteLibp2p2.start() + ]) // Topology: // A -> B @@ -165,8 +173,12 @@ describe('peer discovery scenarios', () => { remoteLibp2p2.dial(remotePeerInfo1) ]) + libp2p.start() + await deferred.promise - await remoteLibp2p1.stop() - await remoteLibp2p2.stop() + return Promise.all([ + remoteLibp2p1.stop(), + remoteLibp2p2.stop() + ]) }) }) diff --git a/test/peer-store/peer-store.spec.js b/test/peer-store/peer-store.spec.js index 08da6e12bf..95be069f99 100644 --- a/test/peer-store/peer-store.spec.js +++ b/test/peer-store/peer-store.spec.js @@ -56,7 +56,6 @@ describe('peer-store', () => { // Put the peer in the store peerStore.put(peerInfo) - sinon.spy(peerStore, 'put') sinon.spy(peerStore, 'add') sinon.spy(peerStore, 'update') @@ -75,7 +74,6 @@ describe('peer-store', () => { peerStore.put(peerInfo) - expect(peerStore.put.callCount).to.equal(1) expect(peerStore.add.callCount).to.equal(0) expect(peerStore.update.callCount).to.equal(1) }) diff --git a/test/registrar/registrar.spec.js b/test/registrar/registrar.spec.js index a56854d42b..563762376a 100644 --- a/test/registrar/registrar.spec.js +++ b/test/registrar/registrar.spec.js @@ -166,7 +166,7 @@ describe('registrar', () => { // Remove protocol to peer and update it peerInfo.protocols.delete(multicodec) - peerStore.put(peerInfo) + peerStore.replace(peerInfo) await onDisconnectDefer.promise }) From c4bc00be9c33f4f6410aca8a402db652747a8c1a Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Sun, 1 Dec 2019 18:10:31 +0100 Subject: [PATCH 24/92] fix: correct release readme --- RELEASE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 598d1ecacd..00492da3b8 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -50,8 +50,8 @@ Would you like to contribute to the libp2p project and don't know how? Well, the - Check the issues with the `help wanted` label in the [libp2p repo](https://github.com/libp2p/js-libp2p/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) - Join an IPFS All Hands, introduce yourself and let us know where you would like to contribute - https://github.com/ipfs/team-mgmt#all-hands-call - Hack with IPFS and show us what you made! The All Hands call is also the perfect venue for demos, join in and show us what you built -- Join the discussion at http://discuss.ipfs.io/ and help users finding their answers. -- Join the [⚡️ⒿⓈ Core Dev Team Weekly Sync 🙌🏽 ](https://github.com/ipfs/team-mgmt/issues/650) and be part of the Sprint action! +- Join the discussion at http://discuss.libp2p.io/ and help users finding their answers. +- Join the [⚡️IPFS Core Implementations Weekly Sync 🙌🏽 ](https://github.com/ipfs/team-mgmt/issues/992) and be part of the Sprint action! # ⁉️ Do you have questions? From 0cacfe29a52989ebd30a133046ef96eb7e4f30bd Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Mon, 2 Dec 2019 11:38:27 +0100 Subject: [PATCH 25/92] doc: add initial dialer readme --- doc/DIALER.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 doc/DIALER.md diff --git a/doc/DIALER.md b/doc/DIALER.md new file mode 100644 index 0000000000..eb91038a4d --- /dev/null +++ b/doc/DIALER.md @@ -0,0 +1,36 @@ +# js-libp2p Dialer + +**Synopsis** +* All Dial Requests in js-libp2p must request a token(s) from the Dialer. + * The number of tokens requested should be between 1 and the PER_PEER_LIMIT max set in the Dialer. + * If the number of available tokens is less than requested, the Dialer may return less than requested. +* The number of tokens a DialRequest obtains reflects the maximum number of parallel Multiaddr Dials it can make. +* If no tokens are available a DialRequest should immediately end and throw. This deviates from the existing queue system to avoid queue congestion and provide more visibility to users. +* As tokens are limited, DialRequests should be given a prioritized list of Multiaddrs to minimize the potential request time. +* Once a single Multiaddr Dial has succeeded, all pending dials in that Dial Request should be aborted. All tokens should be immediately released to the Dialer. +* If all Multiaddr Dials fail, or the DIAL_TIMEOUT max is reached for the entire DialRequest, all in progress dials for that DialRequest should be aborted. All tokens should immediately be released to the Dialer. + +## Multiaddr Confidence + +**Not Yet Implemented** + +An effective dialing system should involve the inclusion of a Confidence system for Multiaddrs. This enables ranking of Multiaddrs so that a prioritized list can be passed to DialRequests to maximize usage of Dialer Tokens. + +* All Multiaddrs SHOULD start with a confidence of 1 (give it some trust by default) +* A failed dial SHOULD lower the confidence by 1, to a min of 0. +* A successful dial SHOULD increase the confidence of a multiaddr by 1, to a max of 10. +* Multiaddrs of confidence 0 MAY be pruned if higher confidence addresses exist. If this is done, it should be done an an actual failure. Aborted dials SHOULD NOT be considered failures, but timeouts SHOULD. + +## Notes + +* A DialRequest gets a set of tokens from the Dialer, up to the PER_PEER_LIMIT max. +* A DialRequest SHOULD release its tokens after the DIAL_TIMEOUT has expired. This ensures that a peer with a large set of hanging addresses, does not block future dials. +* Upon releasing the tokens, the Dialer may allocate them to another DialRequest. +* A DialRequest SHOULD fail if no dial tokens are available. The DialRequest MAY proceed without tokens, but this should be reserved for High Priority actions and should be rare. +* A DialRequest MUST NOT request more tokens than it has addresses to dial. Example: If the PER_PEER_LIMIT max is 4 and a DialRequest has 1 address, it should only request 1 token. +* A DialRequest MUST execute parallel dials for each of its addresses up the total number of tokens it has. +* On a successful dial, the DialRequest MUST abort any other in progress dials, return the successful connection and release all tokens. +* A new DialRequest SHOULD be given a descending list of prioritized Multiaddrs, based on their confidence. Having higher confidence Multiaddrs first can help minimize the time a DialRequest is active. +* A failed dial to a Multiaddr SHOULD add that Multiaddr to a temporary denyList. +* A failed dial to a Multiaddr SHOULD lower the confidence of that Multiaddr. +* A successful dial to a Multiaddr SHOULD increase the confidence of that Multiaddr. From f8540fa3ed5339cb8fc27ad454d19f3f9f4b82ff Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Tue, 3 Dec 2019 10:28:52 +0100 Subject: [PATCH 26/92] feat: add token based dialer --- doc/DIALER.md | 1 + package.json | 2 + src/constants.js | 1 + src/dialer.js | 161 +++++++++++++---------- src/dialer/dial-request.js | 197 +++++++++++++++++++++++++++++ src/dialer/token-holder.js | 63 +++++++++ src/index.js | 20 ++- src/peer-store/index.js | 10 ++ src/util/index.js | 32 +++++ test/dialing/dial-resolver.spec.js | 83 ++++++++++++ test/dialing/direct.node.js | 69 +++++++--- test/dialing/direct.spec.js | 77 +++++++---- test/dialing/relay.node.js | 17 +-- test/identify/index.spec.js | 16 +-- 14 files changed, 611 insertions(+), 138 deletions(-) create mode 100644 src/dialer/dial-request.js create mode 100644 src/dialer/token-holder.js create mode 100644 test/dialing/dial-resolver.spec.js diff --git a/doc/DIALER.md b/doc/DIALER.md index eb91038a4d..211f07d101 100644 --- a/doc/DIALER.md +++ b/doc/DIALER.md @@ -9,6 +9,7 @@ * As tokens are limited, DialRequests should be given a prioritized list of Multiaddrs to minimize the potential request time. * Once a single Multiaddr Dial has succeeded, all pending dials in that Dial Request should be aborted. All tokens should be immediately released to the Dialer. * If all Multiaddr Dials fail, or the DIAL_TIMEOUT max is reached for the entire DialRequest, all in progress dials for that DialRequest should be aborted. All tokens should immediately be released to the Dialer. +* If a Multiaddr Dial fails and there are no more dials to use its token, that token should be immediately released to the Dialer. ## Multiaddr Confidence diff --git a/package.json b/package.json index 755088762d..4c844ac004 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ }, "dependencies": { "abort-controller": "^3.0.0", + "aggregate-error": "^3.0.1", "async": "^2.6.2", "async-iterator-all": "^1.0.0", "bignumber.js": "^9.0.0", @@ -67,6 +68,7 @@ "p-map": "^3.0.0", "p-queue": "^6.1.1", "p-settle": "^3.1.0", + "paramap-it": "^0.1.1", "peer-id": "^0.13.4", "peer-info": "^0.17.0", "promisify-es6": "^1.0.3", diff --git a/src/constants.js b/src/constants.js index 72c442d275..a66926b18c 100644 --- a/src/constants.js +++ b/src/constants.js @@ -6,6 +6,7 @@ module.exports = { DIAL_TIMEOUT: 30e3, // How long in ms a dial attempt is allowed to take MAX_COLD_CALLS: 50, // How many dials w/o protocols that can be queued MAX_PARALLEL_DIALS: 100, // Maximum allowed concurrent dials + PER_PEER_LIMIT: 4, // Allowed parallel dials per DialRequest QUARTER_HOUR: 15 * 60e3, PRIORITY_HIGH: 10, PRIORITY_LOW: 20 diff --git a/src/dialer.js b/src/dialer.js index 2154554fa6..1766e28de2 100644 --- a/src/dialer.js +++ b/src/dialer.js @@ -1,19 +1,20 @@ 'use strict' -const nextTick = require('async/nextTick') const multiaddr = require('multiaddr') const errCode = require('err-code') -const { default: PQueue } = require('p-queue') const AbortController = require('abort-controller') +const delay = require('delay') const debug = require('debug') const log = debug('libp2p:dialer') log.error = debug('libp2p:dialer:error') -const PeerId = require('peer-id') +const { DialRequest } = require('./dialer/dial-request') +const { anySignal } = require('./util') const { codes } = require('./errors') const { + DIAL_TIMEOUT, MAX_PARALLEL_DIALS, - DIAL_TIMEOUT + PER_PEER_LIMIT } = require('./constants') class Dialer { @@ -29,74 +30,76 @@ class Dialer { transportManager, peerStore, concurrency = MAX_PARALLEL_DIALS, - timeout = DIAL_TIMEOUT + timeout = DIAL_TIMEOUT, + perPeerLimit = PER_PEER_LIMIT }) { this.transportManager = transportManager this.peerStore = peerStore this.concurrency = concurrency this.timeout = timeout - this.queue = new PQueue({ concurrency, timeout, throwOnTimeout: true }) + this.perPeerLimit = perPeerLimit + this.tokens = [...new Array(concurrency)].map((_, index) => index) - /** - * @property {IdentifyService} - */ - this._identifyService = null - } - - set identifyService (service) { - this._identifyService = service - } - - /** - * @type {IdentifyService} - */ - get identifyService () { - return this._identifyService + this.releaseToken = this.releaseToken.bind(this) } /** * Connects to a given `Multiaddr`. `addr` should include the id of the peer being * dialed, it will be used for encryption verification. * - * @async * @param {Multiaddr} addr The address to dial * @param {object} [options] * @param {AbortSignal} [options.signal] An AbortController signal * @returns {Promise} */ - async connectToMultiaddr (addr, options = {}) { + connectToMultiaddr (addr, options = {}) { addr = multiaddr(addr) - let conn - let controller - if (!options.signal) { - controller = new AbortController() - options.signal = controller.signal - } + return this.connectToMultiaddrs([addr], options) + } + + /** + * Connects to the first success of a given list of `Multiaddr`. `addrs` should + * include the id of the peer being dialed, it will be used for encryption verification. + * + * @param {Array} addrs + * @param {object} [options] + * @param {AbortSignal} [options.signal] An AbortController signal + * @returns {Promise} + */ + async connectToMultiaddrs (addrs, options = {}) { + const dialAction = (addr, options) => this.transportManager.dial(addr, options) + const dialRequest = new DialRequest({ + addrs, + dialAction, + dialer: this + }) + + // Combine the timeout signal and options.signal, if provided + const timeoutController = new AbortController() + const signals = [timeoutController.signal] + options.signal && signals.push(options.signal) + const signal = anySignal(signals) + const timeoutPromise = delay.reject(this.timeout, { + value: errCode(new Error('Dial timed out'), codes.ERR_TIMEOUT) + }) try { - conn = await this.queue.add(() => this.transportManager.dial(addr, options)) + // Race the dial request and the timeout + const dialResult = await Promise.race([ + dialRequest.run({ + ...options, + signal + }), + timeoutPromise + ]) + timeoutPromise.clear() + return dialResult } catch (err) { - if (err.name === 'TimeoutError') { - controller.abort() - err.code = codes.ERR_TIMEOUT - } - log.error('Error dialing address %s,', addr, err) + log.error(err) + timeoutController.abort() throw err } - - // Perform a delayed Identify handshake - if (this.identifyService) { - nextTick(async () => { - try { - await this.identifyService.identify(conn, conn.remotePeer) - } catch (err) { - log.error(err) - } - }) - } - - return conn } /** @@ -104,31 +107,57 @@ class Dialer { * The dial to the first address that is successfully able to upgrade a connection * will be used. * - * @async - * @param {PeerInfo|PeerId} peer The remote peer to dial + * @param {PeerId} peerId The remote peer id to dial * @param {object} [options] * @param {AbortSignal} [options.signal] An AbortController signal * @returns {Promise} */ - async connectToPeer (peer, options = {}) { - if (PeerId.isPeerId(peer)) { - peer = this.peerStore.get(peer.toB58String()) - } + connectToPeer (peerId, options = {}) { + const addrs = this.peerStore.multiaddrsForPeer(peerId) - const addrs = peer.multiaddrs.toArray() - for (const addr of addrs) { - try { - return await this.connectToMultiaddr(addr, options) - } catch (_) { - // The error is already logged, just move to the next addr - continue - } - } + // TODO: ensure the peer id is on the multiaddr + + return this.connectToMultiaddrs(addrs, options) + } - const err = errCode(new Error('Could not dial peer, all addresses failed'), codes.ERR_CONNECTION_FAILED) - log.error(err) - throw err + getTokens (num) { + const total = Math.min(num, this.perPeerLimit, this.tokens.length) + const tokens = this.tokens.splice(0, total) + log('%d tokens request, returning %d, %d remaining', num, total, this.tokens.length) + return tokens + } + + releaseToken (token) { + log('token %d released', token) + this.tokens.push(token) } } module.exports = Dialer + +// class ActionLimiter { +// constructor(actions, options = {}) { +// this.actions = actions +// this.limit = options.limit || 4 +// this.controller = options.controller || new AbortController() +// } +// async abort () { +// this.controller.abort() +// } +// async run () { +// const limit = pLimit(this.limit) +// let result +// try { +// result = await pAny(this.actions.map(action => limit(action))) +// } catch (err) { +// console.log(err) +// if (!err.code) err.code = codes.ERR_CONNECTION_FAILED +// log.error(err) +// throw err +// } finally { +// console.log('RES', result) +// this.controller.abort() +// } +// return result +// } +// } diff --git a/src/dialer/dial-request.js b/src/dialer/dial-request.js new file mode 100644 index 0000000000..e45282bdc8 --- /dev/null +++ b/src/dialer/dial-request.js @@ -0,0 +1,197 @@ +'use strict' + +const AbortController = require('abort-controller') +const AggregateError = require('aggregate-error') +const pDefer = require('p-defer') +const debug = require('debug') +const log = debug('libp2p:dialer:request') +log.error = debug('libp2p:dialer:request:error') +const { AbortError } = require('libp2p-interfaces/src/transport/errors') + +const { anySignal } = require('../util') +const { TokenHolder } = require('./token-holder') + +class DialRequest { + /** + * + * @param {object} options + * @param {Multiaddr[]} options.addrs + * @param {TransportManager} options.transportManager + * @param {Dialer} options.dialer + */ + constructor ({ + addrs, + dialAction, + dialer + }) { + this.addrs = addrs + this.dialer = dialer + this.dialAction = dialAction + } + + /** + * @async + * @param {object} options + * @param {AbortSignal} options.signal An AbortController signal + * @param {number} options.timeout The max dial time for each request + * @returns {Connection} + */ + async run (options) { + // Determine how many tokens we need + const tokensWanted = Math.min(this.addrs.length, this.dialer.perPeerLimit) + // Get the tokens + const tokens = this.dialer.getTokens(tokensWanted) + // If no tokens are available, throw + if (tokens.length < 1) { + throw Object.assign(new Error('No dial tokens available'), { code: 'ERR_NO_DIAL_TOKENS' }) + } + + // For every token, run a multiaddr dial + // If there are tokens left, release them + // If there are multiaddrs left, wait for tokens to finish + const th = new TokenHolder(tokens, this.dialer.releaseToken) + + // Create the dial functions + const dials = this.addrs.map(addr => { + return () => this._abortableDial(addr, options) + }) + + const dialResolver = new DialResolver() + while (dials.length > 0) { + if (dialResolver.finished) break + // Wait for the next available token + const token = await th.getToken() + const dial = dials.shift() + dialResolver.add(dial, () => th.releaseToken(token)) + } + + // Start giving back the tokens + th.drain() + // Flush all the dials to get the final response + return dialResolver.flush() + } + + /** + * @private + * @param {Multiaddr} addr + * @param {object} options + * @param {AbortSignal} options.signal An AbortController signal + * @param {number} options.timeout The max dial time for each request + * @returns {{abort: function(), promise: Promise}} An AbortableDial + */ + _abortableDial (addr, options) { + log('starting dial to %s', addr) + const controller = new AbortController() + const signals = [controller.signal] + options.signal && signals.push(options.signal) + const signal = anySignal([controller.signal, options.signal]) + + const promise = this.dialAction(addr, { signal, timeout: options.timeout }) + return { + abort: () => controller.abort(), + promise + } + } +} + +class DialResolver { + constructor () { + this.dials = new Set() + this.errors = [] + this.finished = false + this.didFlush = false + this._waiting = null + } + + /** + * Adds a dial function to the resolver. The function will be immediately + * executed and its resolution tracked. + * @async + * @param {function()} dial A function that returns an AbortableDial + * @param {function()} [finallyHandler] Called when the dial resolves or rejects + */ + async add (dial, finallyHandler) { + if (this.finished) return + const abortableDial = dial() + this.dials.add(abortableDial) + try { + this._onResolve(await abortableDial.promise) + } catch (err) { + this._onReject(err) + } finally { + this._onFinally(abortableDial) + finallyHandler && finallyHandler() + } + } + + /** + * Called when a dial resolves + * @param {Connection} result + */ + _onResolve (result) { + this.result = result + } + + /** + * Called when a dial rejects + * @param {Error} err + */ + _onReject (err) { + if (err.code === AbortError.code) return + this.errors.push(err) + } + + _onFinally (dial) { + this.dials.delete(dial) + // If we have a result, or all dials have finished + if (this.result || (this._waiting && this.dials.size === 0)) { + this._onFinish() + } + } + + /** + * Called when dialing is completed, which means one of: + * 1. One dial succeeded + * 2. All dials failed + * 3. All dials were aborted + * @private + */ + _onFinish () { + this.finished = true + // Abort all remaining dials + for (const abortableDial of this.dials) { + abortableDial.abort() + } + this.dials.clear() + + // Flush must be called + if (!this._waiting) return + // If we have a result, or an abort occurred (no errors and no result) + if (this.result || this.errors.length === 0) { + this._waiting.resolve(this.result) + } else { + this._waiting.reject(new AggregateError(this.errors)) + } + } + + /** + * Flushes any remaining dials and resolves the first + * successful `Connection`. Flush should be called after all + * dials have been added. + * @returns {Promise} + */ + flush () { + if (this.finished) { + if (this.result) { + return Promise.resolve(this.result) + } else { + return Promise.reject(new AggregateError(this.errors)) + } + } + this._waiting = pDefer() + return this._waiting.promise + } +} + +module.exports.DialResolver = DialResolver +module.exports.DialRequest = DialRequest diff --git a/src/dialer/token-holder.js b/src/dialer/token-holder.js new file mode 100644 index 0000000000..acef56e296 --- /dev/null +++ b/src/dialer/token-holder.js @@ -0,0 +1,63 @@ +'use strict' + +/** + * @class TokenHolder + * @example + * const th = new TokenHolder(tokens, dialer.releaseToken) + * for (const action of actions) { + * const token = await th.getToken() + * action(token).then(() => th.releaseToken(token)) + * } + * + * await th.drain() + */ +class TokenHolder { + /** + * @param {Array<*>} tokens Tokens to track + * @param {function(*)} release Called when releasing control of the tokens + */ + constructor (tokens, release) { + this.originalTokens = tokens + this.tokens = [...tokens] + this._release = release + } + + /** + * Resolves a token once once is available. Once the token is no + * longer needed it MUST be release with `releaseToken()`. + * @returns {Promise<*>} + */ + getToken () { + if (this.tokens.length) return Promise.resolve(this.tokens.shift()) + return new Promise(resolve => { + const _push = this.tokens.push + this.tokens.push = (token) => { + this.tokens.push = _push + resolve(token) + } + }) + } + + /** + * Makes the token available via `getToken()` + * @param {*} token + */ + releaseToken (token) { + this.tokens.push(token) + } + + /** + * Once tokens are no longer needed for a series of actions, + * drain will release them to the owner via `this._release()` + */ + async drain () { + let drained = 0 + while (drained < this.originalTokens.length) { + this._release(await this.getToken()) + // Remove the token + drained++ + } + } +} + +module.exports.TokenHolder = TokenHolder diff --git a/src/index.js b/src/index.js index 9d99228c55..609edd32b7 100644 --- a/src/index.js +++ b/src/index.js @@ -57,6 +57,12 @@ class Libp2p extends EventEmitter { const peerInfo = this.peerStore.put(new PeerInfo(connection.remotePeer)) this.registrar.onConnect(peerInfo, connection) this.emit('peer:connect', peerInfo) + + // Run identify for every connection + if (this.identifyService) { + this.identifyService.identify(connection, connection.remotePeer) + .catch(log.error) + } }, onConnectionEnd: (connection) => { const peerInfo = getPeerInfo(connection.remotePeer) @@ -104,12 +110,12 @@ class Libp2p extends EventEmitter { }) // Add the identify service since we can multiplex - this.dialer.identifyService = new IdentifyService({ + this.identifyService = new IdentifyService({ registrar: this.registrar, peerInfo: this.peerInfo, protocols: this.upgrader.protocols }) - this.handle(Object.values(IDENTIFY_PROTOCOLS), this.dialer.identifyService.handleMessage) + this.handle(Object.values(IDENTIFY_PROTOCOLS), this.identifyService.handleMessage) } // Attach private network protector @@ -236,7 +242,7 @@ class Libp2p extends EventEmitter { connection = await this.dialer.connectToMultiaddr(peer, options) } else { peer = await getPeerInfoRemote(peer, this) - connection = await this.dialer.connectToPeer(peer, options) + connection = await this.dialer.connectToPeer(peer.id, options) } const peerInfo = getPeerInfo(connection.remotePeer) @@ -293,8 +299,8 @@ class Libp2p extends EventEmitter { }) // Only push if libp2p is running - if (this.isStarted()) { - this.dialer.identifyService.pushToPeerStore(this.peerStore) + if (this.isStarted() && this.identifyService) { + this.identifyService.pushToPeerStore(this.peerStore) } } @@ -310,8 +316,8 @@ class Libp2p extends EventEmitter { }) // Only push if libp2p is running - if (this.isStarted()) { - this.dialer.identifyService.pushToPeerStore(this.peerStore) + if (this.isStarted() && this.identifyService) { + this.identifyService.pushToPeerStore(this.peerStore) } } diff --git a/src/peer-store/index.js b/src/peer-store/index.js index 27f6d5929e..321fd204e9 100644 --- a/src/peer-store/index.js +++ b/src/peer-store/index.js @@ -217,6 +217,16 @@ class PeerStore extends EventEmitter { protocols: Array.from(peerInfo.protocols) }) } + + /** + * Returns the known multiaddrs for a given `PeerId` + * @param {PeerId} peerId + * @returns {Array} + */ + multiaddrsForPeer (peerId) { + const peerInfo = this.get(peerId.toB58String()) + return peerInfo.multiaddrs.toArray() + } } module.exports = PeerStore diff --git a/src/util/index.js b/src/util/index.js index bca13a4530..a53d537ef5 100644 --- a/src/util/index.js +++ b/src/util/index.js @@ -1,5 +1,7 @@ 'use strict' +const AbortController = require('abort-controller') + /** * Converts BufferList messages to Buffers * @param {*} source @@ -13,4 +15,34 @@ function toBuffer (source) { })() } +/** + * Takes an array of AbortSignals and returns a single signal. + * If any signals are aborted, the returned signal will be aborted. + * @param {Array} signals + * @returns {AbortSignal} + */ +function anySignal (signals) { + const controller = new AbortController() + + function onAbort () { + controller.abort() + + // Cleanup + for (const signal of signals) { + signal.removeEventListener('abort', onAbort) + } + } + + for (const signal of signals) { + if (signal.aborted) { + onAbort() + break + } + signal.addEventListener('abort', onAbort) + } + + return controller.signal +} + module.exports.toBuffer = toBuffer +module.exports.anySignal = anySignal diff --git a/test/dialing/dial-resolver.spec.js b/test/dialing/dial-resolver.spec.js new file mode 100644 index 0000000000..105b71e211 --- /dev/null +++ b/test/dialing/dial-resolver.spec.js @@ -0,0 +1,83 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +chai.use(require('chai-as-promised')) +const { expect } = chai +const sinon = require('sinon') +const pDefer = require('p-defer') +const pWaitFor = require('p-wait-for') +const AggregateError = require('aggregate-error') +const { AbortError } = require('libp2p-interfaces/src/transport/errors') + +const { DialResolver } = require('../../src/dialer/dial-request') + +const mockAbortableDial = () => { + const deferred = pDefer() + function dial () { + return { + promise: deferred.promise, + abort: () => deferred.reject(new AbortError()) + } + } + dial.reject = deferred.reject + dial.resolve = deferred.resolve + return dial +} + +describe('DialResolver', () => { + it('should not run subsequent dials if finished', async () => { + const deferred = pDefer() + const dial = sinon.stub().callsFake(() => { + return deferred + }) + const dialResolver = new DialResolver() + dialResolver.add(dial) + deferred.resolve(true) + + await pWaitFor(() => dialResolver.finished === true) + + dialResolver.add(dial) + expect(dial.callCount).to.equal(1) + }) + + it('.flush should throw if all dials errored', async () => { + const dialResolver = new DialResolver() + const dials = [ + mockAbortableDial(), + mockAbortableDial(), + mockAbortableDial() + ] + for (const dial of dials) { + dialResolver.add(dial) + dial.reject(new Error('transport error')) + } + + await expect(dialResolver.flush()).to.eventually.be.rejectedWith(AggregateError) + .and.to.have.nested.property('._errors.length', 3) + }) + + it('.flush should resolve the successful dial', async () => { + const dialResolver = new DialResolver() + const mockConn = {} + const dials = [ + mockAbortableDial(), + mockAbortableDial(), + mockAbortableDial() + ] + + // Make the first succeed + const successfulDial = dials.shift() + dialResolver.add(successfulDial) + successfulDial.resolve(mockConn) + + // Error the rest + for (const dial of dials) { + dialResolver.add(dial) + dial.reject(new Error('transport error')) + } + + await expect(dialResolver.flush()).to.eventually.be(mockConn) + }) +}) diff --git a/test/dialing/direct.node.js b/test/dialing/direct.node.js index 8fbe5d7944..fa903e2f4b 100644 --- a/test/dialing/direct.node.js +++ b/test/dialing/direct.node.js @@ -15,6 +15,7 @@ const PeerInfo = require('peer-info') const delay = require('delay') const pDefer = require('p-defer') const pipe = require('it-pipe') +const AggregateError = require('aggregate-error') const Libp2p = require('../../src') const Dialer = require('../../src/dialer') @@ -79,15 +80,19 @@ describe('Dialing (direct, TCP)', () => { const dialer = new Dialer({ transportManager: localTM }) await expect(dialer.connectToMultiaddr(unsupportedAddr)) - .to.eventually.be.rejected() - .and.to.have.property('code', ErrorCodes.ERR_TRANSPORT_UNAVAILABLE) + .to.eventually.be.rejectedWith(AggregateError) + .and.to.have.nested.property('._errors[0].code', ErrorCodes.ERR_TRANSPORT_UNAVAILABLE) }) it('should be able to connect to a given peer info', async () => { - const dialer = new Dialer({ transportManager: localTM }) + const dialer = new Dialer({ + transportManager: localTM, + peerStore: { + multiaddrsForPeer: () => [remoteAddr] + } + }) const peerId = await PeerId.createFromJSON(Peers[0]) const peerInfo = new PeerInfo(peerId) - peerInfo.multiaddrs.add(remoteAddr) const connection = await dialer.connectToPeer(peerInfo) expect(connection).to.exist() @@ -112,14 +117,18 @@ describe('Dialing (direct, TCP)', () => { }) it('should fail to connect to a given peer with unsupported addresses', async () => { - const dialer = new Dialer({ transportManager: localTM }) + const dialer = new Dialer({ + transportManager: localTM, + peerStore: { + multiaddrsForPeer: () => [unsupportedAddr] + } + }) const peerId = await PeerId.createFromJSON(Peers[0]) const peerInfo = new PeerInfo(peerId) - peerInfo.multiaddrs.add(unsupportedAddr) await expect(dialer.connectToPeer(peerInfo)) - .to.eventually.be.rejected() - .and.to.have.property('code', ErrorCodes.ERR_CONNECTION_FAILED) + .to.eventually.be.rejectedWith(AggregateError) + .and.to.have.nested.property('._errors[0].code', ErrorCodes.ERR_TRANSPORT_UNAVAILABLE) }) it('should abort dials on queue task timeout', async () => { @@ -136,7 +145,7 @@ describe('Dialing (direct, TCP)', () => { }) await expect(dialer.connectToMultiaddr(remoteAddr)) - .to.eventually.be.rejected() + .to.eventually.be.rejectedWith(Error) .and.to.have.property('code', ErrorCodes.ERR_TIMEOUT) }) @@ -146,25 +155,21 @@ describe('Dialing (direct, TCP)', () => { concurrency: 2 }) + expect(dialer.tokens).to.have.length(2) + const deferredDial = pDefer() sinon.stub(localTM, 'dial').callsFake(async () => { await deferredDial.promise }) - // Add 3 dials - Promise.all([ - dialer.connectToMultiaddr(remoteAddr), - dialer.connectToMultiaddr(remoteAddr), - dialer.connectToMultiaddr(remoteAddr) - ]) + // Perform 3 multiaddr dials + dialer.connectToMultiaddrs([remoteAddr, remoteAddr, remoteAddr]) // Let the call stack run await delay(0) // We should have 2 in progress, and 1 waiting - expect(localTM.dial.callCount).to.equal(2) - expect(dialer.queue.pending).to.equal(2) - expect(dialer.queue.size).to.equal(1) + expect(dialer.tokens).to.have.length(0) deferredDial.resolve() @@ -172,8 +177,7 @@ describe('Dialing (direct, TCP)', () => { await delay(0) // All dials should have executed expect(localTM.dial.callCount).to.equal(3) - expect(dialer.queue.pending).to.equal(0) - expect(dialer.queue.size).to.equal(0) + expect(dialer.tokens).to.have.length(2) }) describe('libp2p.dialer', () => { @@ -214,7 +218,7 @@ describe('Dialing (direct, TCP)', () => { after(() => remoteLibp2p.stop()) - it('should use the dialer for connecting', async () => { + it('should use the dialer for connecting to a multiaddr', async () => { libp2p = new Libp2p({ peerInfo, modules: { @@ -235,6 +239,29 @@ describe('Dialing (direct, TCP)', () => { expect(libp2p.dialer.connectToMultiaddr.callCount).to.equal(1) }) + it('should use the dialer for connecting to a peer', async () => { + libp2p = new Libp2p({ + peerInfo, + modules: { + transport: [Transport], + streamMuxer: [Muxer], + connEncryption: [Crypto] + } + }) + + sinon.spy(libp2p.dialer, 'connectToMultiaddrs') + const remotePeer = new PeerInfo(remoteLibp2p.peerInfo.id) + remotePeer.multiaddrs.add(remoteAddr) + + const connection = await libp2p.dial(remotePeer) + expect(connection).to.exist() + const { stream, protocol } = await connection.newStream('/echo/1.0.0') + expect(stream).to.exist() + expect(protocol).to.equal('/echo/1.0.0') + await connection.close() + expect(libp2p.dialer.connectToMultiaddrs.callCount).to.equal(1) + }) + it('should be able to use hangup to close connections', async () => { libp2p = new Libp2p({ peerInfo, diff --git a/test/dialing/direct.spec.js b/test/dialing/direct.spec.js index 57ff1d0048..ad538251a6 100644 --- a/test/dialing/direct.spec.js +++ b/test/dialing/direct.spec.js @@ -7,6 +7,7 @@ chai.use(require('chai-as-promised')) const { expect } = chai const sinon = require('sinon') const pDefer = require('p-defer') +const pWaitFor = require('p-wait-for') const delay = require('delay') const Transport = require('libp2p-websockets') const Muxer = require('libp2p-mplex') @@ -14,6 +15,7 @@ const Crypto = require('libp2p-secio') const multiaddr = require('multiaddr') const PeerId = require('peer-id') const PeerInfo = require('peer-info') +const AggregateError = require('aggregate-error') const { codes: ErrorCodes } = require('../../src/errors') const Constants = require('../../src/constants') @@ -49,6 +51,22 @@ describe('Dialing (direct, WebSockets)', () => { expect(dialer.timeout).to.equal(Constants.DIAL_TIMEOUT) }) + it('should limit the number of tokens it provides', () => { + const dialer = new Dialer({ transportManager: localTM }) + const maxPerPeer = Constants.PER_PEER_LIMIT + expect(dialer.tokens).to.have.length(Constants.MAX_PARALLEL_DIALS) + const tokens = dialer.getTokens(maxPerPeer + 1) + expect(tokens).to.have.length(maxPerPeer) + expect(dialer.tokens).to.have.length(Constants.MAX_PARALLEL_DIALS - maxPerPeer) + }) + + it('should not return tokens if non are left', () => { + const dialer = new Dialer({ transportManager: localTM }) + sinon.stub(dialer, 'tokens').value([]) + const tokens = dialer.getTokens(1) + expect(tokens.length).to.equal(0) + }) + it('should be able to connect to a remote node via its multiaddr', async () => { const dialer = new Dialer({ transportManager: localTM }) @@ -69,30 +87,36 @@ describe('Dialing (direct, WebSockets)', () => { const dialer = new Dialer({ transportManager: localTM }) await expect(dialer.connectToMultiaddr(unsupportedAddr)) - .to.eventually.be.rejected() - .and.to.have.property('code', ErrorCodes.ERR_TRANSPORT_DIAL_FAILED) + .to.eventually.be.rejectedWith(AggregateError) + .and.to.have.nested.property('._errors[0].code', ErrorCodes.ERR_TRANSPORT_DIAL_FAILED) }) it('should be able to connect to a given peer', async () => { - const dialer = new Dialer({ transportManager: localTM }) + const dialer = new Dialer({ + transportManager: localTM, + peerStore: { + multiaddrsForPeer: () => [remoteAddr] + } + }) const peerId = await PeerId.createFromJSON(Peers[0]) - const peerInfo = new PeerInfo(peerId) - peerInfo.multiaddrs.add(remoteAddr) - const connection = await dialer.connectToPeer(peerInfo) + const connection = await dialer.connectToPeer(peerId) expect(connection).to.exist() await connection.close() }) it('should fail to connect to a given peer with unsupported addresses', async () => { - const dialer = new Dialer({ transportManager: localTM }) + const dialer = new Dialer({ + transportManager: localTM, + peerStore: { + multiaddrsForPeer: () => [unsupportedAddr] + } + }) const peerId = await PeerId.createFromJSON(Peers[0]) - const peerInfo = new PeerInfo(peerId) - peerInfo.multiaddrs.add(unsupportedAddr) - await expect(dialer.connectToPeer(peerInfo)) - .to.eventually.be.rejected() - .and.to.have.property('code', ErrorCodes.ERR_CONNECTION_FAILED) + await expect(dialer.connectToPeer(peerId)) + .to.eventually.be.rejectedWith(AggregateError) + .and.to.have.nested.property('._errors[0].code', ErrorCodes.ERR_TRANSPORT_DIAL_FAILED) }) it('should abort dials on queue task timeout', async () => { @@ -119,25 +143,21 @@ describe('Dialing (direct, WebSockets)', () => { concurrency: 2 }) + expect(dialer.tokens).to.have.length(2) + const deferredDial = pDefer() sinon.stub(localTM, 'dial').callsFake(async () => { await deferredDial.promise }) - // Add 3 dials - Promise.all([ - dialer.connectToMultiaddr(remoteAddr), - dialer.connectToMultiaddr(remoteAddr), - dialer.connectToMultiaddr(remoteAddr) - ]) + // Perform 3 multiaddr dials + dialer.connectToMultiaddrs([remoteAddr, remoteAddr, remoteAddr]) // Let the call stack run await delay(0) // We should have 2 in progress, and 1 waiting - expect(localTM.dial.callCount).to.equal(2) - expect(dialer.queue.pending).to.equal(2) - expect(dialer.queue.size).to.equal(1) + expect(dialer.tokens).to.have.length(0) deferredDial.resolve() @@ -145,8 +165,7 @@ describe('Dialing (direct, WebSockets)', () => { await delay(0) // All dials should have executed expect(localTM.dial.callCount).to.equal(3) - expect(dialer.queue.pending).to.equal(0) - expect(dialer.queue.size).to.equal(0) + expect(dialer.tokens).to.have.length(2) }) describe('libp2p.dialer', () => { @@ -215,16 +234,18 @@ describe('Dialing (direct, WebSockets)', () => { } }) - sinon.spy(libp2p.dialer.identifyService, 'identify') + sinon.spy(libp2p.identifyService, 'identify') sinon.spy(libp2p.peerStore, 'replace') sinon.spy(libp2p.upgrader, 'onConnection') const connection = await libp2p.dialer.connectToMultiaddr(remoteAddr) expect(connection).to.exist() - // Wait for setImmediate to trigger the identify call - await delay(1) - expect(libp2p.dialer.identifyService.identify.callCount).to.equal(1) - await libp2p.dialer.identifyService.identify.firstCall.returnValue + + // Wait for onConnection to be called + await pWaitFor(() => libp2p.upgrader.onConnection.callCount === 1) + + expect(libp2p.identifyService.identify.callCount).to.equal(1) + await libp2p.identifyService.identify.firstCall.returnValue expect(libp2p.peerStore.replace.callCount).to.equal(1) }) diff --git a/test/dialing/relay.node.js b/test/dialing/relay.node.js index 8eb0be2dfd..b5ce59522f 100644 --- a/test/dialing/relay.node.js +++ b/test/dialing/relay.node.js @@ -10,6 +10,7 @@ const sinon = require('sinon') const multiaddr = require('multiaddr') const { collect } = require('streaming-iterables') const pipe = require('it-pipe') +const AggregateError = require('aggregate-error') const { createPeerInfo } = require('../utils/creators/peer') const baseOptions = require('../utils/base-options') const Libp2p = require('../../src') @@ -93,8 +94,8 @@ describe('Dialing (via relay, TCP)', () => { .encapsulate(`/p2p-circuit/p2p/${dstLibp2p.peerInfo.id.toString()}`) await expect(srcLibp2p.dial(dialAddr)) - .to.eventually.be.rejected() - .and.to.have.property('code', Errors.ERR_HOP_REQUEST_FAILED) + .to.eventually.be.rejectedWith(AggregateError) + .and.to.have.nested.property('._errors[0].code', Errors.ERR_HOP_REQUEST_FAILED) }) it('should not stay connected to a relay when not already connected and HOP fails', async () => { @@ -106,8 +107,8 @@ describe('Dialing (via relay, TCP)', () => { .encapsulate(`/p2p-circuit/p2p/${dstLibp2p.peerInfo.id.toString()}`) await expect(srcLibp2p.dial(dialAddr)) - .to.eventually.be.rejected() - .and.to.have.property('code', Errors.ERR_HOP_REQUEST_FAILED) + .to.eventually.be.rejectedWith(AggregateError) + .and.to.have.nested.property('._errors[0].code', Errors.ERR_HOP_REQUEST_FAILED) // We should not be connected to the relay, because we weren't before the dial const srcToRelayConn = srcLibp2p.registrar.getConnection(relayLibp2p.peerInfo) @@ -125,8 +126,8 @@ describe('Dialing (via relay, TCP)', () => { await srcLibp2p.dial(relayAddr) await expect(srcLibp2p.dial(dialAddr)) - .to.eventually.be.rejected() - .and.to.have.property('code', Errors.ERR_HOP_REQUEST_FAILED) + .to.eventually.be.rejectedWith(AggregateError) + .and.to.have.nested.property('._errors[0].code', Errors.ERR_HOP_REQUEST_FAILED) const srcToRelayConn = srcLibp2p.registrar.getConnection(relayLibp2p.peerInfo) expect(srcToRelayConn).to.exist() @@ -152,8 +153,8 @@ describe('Dialing (via relay, TCP)', () => { }]) await expect(srcLibp2p.dial(dialAddr)) - .to.eventually.be.rejected() - .and.to.have.property('code', Errors.ERR_HOP_REQUEST_FAILED) + .to.eventually.be.rejectedWith(AggregateError) + .and.to.have.nested.property('._errors[0].code', Errors.ERR_HOP_REQUEST_FAILED) const dstToRelayConn = dstLibp2p.registrar.getConnection(relayLibp2p.peerInfo) expect(dstToRelayConn).to.exist() diff --git a/test/identify/index.spec.js b/test/identify/index.spec.js index d39ac0e295..5eede683db 100644 --- a/test/identify/index.spec.js +++ b/test/identify/index.spec.js @@ -197,15 +197,15 @@ describe('Identify', () => { peerInfo }) - sinon.spy(libp2p.dialer.identifyService, 'identify') + sinon.spy(libp2p.identifyService, 'identify') sinon.spy(libp2p.peerStore, 'replace') const connection = await libp2p.dialer.connectToMultiaddr(remoteAddr) expect(connection).to.exist() // Wait for nextTick to trigger the identify call await delay(1) - expect(libp2p.dialer.identifyService.identify.callCount).to.equal(1) - await libp2p.dialer.identifyService.identify.firstCall.returnValue + expect(libp2p.identifyService.identify.callCount).to.equal(1) + await libp2p.identifyService.identify.firstCall.returnValue expect(libp2p.peerStore.replace.callCount).to.equal(1) await connection.close() @@ -217,8 +217,8 @@ describe('Identify', () => { peerInfo }) - sinon.spy(libp2p.dialer.identifyService, 'identify') - sinon.spy(libp2p.dialer.identifyService, 'push') + sinon.spy(libp2p.identifyService, 'identify') + sinon.spy(libp2p.identifyService, 'push') sinon.spy(libp2p.peerStore, 'update') const connection = await libp2p.dialer.connectToMultiaddr(remoteAddr) @@ -227,15 +227,15 @@ describe('Identify', () => { await delay(1) // Wait for identify to finish - await libp2p.dialer.identifyService.identify.firstCall.returnValue + await libp2p.identifyService.identify.firstCall.returnValue sinon.stub(libp2p, 'isStarted').returns(true) libp2p.handle('/echo/2.0.0', () => {}) libp2p.unhandle('/echo/2.0.0') // Verify the remote peer is notified of both changes - expect(libp2p.dialer.identifyService.push.callCount).to.equal(2) - for (const call of libp2p.dialer.identifyService.push.getCalls()) { + expect(libp2p.identifyService.push.callCount).to.equal(2) + for (const call of libp2p.identifyService.push.getCalls()) { const [connections] = call.args expect(connections.length).to.equal(1) expect(connections[0].remotePeer.toB58String()).to.equal(remoteAddr.getPeerId()) From cba2c6d8b2492534108a254117b8be0b3ec31f63 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Tue, 3 Dec 2019 16:47:05 +0100 Subject: [PATCH 27/92] chore: remove commented code --- src/dialer.js | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/src/dialer.js b/src/dialer.js index 1766e28de2..8074200a96 100644 --- a/src/dialer.js +++ b/src/dialer.js @@ -134,30 +134,3 @@ class Dialer { } module.exports = Dialer - -// class ActionLimiter { -// constructor(actions, options = {}) { -// this.actions = actions -// this.limit = options.limit || 4 -// this.controller = options.controller || new AbortController() -// } -// async abort () { -// this.controller.abort() -// } -// async run () { -// const limit = pLimit(this.limit) -// let result -// try { -// result = await pAny(this.actions.map(action => limit(action))) -// } catch (err) { -// console.log(err) -// if (!err.code) err.code = codes.ERR_CONNECTION_FAILED -// log.error(err) -// throw err -// } finally { -// console.log('RES', result) -// this.controller.abort() -// } -// return result -// } -// } From 571fd3b7d14f564520ff642eb0df5de449331b9b Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Wed, 4 Dec 2019 13:53:32 +0100 Subject: [PATCH 28/92] chore: apply suggestions from code review Co-Authored-By: Vasco Santos Co-Authored-By: Alan Shaw --- src/constants.js | 2 +- src/dialer/dial-request.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/constants.js b/src/constants.js index a66926b18c..9cfcb77661 100644 --- a/src/constants.js +++ b/src/constants.js @@ -6,7 +6,7 @@ module.exports = { DIAL_TIMEOUT: 30e3, // How long in ms a dial attempt is allowed to take MAX_COLD_CALLS: 50, // How many dials w/o protocols that can be queued MAX_PARALLEL_DIALS: 100, // Maximum allowed concurrent dials - PER_PEER_LIMIT: 4, // Allowed parallel dials per DialRequest + MAX_PER_PEER_DIALS: 4, // Allowed parallel dials per DialRequest QUARTER_HOUR: 15 * 60e3, PRIORITY_HIGH: 10, PRIORITY_LOW: 20 diff --git a/src/dialer/dial-request.js b/src/dialer/dial-request.js index e45282bdc8..76c244331d 100644 --- a/src/dialer/dial-request.js +++ b/src/dialer/dial-request.js @@ -49,7 +49,7 @@ class DialRequest { // For every token, run a multiaddr dial // If there are tokens left, release them // If there are multiaddrs left, wait for tokens to finish - const th = new TokenHolder(tokens, this.dialer.releaseToken) + const th = new TokenHolder(tokens, t => this.dialer.releaseToken(t)) // Create the dial functions const dials = this.addrs.map(addr => { @@ -76,7 +76,7 @@ class DialRequest { * @param {Multiaddr} addr * @param {object} options * @param {AbortSignal} options.signal An AbortController signal - * @param {number} options.timeout The max dial time for each request + * @param {number} options.timeout The max dial time for each request in ms * @returns {{abort: function(), promise: Promise}} An AbortableDial */ _abortableDial (addr, options) { From d5405dbb08838d67210b4ea56ef33a012d3339c6 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Wed, 4 Dec 2019 13:58:23 +0100 Subject: [PATCH 29/92] refactor: PER_PEER_LIMIT is now MAX_PER_PEER_DIALS --- doc/DIALER.md | 6 +++--- src/dialer.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/DIALER.md b/doc/DIALER.md index 211f07d101..b3cb2745a8 100644 --- a/doc/DIALER.md +++ b/doc/DIALER.md @@ -2,7 +2,7 @@ **Synopsis** * All Dial Requests in js-libp2p must request a token(s) from the Dialer. - * The number of tokens requested should be between 1 and the PER_PEER_LIMIT max set in the Dialer. + * The number of tokens requested should be between 1 and the MAX_PER_PEER_DIALS max set in the Dialer. * If the number of available tokens is less than requested, the Dialer may return less than requested. * The number of tokens a DialRequest obtains reflects the maximum number of parallel Multiaddr Dials it can make. * If no tokens are available a DialRequest should immediately end and throw. This deviates from the existing queue system to avoid queue congestion and provide more visibility to users. @@ -24,11 +24,11 @@ An effective dialing system should involve the inclusion of a Confidence system ## Notes -* A DialRequest gets a set of tokens from the Dialer, up to the PER_PEER_LIMIT max. +* A DialRequest gets a set of tokens from the Dialer, up to the MAX_PER_PEER_DIALS max. * A DialRequest SHOULD release its tokens after the DIAL_TIMEOUT has expired. This ensures that a peer with a large set of hanging addresses, does not block future dials. * Upon releasing the tokens, the Dialer may allocate them to another DialRequest. * A DialRequest SHOULD fail if no dial tokens are available. The DialRequest MAY proceed without tokens, but this should be reserved for High Priority actions and should be rare. -* A DialRequest MUST NOT request more tokens than it has addresses to dial. Example: If the PER_PEER_LIMIT max is 4 and a DialRequest has 1 address, it should only request 1 token. +* A DialRequest MUST NOT request more tokens than it has addresses to dial. Example: If the MAX_PER_PEER_DIALS max is 4 and a DialRequest has 1 address, it should only request 1 token. * A DialRequest MUST execute parallel dials for each of its addresses up the total number of tokens it has. * On a successful dial, the DialRequest MUST abort any other in progress dials, return the successful connection and release all tokens. * A new DialRequest SHOULD be given a descending list of prioritized Multiaddrs, based on their confidence. Having higher confidence Multiaddrs first can help minimize the time a DialRequest is active. diff --git a/src/dialer.js b/src/dialer.js index 8074200a96..9bd2d31410 100644 --- a/src/dialer.js +++ b/src/dialer.js @@ -14,7 +14,7 @@ const { codes } = require('./errors') const { DIAL_TIMEOUT, MAX_PARALLEL_DIALS, - PER_PEER_LIMIT + MAX_PER_PEER_DIALS } = require('./constants') class Dialer { @@ -31,7 +31,7 @@ class Dialer { peerStore, concurrency = MAX_PARALLEL_DIALS, timeout = DIAL_TIMEOUT, - perPeerLimit = PER_PEER_LIMIT + perPeerLimit = MAX_PER_PEER_DIALS }) { this.transportManager = transportManager this.peerStore = peerStore From f9fe44f6b74a096a0138fb5be233d6760fb1e989 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Wed, 4 Dec 2019 15:59:01 +0100 Subject: [PATCH 30/92] chore: use any-signal module --- package.json | 1 + src/dialer.js | 2 +- src/dialer/dial-request.js | 2 +- src/util/index.js | 30 ------------------------------ 4 files changed, 3 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index 4c844ac004..8c33202a1e 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "dependencies": { "abort-controller": "^3.0.0", "aggregate-error": "^3.0.1", + "any-signal": "^1.0.0", "async": "^2.6.2", "async-iterator-all": "^1.0.0", "bignumber.js": "^9.0.0", diff --git a/src/dialer.js b/src/dialer.js index 9bd2d31410..6000c8d693 100644 --- a/src/dialer.js +++ b/src/dialer.js @@ -4,11 +4,11 @@ const multiaddr = require('multiaddr') const errCode = require('err-code') const AbortController = require('abort-controller') const delay = require('delay') +const anySignal = require('any-signal') const debug = require('debug') const log = debug('libp2p:dialer') log.error = debug('libp2p:dialer:error') const { DialRequest } = require('./dialer/dial-request') -const { anySignal } = require('./util') const { codes } = require('./errors') const { diff --git a/src/dialer/dial-request.js b/src/dialer/dial-request.js index 76c244331d..b273979904 100644 --- a/src/dialer/dial-request.js +++ b/src/dialer/dial-request.js @@ -3,12 +3,12 @@ const AbortController = require('abort-controller') const AggregateError = require('aggregate-error') const pDefer = require('p-defer') +const anySignal = require('any-signal') const debug = require('debug') const log = debug('libp2p:dialer:request') log.error = debug('libp2p:dialer:request:error') const { AbortError } = require('libp2p-interfaces/src/transport/errors') -const { anySignal } = require('../util') const { TokenHolder } = require('./token-holder') class DialRequest { diff --git a/src/util/index.js b/src/util/index.js index a53d537ef5..221b97f5be 100644 --- a/src/util/index.js +++ b/src/util/index.js @@ -15,34 +15,4 @@ function toBuffer (source) { })() } -/** - * Takes an array of AbortSignals and returns a single signal. - * If any signals are aborted, the returned signal will be aborted. - * @param {Array} signals - * @returns {AbortSignal} - */ -function anySignal (signals) { - const controller = new AbortController() - - function onAbort () { - controller.abort() - - // Cleanup - for (const signal of signals) { - signal.removeEventListener('abort', onAbort) - } - } - - for (const signal of signals) { - if (signal.aborted) { - onAbort() - break - } - signal.addEventListener('abort', onAbort) - } - - return controller.signal -} - module.exports.toBuffer = toBuffer -module.exports.anySignal = anySignal From ea62c52701a3cc2ca0451f2501a7120aaada20ad Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Wed, 4 Dec 2019 16:27:33 +0100 Subject: [PATCH 31/92] refactor: simplify DialRequest logic per feedback --- package.json | 1 + src/dialer.js | 7 +- src/dialer/dial-request.js | 174 ++++------------------------- src/dialer/token-holder.js | 63 ----------- test/dialing/dial-resolver.spec.js | 83 -------------- test/dialing/direct.node.js | 5 +- test/dialing/direct.spec.js | 7 +- 7 files changed, 36 insertions(+), 304 deletions(-) delete mode 100644 src/dialer/token-holder.js delete mode 100644 test/dialing/dial-resolver.spec.js diff --git a/package.json b/package.json index 8c33202a1e..df8c77b384 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "multistream-select": "^0.15.0", "once": "^1.4.0", "p-any": "^2.1.0", + "p-fifo": "^1.0.0", "p-map": "^3.0.0", "p-queue": "^6.1.1", "p-settle": "^3.1.0", diff --git a/src/dialer.js b/src/dialer.js index 6000c8d693..3306af0733 100644 --- a/src/dialer.js +++ b/src/dialer.js @@ -39,8 +39,6 @@ class Dialer { this.timeout = timeout this.perPeerLimit = perPeerLimit this.tokens = [...new Array(concurrency)].map((_, index) => index) - - this.releaseToken = this.releaseToken.bind(this) } /** @@ -68,7 +66,10 @@ class Dialer { * @returns {Promise} */ async connectToMultiaddrs (addrs, options = {}) { - const dialAction = (addr, options) => this.transportManager.dial(addr, options) + const dialAction = (addr, options) => { + if (options.signal.aborted) throw errCode(new Error('already aborted'), 'ERR_ALREADY_ABORTED') + return this.transportManager.dial(addr, options) + } const dialRequest = new DialRequest({ addrs, dialAction, diff --git a/src/dialer/dial-request.js b/src/dialer/dial-request.js index b273979904..910ced438e 100644 --- a/src/dialer/dial-request.js +++ b/src/dialer/dial-request.js @@ -2,14 +2,13 @@ const AbortController = require('abort-controller') const AggregateError = require('aggregate-error') -const pDefer = require('p-defer') const anySignal = require('any-signal') const debug = require('debug') +const errCode = require('err-code') const log = debug('libp2p:dialer:request') log.error = debug('libp2p:dialer:request:error') -const { AbortError } = require('libp2p-interfaces/src/transport/errors') - -const { TokenHolder } = require('./token-holder') +const FIFO = require('p-fifo') +const pAny = require('p-any') class DialRequest { /** @@ -37,161 +36,36 @@ class DialRequest { * @returns {Connection} */ async run (options) { - // Determine how many tokens we need - const tokensWanted = Math.min(this.addrs.length, this.dialer.perPeerLimit) - // Get the tokens - const tokens = this.dialer.getTokens(tokensWanted) + const tokens = this.dialer.getTokens(this.addrs.length) // If no tokens are available, throw if (tokens.length < 1) { - throw Object.assign(new Error('No dial tokens available'), { code: 'ERR_NO_DIAL_TOKENS' }) - } - - // For every token, run a multiaddr dial - // If there are tokens left, release them - // If there are multiaddrs left, wait for tokens to finish - const th = new TokenHolder(tokens, t => this.dialer.releaseToken(t)) - - // Create the dial functions - const dials = this.addrs.map(addr => { - return () => this._abortableDial(addr, options) - }) - - const dialResolver = new DialResolver() - while (dials.length > 0) { - if (dialResolver.finished) break - // Wait for the next available token - const token = await th.getToken() - const dial = dials.shift() - dialResolver.add(dial, () => th.releaseToken(token)) - } - - // Start giving back the tokens - th.drain() - // Flush all the dials to get the final response - return dialResolver.flush() - } - - /** - * @private - * @param {Multiaddr} addr - * @param {object} options - * @param {AbortSignal} options.signal An AbortController signal - * @param {number} options.timeout The max dial time for each request in ms - * @returns {{abort: function(), promise: Promise}} An AbortableDial - */ - _abortableDial (addr, options) { - log('starting dial to %s', addr) - const controller = new AbortController() - const signals = [controller.signal] - options.signal && signals.push(options.signal) - const signal = anySignal([controller.signal, options.signal]) - - const promise = this.dialAction(addr, { signal, timeout: options.timeout }) - return { - abort: () => controller.abort(), - promise + throw errCode(new Error('No dial tokens available'), 'ERR_NO_DIAL_TOKENS') } - } -} -class DialResolver { - constructor () { - this.dials = new Set() - this.errors = [] - this.finished = false - this.didFlush = false - this._waiting = null - } + const th = new FIFO() + tokens.forEach(t => th.push(t)) + const dialAbortControllers = this.addrs.map(() => new AbortController()) - /** - * Adds a dial function to the resolver. The function will be immediately - * executed and its resolution tracked. - * @async - * @param {function()} dial A function that returns an AbortableDial - * @param {function()} [finallyHandler] Called when the dial resolves or rejects - */ - async add (dial, finallyHandler) { - if (this.finished) return - const abortableDial = dial() - this.dials.add(abortableDial) try { - this._onResolve(await abortableDial.promise) - } catch (err) { - this._onReject(err) + return await pAny(this.addrs.map(async (addr, i) => { + const token = await th.shift() // get token + let conn + try { + const signal = dialAbortControllers[i].signal + conn = await this.dialAction(addr, { ...options, signal: anySignal([signal, options.signal]) }) + // Remove the successful AbortController so it is no aborted + dialAbortControllers.splice(i, 1) + } catch (err) { + th.push(token) // return to token holder on error so another ma can be attempted + throw err + } + return conn + })) } finally { - this._onFinally(abortableDial) - finallyHandler && finallyHandler() - } - } - - /** - * Called when a dial resolves - * @param {Connection} result - */ - _onResolve (result) { - this.result = result - } - - /** - * Called when a dial rejects - * @param {Error} err - */ - _onReject (err) { - if (err.code === AbortError.code) return - this.errors.push(err) - } - - _onFinally (dial) { - this.dials.delete(dial) - // If we have a result, or all dials have finished - if (this.result || (this._waiting && this.dials.size === 0)) { - this._onFinish() - } - } - - /** - * Called when dialing is completed, which means one of: - * 1. One dial succeeded - * 2. All dials failed - * 3. All dials were aborted - * @private - */ - _onFinish () { - this.finished = true - // Abort all remaining dials - for (const abortableDial of this.dials) { - abortableDial.abort() - } - this.dials.clear() - - // Flush must be called - if (!this._waiting) return - // If we have a result, or an abort occurred (no errors and no result) - if (this.result || this.errors.length === 0) { - this._waiting.resolve(this.result) - } else { - this._waiting.reject(new AggregateError(this.errors)) - } - } - - /** - * Flushes any remaining dials and resolves the first - * successful `Connection`. Flush should be called after all - * dials have been added. - * @returns {Promise} - */ - flush () { - if (this.finished) { - if (this.result) { - return Promise.resolve(this.result) - } else { - return Promise.reject(new AggregateError(this.errors)) - } + dialAbortControllers.map(c => c.abort()) // success/failure happened, abort everything else + tokens.forEach(t => this.dialer.releaseToken(t)) // release tokens back to the dialer } - this._waiting = pDefer() - return this._waiting.promise } } -module.exports.DialResolver = DialResolver module.exports.DialRequest = DialRequest diff --git a/src/dialer/token-holder.js b/src/dialer/token-holder.js deleted file mode 100644 index acef56e296..0000000000 --- a/src/dialer/token-holder.js +++ /dev/null @@ -1,63 +0,0 @@ -'use strict' - -/** - * @class TokenHolder - * @example - * const th = new TokenHolder(tokens, dialer.releaseToken) - * for (const action of actions) { - * const token = await th.getToken() - * action(token).then(() => th.releaseToken(token)) - * } - * - * await th.drain() - */ -class TokenHolder { - /** - * @param {Array<*>} tokens Tokens to track - * @param {function(*)} release Called when releasing control of the tokens - */ - constructor (tokens, release) { - this.originalTokens = tokens - this.tokens = [...tokens] - this._release = release - } - - /** - * Resolves a token once once is available. Once the token is no - * longer needed it MUST be release with `releaseToken()`. - * @returns {Promise<*>} - */ - getToken () { - if (this.tokens.length) return Promise.resolve(this.tokens.shift()) - return new Promise(resolve => { - const _push = this.tokens.push - this.tokens.push = (token) => { - this.tokens.push = _push - resolve(token) - } - }) - } - - /** - * Makes the token available via `getToken()` - * @param {*} token - */ - releaseToken (token) { - this.tokens.push(token) - } - - /** - * Once tokens are no longer needed for a series of actions, - * drain will release them to the owner via `this._release()` - */ - async drain () { - let drained = 0 - while (drained < this.originalTokens.length) { - this._release(await this.getToken()) - // Remove the token - drained++ - } - } -} - -module.exports.TokenHolder = TokenHolder diff --git a/test/dialing/dial-resolver.spec.js b/test/dialing/dial-resolver.spec.js deleted file mode 100644 index 105b71e211..0000000000 --- a/test/dialing/dial-resolver.spec.js +++ /dev/null @@ -1,83 +0,0 @@ -'use strict' -/* eslint-env mocha */ - -const chai = require('chai') -chai.use(require('dirty-chai')) -chai.use(require('chai-as-promised')) -const { expect } = chai -const sinon = require('sinon') -const pDefer = require('p-defer') -const pWaitFor = require('p-wait-for') -const AggregateError = require('aggregate-error') -const { AbortError } = require('libp2p-interfaces/src/transport/errors') - -const { DialResolver } = require('../../src/dialer/dial-request') - -const mockAbortableDial = () => { - const deferred = pDefer() - function dial () { - return { - promise: deferred.promise, - abort: () => deferred.reject(new AbortError()) - } - } - dial.reject = deferred.reject - dial.resolve = deferred.resolve - return dial -} - -describe('DialResolver', () => { - it('should not run subsequent dials if finished', async () => { - const deferred = pDefer() - const dial = sinon.stub().callsFake(() => { - return deferred - }) - const dialResolver = new DialResolver() - dialResolver.add(dial) - deferred.resolve(true) - - await pWaitFor(() => dialResolver.finished === true) - - dialResolver.add(dial) - expect(dial.callCount).to.equal(1) - }) - - it('.flush should throw if all dials errored', async () => { - const dialResolver = new DialResolver() - const dials = [ - mockAbortableDial(), - mockAbortableDial(), - mockAbortableDial() - ] - for (const dial of dials) { - dialResolver.add(dial) - dial.reject(new Error('transport error')) - } - - await expect(dialResolver.flush()).to.eventually.be.rejectedWith(AggregateError) - .and.to.have.nested.property('._errors.length', 3) - }) - - it('.flush should resolve the successful dial', async () => { - const dialResolver = new DialResolver() - const mockConn = {} - const dials = [ - mockAbortableDial(), - mockAbortableDial(), - mockAbortableDial() - ] - - // Make the first succeed - const successfulDial = dials.shift() - dialResolver.add(successfulDial) - successfulDial.resolve(mockConn) - - // Error the rest - for (const dial of dials) { - dialResolver.add(dial) - dial.reject(new Error('transport error')) - } - - await expect(dialResolver.flush()).to.eventually.be(mockConn) - }) -}) diff --git a/test/dialing/direct.node.js b/test/dialing/direct.node.js index fa903e2f4b..a24a39acc5 100644 --- a/test/dialing/direct.node.js +++ b/test/dialing/direct.node.js @@ -175,8 +175,9 @@ describe('Dialing (direct, TCP)', () => { // Let the call stack run await delay(0) - // All dials should have executed - expect(localTM.dial.callCount).to.equal(3) + + // Only two dials should be executed, as the first dial will succeed + expect(localTM.dial.callCount).to.equal(2) expect(dialer.tokens).to.have.length(2) }) diff --git a/test/dialing/direct.spec.js b/test/dialing/direct.spec.js index ad538251a6..0b6a9276cb 100644 --- a/test/dialing/direct.spec.js +++ b/test/dialing/direct.spec.js @@ -53,7 +53,7 @@ describe('Dialing (direct, WebSockets)', () => { it('should limit the number of tokens it provides', () => { const dialer = new Dialer({ transportManager: localTM }) - const maxPerPeer = Constants.PER_PEER_LIMIT + const maxPerPeer = Constants.MAX_PER_PEER_DIALS expect(dialer.tokens).to.have.length(Constants.MAX_PARALLEL_DIALS) const tokens = dialer.getTokens(maxPerPeer + 1) expect(tokens).to.have.length(maxPerPeer) @@ -163,8 +163,9 @@ describe('Dialing (direct, WebSockets)', () => { // Let the call stack run await delay(0) - // All dials should have executed - expect(localTM.dial.callCount).to.equal(3) + + // Only two dials will be run, as the first two succeeded + expect(localTM.dial.callCount).to.equal(2) expect(dialer.tokens).to.have.length(2) }) From 24c603741f1ae8cf7503be622848de2ae590e4f9 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Wed, 4 Dec 2019 16:33:04 +0100 Subject: [PATCH 32/92] feat: add early token recycling in --- src/dialer/dial-request.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/dialer/dial-request.js b/src/dialer/dial-request.js index 910ced438e..02a6675a17 100644 --- a/src/dialer/dial-request.js +++ b/src/dialer/dial-request.js @@ -45,6 +45,7 @@ class DialRequest { const th = new FIFO() tokens.forEach(t => th.push(t)) const dialAbortControllers = this.addrs.map(() => new AbortController()) + let completedDials = 0 try { return await pAny(this.addrs.map(async (addr, i) => { @@ -56,9 +57,17 @@ class DialRequest { // Remove the successful AbortController so it is no aborted dialAbortControllers.splice(i, 1) } catch (err) { - th.push(token) // return to token holder on error so another ma can be attempted throw err + } finally { + completedDials++ + // If we have more dials to make, recycle the token, otherwise release it + if (completedDials < this.addrs.length) { + th.push(token) + } else { + this.dialer.releaseToken(tokens.splice(tokens.indexOf(token), 1)[0]) + } } + return conn })) } finally { From a37c5c0144aaabff1ddc9678a746712442ebc4bd Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Wed, 4 Dec 2019 16:59:38 +0100 Subject: [PATCH 33/92] refactor: clean up dial timeout abort --- src/dialer.js | 20 +++++++------------- src/dialer/dial-request.js | 2 +- test/dialing/direct.node.js | 2 ++ test/dialing/direct.spec.js | 2 ++ 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/dialer.js b/src/dialer.js index 3306af0733..3cb7ae005f 100644 --- a/src/dialer.js +++ b/src/dialer.js @@ -81,24 +81,18 @@ class Dialer { const signals = [timeoutController.signal] options.signal && signals.push(options.signal) const signal = anySignal(signals) - const timeoutPromise = delay.reject(this.timeout, { - value: errCode(new Error('Dial timed out'), codes.ERR_TIMEOUT) - }) + const timeoutId = setTimeout(() => timeoutController.abort(), this.timeout) try { - // Race the dial request and the timeout - const dialResult = await Promise.race([ - dialRequest.run({ - ...options, - signal - }), - timeoutPromise - ]) - timeoutPromise.clear() + const dialResult = await dialRequest.run({ ...options, signal }) + clearTimeout(timeoutId) return dialResult } catch (err) { + // Error is a timeout + if (timeoutController.signal.aborted) { + err = errCode(err, codes.ERR_TIMEOUT) + } log.error(err) - timeoutController.abort() throw err } } diff --git a/src/dialer/dial-request.js b/src/dialer/dial-request.js index 02a6675a17..d4e952e82a 100644 --- a/src/dialer/dial-request.js +++ b/src/dialer/dial-request.js @@ -54,7 +54,7 @@ class DialRequest { try { const signal = dialAbortControllers[i].signal conn = await this.dialAction(addr, { ...options, signal: anySignal([signal, options.signal]) }) - // Remove the successful AbortController so it is no aborted + // Remove the successful AbortController so it is not aborted dialAbortControllers.splice(i, 1) } catch (err) { throw err diff --git a/test/dialing/direct.node.js b/test/dialing/direct.node.js index a24a39acc5..bac268c6de 100644 --- a/test/dialing/direct.node.js +++ b/test/dialing/direct.node.js @@ -16,6 +16,7 @@ const delay = require('delay') const pDefer = require('p-defer') const pipe = require('it-pipe') const AggregateError = require('aggregate-error') +const { AbortError } = require('libp2p-interfaces/src/transport/errors') const Libp2p = require('../../src') const Dialer = require('../../src/dialer') @@ -142,6 +143,7 @@ describe('Dialing (direct, TCP)', () => { expect(addr.toString()).to.eql(remoteAddr.toString()) await delay(60) expect(options.signal.aborted).to.equal(true) + throw new AbortError() }) await expect(dialer.connectToMultiaddr(remoteAddr)) diff --git a/test/dialing/direct.spec.js b/test/dialing/direct.spec.js index 0b6a9276cb..6626d54d84 100644 --- a/test/dialing/direct.spec.js +++ b/test/dialing/direct.spec.js @@ -16,6 +16,7 @@ const multiaddr = require('multiaddr') const PeerId = require('peer-id') const PeerInfo = require('peer-info') const AggregateError = require('aggregate-error') +const { AbortError } = require('libp2p-interfaces/src/transport/errors') const { codes: ErrorCodes } = require('../../src/errors') const Constants = require('../../src/constants') @@ -130,6 +131,7 @@ describe('Dialing (direct, WebSockets)', () => { expect(addr.toString()).to.eql(remoteAddr.toString()) await delay(60) expect(options.signal.aborted).to.equal(true) + throw new AbortError() }) await expect(dialer.connectToMultiaddr(remoteAddr)) From c4be5f4aaf2e64d4548602e3aaa148fb2d6a94ac Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Wed, 4 Dec 2019 17:05:09 +0100 Subject: [PATCH 34/92] refactor: consolidation multiaddr dial methods --- src/dialer.js | 23 +++++------------------ test/dialing/direct.node.js | 6 +++--- test/dialing/direct.spec.js | 2 +- 3 files changed, 9 insertions(+), 22 deletions(-) diff --git a/src/dialer.js b/src/dialer.js index 3cb7ae005f..f8c0bc52f5 100644 --- a/src/dialer.js +++ b/src/dialer.js @@ -41,31 +41,18 @@ class Dialer { this.tokens = [...new Array(concurrency)].map((_, index) => index) } - /** - * Connects to a given `Multiaddr`. `addr` should include the id of the peer being - * dialed, it will be used for encryption verification. - * - * @param {Multiaddr} addr The address to dial - * @param {object} [options] - * @param {AbortSignal} [options.signal] An AbortController signal - * @returns {Promise} - */ - connectToMultiaddr (addr, options = {}) { - addr = multiaddr(addr) - - return this.connectToMultiaddrs([addr], options) - } - /** * Connects to the first success of a given list of `Multiaddr`. `addrs` should * include the id of the peer being dialed, it will be used for encryption verification. * - * @param {Array} addrs + * @param {Array|Multiaddr} addrs * @param {object} [options] * @param {AbortSignal} [options.signal] An AbortController signal * @returns {Promise} */ - async connectToMultiaddrs (addrs, options = {}) { + async connectToMultiaddr (addrs, options = {}) { + if (!Array.isArray(addrs)) addrs = [multiaddr(addrs)] + const dialAction = (addr, options) => { if (options.signal.aborted) throw errCode(new Error('already aborted'), 'ERR_ALREADY_ABORTED') return this.transportManager.dial(addr, options) @@ -112,7 +99,7 @@ class Dialer { // TODO: ensure the peer id is on the multiaddr - return this.connectToMultiaddrs(addrs, options) + return this.connectToMultiaddr(addrs, options) } getTokens (num) { diff --git a/test/dialing/direct.node.js b/test/dialing/direct.node.js index bac268c6de..6df08cd631 100644 --- a/test/dialing/direct.node.js +++ b/test/dialing/direct.node.js @@ -165,7 +165,7 @@ describe('Dialing (direct, TCP)', () => { }) // Perform 3 multiaddr dials - dialer.connectToMultiaddrs([remoteAddr, remoteAddr, remoteAddr]) + dialer.connectToMultiaddr([remoteAddr, remoteAddr, remoteAddr]) // Let the call stack run await delay(0) @@ -252,7 +252,7 @@ describe('Dialing (direct, TCP)', () => { } }) - sinon.spy(libp2p.dialer, 'connectToMultiaddrs') + sinon.spy(libp2p.dialer, 'connectToMultiaddr') const remotePeer = new PeerInfo(remoteLibp2p.peerInfo.id) remotePeer.multiaddrs.add(remoteAddr) @@ -262,7 +262,7 @@ describe('Dialing (direct, TCP)', () => { expect(stream).to.exist() expect(protocol).to.equal('/echo/1.0.0') await connection.close() - expect(libp2p.dialer.connectToMultiaddrs.callCount).to.equal(1) + expect(libp2p.dialer.connectToMultiaddr.callCount).to.equal(1) }) it('should be able to use hangup to close connections', async () => { diff --git a/test/dialing/direct.spec.js b/test/dialing/direct.spec.js index 6626d54d84..d4aceac1f6 100644 --- a/test/dialing/direct.spec.js +++ b/test/dialing/direct.spec.js @@ -153,7 +153,7 @@ describe('Dialing (direct, WebSockets)', () => { }) // Perform 3 multiaddr dials - dialer.connectToMultiaddrs([remoteAddr, remoteAddr, remoteAddr]) + dialer.connectToMultiaddr([remoteAddr, remoteAddr, remoteAddr]) // Let the call stack run await delay(0) From 7d1cb5423f0c9690827109fdd02c58d73c622455 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Wed, 4 Dec 2019 17:32:11 +0100 Subject: [PATCH 35/92] chore: fix linting --- package.json | 1 - src/dialer.js | 3 +-- src/dialer/dial-request.js | 3 --- src/util/index.js | 2 -- 4 files changed, 1 insertion(+), 8 deletions(-) diff --git a/package.json b/package.json index df8c77b384..e218458ff8 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,6 @@ "p-map": "^3.0.0", "p-queue": "^6.1.1", "p-settle": "^3.1.0", - "paramap-it": "^0.1.1", "peer-id": "^0.13.4", "peer-info": "^0.17.0", "promisify-es6": "^1.0.3", diff --git a/src/dialer.js b/src/dialer.js index f8c0bc52f5..f065e401a8 100644 --- a/src/dialer.js +++ b/src/dialer.js @@ -3,7 +3,6 @@ const multiaddr = require('multiaddr') const errCode = require('err-code') const AbortController = require('abort-controller') -const delay = require('delay') const anySignal = require('any-signal') const debug = require('debug') const log = debug('libp2p:dialer') @@ -77,7 +76,7 @@ class Dialer { } catch (err) { // Error is a timeout if (timeoutController.signal.aborted) { - err = errCode(err, codes.ERR_TIMEOUT) + err.code = codes.ERR_TIMEOUT } log.error(err) throw err diff --git a/src/dialer/dial-request.js b/src/dialer/dial-request.js index d4e952e82a..1f91a0dd9a 100644 --- a/src/dialer/dial-request.js +++ b/src/dialer/dial-request.js @@ -1,7 +1,6 @@ 'use strict' const AbortController = require('abort-controller') -const AggregateError = require('aggregate-error') const anySignal = require('any-signal') const debug = require('debug') const errCode = require('err-code') @@ -56,8 +55,6 @@ class DialRequest { conn = await this.dialAction(addr, { ...options, signal: anySignal([signal, options.signal]) }) // Remove the successful AbortController so it is not aborted dialAbortControllers.splice(i, 1) - } catch (err) { - throw err } finally { completedDials++ // If we have more dials to make, recycle the token, otherwise release it diff --git a/src/util/index.js b/src/util/index.js index 221b97f5be..bca13a4530 100644 --- a/src/util/index.js +++ b/src/util/index.js @@ -1,7 +1,5 @@ 'use strict' -const AbortController = require('abort-controller') - /** * Converts BufferList messages to Buffers * @param {*} source From e8bf12b68a60ce50a4449749bdad77602983abda Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Wed, 4 Dec 2019 22:58:52 +0100 Subject: [PATCH 36/92] chore: update docs fix: protect against duplicate token releases --- doc/DIALER.md | 24 +++++++++--------------- src/dialer.js | 2 ++ test/dialing/direct.spec.js | 10 ++++++++++ 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/doc/DIALER.md b/doc/DIALER.md index b3cb2745a8..10f4cab1a5 100644 --- a/doc/DIALER.md +++ b/doc/DIALER.md @@ -5,31 +5,25 @@ * The number of tokens requested should be between 1 and the MAX_PER_PEER_DIALS max set in the Dialer. * If the number of available tokens is less than requested, the Dialer may return less than requested. * The number of tokens a DialRequest obtains reflects the maximum number of parallel Multiaddr Dials it can make. -* If no tokens are available a DialRequest should immediately end and throw. This deviates from the existing queue system to avoid queue congestion and provide more visibility to users. +* If no tokens are available a DialRequest should immediately end and throw. This deviates from the existing queue system to avoid queue congestion. * As tokens are limited, DialRequests should be given a prioritized list of Multiaddrs to minimize the potential request time. -* Once a single Multiaddr Dial has succeeded, all pending dials in that Dial Request should be aborted. All tokens should be immediately released to the Dialer. -* If all Multiaddr Dials fail, or the DIAL_TIMEOUT max is reached for the entire DialRequest, all in progress dials for that DialRequest should be aborted. All tokens should immediately be released to the Dialer. -* If a Multiaddr Dial fails and there are no more dials to use its token, that token should be immediately released to the Dialer. +* Once a Multiaddr Dial has succeeded, all pending dials in that Dial Request should be aborted. +* If DIAL_TIMEOUT time has elapsed before any one Multiaddr Dial succeeds, all remaining dials in the DialRequest should be aborted. +* When a Multiaddr Dial is settled, if there are no more addresses to dial, its token should be released back to the dialer. +* Once the DialRequest is settled, any remaining tokens should be released to the dialer. ## Multiaddr Confidence -**Not Yet Implemented** +An effective dialing system should involve the inclusion of a Confidence system for Multiaddrs. This enables ranking of Multiaddrs so that a prioritized list can be passed to DialRequests to maximize usage of Dialer Tokens, and minimize connection times. -An effective dialing system should involve the inclusion of a Confidence system for Multiaddrs. This enables ranking of Multiaddrs so that a prioritized list can be passed to DialRequests to maximize usage of Dialer Tokens. - -* All Multiaddrs SHOULD start with a confidence of 1 (give it some trust by default) -* A failed dial SHOULD lower the confidence by 1, to a min of 0. -* A successful dial SHOULD increase the confidence of a multiaddr by 1, to a max of 10. -* Multiaddrs of confidence 0 MAY be pruned if higher confidence addresses exist. If this is done, it should be done an an actual failure. Aborted dials SHOULD NOT be considered failures, but timeouts SHOULD. +**Not Yet Implemented**: This system will be designed and implemented in a future update. ## Notes * A DialRequest gets a set of tokens from the Dialer, up to the MAX_PER_PEER_DIALS max. -* A DialRequest SHOULD release its tokens after the DIAL_TIMEOUT has expired. This ensures that a peer with a large set of hanging addresses, does not block future dials. -* Upon releasing the tokens, the Dialer may allocate them to another DialRequest. * A DialRequest SHOULD fail if no dial tokens are available. The DialRequest MAY proceed without tokens, but this should be reserved for High Priority actions and should be rare. -* A DialRequest MUST NOT request more tokens than it has addresses to dial. Example: If the MAX_PER_PEER_DIALS max is 4 and a DialRequest has 1 address, it should only request 1 token. -* A DialRequest MUST execute parallel dials for each of its addresses up the total number of tokens it has. +* A DialRequest MUST NOT request more tokens than it has addresses to dial. Example: If the MAX_PER_PEER_DIALS is 4 and a DialRequest has 1 address, it should only request 1 token. +* A DialRequest SHOULD execute parallel dials for each of its addresses up the total number of tokens it has. * On a successful dial, the DialRequest MUST abort any other in progress dials, return the successful connection and release all tokens. * A new DialRequest SHOULD be given a descending list of prioritized Multiaddrs, based on their confidence. Having higher confidence Multiaddrs first can help minimize the time a DialRequest is active. * A failed dial to a Multiaddr SHOULD add that Multiaddr to a temporary denyList. diff --git a/src/dialer.js b/src/dialer.js index f065e401a8..dcdc2c7ed6 100644 --- a/src/dialer.js +++ b/src/dialer.js @@ -109,6 +109,8 @@ class Dialer { } releaseToken (token) { + // Guard against duplicate releases + if (this.tokens.indexOf(token) > -1) return log('token %d released', token) this.tokens.push(token) } diff --git a/test/dialing/direct.spec.js b/test/dialing/direct.spec.js index d4aceac1f6..a601c8c4a2 100644 --- a/test/dialing/direct.spec.js +++ b/test/dialing/direct.spec.js @@ -68,6 +68,16 @@ describe('Dialing (direct, WebSockets)', () => { expect(tokens.length).to.equal(0) }) + it('should NOT be able to return a token twice', () => { + const dialer = new Dialer({ transportManager: localTM }) + const tokens = dialer.getTokens(1) + expect(tokens).to.have.length(1) + expect(dialer.tokens).to.have.length(Constants.MAX_PARALLEL_DIALS - 1) + dialer.releaseToken(tokens[0]) + dialer.releaseToken(tokens[0]) + expect(dialer.tokens).to.have.length(Constants.MAX_PARALLEL_DIALS) + }) + it('should be able to connect to a remote node via its multiaddr', async () => { const dialer = new Dialer({ transportManager: localTM }) From 43a3b85f1a27d23ffe91e1ec7a84bfadcb4f7fba Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Wed, 4 Dec 2019 23:04:43 +0100 Subject: [PATCH 37/92] refactor: cleanup and reorganize --- src/dialer/dial-request.js | 10 +++++----- src/{dialer.js => dialer/index.js} | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) rename src/{dialer.js => dialer/index.js} (96%) diff --git a/src/dialer/dial-request.js b/src/dialer/dial-request.js index 1f91a0dd9a..171ba4cd31 100644 --- a/src/dialer/dial-request.js +++ b/src/dialer/dial-request.js @@ -41,14 +41,14 @@ class DialRequest { throw errCode(new Error('No dial tokens available'), 'ERR_NO_DIAL_TOKENS') } - const th = new FIFO() - tokens.forEach(t => th.push(t)) + const tokenHolder = new FIFO() + tokens.forEach(token => tokenHolder.push(token)) const dialAbortControllers = this.addrs.map(() => new AbortController()) let completedDials = 0 try { return await pAny(this.addrs.map(async (addr, i) => { - const token = await th.shift() // get token + const token = await tokenHolder.shift() // get token let conn try { const signal = dialAbortControllers[i].signal @@ -59,7 +59,7 @@ class DialRequest { completedDials++ // If we have more dials to make, recycle the token, otherwise release it if (completedDials < this.addrs.length) { - th.push(token) + tokenHolder.push(token) } else { this.dialer.releaseToken(tokens.splice(tokens.indexOf(token), 1)[0]) } @@ -69,7 +69,7 @@ class DialRequest { })) } finally { dialAbortControllers.map(c => c.abort()) // success/failure happened, abort everything else - tokens.forEach(t => this.dialer.releaseToken(t)) // release tokens back to the dialer + tokens.forEach(token => this.dialer.releaseToken(token)) // release tokens back to the dialer } } } diff --git a/src/dialer.js b/src/dialer/index.js similarity index 96% rename from src/dialer.js rename to src/dialer/index.js index dcdc2c7ed6..58ea383553 100644 --- a/src/dialer.js +++ b/src/dialer/index.js @@ -7,14 +7,14 @@ const anySignal = require('any-signal') const debug = require('debug') const log = debug('libp2p:dialer') log.error = debug('libp2p:dialer:error') -const { DialRequest } = require('./dialer/dial-request') +const { DialRequest } = require('./dial-request') -const { codes } = require('./errors') +const { codes } = require('../errors') const { DIAL_TIMEOUT, MAX_PARALLEL_DIALS, MAX_PER_PEER_DIALS -} = require('./constants') +} = require('../constants') class Dialer { /** From 53ce404260b6983545a53ff37318f1d2756668c4 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Wed, 4 Dec 2019 23:22:30 +0100 Subject: [PATCH 38/92] chore: update per feedback --- doc/DIALER.md | 2 +- src/dialer/index.js | 1 + test/dialing/direct.node.js | 7 +++---- test/dialing/direct.spec.js | 7 +++---- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/doc/DIALER.md b/doc/DIALER.md index 10f4cab1a5..920372b1c9 100644 --- a/doc/DIALER.md +++ b/doc/DIALER.md @@ -5,7 +5,7 @@ * The number of tokens requested should be between 1 and the MAX_PER_PEER_DIALS max set in the Dialer. * If the number of available tokens is less than requested, the Dialer may return less than requested. * The number of tokens a DialRequest obtains reflects the maximum number of parallel Multiaddr Dials it can make. -* If no tokens are available a DialRequest should immediately end and throw. This deviates from the existing queue system to avoid queue congestion. +* If no tokens are available a DialRequest should immediately end and throw. * As tokens are limited, DialRequests should be given a prioritized list of Multiaddrs to minimize the potential request time. * Once a Multiaddr Dial has succeeded, all pending dials in that Dial Request should be aborted. * If DIAL_TIMEOUT time has elapsed before any one Multiaddr Dial succeeds, all remaining dials in the DialRequest should be aborted. diff --git a/src/dialer/index.js b/src/dialer/index.js index 58ea383553..b44364a87b 100644 --- a/src/dialer/index.js +++ b/src/dialer/index.js @@ -72,6 +72,7 @@ class Dialer { try { const dialResult = await dialRequest.run({ ...options, signal }) clearTimeout(timeoutId) + log('dial succeeded to %s', dialResult.remoteAddr) return dialResult } catch (err) { // Error is a timeout diff --git a/test/dialing/direct.node.js b/test/dialing/direct.node.js index 6df08cd631..fd4ec73b78 100644 --- a/test/dialing/direct.node.js +++ b/test/dialing/direct.node.js @@ -27,6 +27,7 @@ const Protector = require('../../src/pnet') const swarmKeyBuffer = Buffer.from(require('../fixtures/swarm.key')) const mockUpgrader = require('../utils/mockUpgrader') +const createMockConnection = require('../utils/mockConnection') const Peers = require('../fixtures/peers') const listenAddr = multiaddr('/ip4/127.0.0.1/tcp/0') @@ -160,9 +161,7 @@ describe('Dialing (direct, TCP)', () => { expect(dialer.tokens).to.have.length(2) const deferredDial = pDefer() - sinon.stub(localTM, 'dial').callsFake(async () => { - await deferredDial.promise - }) + sinon.stub(localTM, 'dial').callsFake(() => deferredDial.promise) // Perform 3 multiaddr dials dialer.connectToMultiaddr([remoteAddr, remoteAddr, remoteAddr]) @@ -173,7 +172,7 @@ describe('Dialing (direct, TCP)', () => { // We should have 2 in progress, and 1 waiting expect(dialer.tokens).to.have.length(0) - deferredDial.resolve() + deferredDial.resolve(await createMockConnection()) // Let the call stack run await delay(0) diff --git a/test/dialing/direct.spec.js b/test/dialing/direct.spec.js index a601c8c4a2..155b6dbe76 100644 --- a/test/dialing/direct.spec.js +++ b/test/dialing/direct.spec.js @@ -27,6 +27,7 @@ const Libp2p = require('../../src') const Peers = require('../fixtures/peers') const { MULTIADDRS_WEBSOCKETS } = require('../fixtures/browser') const mockUpgrader = require('../utils/mockUpgrader') +const createMockConnection = require('../utils/mockConnection') const unsupportedAddr = multiaddr('/ip4/127.0.0.1/tcp/9999/ws') const remoteAddr = MULTIADDRS_WEBSOCKETS[0] @@ -158,9 +159,7 @@ describe('Dialing (direct, WebSockets)', () => { expect(dialer.tokens).to.have.length(2) const deferredDial = pDefer() - sinon.stub(localTM, 'dial').callsFake(async () => { - await deferredDial.promise - }) + sinon.stub(localTM, 'dial').callsFake(() => deferredDial.promise) // Perform 3 multiaddr dials dialer.connectToMultiaddr([remoteAddr, remoteAddr, remoteAddr]) @@ -171,7 +170,7 @@ describe('Dialing (direct, WebSockets)', () => { // We should have 2 in progress, and 1 waiting expect(dialer.tokens).to.have.length(0) - deferredDial.resolve() + deferredDial.resolve(await createMockConnection()) // Let the call stack run await delay(0) From 74bfe6bea5cad1f2c03c50558ada134942398f87 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Wed, 4 Dec 2019 23:26:00 +0100 Subject: [PATCH 39/92] docs(release): point to libp2p weekly sync --- RELEASE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE.md b/RELEASE.md index 00492da3b8..0085e5d645 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -51,7 +51,7 @@ Would you like to contribute to the libp2p project and don't know how? Well, the - Join an IPFS All Hands, introduce yourself and let us know where you would like to contribute - https://github.com/ipfs/team-mgmt#all-hands-call - Hack with IPFS and show us what you made! The All Hands call is also the perfect venue for demos, join in and show us what you built - Join the discussion at http://discuss.libp2p.io/ and help users finding their answers. -- Join the [⚡️IPFS Core Implementations Weekly Sync 🙌🏽 ](https://github.com/ipfs/team-mgmt/issues/992) and be part of the Sprint action! +- Join the [⚡️libp2p Weekly Sync 🙌🏽](https://github.com/libp2p/team-mgmt/issues/16) and be part of the Sprint action! # ⁉️ Do you have questions? From 3b06283ad87658af4415897ff0fbe6bbe3db89d3 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Fri, 6 Dec 2019 12:42:36 +0100 Subject: [PATCH 40/92] test(fix): fix support for it.only, it.skip, etc --- package.json | 4 ++-- test/node.js | 14 -------------- 2 files changed, 2 insertions(+), 16 deletions(-) delete mode 100644 test/node.js diff --git a/package.json b/package.json index e218458ff8..9d1b4ef9e3 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "scripts": { "lint": "aegir lint", "build": "aegir build", - "test": "aegir test -t node -t browser", - "test:node": "aegir test -t node", + "test": "npm run test:node && npm run test:browser", + "test:node": "aegir test -t node -f test/**/*.node.js test/**/*.spec.js", "test:browser": "aegir test -t browser", "release": "aegir release -t node -t browser", "release-minor": "aegir release --type minor -t node -t browser", diff --git a/test/node.js b/test/node.js deleted file mode 100644 index 61887dac95..0000000000 --- a/test/node.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict' - -const glob = require('glob') -const path = require('path') - -// Automatically require test files so we don't have to worry about adding new ones -glob('test/**/*.node.js', function (err, testPaths) { - if (err) throw err - if (testPaths.length < 1) throw new Error('Could not find any node test files') - - testPaths.forEach(file => { - require(path.resolve(file)) - }) -}) From c7dcfe5e48ce82075b084d7918b33857852b57fe Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Fri, 6 Dec 2019 14:26:21 +0100 Subject: [PATCH 41/92] test: add tests for DialRequest --- src/dialer/dial-request.js | 3 +- test/dialing/dial-request.spec.js | 180 ++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 test/dialing/dial-request.spec.js diff --git a/src/dialer/dial-request.js b/src/dialer/dial-request.js index 171ba4cd31..d297257b06 100644 --- a/src/dialer/dial-request.js +++ b/src/dialer/dial-request.js @@ -14,7 +14,7 @@ class DialRequest { * * @param {object} options * @param {Multiaddr[]} options.addrs - * @param {TransportManager} options.transportManager + * @param {function(Multiaddr):Promise} options.dialAction * @param {Dialer} options.dialer */ constructor ({ @@ -31,7 +31,6 @@ class DialRequest { * @async * @param {object} options * @param {AbortSignal} options.signal An AbortController signal - * @param {number} options.timeout The max dial time for each request * @returns {Connection} */ async run (options) { diff --git a/test/dialing/dial-request.spec.js b/test/dialing/dial-request.spec.js new file mode 100644 index 0000000000..477e992d55 --- /dev/null +++ b/test/dialing/dial-request.spec.js @@ -0,0 +1,180 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +chai.use(require('chai-as-promised')) +const { expect } = chai +const sinon = require('sinon') + +const { AbortError } = require('libp2p-interfaces/src/transport/errors') +const AbortController = require('abort-controller') +const AggregateError = require('aggregate-error') +const pDefer = require('p-defer') +const delay = require('delay') + +const { DialRequest } = require('../../src/dialer/dial-request') +const createMockConnection = require('../utils/mockConnection') + +describe('Dial Request', () => { + it('should end when a single multiaddr dials succeeds', async () => { + const mockConnection = await createMockConnection() + const actions = { + [1]: () => Promise.reject(), + [2]: () => Promise.resolve(mockConnection), + [3]: () => Promise.reject() + } + const dialAction = (num) => actions[num]() + const tokens = ['a', 'b'] + const controller = new AbortController() + const dialer = { + getTokens: () => [...tokens], + releaseToken: () => {} + } + + const dialRequest = new DialRequest({ + addrs: Object.keys(actions), + dialer, + dialAction + }) + + sinon.spy(actions, 1) + sinon.spy(actions, 2) + sinon.spy(actions, 3) + sinon.spy(dialer, 'releaseToken') + const result = await dialRequest.run({ signal: controller.signal }) + expect(result).to.equal(mockConnection) + expect(actions[1]).to.have.property('callCount', 1) + expect(actions[2]).to.have.property('callCount', 1) + expect(actions[3]).to.have.property('callCount', 0) + expect(dialer.releaseToken).to.have.property('callCount', tokens.length) + }) + + it('should throw an AggregateError if all dials fail', async () => { + const actions = { + [1]: () => Promise.reject(), + [2]: () => Promise.reject(), + [3]: () => Promise.reject() + } + const dialAction = (num) => actions[num]() + const addrs = Object.keys(actions) + const tokens = ['a', 'b'] + const controller = new AbortController() + const dialer = { + getTokens: () => [...tokens], + releaseToken: () => {} + } + + const dialRequest = new DialRequest({ + addrs, + dialer, + dialAction + }) + + sinon.spy(actions, 1) + sinon.spy(actions, 2) + sinon.spy(actions, 3) + sinon.spy(dialer, 'getTokens') + sinon.spy(dialer, 'releaseToken') + + try { + await dialRequest.run({ signal: controller.signal }) + expect.fail('Should have thrown') + } catch (err) { + expect(err).to.be.an.instanceof(AggregateError) + } + + expect(actions[1]).to.have.property('callCount', 1) + expect(actions[2]).to.have.property('callCount', 1) + expect(actions[3]).to.have.property('callCount', 1) + expect(dialer.getTokens.calledWith(addrs.length)).to.equal(true) + expect(dialer.releaseToken).to.have.property('callCount', tokens.length) + }) + + it('should handle a large number of addrs', async () => { + const reject = sinon.stub().callsFake(() => Promise.reject()) + const actions = {} + const addrs = [...new Array(25)].map((_, index) => index + 1) + addrs.forEach(addr => { + actions[addr] = reject + }) + + const dialAction = (addr) => actions[addr]() + const tokens = ['a', 'b'] + const controller = new AbortController() + const dialer = { + getTokens: () => [...tokens], + releaseToken: () => {} + } + + const dialRequest = new DialRequest({ + addrs, + dialer, + dialAction + }) + + sinon.spy(dialer, 'releaseToken') + try { + await dialRequest.run({ signal: controller.signal }) + expect.fail('Should have thrown') + } catch (err) { + expect(err).to.be.an.instanceof(AggregateError) + } + + expect(reject).to.have.property('callCount', addrs.length) + expect(dialer.releaseToken).to.have.property('callCount', tokens.length) + }) + + it('should abort all dials when its signal is aborted', async () => { + const deferToAbort = ({ signal }) => { + if (signal.aborted) throw new Error('already aborted') + const deferred = pDefer() + const onAbort = () => { + deferred.reject(new AbortError()) + signal.removeEventListener('abort', onAbort) + } + signal.addEventListener('abort', onAbort) + return deferred.promise + } + + const actions = { + [1]: deferToAbort, + [2]: deferToAbort, + [3]: deferToAbort + } + const dialAction = (num, opts) => actions[num](opts) + const addrs = Object.keys(actions) + const tokens = ['a', 'b'] + const controller = new AbortController() + const dialer = { + getTokens: () => [...tokens], + releaseToken: () => {} + } + + const dialRequest = new DialRequest({ + addrs, + dialer, + dialAction + }) + + sinon.spy(actions, 1) + sinon.spy(actions, 2) + sinon.spy(actions, 3) + sinon.spy(dialer, 'getTokens') + sinon.spy(dialer, 'releaseToken') + + try { + setTimeout(() => controller.abort(), 100) + await dialRequest.run({ signal: controller.signal }) + expect.fail('dial should have failed') + } catch (err) { + expect(err).to.be.an.instanceof(AggregateError) + } + + expect(actions[1]).to.have.property('callCount', 1) + expect(actions[2]).to.have.property('callCount', 1) + expect(actions[3]).to.have.property('callCount', 1) + expect(dialer.getTokens.calledWith(addrs.length)).to.equal(true) + expect(dialer.releaseToken).to.have.property('callCount', tokens.length) + }) +}) From 3b52236dee51321c1d8041a208bb871fb3b7c746 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Fri, 6 Dec 2019 14:28:52 +0100 Subject: [PATCH 42/92] chore: fix lint test: reduce interval of randomwalk in test chore(test): glob fix --- package.json | 4 ++-- test/dialing/dial-request.spec.js | 22 +++++++++++----------- test/peer-discovery/index.node.js | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 9d1b4ef9e3..94559e8c63 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "lint": "aegir lint", "build": "aegir build", "test": "npm run test:node && npm run test:browser", - "test:node": "aegir test -t node -f test/**/*.node.js test/**/*.spec.js", + "test:node": "aegir test -t node -f \"./test/**/*.{node,spec}.js\"", "test:browser": "aegir test -t browser", "release": "aegir release -t node -t browser", "release-minor": "aegir release --type minor -t node -t browser", @@ -99,7 +99,7 @@ "libp2p-delegated-peer-routing": "^0.4.0", "libp2p-floodsub": "^0.20.0", "libp2p-gossipsub": "^0.2.0", - "libp2p-kad-dht": "^0.18.0", + "libp2p-kad-dht": "^0.18.2", "libp2p-mdns": "^0.13.0", "libp2p-mplex": "^0.9.1", "libp2p-pnet": "~0.1.0", diff --git a/test/dialing/dial-request.spec.js b/test/dialing/dial-request.spec.js index 477e992d55..821546289f 100644 --- a/test/dialing/dial-request.spec.js +++ b/test/dialing/dial-request.spec.js @@ -11,18 +11,18 @@ const { AbortError } = require('libp2p-interfaces/src/transport/errors') const AbortController = require('abort-controller') const AggregateError = require('aggregate-error') const pDefer = require('p-defer') -const delay = require('delay') const { DialRequest } = require('../../src/dialer/dial-request') const createMockConnection = require('../utils/mockConnection') +const error = new Error('dial failes') describe('Dial Request', () => { it('should end when a single multiaddr dials succeeds', async () => { const mockConnection = await createMockConnection() const actions = { - [1]: () => Promise.reject(), - [2]: () => Promise.resolve(mockConnection), - [3]: () => Promise.reject() + 1: () => Promise.reject(error), + 2: () => Promise.resolve(mockConnection), + 3: () => Promise.reject(error) } const dialAction = (num) => actions[num]() const tokens = ['a', 'b'] @@ -52,9 +52,9 @@ describe('Dial Request', () => { it('should throw an AggregateError if all dials fail', async () => { const actions = { - [1]: () => Promise.reject(), - [2]: () => Promise.reject(), - [3]: () => Promise.reject() + 1: () => Promise.reject(error), + 2: () => Promise.reject(error), + 3: () => Promise.reject(error) } const dialAction = (num) => actions[num]() const addrs = Object.keys(actions) @@ -92,7 +92,7 @@ describe('Dial Request', () => { }) it('should handle a large number of addrs', async () => { - const reject = sinon.stub().callsFake(() => Promise.reject()) + const reject = sinon.stub().callsFake(() => Promise.reject(error)) const actions = {} const addrs = [...new Array(25)].map((_, index) => index + 1) addrs.forEach(addr => { @@ -138,9 +138,9 @@ describe('Dial Request', () => { } const actions = { - [1]: deferToAbort, - [2]: deferToAbort, - [3]: deferToAbort + 1: deferToAbort, + 2: deferToAbort, + 3: deferToAbort } const dialAction = (num, opts) => actions[num](opts) const addrs = Object.keys(actions) diff --git a/test/peer-discovery/index.node.js b/test/peer-discovery/index.node.js index 72babbf00a..a7b6dc911d 100644 --- a/test/peer-discovery/index.node.js +++ b/test/peer-discovery/index.node.js @@ -140,8 +140,8 @@ describe('peer discovery scenarios', () => { dht: { randomWalk: { enabled: true, - delay: 100, // start the first query quickly - interval: 1000, + delay: 1000, // start the first query quickly + interval: 10000, timeout: 3000 }, enabled: true From 0a8f9f32387b558606fe2d31766e69d90c6820b4 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Fri, 6 Dec 2019 17:07:01 +0100 Subject: [PATCH 43/92] test: reduce randomwalk timeout --- test/peer-discovery/index.node.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/peer-discovery/index.node.js b/test/peer-discovery/index.node.js index a7b6dc911d..b346f80eb1 100644 --- a/test/peer-discovery/index.node.js +++ b/test/peer-discovery/index.node.js @@ -142,7 +142,7 @@ describe('peer discovery scenarios', () => { enabled: true, delay: 1000, // start the first query quickly interval: 10000, - timeout: 3000 + timeout: 1000 }, enabled: true } From 754fbc2d0b9e8b82bfa3b78bebdacb42cd16c3dd Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Fri, 6 Dec 2019 17:45:29 +0100 Subject: [PATCH 44/92] feat: abort all pending dials on stop --- src/dialer/index.js | 9 +++++++++ src/index.js | 11 +++++++++-- test/dialing/direct.spec.js | 20 ++++++++++++++++++++ test/peer-discovery/index.node.js | 4 +++- 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/dialer/index.js b/src/dialer/index.js index b44364a87b..ff6add0bec 100644 --- a/src/dialer/index.js +++ b/src/dialer/index.js @@ -38,6 +38,7 @@ class Dialer { this.timeout = timeout this.perPeerLimit = perPeerLimit this.tokens = [...new Array(concurrency)].map((_, index) => index) + this.pendingDials = new Set() } /** @@ -69,6 +70,12 @@ class Dialer { const signal = anySignal(signals) const timeoutId = setTimeout(() => timeoutController.abort(), this.timeout) + const dial = { + dialRequest, + controller: timeoutController + } + this.pendingDials.add(dial) + try { const dialResult = await dialRequest.run({ ...options, signal }) clearTimeout(timeoutId) @@ -81,6 +88,8 @@ class Dialer { } log.error(err) throw err + } finally { + this.pendingDials.delete(dial) } } diff --git a/src/index.js b/src/index.js index 609edd32b7..1a8ab7f964 100644 --- a/src/index.js +++ b/src/index.js @@ -194,8 +194,15 @@ class Libp2p extends EventEmitter { log('libp2p is stopping') try { - this.pubsub && await this.pubsub.stop() - this._dht && await this._dht.stop() + await Promise.all([ + this.pubsub && this.pubsub.stop(), + this._dht && this._dht.stop() + ]) + + for (const dial of this.dialer.pendingDials.values()) { + dial.abort() + } + await this.transportManager.close() await this.registrar.close() } catch (err) { diff --git a/test/dialing/direct.spec.js b/test/dialing/direct.spec.js index 155b6dbe76..f22a620348 100644 --- a/test/dialing/direct.spec.js +++ b/test/dialing/direct.spec.js @@ -169,6 +169,7 @@ describe('Dialing (direct, WebSockets)', () => { // We should have 2 in progress, and 1 waiting expect(dialer.tokens).to.have.length(0) + expect(dialer.pendingDials.size).to.equal(1) // 1 dial request deferredDial.resolve(await createMockConnection()) @@ -178,6 +179,7 @@ describe('Dialing (direct, WebSockets)', () => { // Only two dials will be run, as the first two succeeded expect(localTM.dial.callCount).to.equal(2) expect(dialer.tokens).to.have.length(2) + expect(dialer.pendingDials.size).to.equal(0) }) describe('libp2p.dialer', () => { @@ -278,5 +280,23 @@ describe('Dialing (direct, WebSockets)', () => { await libp2p.hangUp(connection.remotePeer) expect(connection.stat.timeline.close).to.exist() }) + + it('should abort pending dials on stop', async () => { + libp2p = new Libp2p({ + peerInfo, + modules: { + transport: [Transport], + streamMuxer: [Muxer], + connEncryption: [Crypto] + } + }) + const abort = sinon.stub() + const dials = [{ abort }, { abort }, { abort }] + sinon.stub(libp2p.dialer, 'pendingDials').value(new Set(dials)) + + await libp2p.stop() + + expect(abort).to.have.property('callCount', 3) + }) }) }) diff --git a/test/peer-discovery/index.node.js b/test/peer-discovery/index.node.js index b346f80eb1..0d4b7b1cd8 100644 --- a/test/peer-discovery/index.node.js +++ b/test/peer-discovery/index.node.js @@ -29,7 +29,9 @@ describe('peer discovery scenarios', () => { remotePeerInfo2.multiaddrs.add(multiaddr('/ip4/127.0.0.1/tcp/0')) }) - afterEach(async () => { + afterEach(async function () { + // Increase timeout until abort support for dht queries is in place + this.timeout(10e3) libp2p && await libp2p.stop() }) From 962081f44820a159162c0298cb883da4737c1750 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Fri, 6 Dec 2019 17:53:05 +0100 Subject: [PATCH 45/92] test: remove timeout --- test/peer-discovery/index.node.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/peer-discovery/index.node.js b/test/peer-discovery/index.node.js index 0d4b7b1cd8..b346f80eb1 100644 --- a/test/peer-discovery/index.node.js +++ b/test/peer-discovery/index.node.js @@ -29,9 +29,7 @@ describe('peer discovery scenarios', () => { remotePeerInfo2.multiaddrs.add(multiaddr('/ip4/127.0.0.1/tcp/0')) }) - afterEach(async function () { - // Increase timeout until abort support for dht queries is in place - this.timeout(10e3) + afterEach(async () => { libp2p && await libp2p.stop() }) From 43b98e64b6786ad7f5d5b9316fcb345840f76369 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Fri, 6 Dec 2019 18:43:59 +0100 Subject: [PATCH 46/92] docs: add DialRequest description --- src/dialer/dial-request.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/dialer/dial-request.js b/src/dialer/dial-request.js index d297257b06..7063de1a90 100644 --- a/src/dialer/dial-request.js +++ b/src/dialer/dial-request.js @@ -11,7 +11,11 @@ const pAny = require('p-any') class DialRequest { /** - * + * Manages running the `dialAction` on multiple provided `addrs` in parallel + * up to a maximum determined by the number of tokens returned + * from `dialer.getTokens`. Once a DialRequest is created, it can be + * started using `DialRequest.run(options)`. Once a single dial has succeeded, + * all other dials in the request will be cancelled. * @param {object} options * @param {Multiaddr[]} options.addrs * @param {function(Multiaddr):Promise} options.dialAction From 7c3371bf17106c8240c42e9c4ac97369d49c96d6 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Tue, 10 Dec 2019 13:48:34 +0100 Subject: [PATCH 47/92] fix: clean up pending dials abort per feedback --- package.json | 3 ++- src/dialer/index.js | 27 ++++++++++++++------ src/index.js | 4 +-- test/dialing/direct.spec.js | 49 ++++++++++++++++++++++++++++++++----- 4 files changed, 66 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 94559e8c63..9deef33de6 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "dependencies": { "abort-controller": "^3.0.0", "aggregate-error": "^3.0.1", - "any-signal": "^1.0.0", + "any-signal": "^1.1.0", "async": "^2.6.2", "async-iterator-all": "^1.0.0", "bignumber.js": "^9.0.0", @@ -78,6 +78,7 @@ "pull-handshake": "^1.1.4", "pull-stream": "^3.6.9", "retimer": "^2.0.0", + "timeout-abort-controller": "^1.0.0", "xsalsa20": "^1.0.2" }, "devDependencies": { diff --git a/src/dialer/index.js b/src/dialer/index.js index ff6add0bec..47bda2d436 100644 --- a/src/dialer/index.js +++ b/src/dialer/index.js @@ -2,7 +2,7 @@ const multiaddr = require('multiaddr') const errCode = require('err-code') -const AbortController = require('abort-controller') +const TimeoutController = require('timeout-abort-controller') const anySignal = require('any-signal') const debug = require('debug') const log = debug('libp2p:dialer') @@ -38,7 +38,21 @@ class Dialer { this.timeout = timeout this.perPeerLimit = perPeerLimit this.tokens = [...new Array(concurrency)].map((_, index) => index) - this.pendingDials = new Set() + this._pendingDials = new Set() + } + + /** + * Clears any pending dials + */ + destroy () { + for (const dial of this._pendingDials.values()) { + try { + dial.controller.abort() + } catch (err) { + log.error(err) + } + } + this._pendingDials.clear() } /** @@ -64,21 +78,20 @@ class Dialer { }) // Combine the timeout signal and options.signal, if provided - const timeoutController = new AbortController() + const timeoutController = new TimeoutController(this.timeout) const signals = [timeoutController.signal] options.signal && signals.push(options.signal) const signal = anySignal(signals) - const timeoutId = setTimeout(() => timeoutController.abort(), this.timeout) const dial = { dialRequest, controller: timeoutController } - this.pendingDials.add(dial) + this._pendingDials.add(dial) try { const dialResult = await dialRequest.run({ ...options, signal }) - clearTimeout(timeoutId) + timeoutController.clear() log('dial succeeded to %s', dialResult.remoteAddr) return dialResult } catch (err) { @@ -89,7 +102,7 @@ class Dialer { log.error(err) throw err } finally { - this.pendingDials.delete(dial) + this._pendingDials.delete(dial) } } diff --git a/src/index.js b/src/index.js index 1a8ab7f964..c54e3fb8e5 100644 --- a/src/index.js +++ b/src/index.js @@ -199,9 +199,7 @@ class Libp2p extends EventEmitter { this._dht && this._dht.stop() ]) - for (const dial of this.dialer.pendingDials.values()) { - dial.abort() - } + this.dialer.destroy() await this.transportManager.close() await this.registrar.close() diff --git a/test/dialing/direct.spec.js b/test/dialing/direct.spec.js index f22a620348..5281acd574 100644 --- a/test/dialing/direct.spec.js +++ b/test/dialing/direct.spec.js @@ -169,7 +169,7 @@ describe('Dialing (direct, WebSockets)', () => { // We should have 2 in progress, and 1 waiting expect(dialer.tokens).to.have.length(0) - expect(dialer.pendingDials.size).to.equal(1) // 1 dial request + expect(dialer._pendingDials.size).to.equal(1) // 1 dial request deferredDial.resolve(await createMockConnection()) @@ -179,7 +179,45 @@ describe('Dialing (direct, WebSockets)', () => { // Only two dials will be run, as the first two succeeded expect(localTM.dial.callCount).to.equal(2) expect(dialer.tokens).to.have.length(2) - expect(dialer.pendingDials.size).to.equal(0) + expect(dialer._pendingDials.size).to.equal(0) + }) + + it('.destroy should abort pending dials', async () => { + const dialer = new Dialer({ + transportManager: localTM, + concurrency: 2 + }) + + expect(dialer.tokens).to.have.length(2) + + sinon.stub(localTM, 'dial').callsFake((_, options) => { + const deferredDial = pDefer() + const onAbort = () => { + options.signal.removeEventListener('abort', onAbort) + deferredDial.reject(new AbortError()) + } + options.signal.addEventListener('abort', onAbort) + return deferredDial.promise + }) + + // Perform 3 multiaddr dials + const dialPromise = dialer.connectToMultiaddr([remoteAddr, remoteAddr, remoteAddr]) + + // Let the call stack run + await delay(0) + + // We should have 2 in progress, and 1 waiting + expect(dialer.tokens).to.have.length(0) + expect(dialer._pendingDials.size).to.equal(1) // 1 dial request + + try { + dialer.destroy() + await dialPromise + expect.fail('should have failed') + } catch (err) { + expect(err).to.be.an.instanceof(AggregateError) + expect(dialer._pendingDials.size).to.equal(0) // 1 dial request + } }) describe('libp2p.dialer', () => { @@ -290,13 +328,12 @@ describe('Dialing (direct, WebSockets)', () => { connEncryption: [Crypto] } }) - const abort = sinon.stub() - const dials = [{ abort }, { abort }, { abort }] - sinon.stub(libp2p.dialer, 'pendingDials').value(new Set(dials)) + + sinon.spy(libp2p.dialer, 'destroy') await libp2p.stop() - expect(abort).to.have.property('callCount', 3) + expect(libp2p.dialer.destroy).to.have.property('callCount', 1) }) }) }) From 43440aa8a6b8c6a72412a06086ef270c606e6d91 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Tue, 10 Dec 2019 14:02:18 +0100 Subject: [PATCH 48/92] fix: release tokens as soon as they are available --- src/dialer/dial-request.js | 6 ++-- test/dialing/dial-request.spec.js | 49 +++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/dialer/dial-request.js b/src/dialer/dial-request.js index 7063de1a90..f27749292b 100644 --- a/src/dialer/dial-request.js +++ b/src/dialer/dial-request.js @@ -47,11 +47,12 @@ class DialRequest { const tokenHolder = new FIFO() tokens.forEach(token => tokenHolder.push(token)) const dialAbortControllers = this.addrs.map(() => new AbortController()) - let completedDials = 0 + let startedDials = 0 try { return await pAny(this.addrs.map(async (addr, i) => { const token = await tokenHolder.shift() // get token + startedDials++ let conn try { const signal = dialAbortControllers[i].signal @@ -59,9 +60,8 @@ class DialRequest { // Remove the successful AbortController so it is not aborted dialAbortControllers.splice(i, 1) } finally { - completedDials++ // If we have more dials to make, recycle the token, otherwise release it - if (completedDials < this.addrs.length) { + if (startedDials < this.addrs.length) { tokenHolder.push(token) } else { this.dialer.releaseToken(tokens.splice(tokens.indexOf(token), 1)[0]) diff --git a/test/dialing/dial-request.spec.js b/test/dialing/dial-request.spec.js index 821546289f..1a85dad4ad 100644 --- a/test/dialing/dial-request.spec.js +++ b/test/dialing/dial-request.spec.js @@ -11,6 +11,7 @@ const { AbortError } = require('libp2p-interfaces/src/transport/errors') const AbortController = require('abort-controller') const AggregateError = require('aggregate-error') const pDefer = require('p-defer') +const delay = require('delay') const { DialRequest } = require('../../src/dialer/dial-request') const createMockConnection = require('../utils/mockConnection') @@ -50,6 +51,54 @@ describe('Dial Request', () => { expect(dialer.releaseToken).to.have.property('callCount', tokens.length) }) + it('should release tokens when all addr dials have started', async () => { + const mockConnection = await createMockConnection() + const deferred = pDefer() + const actions = { + 1: async () => { + await delay(0) + return Promise.reject(error) + }, + 2: async () => { + await delay(0) + return Promise.reject(error) + }, + 3: () => deferred.promise + } + const dialAction = (num) => actions[num]() + const tokens = ['a', 'b'] + const controller = new AbortController() + const dialer = { + getTokens: () => [...tokens], + releaseToken: () => {} + } + + const dialRequest = new DialRequest({ + addrs: Object.keys(actions), + dialer, + dialAction + }) + + sinon.spy(actions, 1) + sinon.spy(actions, 2) + sinon.spy(actions, 3) + sinon.spy(dialer, 'releaseToken') + dialRequest.run({ signal: controller.signal }) + // Let the first dials run + await delay(10) + + // Only 1 dial should remain, so 1 token should have been released + expect(actions[1]).to.have.property('callCount', 1) + expect(actions[2]).to.have.property('callCount', 1) + expect(actions[3]).to.have.property('callCount', 1) + expect(dialer.releaseToken).to.have.property('callCount', 1) + + // Finish the dial + deferred.resolve(mockConnection) + await delay(0) + expect(dialer.releaseToken).to.have.property('callCount', 2) + }) + it('should throw an AggregateError if all dials fail', async () => { const actions = { 1: () => Promise.reject(error), From 3cadeb39cb87c74aaa85cafb34b71980b5fbb019 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Tue, 10 Dec 2019 14:14:29 +0100 Subject: [PATCH 49/92] test: bump delay for ci --- test/dialing/dial-request.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/dialing/dial-request.spec.js b/test/dialing/dial-request.spec.js index 1a85dad4ad..d785b00051 100644 --- a/test/dialing/dial-request.spec.js +++ b/test/dialing/dial-request.spec.js @@ -85,7 +85,7 @@ describe('Dial Request', () => { sinon.spy(dialer, 'releaseToken') dialRequest.run({ signal: controller.signal }) // Let the first dials run - await delay(10) + await delay(100) // Only 1 dial should remain, so 1 token should have been released expect(actions[1]).to.have.property('callCount', 1) From 1838a641d9726d9cfcccf1b35aa3abe7e7cda5be Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Tue, 10 Dec 2019 14:45:52 +0100 Subject: [PATCH 50/92] fix: token release logic --- src/dialer/dial-request.js | 8 ++++---- test/dialing/dial-request.spec.js | 19 +++++++++---------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/dialer/dial-request.js b/src/dialer/dial-request.js index f27749292b..2f97f997ac 100644 --- a/src/dialer/dial-request.js +++ b/src/dialer/dial-request.js @@ -47,12 +47,11 @@ class DialRequest { const tokenHolder = new FIFO() tokens.forEach(token => tokenHolder.push(token)) const dialAbortControllers = this.addrs.map(() => new AbortController()) - let startedDials = 0 + let completedDials = 0 try { return await pAny(this.addrs.map(async (addr, i) => { const token = await tokenHolder.shift() // get token - startedDials++ let conn try { const signal = dialAbortControllers[i].signal @@ -60,8 +59,9 @@ class DialRequest { // Remove the successful AbortController so it is not aborted dialAbortControllers.splice(i, 1) } finally { - // If we have more dials to make, recycle the token, otherwise release it - if (startedDials < this.addrs.length) { + completedDials++ + // If we have more or equal dials remaining than tokens, recycle the token, otherwise release it + if (this.addrs.length - completedDials >= tokens.length) { tokenHolder.push(token) } else { this.dialer.releaseToken(tokens.splice(tokens.indexOf(token), 1)[0]) diff --git a/test/dialing/dial-request.spec.js b/test/dialing/dial-request.spec.js index d785b00051..f88db77275 100644 --- a/test/dialing/dial-request.spec.js +++ b/test/dialing/dial-request.spec.js @@ -53,16 +53,11 @@ describe('Dial Request', () => { it('should release tokens when all addr dials have started', async () => { const mockConnection = await createMockConnection() + const firstDials = pDefer() const deferred = pDefer() const actions = { - 1: async () => { - await delay(0) - return Promise.reject(error) - }, - 2: async () => { - await delay(0) - return Promise.reject(error) - }, + 1: () => firstDials.promise, + 2: () => firstDials.promise, 3: () => deferred.promise } const dialAction = (num) => actions[num]() @@ -85,7 +80,11 @@ describe('Dial Request', () => { sinon.spy(dialer, 'releaseToken') dialRequest.run({ signal: controller.signal }) // Let the first dials run - await delay(100) + await delay(0) + + // Finish the first 2 dials + firstDials.reject(error) + await delay(0) // Only 1 dial should remain, so 1 token should have been released expect(actions[1]).to.have.property('callCount', 1) @@ -93,7 +92,7 @@ describe('Dial Request', () => { expect(actions[3]).to.have.property('callCount', 1) expect(dialer.releaseToken).to.have.property('callCount', 1) - // Finish the dial + // Finish the dial and release the 2nd token deferred.resolve(mockConnection) await delay(0) expect(dialer.releaseToken).to.have.property('callCount', 2) From 8c6ad79630138dd66acf23bb53289bbe15484174 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Tue, 10 Dec 2019 15:23:04 +0100 Subject: [PATCH 51/92] docs: new api (#472) * docs: new api * chore: new iteration * chore: apply suggestions from code review Co-Authored-By: Alan Shaw * chore: apply suggestions from code review Co-Authored-By: Jacob Heun * chore: address review * docs: add events * chore: apply suggestions from code review Co-Authored-By: Jacob Heun --- README.md | 317 +-------------------------- doc/API.md | 611 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 612 insertions(+), 316 deletions(-) create mode 100644 doc/API.md diff --git a/README.md b/README.md index 1a28d9b9dd..cbac8cfc9c 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,6 @@ We've come a long way, but this project is still in Alpha, lots of development i - [Install](#install) - [Usage](#usage) - [API](#api) - - [Events](#events) - [Development](#development) - [Tests](#tests) - [Packages](#packages) @@ -209,321 +208,7 @@ class Node extends Libp2p { ### API -**IMPORTANT NOTE**: All the methods listed in the API section that take a callback are also now Promisified. Libp2p is migrating away from callbacks to async/await, and in a future release (that will be announced in advance), callback support will be removed entirely. You can follow progress of the async/await endeavor at https://github.com/ipfs/js-ipfs/issues/1670. - -#### Create a Node - `Libp2p.create(options)` - -> Behaves exactly like `new Libp2p(options)`, but doesn't require a PeerInfo. One will be generated instead - -```js -const { create } = require('libp2p') -const libp2p = await create(options) - -await libp2p.start() -``` - -- `options`: Object of libp2p configuration options - -#### Create a Node alternative - `new Libp2p(options)` - -> Creates an instance of Libp2p with a custom `PeerInfo` provided via `options.peerInfo`. - -Required keys in the `options` object: - -- `peerInfo`: instance of [PeerInfo][] that contains the [PeerId][], Keys and [multiaddrs][multiaddr] of the libp2p Node. -- `modules.transport`: An array that must include at least 1 transport, such as `libp2p-tcp`. - -#### `libp2p.start(callback)` - -> Start the libp2p Node. - -`callback` following signature `function (err) {}`, where `err` is an Error in case starting the node fails. - -#### `libp2p.stop(callback)` - -> Stop the libp2p Node. - -`callback` following signature `function (err) {}`, where `err` is an Error in case stopping the node fails. - -#### `libp2p.dial(peer, callback)` - -> Dials to another peer in the network, establishes the connection. - -- `peer`: can be an instance of [PeerInfo][], [PeerId][], [multiaddr][], or a multiaddr string -- `callback` following signature `function (err, conn) {}`, where `err` is an Error in of failure to dial the connection and `conn` is a [Connection][] instance in case of a protocol selected, if not it is undefined. - -#### `libp2p.dialProtocol(peer, protocol, callback)` - -> Dials to another peer in the network and selects a protocol to talk with that peer. - -- `peer`: can be an instance of [PeerInfo][], [PeerId][], [multiaddr][], or a multiaddr string -- `protocol`: String that defines the protocol (e.g '/ipfs/bitswap/1.1.0') -- `callback`: Function with signature `function (err, conn) {}`, where `conn` is a [Connection](https://github.com/libp2p/interface-connection) object - -`callback` following signature `function (err, conn) {}`, where `err` is an Error in of failure to dial the connection and `conn` is a [Connection][] instance in case of a protocol selected, if not it is undefined. - -#### `libp2p.dialFSM(peer, protocol, callback)` - -> Behaves like `.dial` and `.dialProtocol` but calls back with a Connection State Machine - -- `peer`: can be an instance of [PeerInfo][], [PeerId][], [multiaddr][], or a multiaddr string -- `protocol`: an optional String that defines the protocol (e.g '/ipfs/bitswap/1.1.0') -- `callback`: following signature `function (err, connFSM) {}`, where `connFSM` is a [Connection State Machine](https://github.com/libp2p/js-libp2p-switch#connection-state-machine) - -#### `libp2p.hangUp(peer, callback)` - -> Closes an open connection with a peer, graciously. - -- `peer`: can be an instance of [PeerInfo][], [PeerId][] or [multiaddr][] - -`callback` following signature `function (err) {}`, where `err` is an Error in case stopping the node fails. - -#### `libp2p.peerRouting.findPeer(id, options, callback)` - -> Looks up for multiaddrs of a peer in the DHT - -- `id`: instance of [PeerId][] -- `options`: object of options -- `options.maxTimeout`: Number milliseconds - -#### `libp2p.contentRouting.findProviders(key, options, callback)` - -- `key`: Buffer -- `options`: object of options -- `options.maxTimeout`: Number milliseconds -- `options.maxNumProviders` maximum number of providers to find - -#### `libp2p.contentRouting.provide(key, callback)` - -- `key`: Buffer - -#### `libp2p.handle(protocol, handlerFunc [, matchFunc])` - -> Handle new protocol - -- `protocol`: String that defines the protocol (e.g '/ipfs/bitswap/1.1.0') -- `handlerFunc`: following signature `function (protocol, conn) {}`, where `conn` is a [Connection](https://github.com/libp2p/interface-connection) object -- `matchFunc`: Function for matching on protocol (exact matching, semver, etc). Default to exact match. - -#### `libp2p.unhandle(protocol)` - -> Stop handling protocol - -- `protocol`: String that defines the protocol (e.g '/ipfs/bitswap/1.1.0') - -#### Events - -##### `libp2p.on('start', () => {})` - -> Libp2p has started, along with all its services. - -##### `libp2p.on('stop', () => {})` - -> Libp2p has stopped, along with all its services. - -##### `libp2p.on('error', (err) => {})` - -> An error has occurred - -- `err`: instance of `Error` - -##### `libp2p.on('peer:discovery', (peer) => {})` - -> Peer has been discovered. - -If `autoDial` is `true`, applications should **not** attempt to connect to the peer -unless they are performing a specific action. See [peer discovery and auto dial](./doc/PEER_DISCOVERY.md) for more information. - -- `peer`: instance of [PeerInfo][] - -##### `libp2p.on('peer:connect', (peer) => {})` - -> We have a new muxed connection to a peer - -- `peer`: instance of [PeerInfo][] - -##### `libp2p.on('peer:disconnect', (peer) => {})` - -> We have closed a connection to a peer - -- `peer`: instance of [PeerInfo][] - -##### `libp2p.on('connection:start', (peer) => {})` - -> We created a new connection to a peer - -- `peer`: instance of [PeerInfo][] - -##### `libp2p.on('connection:end', (peer) => {})` - -> We closed a connection to a peer - -- `peer`: instance of [PeerInfo][] - -#### `libp2p.isStarted()` - -> Check if libp2p is started - -#### `libp2p.ping(peer [, options], callback)` - -> Ping a node in the network - -#### `libp2p.peerBook` - -> PeerBook instance of the node - -#### `libp2p.peerInfo` - -> PeerInfo instance of the node - -#### `libp2p.pubsub` - -> Same API as IPFS PubSub, defined in the [CORE API Spec](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/PUBSUB.md). Just replace `ipfs` by `libp2p` and you are golden. - ---------------------- - -`DHT methods also exposed for the time being` - -#### `libp2p.dht.put(key, value, callback)` - -- `key`: Buffer -- `value`: Buffer - -#### `libp2p.dht.get(key, options, callback)` - -- `key`: Buffer -- `options`: object of options -- `options.maxTimeout`: Number milliseconds - -#### `libp2p.dht.getMany(key, nVals, options, callback)` - -- `key`: Buffer -- `nVals`: Number -- `options`: object of options -- `options.maxTimeout`: Number milliseconds - -[PeerInfo]: https://github.com/libp2p/js-peer-info -[PeerId]: https://github.com/libp2p/js-peer-id -[PeerBook]: https://github.com/libp2p/js-peer-book -[multiaddr]: https://github.com/multiformats/js-multiaddr -[Connection]: https://github.com/libp2p/interface-connection - -------- - -### Switch Stats API - -##### `libp2p.stats.emit('update')` - -Every time any stat value changes, this object emits an `update` event. - -#### Global stats - -##### `libp2p.stats.global.snapshot` - -Should return a stats snapshot, which is an object containing the following keys and respective values: - -- dataSent: amount of bytes sent, [Big](https://github.com/MikeMcl/big.js#readme) number -- dataReceived: amount of bytes received, [Big](https://github.com/MikeMcl/big.js#readme) number - -##### `libp2p.stats.global.movingAverages` - -Returns an object containing the following keys: - -- dataSent -- dataReceived - -Each one of them contains an object that has a key for each interval (`60000`, `300000` and `900000` milliseconds). - -Each one of these values is [an exponential moving-average instance](https://github.com/pgte/moving-average#readme). - -#### Per-transport stats - -##### `libp2p.stats.transports()` - -Returns an array containing the tags (string) for each observed transport. - -##### `libp2p.stats.forTransport(transportTag).snapshot` - -Should return a stats snapshot, which is an object containing the following keys and respective values: - -- dataSent: amount of bytes sent, [Big](https://github.com/MikeMcl/big.js#readme) number -- dataReceived: amount of bytes received, [Big](https://github.com/MikeMcl/big.js#readme) number - -##### `libp2p.stats.forTransport(transportTag).movingAverages` - -Returns an object containing the following keys: - - dataSent - dataReceived - -Each one of them contains an object that has a key for each interval (`60000`, `300000` and `900000` milliseconds). - -Each one of these values is [an exponential moving-average instance](https://github.com/pgte/moving-average#readme). - -#### Per-protocol stats - -##### `libp2p.stats.protocols()` - -Returns an array containing the tags (string) for each observed protocol. - -##### `libp2p.stats.forProtocol(protocolTag).snapshot` - -Should return a stats snapshot, which is an object containing the following keys and respective values: - -- dataSent: amount of bytes sent, [Big](https://github.com/MikeMcl/big.js#readme) number -- dataReceived: amount of bytes received, [Big](https://github.com/MikeMcl/big.js#readme) number - - -##### `libp2p.stats.forProtocol(protocolTag).movingAverages` - -Returns an object containing the following keys: - -- dataSent -- dataReceived - -Each one of them contains an object that has a key for each interval (`60000`, `300000` and `900000` milliseconds). - -Each one of these values is [an exponential moving-average instance](https://github.com/pgte/moving-average#readme). - -#### Per-peer stats - -##### `libp2p.stats.peers()` - -Returns an array containing the peerIDs (B58-encoded string) for each observed peer. - -##### `libp2p.stats.forPeer(peerId:String).snapshot` - -Should return a stats snapshot, which is an object containing the following keys and respective values: - -- dataSent: amount of bytes sent, [Big](https://github.com/MikeMcl/big.js#readme) number -- dataReceived: amount of bytes received, [Big](https://github.com/MikeMcl/big.js#readme) number - - -##### `libp2p.stats.forPeer(peerId:String).movingAverages` - -Returns an object containing the following keys: - -- dataSent -- dataReceived - -Each one of them contains an object that has a key for each interval (`60000`, `300000` and `900000` milliseconds). - -Each one of these values is [an exponential moving-average instance](https://github.com/pgte/moving-average#readme). - -#### Stats update interval - -Stats are not updated in real-time. Instead, measurements are buffered and stats are updated at an interval. The maximum interval can be defined through the `Switch` constructor option `stats.computeThrottleTimeout`, defined in milliseconds. - -### Private Networks - -#### Enforcement - -Libp2p provides support for connection protection, such as for private networks. You can enforce network protection by setting the environment variable `LIBP2P_FORCE_PNET=1`. When this variable is on, if no protector is set via `options.connProtector`, Libp2p will throw an error upon creation. - -#### Protectors - -Some available network protectors: -* [libp2p-pnet](https://github.com/libp2p/js-libp2p/tree/master/src/pnet) +See [API.md](./doc/API.md). ## Development diff --git a/doc/API.md b/doc/API.md new file mode 100644 index 0000000000..38886c69bc --- /dev/null +++ b/doc/API.md @@ -0,0 +1,611 @@ +# API + +* [Static Functions](#static-functions) + * [`create`](#create) +* [Instance Methods](#instance-methods) + * [`start`](#start) + * [`stop`](#stop) + * [`dial`](#dial) + * [`dialProtocol`](#dialProtocol) + * [`hangUp`](#hangUp) + * [`handle`](#handle) + * [`unhandle`](#unhandle) + * [`peerRouting.findPeer`](#peerRouting.findPeer) + * [`contentRouting.findProviders`](#contentRouting.findProviders) + * [`contentRouting.provide`](#contentRouting.provide) + * [`contentRouting.put`](#contentRouting.put) + * [`contentRouting.get`](#contentRouting.get) + * [`contentRouting.getMany`](#contentRouting.getMany) + * [`pubsub.getSubscribers`](#pubsub.getSubscribers) + * [`pubsub.getTopics`](#pubsub.getTopics) + * [`pubsub.publish`](#pubsub.publish) + * [`pubsub.subscribe`](#pubsub.subscribe) + +## Static Functions + +### create + +Creates an instance of Libp2p. + +`create(options)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| options | `Object` | libp2p options | +| options.modules | `Array` | libp2p modules to use | +| [options.config] | `Object` | libp2p modules configuration and core configuration | +| [options.datastore] | `Object` | must implement [ipfs/interface-datastore](https://github.com/ipfs/interface-datastore) (in memory datastore will be used if not provided) | +| [options.peerInfo] | [PeerInfo](https://github.com/libp2p/js-peer-info) | peerInfo instance (it will be created if not provided) | + +For Libp2p configurations and modules details read the [Configuration Document](./CONFIGURATION.md). + +#### Returns + +| Type | Description | +|------|-------------| +| `Promise` | Promise resolves with the Libp2p instance | + +#### Example + +```js +const Libp2p = require('libp2p') + +// specify options +const options = {} + +// create libp2p +const libp2p = await Libp2p.create(options) +``` + +Note: The `PeerInfo` option is not required and will be generated if it is not provided. + +
Alternative +As an alternative, it is possible to create a Libp2p instance with the constructor: + +#### Example + +```js +const Libp2p = require('libp2p') + +// specify options +const options = {} + +// create libp2p +const libp2p = new Libp2p(options) +``` + +Required keys in the `options` object: + +- `peerInfo`: instance of [PeerInfo][] that contains the [PeerId][], Keys and [multiaddrs][multiaddr] of the libp2p Node (optional when using `.create`). +- `modules.transport`: An array that must include at least 1 compliant transport. See [modules that implement the transport interface](https://github.com/libp2p/js-interfaces/tree/master/src/transport#modules-that-implement-the-interface). + +
+ +Once you have a libp2p instance, you are able to listen to several events it emmits, so that you can be noticed of relevant network events. + +
Events + +#### An error has occurred + +`libp2p.on('error', (err) => {})` + +- `err`: instance of `Error` + +#### A peer has been discovered + +`libp2p.on('peer:discovery', (peer) => {})` + +If `autoDial` option is `true`, applications should **not** attempt to connect to the peer +unless they are performing a specific action. See [peer discovery and auto dial](./PEER_DISCOVERY.md) for more information. + +- `peer`: instance of [PeerInfo][https://github.com/libp2p/js-peer-info] + +#### We have a new connection to a peer + +`libp2p.on('peer:connect', (peer) => {})` + +- `peer`: instance of [PeerInfo][https://github.com/libp2p/js-peer-info] + +#### We have closed a connection to a peer + +`libp2p.on('peer:disconnect', (peer) => {})` + +- `peer`: instance of [PeerInfo][https://github.com/libp2p/js-peer-info] + +
+ +## Libp2p Instance Methods + +### start + +Starts the libp2p node. + +`libp2p.start()` + +#### Returns + +| Type | Description | +|------|-------------| +| `Promise` | Promise resolves when the node is ready | + +#### Example + +```js +const Libp2p = require('libp2p') + +// ... + +const libp2p = await Libp2p.create(options) + +// start libp2p +await libp2p.start() +``` + +### stop + +Stops the libp2p node. + +`libp2p.stop()` + +#### Returns + +| Type | Description | +|------|-------------| +| `Promise` | Promise resolves when the node is fully stopped | + +#### Example + +```js +const Libp2p = require('libp2p') + +// ... +const libp2p = await Libp2p.create(options) +// ... + +// stop libp2p +await libp2p.stop() +``` + +### dial + +Dials to another peer in the network and establishes the connection. + +`dial(peer, options)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| peer | [PeerInfo](https://github.com/libp2p/js-peer-info), [PeerId](https://github.com/libp2p/js-peer-id), [multiaddr](https://github.com/multiformats/js-multiaddr), `string` | peer to dial | +| [options] | `Object` | dial options | +| [options.signal] | [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) | An `AbortSignal` instance obtained from an [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) that can be used to abort the connection before it completes | + +#### Returns + +| Type | Description | +|------|-------------| +| `Promise` | Promise resolves with the [Connection](https://github.com/libp2p/js-interfaces/tree/master/src/connection) instance | + +#### Example + +```js +// ... +const conn = await libp2p.dial(remotePeerInfo) + +// create a new stream within the connection +const { stream, protocol } = await conn.newStream(['/echo/1.1.0', '/echo/1.0.0']) + +// protocol negotiated: 'echo/1.0.0' means that the other party only supports the older version + +// ... +await conn.close() +``` + +### dialProtocol + +Dials to another peer in the network and selects a protocol to communicate with that peer. The stream between both parties is returned, together with the negotiated protocol. + +`dialProtocol(peer, protocols, options)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| peer | [PeerInfo](https://github.com/libp2p/js-peer-info), [PeerId](https://github.com/libp2p/js-peer-id), [multiaddr](https://github.com/multiformats/js-multiaddr), `string` | peer to dial | +| protocols | `String|Array` | A list of protocols (or single protocol) to negotiate with. Protocols are attempted in order until a match is made. (e.g '/ipfs/bitswap/1.1.0') | +| [options] | `Object` | dial options | +| [options.signal] | [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) | An `AbortSignal` instance obtained from an [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) that can be used to abort the connection before it completes | + +#### Returns + +| Type | Description | +|------|-------------| +| `Promise<{ stream:*, protocol:string }>` | Promise resolves with a [duplex stream](https://gist.github.com/alanshaw/591dc7dd54e4f99338a347ef568d6ee9#duplex-it) and the protocol used | + +#### Example + +```js +// ... +const pipe = require('it-pipe') + +const { stream, protocol } = await libp2p.dialProtocol(remotePeerInfo, protocols) + +// Use this new stream like any other duplex stream +pipe([1, 2, 3], stream, consume) +``` + +### hangUp + +Attempts to gracefully close an open connection to the given peer. If the connection is not closed in the grace period, it will be forcefully closed. + +`hangUp(peer)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| peer | [PeerInfo](https://github.com/libp2p/js-peer-info), [PeerId](https://github.com/libp2p/js-peer-id), [multiaddr](https://github.com/multiformats/js-multiaddr), `string` | peer to hang up | + +#### Returns + +| Type | Description | +|------|-------------| +| `Promise` | Promise resolves once connection closes | + +#### Example + +```js +// ... +await libp2p.hangUp(remotePeerInfo) +``` + +### handle + +Sets up [multistream-select routing](https://github.com/multiformats/multistream-select) of protocols to their application handlers. Whenever a stream is opened on one of the provided protocols, the handler will be called. `handle` must be called in order to register a handler and support for a given protocol. This also informs other peers of the protocols you support. + +`libp2p.handle(protocols, handler)` + +In the event of a new handler for the same protocol being added, the first one is discarded. + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| protocols | `Array|String` | protocols to register | +| handler | `function({ connection:*, stream:*, protocol:string })` | handler to call | + + +#### Example + +```js +// ... +const handler = ({ connection, stream, protocol }) => { + // use stream or connection according to the needs +} + +libp2p.handle('/echo/1.0.0', handler) +``` + +### unhandle + +Unregisters all handlers with the given protocols + +`libp2p.unhandle(protocols)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| protocols | `Array|String` | protocols to unregister | + +#### Example + +```js +// ... +libp2p.unhandle(['/echo/1.0.0']) +``` + +### peerRouting.findPeer + +Iterates over all peer routers in series to find the given peer. If the DHT is enabled, it will be tried first. + +`libp2p.peerRouting.findPeer(peerId, options)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| peerId | [`PeerId`](https://github.com/libp2p/js-peer-id) | ID of the peer to find | +| options | `Object` | operation options | +| options.timeout | `number` | maximum time the query should run | + +#### Returns + +| Type | Description | +|------|-------------| +| `Promise` | Peer info of a known peer | + +#### Example + +```js +// ... +const peerInfo = await libp2p.peerRouting.findPeer(peerId, options) +``` + +### contentRouting.findProviders + +Iterates over all content routers in series to find providers of the given key. +Once a content router succeeds, the iteration will stop. If the DHT is enabled, it will be queried first. + +`libp2p.contentRouting.findProviders(cid, options)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| cid | [`CID`](https://github.com/multiformats/js-cid) | cid to find | +| options | `Object` | operation options | +| options.timeout | `number` | maximum time the query should run | +| options.maxNumProviders | `number` | maximum number of providers to find | + +#### Returns + +| Type | Description | +|------|-------------| +| `AsyncIterator` | Async iterator for [`PeerInfo`](https://github.com/libp2p/js-peer-info) | + +#### Example + +```js +// Iterate over the providers found for the given cid +for await (const provider of libp2p.contentRouting.findProviders(cid)) { + console.log(provider) +} +``` + +### contentRouting.provide + +Iterates over all content routers in parallel, in order to notify it is a provider of the given key. + +`libp2p.contentRouting.provide(cid)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| cid | [`CID`](https://github.com/multiformats/js-cid) | cid to provide | + +#### Returns + +| Type | Description | +|------|-------------| +| `Promise` | Promise resolves once notifications are sent | + +#### Example + +```js +// ... +await libp2p.contentRouting.provide(cid) +``` + +### contentRouting.put + +Writes a value to a key in the DHT. + +`libp2p.contentRouting.put(key, value, options)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| key | `String` | key to add to the dht | +| value | `Buffer` | value to add to the dht | +| [options] | `Object` | put options | +| [options.minPeers] | `number` | minimum number of peers required to successfully put (default: closestPeers.length) | + +#### Returns + +| Type | Description | +|------|-------------| +| `Promise` | Promise resolves once value is stored | + +#### Example + +```js +// ... +const key = '/key' +const value = Buffer.from('oh hello there') + +await libp2p.contentRouting.put(key, value) +``` + +### contentRouting.get + +Queries the DHT for a value stored for a given key. + +`libp2p.contentRouting.get(key, options)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| key | `String` | key to get from the dht | +| [options] | `Object` | get options | +| [options.timeout] | `number` | maximum time the query should run | + +#### Returns + +| Type | Description | +|------|-------------| +| `Promise` | Value obtained from the DHT | + +#### Example + +```js +// ... + +const key = '/key' +const value = await libp2p.contentRouting.get(key) +``` + +### contentRouting.getMany + +Queries the DHT for the n values stored for the given key (without sorting). + +`libp2p.contentRouting.getMany(key, nvals, options)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| key | `String` | key to get from the dht | +| nvals | `number` | number of values aimed | +| [options] | `Object` | get options | +| [options.timeout] | `number` | maximum time the query should run | + +#### Returns + +| Type | Description | +|------|-------------| +| `Promise>` | Array of records obtained from the DHT | + +#### Example + +```js +// ... + +const key = '/key' +const { from, val } = await libp2p.contentRouting.get(key) +``` + +### pubsub.getSubscribers + +Gets a list of the peer-ids that are subscribed to one topic. + +`libp2p.pubsub.getSubscribers(topic)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| topic | `string` | topic to publish | + +#### Returns + +| Type | Description | +|------|-------------| +| `Array` | peer-id subscribed to the topic | + +#### Example + +```js +const peerIds = libp2p.pubsub.getSubscribers(topic) +``` + +### pubsub.getTopics + +Gets a list of topics the node is subscribed to. + +`libp2p.pubsub.getTopics()` + +#### Returns + +| Type | Description | +|------|-------------| +| `Array` | topics the node is subscribed to | + +#### Example + +```js +const topics = libp2p.pubsub.getTopics() +``` + +### pubsub.publish + +Publishes messages to the given topics. + +`libp2p.pubsub.publish(topic, data)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| topic | `string` | topic to publish | +| data | `Buffer` | data to publish | + +#### Returns + +| Type | Description | +|------|-------------| +| `Promise` | publish success | + +#### Example + +```js +const topic = 'topic' +const data = Buffer.from('data') + +await libp2p.pubsub.publish(topic, data) +``` + +### pubsub.subscribe + +Subscribes the given handler to a pubsub topic. + +`libp2p.pubsub.subscribe(topic, handler)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| topic | `string` | topic to subscribe | +| handler | `function({ from: String, data: Buffer, seqno: Buffer, topicIDs: Array, signature: Buffer, key: Buffer })` | handler for new data on topic | + +#### Returns + +| Type | Description | +|------|-------------| +| `void` | | + +#### Example + +```js +const topic = 'topic' +const handler = (msg) => { + // msg.data - pubsub data received +} + +libp2p.pubsub.subscribe(topic, handler) +``` + +### pubsub.unsubscribe + +Unsubscribes the given handler from a pubsub topic. If no handler is provided, all handlers for the topic are removed. + +`libp2p.pubsub.unsubscribe(topic, handler)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| topic | `string` | topic to unsubscribe | +| handler | `function()` | handler subscribed | + +#### Returns + +| Type | Description | +|------|-------------| +| `void` | | + +#### Example + +```js +const topic = 'topic' +const handler = (msg) => { + // msg.data - pubsub data received +} + +libp2p.pubsub.unsubscribe(topic, handler) +``` From 9b10e09cc0f4144626a38cc7b8c42f2ed6be33d6 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Sun, 8 Dec 2019 11:02:47 +0100 Subject: [PATCH 52/92] chore: move stats folder and delete old switch code --- src/{switch => }/stats/index.js | 0 src/{switch => }/stats/old-peers.js | 0 src/{switch => }/stats/stat.js | 0 src/switch/README.md | 423 ----------------------- src/switch/connection/base.js | 126 ------- src/switch/connection/handler.js | 47 --- src/switch/connection/incoming.js | 115 ------- src/switch/connection/index.js | 498 ---------------------------- src/switch/connection/manager.js | 289 ---------------- src/switch/constants.js | 12 - src/switch/dialer/index.js | 119 ------- src/switch/dialer/queue.js | 281 ---------------- src/switch/dialer/queueManager.js | 220 ------------ src/switch/errors.js | 20 -- src/switch/index.js | 274 --------------- src/switch/limit-dialer/index.js | 88 ----- src/switch/limit-dialer/queue.js | 109 ------ src/switch/observe-connection.js | 44 --- src/switch/observer.js | 48 --- src/switch/plaintext.js | 20 -- src/switch/protocol-muxer.js | 48 --- src/switch/transport.js | 272 --------------- src/switch/utils.js | 60 ---- 23 files changed, 3113 deletions(-) rename src/{switch => }/stats/index.js (100%) rename src/{switch => }/stats/old-peers.js (100%) rename src/{switch => }/stats/stat.js (100%) delete mode 100644 src/switch/README.md delete mode 100644 src/switch/connection/base.js delete mode 100644 src/switch/connection/handler.js delete mode 100644 src/switch/connection/incoming.js delete mode 100644 src/switch/connection/index.js delete mode 100644 src/switch/connection/manager.js delete mode 100644 src/switch/constants.js delete mode 100644 src/switch/dialer/index.js delete mode 100644 src/switch/dialer/queue.js delete mode 100644 src/switch/dialer/queueManager.js delete mode 100644 src/switch/errors.js delete mode 100644 src/switch/index.js delete mode 100644 src/switch/limit-dialer/index.js delete mode 100644 src/switch/limit-dialer/queue.js delete mode 100644 src/switch/observe-connection.js delete mode 100644 src/switch/observer.js delete mode 100644 src/switch/plaintext.js delete mode 100644 src/switch/protocol-muxer.js delete mode 100644 src/switch/transport.js delete mode 100644 src/switch/utils.js diff --git a/src/switch/stats/index.js b/src/stats/index.js similarity index 100% rename from src/switch/stats/index.js rename to src/stats/index.js diff --git a/src/switch/stats/old-peers.js b/src/stats/old-peers.js similarity index 100% rename from src/switch/stats/old-peers.js rename to src/stats/old-peers.js diff --git a/src/switch/stats/stat.js b/src/stats/stat.js similarity index 100% rename from src/switch/stats/stat.js rename to src/stats/stat.js diff --git a/src/switch/README.md b/src/switch/README.md deleted file mode 100644 index 3ca9e9d595..0000000000 --- a/src/switch/README.md +++ /dev/null @@ -1,423 +0,0 @@ -libp2p-switch JavaScript implementation -====================================== - -> libp2p-switch is a dialer machine, it leverages the multiple libp2p transports, stream muxers, crypto channels and other connection upgrades to dial to peers in the libp2p network. It also supports Protocol Multiplexing through a multicodec and multistream-select handshake. - -**Note**: git history prior to merging into js-libp2p can be found in the original repository, https://github.com/libp2p/js-libp2p-switch. - -## Table of Contents - -- [Install](#install) -- [Usage](#usage) - - [Create a libp2p switch](#create-a-libp2p-switch) -- [API](#api) - - [`switch.connection`](#switchconnection) - - [`switch.dial(peer, protocol, callback)`](#switchdialpeer-protocol-callback) - - [`switch.dialFSM(peer, protocol, callback)`](#switchdialfsmpeer-protocol-callback) - - [`switch.handle(protocol, handlerFunc, matchFunc)`](#switchhandleprotocol-handlerfunc-matchfunc) - - [`switch.hangUp(peer, callback)`](#switchhanguppeer-callback) - - [`switch.start(callback)`](#switchstartcallback) - - [`switch.stop(callback)`](#switchstopcallback) - - [`switch.stats`](#stats-api) - - [`switch.unhandle(protocol)`](#switchunhandleprotocol) - - [Internal Transports API](#internal-transports-api) -- [Design Notes](#design-notes) - - [Multitransport](#multitransport) - - [Connection upgrades](#connection-upgrades) - - [Identify](#identify) - - [Notes](#notes) -- [Contribute](#contribute) -- [License](#license) - -## Install - -```bash -> npm install libp2p-switch --save -``` - -## Usage - -### Create a libp2p Switch - -```JavaScript -const switch = require('libp2p-switch') - -const sw = new switch(peerInfo , peerBook [, options]) -``` - -If defined, `options` should be an object with the following keys and respective values: - -- `denyTTL`: - number of ms a peer should not be dialable to after it errors. Each successive deny will increase the TTL from the base value. Defaults to 5 minutes -- `denyAttempts`: - number of times a peer can be denied before they are permanently denied. Defaults to 5. -- `maxParallelDials`: - number of concurrent dials the switch should allow. Defaults to `100` -- `maxColdCalls`: - number of queued cold calls that are allowed. Defaults to `50` -- `dialTimeout`: - number of ms a dial to a peer should be allowed to run. Defaults to `30000` (30 seconds) -- `stats`: an object with the following keys and respective values: - - `maxOldPeersRetention`: maximum old peers retention. For when peers disconnect and keeping the stats around in case they reconnect. Defaults to `100`. - - `computeThrottleMaxQueueSize`: maximum queue size to perform stats computation throttling. Defaults to `1000`. - - `computeThrottleTimeout`: Throttle timeout, in miliseconds. Defaults to `2000`, - - `movingAverageIntervals`: Array containin the intervals, in miliseconds, for which moving averages are calculated. Defaults to: - - ```js - [ - 60 * 1000, // 1 minute - 5 * 60 * 1000, // 5 minutes - 15 * 60 * 1000 // 15 minutes - ] - ``` - -### Private Networks - -libp2p-switch supports private networking. In order to enabled private networks, the `switch.protector` must be -set and must contain a `protect` method. You can see an example of this in the [private network -tests]([./test/pnet.node.js]). - -## API - -- peerInfo is a [PeerInfo](https://github.com/libp2p/js-peer-info) object that has the peer information. -- peerBook is a [PeerBook](https://github.com/libp2p/js-peer-book) object that stores all the known peers. - -### `switch.connection` - -##### `switch.connection.addUpgrade()` - -A connection upgrade must be able to receive and return something that implements the [interface-connection](https://github.com/libp2p/js-interfaces/tree/master/src/connection) specification. - -> **WIP** - -##### `switch.connection.addStreamMuxer(muxer)` - -Upgrading a connection to use a stream muxer is still considered an upgrade, but a special case since once this connection is applied, the returned obj will implement the [interface-stream-muxer](https://github.com/libp2p/interface-stream-muxer) spec. - -- `muxer` - -##### `switch.connection.reuse()` - -Enable the identify protocol. - -##### `switch.connection.crypto([tag, encrypt])` - -Enable a specified crypto protocol. By default no encryption is used, aka `plaintext`. If called with no arguments it resets to use `plaintext`. - -You can use for example [libp2p-secio](https://github.com/libp2p/js-libp2p-secio) like this - -```js -const secio = require('libp2p-secio') -switch.connection.crypto(secio.tag, secio.encrypt) -``` - -##### `switch.connection.enableCircuitRelay(options, callback)` - -Enable circuit relaying. - -- `options` - - enabled - activates relay dialing and listening functionality - - hop - an object with two properties - - enabled - enables circuit relaying - - active - is it an active or passive relay (default false) -- `callback` - -### `switch.dial(peer, protocol, callback)` - -dial uses the best transport (whatever works first, in the future we can have some criteria), and jump starts the connection until the point where we have to negotiate the protocol. If a muxer is available, then drop the muxer onto that connection. Good to warm up connections or to check for connectivity. If we have already a muxer for that peerInfo, then do nothing. - -- `peer`: can be an instance of [PeerInfo][], [PeerId][] or [multiaddr][] -- `protocol` -- `callback` - -### `switch.dialFSM(peer, protocol, callback)` - -works like dial, but calls back with a [Connection State Machine](#connection-state-machine) - -- `peer`: can be an instance of [PeerInfo][], [PeerId][] or [multiaddr][] -- `protocol`: String that defines the protocol (e.g '/ipfs/bitswap/1.1.0') to be used -- `callback`: Function with signature `function (err, connFSM) {}` where `connFSM` is a [Connection State Machine](#connection-state-machine) - -#### Connection State Machine -Connection state machines emit a number of events that can be used to determine the current state of the connection -and to received the underlying connection that can be used to transfer data. - -### `switch.dialer.connect(peer, options, callback)` - -a low priority dial to the provided peer. Calls to `dial` and `dialFSM` will take priority. This should be used when an application only wishes to establish connections to new peers, such as during peer discovery when there is a low peer count. Currently, anything greater than the HIGH_PRIORITY (10) will be placed into the cold call queue, and anything less than or equal to the HIGH_PRIORITY will be added to the normal queue. - -- `peer`: can be an instance of [PeerInfo][], [PeerId][] or [multiaddr][] -- `options`: Optional -- `options.priority`: Number of the priority of the dial, defaults to 20. -- `options.useFSM`: Boolean of whether or not to callback with a [Connection State Machine](#connection-state-machine) -- `callback`: Function with signature `function (err, connFSM) {}` where `connFSM` is a [Connection State Machine](#connection-state-machine) - -##### Events -- `error`: emitted whenever a fatal error occurs with the connection; the error will be emitted. -- `error:upgrade_failed`: emitted whenever the connection fails to upgrade with a muxer, this is not fatal. -- `error:connection_attempt_failed`: emitted whenever a dial attempt fails for a given transport. An array of errors is emitted. -- `connection`: emitted whenever a useable connection has been established; the underlying [Connection](https://github.com/libp2p/js-interfaces/tree/master/src/connection) will be emitted. -- `close`: emitted when the connection has closed. - -### `switch.handle(protocol, handlerFunc, matchFunc)` - -Handle a new protocol. - -- `protocol` -- `handlerFunc` - function called when we receive a dial on `protocol. Signature must be `function (protocol, conn) {}` -- `matchFunc` - matchFunc for multistream-select - -### `switch.hangUp(peer, callback)` - -Hang up the muxed connection we have with the peer. - -- `peer`: can be an instance of [PeerInfo][], [PeerId][] or [multiaddr][] -- `callback` - -### `switch.on('error', (err) => {})` - -Emitted when the switch encounters an error. - -- `err`: instance of [Error][] - -### `switch.on('peer-mux-established', (peer) => {})` - -- `peer`: is instance of [PeerInfo][] that has info of the peer we have just established a muxed connection with. - -### `switch.on('peer-mux-closed', (peer) => {})` - -- `peer`: is instance of [PeerInfo][] that has info of the peer we have just closed a muxed connection with. - -### `switch.on('connection:start', (peer) => {})` -This will be triggered anytime a new connection is created. - -- `peer`: is instance of [PeerInfo][] that has info of the peer we have just started a connection with. - -### `switch.on('connection:end', (peer) => {})` -This will be triggered anytime an existing connection, regardless of state, is removed from the switch's internal connection tracking. - -- `peer`: is instance of [PeerInfo][] that has info of the peer we have just closed a connection with. - -### `switch.on('start', () => {})` - -Emitted when the switch has successfully started. - -### `switch.on('stop', () => {})` - -Emitted when the switch has successfully stopped. - -### `switch.start(callback)` - -Start listening on all added transports that are available on the current `peerInfo`. - -### `switch.stop(callback)` - -Close all the listeners and muxers. - -- `callback` - -### Stats API - -##### `switch.stats.emit('update')` - -Every time any stat value changes, this object emits an `update` event. - -#### Global stats - -##### `switch.stats.global.snapshot` - -Should return a stats snapshot, which is an object containing the following keys and respective values: - -- dataSent: amount of bytes sent, [Big](https://github.com/MikeMcl/big.js#readme) number -- dataReceived: amount of bytes received, [Big](https://github.com/MikeMcl/big.js#readme) number - -##### `switch.stats.global.movingAverages` - -Returns an object containing the following keys: - -- dataSent -- dataReceived - -Each one of them contains an object that has a key for each interval (`60000`, `300000` and `900000` miliseconds). - -Each one of these values is [an exponential moving-average instance](https://github.com/pgte/moving-average#readme). - -#### Per-transport stats - -##### `switch.stats.transports()` - -Returns an array containing the tags (string) for each observed transport. - -##### `switch.stats.forTransport(transportTag).snapshot` - -Should return a stats snapshot, which is an object containing the following keys and respective values: - -- dataSent: amount of bytes sent, [Big](https://github.com/MikeMcl/big.js#readme) number -- dataReceived: amount of bytes received, [Big](https://github.com/MikeMcl/big.js#readme) number - -##### `switch.stats.forTransport(transportTag).movingAverages` - -Returns an object containing the following keys: - - dataSent - dataReceived - -Each one of them contains an object that has a key for each interval (`60000`, `300000` and `900000` miliseconds). - -Each one of these values is [an exponential moving-average instance](https://github.com/pgte/moving-average#readme). - -#### Per-protocol stats - -##### `switch.stats.protocols()` - -Returns an array containing the tags (string) for each observed protocol. - -##### `switch.stats.forProtocol(protocolTag).snapshot` - -Should return a stats snapshot, which is an object containing the following keys and respective values: - -- dataSent: amount of bytes sent, [Big](https://github.com/MikeMcl/big.js#readme) number -- dataReceived: amount of bytes received, [Big](https://github.com/MikeMcl/big.js#readme) number - - -##### `switch.stats.forProtocol(protocolTag).movingAverages` - -Returns an object containing the following keys: - -- dataSent -- dataReceived - -Each one of them contains an object that has a key for each interval (`60000`, `300000` and `900000` miliseconds). - -Each one of these values is [an exponential moving-average instance](https://github.com/pgte/moving-average#readme). - -#### Per-peer stats - -##### `switch.stats.peers()` - -Returns an array containing the peerIDs (B58-encoded string) for each observed peer. - -##### `switch.stats.forPeer(peerId:String).snapshot` - -Should return a stats snapshot, which is an object containing the following keys and respective values: - -- dataSent: amount of bytes sent, [Big](https://github.com/MikeMcl/big.js#readme) number -- dataReceived: amount of bytes received, [Big](https://github.com/MikeMcl/big.js#readme) number - - -##### `switch.stats.forPeer(peerId:String).movingAverages` - -Returns an object containing the following keys: - -- dataSent -- dataReceived - -Each one of them contains an object that has a key for each interval (`60000`, `300000` and `900000` miliseconds). - -Each one of these values is [an exponential moving-average instance](https://github.com/pgte/moving-average#readme). - -#### Stats update interval - -Stats are not updated in real-time. Instead, measurements are buffered and stats are updated at an interval. The maximum interval can be defined through the `Switch` constructor option `stats.computeThrottleTimeout`, defined in miliseconds. - -### `switch.unhandle(protocol)` - -Unhandle a protocol. - -- `protocol` - -### Internal Transports API - -##### `switch.transport.add(key, transport, options)` - -libp2p-switch expects transports that implement [interface-transport](https://github.com/libp2p/interface-transport). For example [libp2p-tcp](https://github.com/libp2p/js-libp2p-tcp). - -- `key` - the transport identifier. -- `transport` - -- `options` - - -##### `switch.transport.dial(key, multiaddrs, callback)` - -Dial to a peer on a specific transport. - -- `key` -- `multiaddrs` -- `callback` - -##### `switch.transport.listen(key, options, handler, callback)` - -Set a transport to start listening mode. - -- `key` -- `options` -- `handler` -- `callback` - -##### `switch.transport.close(key, callback)` - -Close the listeners of a given transport. - -- `key` -- `callback` - -## Design Notes - -### Multitransport - -libp2p is designed to support multiple transports at the same time. While peers are identified by their ID (which are generated from their public keys), the addresses of each pair may vary, depending the device where they are being run or the network in which they are accessible through. - -In order for a transport to be supported, it has to follow the [interface-transport](https://github.com/libp2p/interface-transport) spec. - -### Connection upgrades - -Each connection in libp2p follows the [interface-connection](https://github.com/libp2p/js-interfaces/tree/master/src/connection) spec. This design decision enables libp2p to have upgradable transports. - -We think of `upgrade` as a very important notion when we are talking about connections, we can see mechanisms like: stream multiplexing, congestion control, encrypted channels, multipath, simulcast, etc, as `upgrades` to a connection. A connection can be a simple and with no guarantees, drop a packet on the network with a destination thing, a transport in the other hand can be a connection and or a set of different upgrades that are mounted on top of each other, giving extra functionality to that connection and therefore `upgrading` it. - -Types of upgrades to a connection: - -- encrypted channel (with TLS for e.g) -- congestion flow (some transports don't have it by default) -- multipath (open several connections and abstract it as a single connection) -- simulcast (still really thinking this one through, it might be interesting to send a packet through different connections under some hard network circumstances) -- stream-muxer - this a special case, because once we upgrade a connection to a stream-muxer, we can open more streams (multiplex them) on a single stream, also enabling us to reuse the underlying dialed transport - -We also want to enable flexibility when it comes to upgrading a connection, for example, we might that all dialed transports pass through the encrypted channel upgrade, but not the congestion flow, specially when a transport might have already some underlying properties (UDP vs TCP vs WebRTC vs every other transport protocol) - -### Identify - -Identify is a protocol that switchs mounts on top of itself, to identify the connections between any two peers. E.g: - -- a) peer A dials a conn to peer B -- b) that conn gets upgraded to a stream multiplexer that both peers agree -- c) peer B executes de identify protocol -- d) peer B now can open streams to peer A, knowing which is the -identity of peer A - -In addition to this, we also share the "observed addresses" by the other peer, which is extremely useful information for different kinds of network topologies. - -### Notes - -To avoid the confusion between connection, stream, transport, and other names that represent an abstraction of data flow between two points, we use terms as: - -- connection - something that implements the transversal expectations of a stream between two peers, including the benefits of using a stream plus having a way to do half duplex, full duplex -- transport - something that as a dial/listen interface and return objs that implement a connection interface - -### This module uses `pull-streams` - -We expose a streaming interface based on `pull-streams`, rather then on the Node.js core streams implementation (aka Node.js streams). `pull-streams` offers us a better mechanism for error handling and flow control guarantees. If you would like to know more about why we did this, see the discussion at this [issue](https://github.com/ipfs/js-ipfs/issues/362). - -You can learn more about pull-streams at: - -- [The history of Node.js streams, nodebp April 2014](https://www.youtube.com/watch?v=g5ewQEuXjsQ) -- [The history of streams, 2016](http://dominictarr.com/post/145135293917/history-of-streams) -- [pull-streams, the simple streaming primitive](http://dominictarr.com/post/149248845122/pull-streams-pull-streams-are-a-very-simple) -- [pull-streams documentation](https://pull-stream.github.io/) - -#### Converting `pull-streams` to Node.js Streams - -If you are a Node.js streams user, you can convert a pull-stream to a Node.js stream using the module [`pull-stream-to-stream`](https://github.com/pull-stream/pull-stream-to-stream), giving you an instance of a Node.js stream that is linked to the pull-stream. For example: - -```js -const pullToStream = require('pull-stream-to-stream') - -const nodeStreamInstance = pullToStream(pullStreamInstance) -// nodeStreamInstance is an instance of a Node.js Stream -``` - -To learn more about this utility, visit https://pull-stream.github.io/#pull-stream-to-stream. diff --git a/src/switch/connection/base.js b/src/switch/connection/base.js deleted file mode 100644 index 36f7842815..0000000000 --- a/src/switch/connection/base.js +++ /dev/null @@ -1,126 +0,0 @@ -'use strict' - -const EventEmitter = require('events').EventEmitter -const debug = require('debug') -const withIs = require('class-is') - -class BaseConnection extends EventEmitter { - constructor ({ _switch, name }) { - super() - - this.switch = _switch - this.ourPeerInfo = this.switch._peerInfo - this.log = debug(`libp2p:conn:${name}`) - this.log.error = debug(`libp2p:conn:${name}:error`) - } - - /** - * Puts the state into its disconnecting flow - * - * @param {Error} err Will be emitted if provided - * @returns {void} - */ - close (err) { - if (this._state._state === 'DISCONNECTING') return - this.log('closing connection to %s', this.theirB58Id) - if (err && this._events.error) { - this.emit('error', err) - } - this._state('disconnect') - } - - emit (eventName, ...args) { - if (eventName === 'error' && !this._events.error) { - this.log.error(...args) - } else { - super.emit(eventName, ...args) - } - } - - /** - * Gets the current state of the connection - * - * @returns {string} The current state of the connection - */ - getState () { - return this._state._state - } - - /** - * Puts the state into encrypting mode - * - * @returns {void} - */ - encrypt () { - this._state('encrypt') - } - - /** - * Puts the state into privatizing mode - * - * @returns {void} - */ - protect () { - this._state('privatize') - } - - /** - * Puts the state into muxing mode - * - * @returns {void} - */ - upgrade () { - this._state('upgrade') - } - - /** - * Event handler for disconnected. - * - * @fires BaseConnection#close - * @returns {void} - */ - _onDisconnected () { - this.switch.connection.remove(this) - this.log('disconnected from %s', this.theirB58Id) - this.emit('close') - this.removeAllListeners() - } - - /** - * Event handler for privatized - * - * @fires BaseConnection#private - * @returns {void} - */ - _onPrivatized () { - this.emit('private', this.conn) - } - - /** - * Wraps this.conn with the Switch.protector for private connections - * - * @private - * @fires ConnectionFSM#error - * @returns {void} - */ - _onPrivatizing () { - if (!this.switch.protector) { - return this._state('done') - } - - this.conn = this.switch.protector.protect(this.conn, (err) => { - if (err) { - return this.close(err) - } - - this.log('successfully privatized conn to %s', this.theirB58Id) - this.conn.setPeerInfo(this.theirPeerInfo) - this._state('done') - }) - } -} - -module.exports = withIs(BaseConnection, { - className: 'BaseConnection', - symbolName: 'libp2p-switch/BaseConnection' -}) diff --git a/src/switch/connection/handler.js b/src/switch/connection/handler.js deleted file mode 100644 index fd48158b38..0000000000 --- a/src/switch/connection/handler.js +++ /dev/null @@ -1,47 +0,0 @@ -'use strict' - -const debug = require('debug') -const IncomingConnection = require('./incoming') -const observeConn = require('../observe-connection') - -function listener (_switch) { - const log = debug('libp2p:switch:listener') - - /** - * Takes a transport key and returns a connection handler function - * - * @param {string} transportKey The key of the transport to handle connections for - * @param {function} handler A custom handler to use - * @returns {function(Connection)} A connection handler function - */ - return function (transportKey, handler) { - /** - * Takes a base connection and manages listening behavior - * - * @param {Connection} conn The connection to manage - * @returns {void} - */ - return function (conn) { - log('received incoming connection for transport %s', transportKey) - conn.getPeerInfo((_, peerInfo) => { - // Add a transport level observer, if needed - const connection = transportKey ? observeConn(transportKey, null, conn, _switch.observer) : conn - const connFSM = new IncomingConnection({ connection, _switch, transportKey, peerInfo }) - - connFSM.once('error', (err) => log(err)) - connFSM.once('private', (_conn) => { - // Use the custom handler, if it was provided - if (handler) { - return handler(_conn) - } - connFSM.encrypt() - }) - connFSM.once('encrypted', () => connFSM.upgrade()) - - connFSM.protect() - }) - } - } -} - -module.exports = listener diff --git a/src/switch/connection/incoming.js b/src/switch/connection/incoming.js deleted file mode 100644 index 10ddfce62e..0000000000 --- a/src/switch/connection/incoming.js +++ /dev/null @@ -1,115 +0,0 @@ -'use strict' - -const FSM = require('fsm-event') -const multistream = require('multistream-select') -const withIs = require('class-is') - -const BaseConnection = require('./base') - -class IncomingConnectionFSM extends BaseConnection { - constructor ({ connection, _switch, transportKey, peerInfo }) { - super({ - _switch, - name: `inc:${_switch._peerInfo.id.toB58String().slice(0, 8)}` - }) - this.conn = connection - this.theirPeerInfo = peerInfo || null - this.theirB58Id = this.theirPeerInfo ? this.theirPeerInfo.id.toB58String() : null - this.ourPeerInfo = this.switch._peerInfo - this.transportKey = transportKey - this.protocolMuxer = this.switch.protocolMuxer(this.transportKey) - this.msListener = new multistream.Listener() - - this._state = FSM('DIALED', { - DISCONNECTED: { - disconnect: 'DISCONNECTED' - }, - DIALED: { // Base connection to peer established - privatize: 'PRIVATIZING', - encrypt: 'ENCRYPTING' - }, - PRIVATIZING: { // Protecting the base connection - done: 'PRIVATIZED', - disconnect: 'DISCONNECTING' - }, - PRIVATIZED: { // Base connection is protected - encrypt: 'ENCRYPTING' - }, - ENCRYPTING: { // Encrypting the base connection - done: 'ENCRYPTED', - disconnect: 'DISCONNECTING' - }, - ENCRYPTED: { // Upgrading could not happen, the connection is encrypted and waiting - upgrade: 'UPGRADING', - disconnect: 'DISCONNECTING' - }, - UPGRADING: { // Attempting to upgrade the connection with muxers - done: 'MUXED' - }, - MUXED: { - disconnect: 'DISCONNECTING' - }, - DISCONNECTING: { // Shutting down the connection - done: 'DISCONNECTED' - } - }) - - this._state.on('DISCONNECTED', () => this._onDisconnected()) - this._state.on('PRIVATIZING', () => this._onPrivatizing()) - this._state.on('PRIVATIZED', () => this._onPrivatized()) - this._state.on('ENCRYPTING', () => this._onEncrypting()) - this._state.on('ENCRYPTED', () => { - this.log('successfully encrypted connection to %s', this.theirB58Id || 'unknown peer') - this.emit('encrypted', this.conn) - }) - this._state.on('UPGRADING', () => this._onUpgrading()) - this._state.on('MUXED', () => { - this.log('successfully muxed connection to %s', this.theirB58Id || 'unknown peer') - this.emit('muxed', this.conn) - }) - this._state.on('DISCONNECTING', () => { - this._state('done') - }) - } - - /** - * Attempts to encrypt `this.conn` with the Switch's crypto. - * - * @private - * @fires IncomingConnectionFSM#error - * @returns {void} - */ - _onEncrypting () { - this.log('encrypting connection via %s', this.switch.crypto.tag) - - this.msListener.addHandler(this.switch.crypto.tag, (protocol, _conn) => { - this.conn = this.switch.crypto.encrypt(this.ourPeerInfo.id, _conn, undefined, (err) => { - if (err) { - return this.close(err) - } - this.conn.getPeerInfo((_, peerInfo) => { - this.theirPeerInfo = peerInfo - this._state('done') - }) - }) - }, null) - - // Start handling the connection - this.msListener.handle(this.conn, (err) => { - if (err) { - this.emit('crypto handshaking failed', err) - } - }) - } - - _onUpgrading () { - this.log('adding the protocol muxer to the connection') - this.protocolMuxer(this.conn, this.msListener) - this._state('done') - } -} - -module.exports = withIs(IncomingConnectionFSM, { - className: 'IncomingConnectionFSM', - symbolName: 'libp2p-switch/IncomingConnectionFSM' -}) diff --git a/src/switch/connection/index.js b/src/switch/connection/index.js deleted file mode 100644 index efdea3d071..0000000000 --- a/src/switch/connection/index.js +++ /dev/null @@ -1,498 +0,0 @@ -'use strict' - -const FSM = require('fsm-event') -const Circuit = require('../../circuit') -const multistream = require('multistream-select') -const withIs = require('class-is') -const BaseConnection = require('./base') -const parallel = require('async/parallel') -const nextTick = require('async/nextTick') -const identify = require('../../identify') -const errCode = require('err-code') -const { msHandle, msSelect, identifyDialer } = require('../utils') - -const observeConnection = require('../observe-connection') -const { - CONNECTION_FAILED, - DIAL_SELF, - INVALID_STATE_TRANSITION, - NO_TRANSPORTS_REGISTERED, - maybeUnexpectedEnd -} = require('../errors') - -/** - * @typedef {Object} ConnectionOptions - * @property {Switch} _switch Our switch instance - * @property {PeerInfo} peerInfo The PeerInfo of the peer to dial - * @property {Muxer} muxer Optional - A muxed connection - * @property {Connection} conn Optional - The base connection - * @property {string} type Optional - identify the connection as incoming or outgoing. Defaults to out. - */ - -/** - * ConnectionFSM handles the complex logic of managing a connection - * between peers. ConnectionFSM is internally composed of a state machine - * to help improve the usability and debuggability of connections. The - * state machine also helps to improve the ability to handle dial backoff, - * coalescing dials and dial locks. - */ -class ConnectionFSM extends BaseConnection { - /** - * @param {ConnectionOptions} connectionOptions - * @constructor - */ - constructor ({ _switch, peerInfo, muxer, conn, type = 'out' }) { - super({ - _switch, - name: `${type}:${_switch._peerInfo.id.toB58String().slice(0, 8)}` - }) - - this.theirPeerInfo = peerInfo - this.theirB58Id = this.theirPeerInfo.id.toB58String() - - this.conn = conn // The base connection - this.muxer = muxer // The upgraded/muxed connection - - let startState = 'DISCONNECTED' - if (this.muxer) { - startState = 'MUXED' - } - - this._state = FSM(startState, { - DISCONNECTED: { // No active connections exist for the peer - dial: 'DIALING', - disconnect: 'DISCONNECTED', - done: 'DISCONNECTED' - }, - DIALING: { // Creating an initial connection - abort: 'ABORTED', - // emit events for different transport dials? - done: 'DIALED', - error: 'ERRORED', - disconnect: 'DISCONNECTING' - }, - DIALED: { // Base connection to peer established - encrypt: 'ENCRYPTING', - privatize: 'PRIVATIZING' - }, - PRIVATIZING: { // Protecting the base connection - done: 'PRIVATIZED', - abort: 'ABORTED', - disconnect: 'DISCONNECTING' - }, - PRIVATIZED: { // Base connection is protected - encrypt: 'ENCRYPTING' - }, - ENCRYPTING: { // Encrypting the base connection - done: 'ENCRYPTED', - error: 'ERRORED', - disconnect: 'DISCONNECTING' - }, - ENCRYPTED: { // Upgrading could not happen, the connection is encrypted and waiting - upgrade: 'UPGRADING', - disconnect: 'DISCONNECTING' - }, - UPGRADING: { // Attempting to upgrade the connection with muxers - stop: 'CONNECTED', // If we cannot mux, stop upgrading - done: 'MUXED', - error: 'ERRORED', - disconnect: 'DISCONNECTING' - }, - MUXED: { - disconnect: 'DISCONNECTING' - }, - CONNECTED: { // A non muxed connection is established - disconnect: 'DISCONNECTING' - }, - DISCONNECTING: { // Shutting down the connection - done: 'DISCONNECTED', - disconnect: 'DISCONNECTING' - }, - ABORTED: { }, // A severe event occurred - ERRORED: { // An error occurred, but future dials may be allowed - disconnect: 'DISCONNECTING' // There could be multiple options here, but this is a likely action - } - }) - - this._state.on('DISCONNECTED', () => this._onDisconnected()) - this._state.on('DIALING', () => this._onDialing()) - this._state.on('DIALED', () => this._onDialed()) - this._state.on('PRIVATIZING', () => this._onPrivatizing()) - this._state.on('PRIVATIZED', () => this._onPrivatized()) - this._state.on('ENCRYPTING', () => this._onEncrypting()) - this._state.on('ENCRYPTED', () => { - this.log('successfully encrypted connection to %s', this.theirB58Id) - this.emit('encrypted', this.conn) - }) - this._state.on('UPGRADING', () => this._onUpgrading()) - this._state.on('MUXED', () => { - this.log('successfully muxed connection to %s', this.theirB58Id) - delete this.switch.conns[this.theirB58Id] - this.emit('muxed', this.muxer) - }) - this._state.on('CONNECTED', () => { - this.log('unmuxed connection opened to %s', this.theirB58Id) - this.emit('unmuxed', this.conn) - }) - this._state.on('DISCONNECTING', () => this._onDisconnecting()) - this._state.on('ABORTED', () => this._onAborted()) - this._state.on('ERRORED', () => this._onErrored()) - this._state.on('error', (err) => this._onStateError(err)) - } - - /** - * Puts the state into dialing mode - * - * @fires ConnectionFSM#Error May emit a DIAL_SELF error - * @returns {void} - */ - dial () { - if (this.theirB58Id === this.ourPeerInfo.id.toB58String()) { - return this.emit('error', DIAL_SELF()) - } else if (this.getState() === 'DIALING') { - return this.log('attempted to dial while already dialing, ignoring') - } - - this._state('dial') - } - - /** - * Initiates a handshake for the given protocol - * - * @param {string} protocol The protocol to negotiate - * @param {function(Error, Connection)} callback - * @returns {void} - */ - shake (protocol, callback) { - // If there is no protocol set yet, don't perform the handshake - if (!protocol) { - return callback(null, null) - } - - if (this.muxer && this.muxer.newStream) { - return this.muxer.newStream((err, stream) => { - if (err) { - return callback(err, null) - } - - this.log('created new stream to %s', this.theirB58Id) - this._protocolHandshake(protocol, stream, callback) - }) - } - - this._protocolHandshake(protocol, this.conn, callback) - } - - /** - * Puts the state into muxing mode - * - * @returns {void} - */ - upgrade () { - this._state('upgrade') - } - - /** - * Event handler for dialing. Transitions state when successful. - * - * @private - * @fires ConnectionFSM#error - * @returns {void} - */ - _onDialing () { - this.log('dialing %s', this.theirB58Id) - - if (!this.switch.hasTransports()) { - return this.close(NO_TRANSPORTS_REGISTERED()) - } - - const tKeys = this.switch.availableTransports(this.theirPeerInfo) - - const circuitEnabled = Boolean(this.switch.transports[Circuit.tag]) - - if (circuitEnabled && !tKeys.includes(Circuit.tag)) { - tKeys.push(Circuit.tag) - } - - const nextTransport = (key) => { - const transport = key - if (!transport) { - if (!circuitEnabled) { - return this.close( - CONNECTION_FAILED(`Circuit not enabled and all transports failed to dial peer ${this.theirB58Id}!`) - ) - } - - return this.close( - CONNECTION_FAILED(`No available transports to dial peer ${this.theirB58Id}!`) - ) - } - - if (transport === Circuit.tag) { - this.theirPeerInfo.multiaddrs.add(`/p2p-circuit/p2p/${this.theirB58Id}`) - } - - this.log('dialing transport %s', transport) - this.switch.transport.dial(transport, this.theirPeerInfo, (errors, _conn) => { - if (errors) { - this.emit('error:connection_attempt_failed', errors) - this.log(errors) - return nextTransport(tKeys.shift()) - } - - this.conn = observeConnection(transport, null, _conn, this.switch.observer) - this._state('done') - }) - } - - nextTransport(tKeys.shift()) - } - - /** - * Once a connection has been successfully dialed, the connection - * will be privatized or encrypted depending on the presence of the - * Switch.protector. - * - * @returns {void} - */ - _onDialed () { - this.log('successfully dialed %s', this.theirB58Id) - - this.emit('connected', this.conn) - } - - /** - * Event handler for disconnecting. Handles any needed cleanup - * - * @returns {void} - */ - _onDisconnecting () { - this.log('disconnecting from %s', this.theirB58Id, Boolean(this.muxer)) - - delete this.switch.conns[this.theirB58Id] - - const tasks = [] - - // Clean up stored connections - if (this.muxer) { - tasks.push((cb) => { - this.muxer.end(() => { - delete this.muxer - cb() - }) - }) - } - - // If we have the base connection, abort it - // Ignore abort errors, since we're closing - if (this.conn) { - try { - this.conn.source.abort() - } catch (_) { } - delete this.conn - } - - parallel(tasks, () => { - this._state('done') - }) - } - - /** - * Attempts to encrypt `this.conn` with the Switch's crypto. - * - * @private - * @fires ConnectionFSM#error - * @returns {void} - */ - _onEncrypting () { - const msDialer = new multistream.Dialer() - msDialer.handle(this.conn, (err) => { - if (err) { - return this.close(maybeUnexpectedEnd(err)) - } - - this.log('selecting crypto %s to %s', this.switch.crypto.tag, this.theirB58Id) - - msDialer.select(this.switch.crypto.tag, (err, _conn) => { - if (err) { - return this.close(maybeUnexpectedEnd(err)) - } - - const observedConn = observeConnection(null, this.switch.crypto.tag, _conn, this.switch.observer) - const encryptedConn = this.switch.crypto.encrypt(this.ourPeerInfo.id, observedConn, this.theirPeerInfo.id, (err) => { - if (err) { - return this.close(err) - } - - this.conn = encryptedConn - this.conn.setPeerInfo(this.theirPeerInfo) - this._state('done') - }) - }) - }) - } - - /** - * Iterates over each Muxer on the Switch and attempts to upgrade - * the given `connection`. Successful muxed connections will be stored - * on the Switch.muxedConns with `b58Id` as their key for future reference. - * - * @private - * @returns {void} - */ - _onUpgrading () { - const muxers = Object.keys(this.switch.muxers) - this.log('upgrading connection to %s', this.theirB58Id) - - if (muxers.length === 0) { - return this._state('stop') - } - - const msDialer = new multistream.Dialer() - msDialer.handle(this.conn, (err) => { - if (err) { - return this._didUpgrade(err) - } - - // 1. try to handshake in one of the muxers available - // 2. if succeeds - // - add the muxedConn to the list of muxedConns - // - add incomming new streams to connHandler - const nextMuxer = (key) => { - this.log('selecting %s', key) - msDialer.select(key, (err, _conn) => { - if (err) { - if (muxers.length === 0) { - return this._didUpgrade(err) - } - - return nextMuxer(muxers.shift()) - } - - // observe muxed connections - const conn = observeConnection(null, key, _conn, this.switch.observer) - - this.muxer = this.switch.muxers[key].dialer(conn) - - this.muxer.once('close', () => { - this.close() - }) - - // For incoming streams, in case identify is on - this.muxer.on('stream', (conn) => { - this.log('new stream created via muxer to %s', this.theirB58Id) - conn.setPeerInfo(this.theirPeerInfo) - this.switch.protocolMuxer(null)(conn) - }) - - this._didUpgrade(null) - - // Run identify on the connection - if (this.switch.identify) { - this._identify((err, results) => { - if (err) { - return this.close(err) - } - this.theirPeerInfo = this.switch._peerBook.put(results.peerInfo) - }) - } - }) - } - - nextMuxer(muxers.shift()) - }) - } - - /** - * Runs the identify protocol on the connection - * @private - * @param {function(error, { PeerInfo })} callback - * @returns {void} - */ - _identify (callback) { - if (!this.muxer) { - return nextTick(callback, errCode('The connection was already closed', 'ERR_CONNECTION_CLOSED')) - } - this.muxer.newStream(async (err, conn) => { - if (err) return callback(err) - const ms = new multistream.Dialer() - let results - try { - await msHandle(ms, conn) - const msConn = await msSelect(ms, identify.multicodec) - results = await identifyDialer(msConn, this.theirPeerInfo) - } catch (err) { - return callback(err) - } - callback(null, results) - }) - } - - /** - * Analyses the given error, if it exists, to determine where the state machine - * needs to go. - * - * @param {Error} err - * @returns {void} - */ - _didUpgrade (err) { - if (err) { - this.log('Error upgrading connection:', err) - this.switch.conns[this.theirB58Id] = this - this.emit('error:upgrade_failed', err) - // Cant upgrade, hold the encrypted connection - return this._state('stop') - } - - // move the state machine forward - this._state('done') - } - - /** - * Performs the protocol handshake for the given protocol - * over the given connection. The resulting error or connection - * will be returned via the callback. - * - * @private - * @param {string} protocol - * @param {Connection} connection - * @param {function(Error, Connection)} callback - * @returns {void} - */ - _protocolHandshake (protocol, connection, callback) { - const msDialer = new multistream.Dialer() - msDialer.handle(connection, (err) => { - if (err) { - return callback(err, null) - } - - msDialer.select(protocol, (err, _conn) => { - if (err) { - this.log('could not perform protocol handshake:', err) - return callback(err, null) - } - - const conn = observeConnection(null, protocol, _conn, this.switch.observer) - this.log('successfully performed handshake of %s to %s', protocol, this.theirB58Id) - this.emit('connection', conn) - callback(null, conn) - }) - }) - } - - /** - * Event handler for state transition errors - * - * @param {Error} err - * @returns {void} - */ - _onStateError (err) { - this.emit('error', INVALID_STATE_TRANSITION(err)) - this.log(err) - } -} - -module.exports = withIs(ConnectionFSM, { - className: 'ConnectionFSM', - symbolName: 'libp2p-switch/ConnectionFSM' -}) diff --git a/src/switch/connection/manager.js b/src/switch/connection/manager.js deleted file mode 100644 index 2654757d8b..0000000000 --- a/src/switch/connection/manager.js +++ /dev/null @@ -1,289 +0,0 @@ -'use strict' - -const identify = require('../../identify') -const multistream = require('multistream-select') -const debug = require('debug') -const log = debug('libp2p:switch:conn-manager') -const once = require('once') -const ConnectionFSM = require('../connection') -const { msHandle, msSelect, identifyDialer } = require('../utils') - -const Circuit = require('../../circuit') - -const plaintext = require('../plaintext') - -/** - * Contains methods for binding handlers to the Switch - * in order to better manage its connections. - */ -class ConnectionManager { - constructor (_switch) { - this.switch = _switch - this.connections = {} - } - - /** - * Adds the connection for tracking if it's not already added - * @private - * @param {ConnectionFSM} connection - * @returns {void} - */ - add (connection) { - this.connections[connection.theirB58Id] = this.connections[connection.theirB58Id] || [] - // Only add it if it's not there - if (!this.get(connection)) { - this.connections[connection.theirB58Id].push(connection) - this.switch.emit('connection:start', connection.theirPeerInfo) - if (connection.getState() === 'MUXED') { - this.switch.emit('peer-mux-established', connection.theirPeerInfo) - // Clear the denylist of the peer - this.switch.dialer.clearDenylist(connection.theirPeerInfo) - } else { - connection.once('muxed', () => { - this.switch.emit('peer-mux-established', connection.theirPeerInfo) - // Clear the denylist of the peer - this.switch.dialer.clearDenylist(connection.theirPeerInfo) - }) - } - } - } - - /** - * Gets the connection from the list if it exists - * @private - * @param {ConnectionFSM} connection - * @returns {ConnectionFSM|null} The found connection or null - */ - get (connection) { - if (!this.connections[connection.theirB58Id]) return null - - for (let i = 0; i < this.connections[connection.theirB58Id].length; i++) { - if (this.connections[connection.theirB58Id][i] === connection) { - return this.connections[connection.theirB58Id][i] - } - } - return null - } - - /** - * Gets a connection associated with the given peer - * @private - * @param {string} peerId The peers id - * @returns {ConnectionFSM|null} The found connection or null - */ - getOne (peerId) { - if (this.connections[peerId]) { - // Only return muxed connections - for (var i = 0; i < this.connections[peerId].length; i++) { - if (this.connections[peerId][i].getState() === 'MUXED') { - return this.connections[peerId][i] - } - } - } - return null - } - - /** - * Removes the connection from tracking - * @private - * @param {ConnectionFSM} connection The connection to remove - * @returns {void} - */ - remove (connection) { - // No record of the peer, disconnect it - if (!this.connections[connection.theirB58Id]) { - if (connection.theirPeerInfo) { - connection.theirPeerInfo.disconnect() - this.switch.emit('peer-mux-closed', connection.theirPeerInfo) - } - return - } - - for (let i = 0; i < this.connections[connection.theirB58Id].length; i++) { - if (this.connections[connection.theirB58Id][i] === connection) { - this.connections[connection.theirB58Id].splice(i, 1) - break - } - } - - // The peer is fully disconnected - if (this.connections[connection.theirB58Id].length === 0) { - delete this.connections[connection.theirB58Id] - connection.theirPeerInfo.disconnect() - this.switch.emit('peer-mux-closed', connection.theirPeerInfo) - } - - // A tracked connection was closed, let the world know - this.switch.emit('connection:end', connection.theirPeerInfo) - } - - /** - * Returns all connections being tracked - * @private - * @returns {ConnectionFSM[]} - */ - getAll () { - let connections = [] - for (const conns of Object.values(this.connections)) { - connections = [...connections, ...conns] - } - return connections - } - - /** - * Returns all connections being tracked for a given peer id - * @private - * @param {string} peerId Stringified peer id - * @returns {ConnectionFSM[]} - */ - getAllById (peerId) { - return this.connections[peerId] || [] - } - - /** - * Adds a listener for the given `muxer` and creates a handler for it - * leveraging the Switch.protocolMuxer handler factory - * - * @param {Muxer} muxer - * @returns {void} - */ - addStreamMuxer (muxer) { - // for dialing - this.switch.muxers[muxer.multicodec] = muxer - - // for listening - this.switch.handle(muxer.multicodec, (protocol, conn) => { - const muxedConn = muxer.listener(conn) - - muxedConn.on('stream', this.switch.protocolMuxer(null)) - - // If identify is enabled - // 1. overload getPeerInfo - // 2. call getPeerInfo - // 3. add this conn to the pool - if (this.switch.identify) { - // Get the peer info from the crypto exchange - conn.getPeerInfo((err, cryptoPI) => { - if (err || !cryptoPI) { - log('crypto peerInfo wasnt found') - } - - // overload peerInfo to use Identify instead - conn.getPeerInfo = async (callback) => { - const conn = muxedConn.newStream() - const ms = new multistream.Dialer() - callback = once(callback) - - let results - try { - await msHandle(ms, conn) - const msConn = await msSelect(ms, identify.multicodec) - results = await identifyDialer(msConn, cryptoPI) - } catch (err) { - return muxedConn.end(() => { - callback(err, null) - }) - } - - const { peerInfo } = results - - if (peerInfo) { - conn.setPeerInfo(peerInfo) - } - callback(null, peerInfo) - } - - conn.getPeerInfo((err, peerInfo) => { - /* eslint no-warning-comments: off */ - if (err) { - return log('identify not successful') - } - const b58Str = peerInfo.id.toB58String() - peerInfo = this.switch._peerBook.put(peerInfo) - - const connection = new ConnectionFSM({ - _switch: this.switch, - peerInfo, - muxer: muxedConn, - conn: conn, - type: 'inc' - }) - this.switch.connection.add(connection) - - // Only update if it's not already connected - if (!peerInfo.isConnected()) { - if (peerInfo.multiaddrs.size > 0) { - // with incomming conn and through identify, going to pick one - // of the available multiaddrs from the other peer as the one - // I'm connected to as we really can't be sure at the moment - // TODO add this consideration to the connection abstraction! - peerInfo.connect(peerInfo.multiaddrs.toArray()[0]) - } else { - // for the case of websockets in the browser, where peers have - // no addr, use just their IPFS id - peerInfo.connect(`/ipfs/${b58Str}`) - } - } - - muxedConn.once('close', () => { - connection.close() - }) - }) - }) - } - - return conn - }) - } - - /** - * Adds the `encrypt` handler for the given `tag` and also sets the - * Switch's crypto to passed `encrypt` function - * - * @param {String} tag - * @param {function(PeerID, Connection, PeerId, Callback)} encrypt - * @returns {void} - */ - crypto (tag, encrypt) { - if (!tag && !encrypt) { - tag = plaintext.tag - encrypt = plaintext.encrypt - } - - this.switch.crypto = { tag, encrypt } - } - - /** - * If config.enabled is true, a Circuit relay will be added to the - * available Switch transports. - * - * @param {any} config - * @returns {void} - */ - enableCircuitRelay (config) { - config = config || {} - - if (config.enabled) { - if (!config.hop) { - Object.assign(config, { hop: { enabled: false, active: false } }) - } - - this.switch.transport.add(Circuit.tag, new Circuit(this.switch, config)) - } - } - - /** - * Sets identify to true on the Switch and performs handshakes - * for libp2p-identify leveraging the Switch's muxer. - * - * @returns {void} - */ - reuse () { - this.switch.identify = true - this.switch.handle(identify.multicodec, (protocol, conn) => { - identify.listener(conn, this.switch._peerInfo) - }) - } -} - -module.exports = ConnectionManager diff --git a/src/switch/constants.js b/src/switch/constants.js deleted file mode 100644 index 72c442d275..0000000000 --- a/src/switch/constants.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict' - -module.exports = { - DENY_TTL: 5 * 60 * 1e3, // How long before an errored peer can be dialed again - DENY_ATTEMPTS: 5, // Num of unsuccessful dials before a peer is permanently denied - DIAL_TIMEOUT: 30e3, // How long in ms a dial attempt is allowed to take - MAX_COLD_CALLS: 50, // How many dials w/o protocols that can be queued - MAX_PARALLEL_DIALS: 100, // Maximum allowed concurrent dials - QUARTER_HOUR: 15 * 60e3, - PRIORITY_HIGH: 10, - PRIORITY_LOW: 20 -} diff --git a/src/switch/dialer/index.js b/src/switch/dialer/index.js deleted file mode 100644 index 46bfeb7269..0000000000 --- a/src/switch/dialer/index.js +++ /dev/null @@ -1,119 +0,0 @@ -'use strict' - -const DialQueueManager = require('./queueManager') -const { getPeerInfo } = require('../../get-peer-info') -const { - DENY_ATTEMPTS, - DENY_TTL, - MAX_COLD_CALLS, - MAX_PARALLEL_DIALS, - PRIORITY_HIGH, - PRIORITY_LOW -} = require('../constants') - -module.exports = function (_switch) { - const dialQueueManager = new DialQueueManager(_switch) - - _switch.state.on('STARTED:enter', start) - _switch.state.on('STOPPING:enter', stop) - - /** - * @param {DialRequest} dialRequest - * @returns {void} - */ - function _dial ({ peerInfo, protocol, options, callback }) { - if (typeof protocol === 'function') { - callback = protocol - protocol = null - } - - try { - peerInfo = getPeerInfo(peerInfo, _switch._peerBook) - } catch (err) { - return callback(err) - } - - // Add it to the queue, it will automatically get executed - dialQueueManager.add({ peerInfo, protocol, options, callback }) - } - - /** - * Starts the `DialQueueManager` - * - * @param {function} callback - */ - function start (callback) { - dialQueueManager.start() - callback() - } - - /** - * Aborts all dials that are queued. This should - * only be used when the Switch is being stopped - * - * @param {function} callback - */ - function stop (callback) { - dialQueueManager.stop() - callback() - } - - /** - * Clears the denylist for a given peer - * @param {PeerInfo} peerInfo - */ - function clearDenylist (peerInfo) { - dialQueueManager.clearDenylist(peerInfo) - } - - /** - * Attempts to establish a connection to the given `peerInfo` at - * a lower priority than a standard dial. - * @param {PeerInfo} peerInfo - * @param {object} options - * @param {boolean} options.useFSM Whether or not to return a `ConnectionFSM`. Defaults to false. - * @param {number} options.priority Lowest priority goes first. Defaults to 20. - * @param {function(Error, Connection)} callback - */ - function connect (peerInfo, options, callback) { - if (typeof options === 'function') { - callback = options - options = null - } - options = { useFSM: false, priority: PRIORITY_LOW, ...options } - _dial({ peerInfo, protocol: null, options, callback }) - } - - /** - * Adds the dial request to the queue for the given `peerInfo` - * The request will be added with a high priority (10). - * @param {PeerInfo} peerInfo - * @param {string} protocol - * @param {function(Error, Connection)} callback - */ - function dial (peerInfo, protocol, callback) { - _dial({ peerInfo, protocol, options: { useFSM: false, priority: PRIORITY_HIGH }, callback }) - } - - /** - * Behaves like dial, except it calls back with a ConnectionFSM - * - * @param {PeerInfo} peerInfo - * @param {string} protocol - * @param {function(Error, ConnectionFSM)} callback - */ - function dialFSM (peerInfo, protocol, callback) { - _dial({ peerInfo, protocol, options: { useFSM: true, priority: PRIORITY_HIGH }, callback }) - } - - return { - connect, - dial, - dialFSM, - clearDenylist, - DENY_ATTEMPTS: isNaN(_switch._options.denyAttempts) ? DENY_ATTEMPTS : _switch._options.denyAttempts, - DENY_TTL: isNaN(_switch._options.denyTTL) ? DENY_TTL : _switch._options.denyTTL, - MAX_COLD_CALLS: isNaN(_switch._options.maxColdCalls) ? MAX_COLD_CALLS : _switch._options.maxColdCalls, - MAX_PARALLEL_DIALS: isNaN(_switch._options.maxParallelDials) ? MAX_PARALLEL_DIALS : _switch._options.maxParallelDials - } -} diff --git a/src/switch/dialer/queue.js b/src/switch/dialer/queue.js deleted file mode 100644 index 976f9ccc34..0000000000 --- a/src/switch/dialer/queue.js +++ /dev/null @@ -1,281 +0,0 @@ -'use strict' - -const ConnectionFSM = require('../connection') -const { DIAL_ABORTED, ERR_DENIED } = require('../errors') -const nextTick = require('async/nextTick') -const once = require('once') -const debug = require('debug') -const log = debug('libp2p:switch:dial') -log.error = debug('libp2p:switch:dial:error') - -/** - * Components required to execute a dial - * @typedef {Object} DialRequest - * @property {PeerInfo} peerInfo - The peer to dial to - * @property {string} [protocol] - The protocol to create a stream for - * @property {object} options - * @property {boolean} options.useFSM - If `callback` should return a ConnectionFSM - * @property {number} options.priority - The priority of the dial - * @property {function(Error, Connection|ConnectionFSM)} callback - */ - -/** - * @typedef {Object} NewConnection - * @property {ConnectionFSM} connectionFSM - * @property {boolean} didCreate - */ - -/** - * Attempts to create a new connection or stream (when muxed), - * via negotiation of the given `protocol`. If no `protocol` is - * provided, no action will be taken and `callback` will be called - * immediately with no error or values. - * - * @param {object} options - * @param {string} options.protocol - * @param {ConnectionFSM} options.connection - * @param {function(Error, Connection)} options.callback - * @returns {void} - */ -function createConnectionWithProtocol ({ protocol, connection, callback }) { - if (!protocol) { - return callback() - } - connection.shake(protocol, (err, conn) => { - if (!conn) { - return callback(err) - } - - conn.setPeerInfo(connection.theirPeerInfo) - callback(null, conn) - }) -} - -/** - * A convenience array wrapper for controlling - * a per peer queue - * - * @returns {Queue} - */ -class Queue { - /** - * @constructor - * @param {string} peerId - * @param {Switch} _switch - * @param {function(string)} onStopped Called when the queue stops - */ - constructor (peerId, _switch, onStopped) { - this.id = peerId - this.switch = _switch - this._queue = [] - this.denylisted = null - this.denylistCount = 0 - this.isRunning = false - this.onStopped = onStopped - } - - get length () { - return this._queue.length - } - - /** - * Adds the dial request to the queue. The queue is not automatically started - * @param {string} protocol - * @param {boolean} useFSM If callback should use a ConnectionFSM instead - * @param {function(Error, Connection)} callback - * @returns {void} - */ - add (protocol, useFSM, callback) { - if (!this.isDialAllowed()) { - return nextTick(callback, ERR_DENIED()) - } - this._queue.push({ protocol, useFSM, callback }) - } - - /** - * Determines whether or not dialing is currently allowed - * @returns {boolean} - */ - isDialAllowed () { - if (this.denylisted) { - // If the deny ttl has passed, reset it - if (Date.now() > this.denylisted) { - this.denylisted = null - return true - } - // Dial is not allowed - return false - } - return true - } - - /** - * Starts the queue. If the queue was started `true` will be returned. - * If the queue was already running `false` is returned. - * @returns {boolean} - */ - start () { - if (!this.isRunning) { - log('starting dial queue to %s', this.id) - this.isRunning = true - this._run() - return true - } - return false - } - - /** - * Stops the queue - */ - stop () { - if (this.isRunning) { - log('stopping dial queue to %s', this.id) - this.isRunning = false - this.onStopped(this.id) - } - } - - /** - * Stops the queue and errors the callback for each dial request - */ - abort () { - while (this.length > 0) { - const dial = this._queue.shift() - dial.callback(DIAL_ABORTED()) - } - this.stop() - } - - /** - * Marks the queue as denylisted. The queue will be immediately aborted. - * @returns {void} - */ - denylist () { - this.denylistCount++ - - if (this.denylistCount >= this.switch.dialer.DENY_ATTEMPTS) { - this.denylisted = Infinity - return - } - - let ttl = this.switch.dialer.DENY_TTL * Math.pow(this.denylistCount, 3) - const minTTL = ttl * 0.9 - const maxTTL = ttl * 1.1 - - // Add a random jitter of 20% to the ttl - ttl = Math.floor(Math.random() * (maxTTL - minTTL) + minTTL) - - this.denylisted = Date.now() + ttl - this.abort() - } - - /** - * Attempts to find a muxed connection for the given peer. If one - * isn't found, a new one will be created. - * - * Returns an array containing two items. The ConnectionFSM and wether - * or not the ConnectionFSM was just created. The latter can be used - * to determine dialing needs. - * - * @private - * @param {PeerInfo} peerInfo - * @returns {NewConnection} - */ - _getOrCreateConnection (peerInfo) { - let connectionFSM = this.switch.connection.getOne(this.id) - let didCreate = false - - if (!connectionFSM) { - connectionFSM = new ConnectionFSM({ - _switch: this.switch, - peerInfo, - muxer: null, - conn: null - }) - - this.switch.connection.add(connectionFSM) - - // Add control events and start the dialer - connectionFSM.once('connected', () => connectionFSM.protect()) - connectionFSM.once('private', () => connectionFSM.encrypt()) - connectionFSM.once('encrypted', () => connectionFSM.upgrade()) - - didCreate = true - } - - return { connectionFSM, didCreate } - } - - /** - * Executes the next dial in the queue for the given peer - * @private - * @returns {void} - */ - _run () { - // If we have no items in the queue or we're stopped, exit - if (this.length < 1 || !this.isRunning) { - log('stopping the queue for %s', this.id) - return this.stop() - } - - const next = once(() => { - log('starting next dial to %s', this.id) - this._run() - }) - - const peerInfo = this.switch._peerBook.get(this.id) - const queuedDial = this._queue.shift() - const { connectionFSM, didCreate } = this._getOrCreateConnection(peerInfo) - - // If the dial expects a ConnectionFSM, we can provide that back now - if (queuedDial.useFSM) { - nextTick(queuedDial.callback, null, connectionFSM) - } - - // If we can handshake protocols, get a new stream and call run again - if (['MUXED', 'CONNECTED'].includes(connectionFSM.getState())) { - queuedDial.connection = connectionFSM - createConnectionWithProtocol(queuedDial) - next() - return - } - - // If we error, error the queued dial - // In the future, it may be desired to error the other queued dials, - // depending on the error. - connectionFSM.once('error', (err) => { - queuedDial.callback(err) - // Dont denylist peers we have identified and that we are connected to - if (peerInfo.protocols.size > 0 && peerInfo.isConnected()) { - return - } - this.denylist() - }) - - connectionFSM.once('close', () => { - next() - }) - - // If we're not muxed yet, add listeners - connectionFSM.once('muxed', () => { - this.denylistCount = 0 // reset denylisting on good connections - queuedDial.connection = connectionFSM - createConnectionWithProtocol(queuedDial) - next() - }) - - connectionFSM.once('unmuxed', () => { - this.denylistCount = 0 - queuedDial.connection = connectionFSM - createConnectionWithProtocol(queuedDial) - next() - }) - - // If we have a new connection, start dialing - if (didCreate) { - connectionFSM.dial() - } - } -} - -module.exports = Queue diff --git a/src/switch/dialer/queueManager.js b/src/switch/dialer/queueManager.js deleted file mode 100644 index 0239a482bc..0000000000 --- a/src/switch/dialer/queueManager.js +++ /dev/null @@ -1,220 +0,0 @@ -'use strict' - -const once = require('once') -const Queue = require('./queue') -const { DIAL_ABORTED } = require('../errors') -const nextTick = require('async/nextTick') -const retimer = require('retimer') -const { QUARTER_HOUR, PRIORITY_HIGH } = require('../constants') -const debug = require('debug') -const log = debug('libp2p:switch:dial:manager') -const noop = () => {} - -class DialQueueManager { - /** - * @constructor - * @param {Switch} _switch - */ - constructor (_switch) { - this._queue = new Set() - this._coldCallQueue = new Set() - this._dialingQueues = new Set() - this._queues = {} - this.switch = _switch - this._cleanInterval = retimer(this._clean.bind(this), QUARTER_HOUR) - this.start() - } - - /** - * Runs through all queues, aborts and removes them if they - * are no longer valid. A queue that is denylisted indefinitely, - * is considered no longer valid. - * @private - */ - _clean () { - const queues = Object.values(this._queues) - queues.forEach(dialQueue => { - // Clear if the queue has reached max denylist - if (dialQueue.denylisted === Infinity) { - dialQueue.abort() - delete this._queues[dialQueue.id] - return - } - - // Keep track of denylisted queues - if (dialQueue.denylisted) return - - // Clear if peer is no longer active - // To avoid reallocating memory, dont delete queues of - // connected peers, as these are highly likely to leverage the - // queues in the immediate term - if (!dialQueue.isRunning && dialQueue.length < 1) { - let isConnected = false - try { - const peerInfo = this.switch._peerBook.get(dialQueue.id) - isConnected = Boolean(peerInfo.isConnected()) - } catch (_) { - // If we get an error, that means the peerbook doesnt have the peer - } - - if (!isConnected) { - dialQueue.abort() - delete this._queues[dialQueue.id] - } - } - }) - - this._cleanInterval.reschedule(QUARTER_HOUR) - } - - /** - * Allows the `DialQueueManager` to execute dials - */ - start () { - this.isRunning = true - } - - /** - * Iterates over all items in the DialerQueue - * and executes there callback with an error. - * - * This causes the entire DialerQueue to be drained - */ - stop () { - this.isRunning = false - // Clear the general queue - this._queue.clear() - // Clear the cold call queue - this._coldCallQueue.clear() - - this._cleanInterval.clear() - - // Abort the individual peer queues - const queues = Object.values(this._queues) - queues.forEach(dialQueue => { - dialQueue.abort() - delete this._queues[dialQueue.id] - }) - } - - /** - * Adds the `dialRequest` to the queue and ensures queue is running - * - * @param {DialRequest} dialRequest - * @returns {void} - */ - add ({ peerInfo, protocol, options, callback }) { - callback = callback ? once(callback) : noop - - // Add the dial to its respective queue - const targetQueue = this.getQueue(peerInfo) - - // Cold Call - if (options.priority > PRIORITY_HIGH) { - // If we have too many cold calls, abort the dial immediately - if (this._coldCallQueue.size >= this.switch.dialer.MAX_COLD_CALLS) { - return nextTick(callback, DIAL_ABORTED()) - } - - if (this._queue.has(targetQueue.id)) { - return nextTick(callback, DIAL_ABORTED()) - } - } - - targetQueue.add(protocol, options.useFSM, callback) - - // If we're already connected to the peer, start the queue now - // While it might cause queues to go over the max parallel amount, - // it avoids denying peers we're already connected to - if (peerInfo.isConnected()) { - targetQueue.start() - return - } - - // If dialing is not allowed, abort - if (!targetQueue.isDialAllowed()) { - return - } - - // Add the id to its respective queue set if the queue isn't running - if (!targetQueue.isRunning) { - if (options.priority <= PRIORITY_HIGH) { - this._queue.add(targetQueue.id) - this._coldCallQueue.delete(targetQueue.id) - // Only add it to the cold queue if it's not in the normal queue - } else { - this._coldCallQueue.add(targetQueue.id) - } - } - - this.run() - } - - /** - * Will execute up to `MAX_PARALLEL_DIALS` dials - */ - run () { - if (!this.isRunning) return - - if (this._dialingQueues.size < this.switch.dialer.MAX_PARALLEL_DIALS) { - let nextQueue = { done: true } - // Check the queue first and fall back to the cold call queue - if (this._queue.size > 0) { - nextQueue = this._queue.values().next() - this._queue.delete(nextQueue.value) - } else if (this._coldCallQueue.size > 0) { - nextQueue = this._coldCallQueue.values().next() - this._coldCallQueue.delete(nextQueue.value) - } - - if (nextQueue.done) { - return - } - - const targetQueue = this._queues[nextQueue.value] - - if (!targetQueue) { - log('missing queue %s, maybe it was aborted?', nextQueue.value) - return - } - - this._dialingQueues.add(targetQueue.id) - targetQueue.start() - } - } - - /** - * Will remove the `peerInfo` from the dial denylist - * @param {PeerInfo} peerInfo - */ - clearDenylist (peerInfo) { - const queue = this.getQueue(peerInfo) - queue.denylisted = null - queue.denylistCount = 0 - } - - /** - * A handler for when dialing queues stop. This will trigger - * `run()` in order to keep the queue processing. - * @private - * @param {string} id peer id of the queue that stopped - */ - _onQueueStopped (id) { - this._dialingQueues.delete(id) - this.run() - } - - /** - * Returns the `Queue` for the given `peerInfo` - * @param {PeerInfo} peerInfo - * @returns {Queue} - */ - getQueue (peerInfo) { - const id = peerInfo.id.toB58String() - - this._queues[id] = this._queues[id] || new Queue(id, this.switch, this._onQueueStopped.bind(this)) - return this._queues[id] - } -} - -module.exports = DialQueueManager diff --git a/src/switch/errors.js b/src/switch/errors.js deleted file mode 100644 index 26803936d1..0000000000 --- a/src/switch/errors.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict' - -const errCode = require('err-code') - -module.exports = { - CONNECTION_FAILED: (err) => errCode(err, 'CONNECTION_FAILED'), - DIAL_ABORTED: () => errCode('Dial was aborted', 'DIAL_ABORTED'), - ERR_DENIED: () => errCode('Dial is currently denied for this peer', 'ERR_DENIED'), - DIAL_SELF: () => errCode('A node cannot dial itself', 'DIAL_SELF'), - INVALID_STATE_TRANSITION: (err) => errCode(err, 'INVALID_STATE_TRANSITION'), - NO_TRANSPORTS_REGISTERED: () => errCode('No transports registered, dial not possible', 'NO_TRANSPORTS_REGISTERED'), - PROTECTOR_REQUIRED: () => errCode('No protector provided with private network enforced', 'PROTECTOR_REQUIRED'), - UNEXPECTED_END: () => errCode('Unexpected end of input from reader.', 'UNEXPECTED_END'), - maybeUnexpectedEnd: (err) => { - if (err === true) { - return module.exports.UNEXPECTED_END() - } - return err - } -} diff --git a/src/switch/index.js b/src/switch/index.js deleted file mode 100644 index d7b3ef6761..0000000000 --- a/src/switch/index.js +++ /dev/null @@ -1,274 +0,0 @@ -'use strict' - -const FSM = require('fsm-event') -const EventEmitter = require('events').EventEmitter -const each = require('async/each') -const eachSeries = require('async/eachSeries') -const series = require('async/series') -const Circuit = require('../circuit') -const TransportManager = require('./transport') -const ConnectionManager = require('./connection/manager') -const { getPeerInfo } = require('../get-peer-info') -const getDialer = require('./dialer') -const connectionHandler = require('./connection/handler') -const ProtocolMuxer = require('./protocol-muxer') -const plaintext = require('./plaintext') -const Observer = require('./observer') -const Stats = require('./stats') -const assert = require('assert') -const Errors = require('./errors') -const debug = require('debug') -const log = debug('libp2p:switch') -log.error = debug('libp2p:switch:error') - -/** - * @fires Switch#stop Triggered when the switch has stopped - * @fires Switch#start Triggered when the switch has started - * @fires Switch#error Triggered whenever an error occurs - */ -class Switch extends EventEmitter { - constructor (peerInfo, peerBook, options) { - super() - assert(peerInfo, 'You must provide a `peerInfo`') - assert(peerBook, 'You must provide a `peerBook`') - - this._peerInfo = peerInfo - this._peerBook = peerBook - this._options = options || {} - - this.setMaxListeners(Infinity) - // transports -- - // { key: transport }; e.g { tcp: } - this.transports = {} - - // connections -- - // { peerIdB58: { conn: }} - this.conns = {} - - // { protocol: handler } - this.protocols = {} - - // { muxerCodec: } e.g { '/spdy/0.3.1': spdy } - this.muxers = {} - - // is the Identify protocol enabled? - this.identify = false - - // Crypto details - this.crypto = plaintext - - this.protector = this._options.protector || null - - this.transport = new TransportManager(this) - this.connection = new ConnectionManager(this) - - this.observer = Observer(this) - this.stats = Stats(this.observer, this._options.stats) - this.protocolMuxer = ProtocolMuxer(this.protocols, this.observer) - - // All purpose connection handler for managing incoming connections - this._connectionHandler = connectionHandler(this) - - // Setup the internal state - this.state = new FSM('STOPPED', { - STOPPED: { - start: 'STARTING', - stop: 'STOPPING' // ensures that any transports that were manually started are stopped - }, - STARTING: { - done: 'STARTED', - stop: 'STOPPING' - }, - STARTED: { - stop: 'STOPPING', - start: 'STARTED' - }, - STOPPING: { - stop: 'STOPPING', - done: 'STOPPED' - } - }) - this.state.on('STARTING', () => { - log('The switch is starting') - this._onStarting() - }) - this.state.on('STOPPING', () => { - log('The switch is stopping') - this._onStopping() - }) - this.state.on('STARTED', () => { - log('The switch has started') - this.emit('start') - }) - this.state.on('STOPPED', () => { - log('The switch has stopped') - this.emit('stop') - }) - this.state.on('error', (err) => { - log.error(err) - this.emit('error', err) - }) - - // higher level (public) API - this.dialer = getDialer(this) - this.dial = this.dialer.dial - this.dialFSM = this.dialer.dialFSM - } - - /** - * Returns a list of the transports peerInfo has addresses for - * - * @param {PeerInfo} peerInfo - * @returns {Array} - */ - availableTransports (peerInfo) { - const myAddrs = peerInfo.multiaddrs.toArray() - const myTransports = Object.keys(this.transports) - - // Only listen on transports we actually have addresses for - return myTransports.filter((ts) => this.transports[ts].filter(myAddrs).length > 0) - // push Circuit to be the last proto to be dialed, and alphabetize the others - .sort((a, b) => { - if (a === Circuit.tag) return 1 - if (b === Circuit.tag) return -1 - return a < b ? -1 : 1 - }) - } - - /** - * Adds the `handlerFunc` and `matchFunc` to the Switch's protocol - * handler list for the given `protocol`. If the `matchFunc` returns - * true for a protocol check, the `handlerFunc` will be called. - * - * @param {string} protocol - * @param {function(string, Connection)} handlerFunc - * @param {function(string, string, function(Error, boolean))} matchFunc - * @returns {void} - */ - handle (protocol, handlerFunc, matchFunc) { - this.protocols[protocol] = { - handlerFunc: handlerFunc, - matchFunc: matchFunc - } - this._peerInfo.protocols.add(protocol) - } - - /** - * Removes the given protocol from the Switch's protocol list - * - * @param {string} protocol - * @returns {void} - */ - unhandle (protocol) { - if (this.protocols[protocol]) { - delete this.protocols[protocol] - } - this._peerInfo.protocols.delete(protocol) - } - - /** - * If a muxed Connection exists for the given peer, it will be closed - * and its reference on the Switch will be removed. - * - * @param {PeerInfo|Multiaddr|PeerId} peer - * @param {function()} callback - * @returns {void} - */ - hangUp (peer, callback) { - const peerInfo = getPeerInfo(peer, this._peerBook) - const key = peerInfo.id.toB58String() - const conns = [...this.connection.getAllById(key)] - each(conns, (conn, cb) => { - conn.once('close', cb) - conn.close() - }, callback) - } - - /** - * Returns whether or not the switch has any transports - * - * @returns {boolean} - */ - hasTransports () { - const transports = Object.keys(this.transports).filter((t) => t !== Circuit.tag) - return transports && transports.length > 0 - } - - /** - * Issues a start on the Switch state. - * - * @param {function} callback deprecated: Listening for the `error` and `start` events are recommended - * @returns {void} - */ - start (callback = () => {}) { - // Add once listener for deprecated callback support - this.once('start', callback) - - this.state('start') - } - - /** - * Issues a stop on the Switch state. - * - * @param {function} callback deprecated: Listening for the `error` and `stop` events are recommended - * @returns {void} - */ - stop (callback = () => {}) { - // Add once listener for deprecated callback support - this.once('stop', callback) - - this.state('stop') - } - - /** - * A listener that will start any necessary services and listeners - * - * @private - * @returns {void} - */ - _onStarting () { - this.stats.start() - eachSeries(this.availableTransports(this._peerInfo), (ts, cb) => { - // Listen on the given transport - this.transport.listen(ts, {}, null, cb) - }, (err) => { - if (err) { - log.error(err) - this.emit('error', err) - return this.state('stop') - } - this.state('done') - }) - } - - /** - * A listener that will turn off all running services and listeners - * - * @private - * @returns {void} - */ - _onStopping () { - this.stats.stop() - series([ - (cb) => { - each(this.transports, (transport, cb) => { - each(transport.listeners, (listener, cb) => { - listener.close((err) => { - if (err) log.error(err) - cb() - }) - }, cb) - }, cb) - }, - (cb) => each(this.connection.getAll(), (conn, cb) => { - conn.once('close', cb) - conn.close() - }, cb) - ], (_) => { - this.state('done') - }) - } -} - -module.exports = Switch -module.exports.errors = Errors diff --git a/src/switch/limit-dialer/index.js b/src/switch/limit-dialer/index.js deleted file mode 100644 index 1702e89ae0..0000000000 --- a/src/switch/limit-dialer/index.js +++ /dev/null @@ -1,88 +0,0 @@ -'use strict' - -const tryEach = require('async/tryEach') -const debug = require('debug') - -const log = debug('libp2p:switch:dialer') - -const DialQueue = require('./queue') - -/** - * Track dials per peer and limited them. - */ -class LimitDialer { - /** - * Create a new dialer. - * - * @param {number} perPeerLimit - * @param {number} dialTimeout - */ - constructor (perPeerLimit, dialTimeout) { - log('create: %s peer limit, %s dial timeout', perPeerLimit, dialTimeout) - this.perPeerLimit = perPeerLimit - this.dialTimeout = dialTimeout - this.queues = new Map() - } - - /** - * Dial a list of multiaddrs on the given transport. - * - * @param {PeerId} peer - * @param {SwarmTransport} transport - * @param {Array} addrs - * @param {function(Error, Connection)} callback - * @returns {void} - */ - dialMany (peer, transport, addrs, callback) { - log('dialMany:start') - // we use a token to track if we want to cancel following dials - const token = { cancel: false } - - const errors = [] - const tasks = addrs.map((m) => { - return (cb) => this.dialSingle(peer, transport, m, token, (err, result) => { - if (err) { - errors.push(err) - return cb(err) - } - return cb(null, result) - }) - }) - - tryEach(tasks, (_, result) => { - if (result && result.conn) { - log('dialMany:success') - return callback(null, result) - } - - log('dialMany:error') - callback(errors) - }) - } - - /** - * Dial a single multiaddr on the given transport. - * - * @param {PeerId} peer - * @param {SwarmTransport} transport - * @param {Multiaddr} addr - * @param {CancelToken} token - * @param {function(Error, Connection)} callback - * @returns {void} - */ - dialSingle (peer, transport, addr, token, callback) { - const ps = peer.toB58String() - log('dialSingle: %s:%s', ps, addr.toString()) - let q - if (this.queues.has(ps)) { - q = this.queues.get(ps) - } else { - q = new DialQueue(this.perPeerLimit, this.dialTimeout) - this.queues.set(ps, q) - } - - q.push(transport, addr, token, callback) - } -} - -module.exports = LimitDialer diff --git a/src/switch/limit-dialer/queue.js b/src/switch/limit-dialer/queue.js deleted file mode 100644 index 94d38c5bf6..0000000000 --- a/src/switch/limit-dialer/queue.js +++ /dev/null @@ -1,109 +0,0 @@ -'use strict' - -const { Connection } = require('libp2p-interfaces/src/connection') -const pull = require('pull-stream/pull') -const empty = require('pull-stream/sources/empty') -const timeout = require('async/timeout') -const queue = require('async/queue') -const debug = require('debug') -const once = require('once') - -const log = debug('libp2p:switch:dialer:queue') -log.error = debug('libp2p:switch:dialer:queue:error') - -/** - * Queue up the amount of dials to a given peer. - */ -class DialQueue { - /** - * Create a new dial queue. - * - * @param {number} limit - * @param {number} dialTimeout - */ - constructor (limit, dialTimeout) { - this.dialTimeout = dialTimeout - - this.queue = queue((task, cb) => { - this._doWork(task.transport, task.addr, task.token, cb) - }, limit) - } - - /** - * The actual work done by the queue. - * - * @param {SwarmTransport} transport - * @param {Multiaddr} addr - * @param {CancelToken} token - * @param {function(Error, Connection)} callback - * @returns {void} - * @private - */ - _doWork (transport, addr, token, callback) { - callback = once(callback) - log('work:start') - this._dialWithTimeout(transport, addr, (err, conn) => { - if (err) { - log.error(`${transport.constructor.name}:work`, err) - return callback(err) - } - - if (token.cancel) { - log('work:cancel') - // clean up already done dials - pull(empty(), conn) - // If we can close the connection, do it - if (typeof conn.close === 'function') { - return conn.close((_) => callback(null)) - } - return callback(null) - } - - // one is enough - token.cancel = true - - log('work:success') - - const proxyConn = new Connection() - proxyConn.setInnerConn(conn) - callback(null, { multiaddr: addr, conn: conn }) - }) - } - - /** - * Dial the given transport, timing out with the set timeout. - * - * @param {SwarmTransport} transport - * @param {Multiaddr} addr - * @param {function(Error, Connection)} callback - * @returns {void} - * - * @private - */ - _dialWithTimeout (transport, addr, callback) { - timeout((cb) => { - const conn = transport.dial(addr, (err) => { - if (err) { - return cb(err) - } - - cb(null, conn) - }) - }, this.dialTimeout)(callback) - } - - /** - * Add new work to the queue. - * - * @param {SwarmTransport} transport - * @param {Multiaddr} addr - * @param {CancelToken} token - * @param {function(Error, Connection)} callback - * @returns {void} - */ - push (transport, addr, token, callback) { - this.queue.push({ transport, addr, token }, callback) - } -} - -module.exports = DialQueue diff --git a/src/switch/observe-connection.js b/src/switch/observe-connection.js deleted file mode 100644 index 45f87cd0ba..0000000000 --- a/src/switch/observe-connection.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict' - -const { Connection } = require('libp2p-interfaces/src/connection') -const pull = require('pull-stream/pull') - -/** - * Creates a pull stream to run the given Connection stream through - * the given Observer. This provides a way to more easily monitor connections - * and their metadata. A new Connection will be returned that contains - * has the attached Observer. - * - * @param {Transport} transport - * @param {string} protocol - * @param {Connection} connection - * @param {Observer} observer - * @returns {Connection} - */ -module.exports = (transport, protocol, connection, observer) => { - const peerInfo = new Promise((resolve, reject) => { - connection.getPeerInfo((err, peerInfo) => { - if (!err && peerInfo) { - resolve(peerInfo) - return - } - - const setPeerInfo = connection.setPeerInfo - connection.setPeerInfo = (pi) => { - setPeerInfo.call(connection, pi) - resolve(pi) - } - }) - }) - - const stream = { - source: pull( - connection, - observer.incoming(transport, protocol, peerInfo)), - sink: pull( - observer.outgoing(transport, protocol, peerInfo), - connection) - } - - return new Connection(stream, connection) -} diff --git a/src/switch/observer.js b/src/switch/observer.js deleted file mode 100644 index d117762869..0000000000 --- a/src/switch/observer.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict' - -const map = require('pull-stream/throughs/map') -const EventEmitter = require('events') - -/** - * Takes a Switch and returns an Observer that can be used in conjunction with - * observe-connection.js. The returned Observer comes with `incoming` and - * `outgoing` properties that can be used in pull streams to emit all metadata - * for messages that pass through a Connection. - * - * @param {Switch} swtch - * @returns {EventEmitter} - */ -module.exports = (swtch) => { - const observer = Object.assign(new EventEmitter(), { - incoming: observe('in'), - outgoing: observe('out') - }) - - swtch.on('peer-mux-established', (peerInfo) => { - observer.emit('peer:connected', peerInfo.id.toB58String()) - }) - - swtch.on('peer-mux-closed', (peerInfo) => { - observer.emit('peer:closed', peerInfo.id.toB58String()) - }) - - return observer - - function observe (direction) { - return (transport, protocol, peerInfo) => { - return map((buffer) => { - willObserve(peerInfo, transport, protocol, direction, buffer.length) - return buffer - }) - } - } - - function willObserve (peerInfo, transport, protocol, direction, bufferLength) { - peerInfo.then((_peerInfo) => { - if (_peerInfo) { - const peerId = _peerInfo.id.toB58String() - observer.emit('message', peerId, transport, protocol, direction, bufferLength) - } - }) - } -} diff --git a/src/switch/plaintext.js b/src/switch/plaintext.js deleted file mode 100644 index 889a07f85d..0000000000 --- a/src/switch/plaintext.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict' - -const setImmediate = require('async/setImmediate') - -/** - * An encryption stub in the instance that the default crypto - * has not been overriden for the Switch - */ -module.exports = { - tag: '/plaintext/1.0.0', - encrypt (myId, conn, remoteId, callback) { - if (typeof remoteId === 'function') { - callback = remoteId - remoteId = undefined - } - - setImmediate(() => callback()) - return conn - } -} diff --git a/src/switch/protocol-muxer.js b/src/switch/protocol-muxer.js deleted file mode 100644 index 09b152fe7d..0000000000 --- a/src/switch/protocol-muxer.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict' - -const multistream = require('multistream-select') -const observeConn = require('./observe-connection') - -const debug = require('debug') -const log = debug('libp2p:switch:protocol-muxer') -log.error = debug('libp2p:switch:protocol-muxer:error') - -module.exports = function protocolMuxer (protocols, observer) { - return (transport) => (_parentConn, msListener) => { - const ms = msListener || new multistream.Listener() - let parentConn - - // Only observe the transport if we have one, and there is not already a listener - if (transport && !msListener) { - parentConn = observeConn(transport, null, _parentConn, observer) - } else { - parentConn = _parentConn - } - - Object.keys(protocols).forEach((protocol) => { - if (!protocol) { - return - } - - const handler = (protocolName, _conn) => { - log('registering handler with protocol %s', protocolName) - const protocol = protocols[protocolName] - if (protocol) { - const handlerFunc = protocol && protocol.handlerFunc - if (handlerFunc) { - const conn = observeConn(null, protocolName, _conn, observer) - handlerFunc(protocol, conn) - } - } - } - - ms.addHandler(protocol, handler, protocols[protocol].matchFunc) - }) - - ms.handle(parentConn, (err) => { - if (err) { - log.error('multistream handshake failed', err) - } - }) - } -} diff --git a/src/switch/transport.js b/src/switch/transport.js deleted file mode 100644 index ce0d298f05..0000000000 --- a/src/switch/transport.js +++ /dev/null @@ -1,272 +0,0 @@ -'use strict' - -/* eslint no-warning-comments: off */ - -const parallel = require('async/parallel') -const once = require('once') -const debug = require('debug') -const log = debug('libp2p:switch:transport') - -const LimitDialer = require('./limit-dialer') -const { DIAL_TIMEOUT } = require('./constants') -const { uniqueBy } = require('./utils') - -// number of concurrent outbound dials to make per peer, same as go-libp2p-swtch -const defaultPerPeerRateLimit = 8 - -/** - * Manages the transports for the switch. This simplifies dialing and listening across - * multiple transports. - */ -class TransportManager { - constructor (_switch) { - this.switch = _switch - this.dialer = new LimitDialer(defaultPerPeerRateLimit, this.switch._options.dialTimeout || DIAL_TIMEOUT) - } - - /** - * Adds a `Transport` to the list of transports on the switch, and assigns it to the given key - * - * @param {String} key - * @param {Transport} transport - * @returns {void} - */ - add (key, transport) { - log('adding %s', key) - if (this.switch.transports[key]) { - throw new Error('There is already a transport with this key') - } - - this.switch.transports[key] = transport - if (!this.switch.transports[key].listeners) { - this.switch.transports[key].listeners = [] - } - } - - /** - * Closes connections for the given transport key - * and removes it from the switch. - * - * @param {String} key - * @param {function(Error)} callback - * @returns {void} - */ - remove (key, callback) { - callback = callback || function () {} - - if (!this.switch.transports[key]) { - return callback() - } - - this.close(key, (err) => { - delete this.switch.transports[key] - callback(err) - }) - } - - /** - * Calls `remove` on each transport the switch has - * - * @param {function(Error)} callback - * @returns {void} - */ - removeAll (callback) { - const tasks = Object.keys(this.switch.transports).map((key) => { - return (cb) => { - this.remove(key, cb) - } - }) - - parallel(tasks, callback) - } - - /** - * For a given transport `key`, dial to all that transport multiaddrs - * - * @param {String} key Key of the `Transport` to dial - * @param {PeerInfo} peerInfo - * @param {function(Error, Connection)} callback - * @returns {void} - */ - dial (key, peerInfo, callback) { - const transport = this.switch.transports[key] - let multiaddrs = peerInfo.multiaddrs.toArray() - - if (!Array.isArray(multiaddrs)) { - multiaddrs = [multiaddrs] - } - - // filter the multiaddrs that are actually valid for this transport - multiaddrs = TransportManager.dialables(transport, multiaddrs, this.switch._peerInfo) - log('dialing %s', key, multiaddrs.map((m) => m.toString())) - - // dial each of the multiaddrs with the given transport - this.dialer.dialMany(peerInfo.id, transport, multiaddrs, (errors, success) => { - if (errors) { - return callback(errors) - } - - peerInfo.connect(success.multiaddr) - callback(null, success.conn) - }) - } - - /** - * For a given Transport `key`, listen on all multiaddrs in the switch's `_peerInfo`. - * If a `handler` is not provided, the Switch's `protocolMuxer` will be used. - * - * @param {String} key - * @param {*} _options Currently ignored - * @param {function(Connection)} handler - * @param {function(Error)} callback - * @returns {void} - */ - listen (key, _options, handler, callback) { - handler = this.switch._connectionHandler(key, handler) - - const transport = this.switch.transports[key] - let originalAddrs = this.switch._peerInfo.multiaddrs.toArray() - - // Until TCP can handle distinct addresses on listen, https://github.com/libp2p/interface-transport/issues/41, - // make sure we aren't trying to listen on duplicate ports. This also applies to websockets. - originalAddrs = uniqueBy(originalAddrs, (addr) => { - // Any non 0 port should register as unique - const port = Number(addr.toOptions().port) - return isNaN(port) || port === 0 ? addr.toString() : port - }) - - const multiaddrs = TransportManager.dialables(transport, originalAddrs) - - if (!transport.listeners) { - transport.listeners = [] - } - - let freshMultiaddrs = [] - - const createListeners = multiaddrs.map((ma) => { - return (cb) => { - const done = once(cb) - const listener = transport.createListener(handler) - listener.once('error', done) - - listener.listen(ma, (err) => { - if (err) { - return done(err) - } - listener.removeListener('error', done) - listener.getAddrs((err, addrs) => { - if (err) { - return done(err) - } - freshMultiaddrs = freshMultiaddrs.concat(addrs) - transport.listeners.push(listener) - done() - }) - }) - } - }) - - parallel(createListeners, (err) => { - if (err) { - return callback(err) - } - - // cause we can listen on port 0 or 0.0.0.0 - this.switch._peerInfo.multiaddrs.replace(multiaddrs, freshMultiaddrs) - callback() - }) - } - - /** - * Closes the transport with the given key, by closing all of its listeners - * - * @param {String} key - * @param {function(Error)} callback - * @returns {void} - */ - close (key, callback) { - const transport = this.switch.transports[key] - - if (!transport) { - return callback(new Error(`Trying to close non existing transport: ${key}`)) - } - - parallel(transport.listeners.map((listener) => { - return (cb) => { - listener.close(cb) - } - }), callback) - } - - /** - * For a given transport, return its multiaddrs that match the given multiaddrs - * - * @param {Transport} transport - * @param {Array} multiaddrs - * @param {PeerInfo} peerInfo Optional - a peer whose addresses should not be returned - * @returns {Array} - */ - static dialables (transport, multiaddrs, peerInfo) { - // If we dont have a proper transport, return no multiaddrs - if (!transport || !transport.filter) return [] - - const transportAddrs = transport.filter(multiaddrs) - if (!peerInfo || !transportAddrs.length) { - return transportAddrs - } - - const ourAddrs = ourAddresses(peerInfo) - - const result = transportAddrs.filter(transportAddr => { - // If our address is in the destination address, filter it out - return !ourAddrs.some(a => getDestination(transportAddr).startsWith(a)) - }) - - return result - } -} - -/** - * Expand addresses in peer info into array of addresses with and without peer - * ID suffix. - * - * @param {PeerInfo} peerInfo Our peer info object - * @returns {String[]} - */ -function ourAddresses (peerInfo) { - const ourPeerId = peerInfo.id.toB58String() - return peerInfo.multiaddrs.toArray() - .reduce((ourAddrs, addr) => { - const peerId = addr.getPeerId() - addr = addr.toString() - const otherAddr = peerId - ? addr.slice(0, addr.lastIndexOf(`/ipfs/${peerId}`)) - : `${addr}/ipfs/${ourPeerId}` - return ourAddrs.concat([addr, otherAddr]) - }, []) - .filter(a => Boolean(a)) - .concat(`/ipfs/${ourPeerId}`) -} - -const RelayProtos = [ - 'p2p-circuit', - 'p2p-websocket-star', - 'p2p-webrtc-star', - 'p2p-stardust' -] - -/** - * Get the destination address of a (possibly relay) multiaddr as a string - * - * @param {Multiaddr} addr - * @returns {String} - */ -function getDestination (addr) { - const protos = addr.protoNames().reverse() - const splitProto = protos.find(p => RelayProtos.includes(p)) - addr = addr.toString() - if (!splitProto) return addr - return addr.slice(addr.lastIndexOf(splitProto) + splitProto.length) -} - -module.exports = TransportManager diff --git a/src/switch/utils.js b/src/switch/utils.js deleted file mode 100644 index 797c4db754..0000000000 --- a/src/switch/utils.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict' - -const Identify = require('../identify') - -/** - * For a given multistream, registers to handle the given connection - * @param {MultistreamDialer} multistream - * @param {Connection} connection - * @returns {Promise} - */ -module.exports.msHandle = (multistream, connection) => { - return new Promise((resolve, reject) => { - multistream.handle(connection, (err) => { - if (err) return reject(err) - resolve() - }) - }) -} - -/** - * For a given multistream, selects the given protocol - * @param {MultistreamDialer} multistream - * @param {string} protocol - * @returns {Promise} Resolves the selected Connection - */ -module.exports.msSelect = (multistream, protocol) => { - return new Promise((resolve, reject) => { - multistream.select(protocol, (err, connection) => { - if (err) return reject(err) - resolve(connection) - }) - }) -} - -/** - * Runs identify for the given connection and verifies it against the - * PeerInfo provided - * @param {Connection} connection - * @param {PeerInfo} cryptoPeerInfo The PeerInfo determined during crypto exchange - * @returns {Promise} Resolves {peerInfo, observedAddrs} - */ -module.exports.identifyDialer = (connection, cryptoPeerInfo) => { - return new Promise((resolve, reject) => { - Identify.dialer(connection, cryptoPeerInfo, (err, peerInfo, observedAddrs) => { - if (err) return reject(err) - resolve({ peerInfo, observedAddrs }) - }) - }) -} - -/** - * Get unique values from `arr` using `getValue` to determine - * what is used for uniqueness - * @param {Array} arr The array to get unique values for - * @param {function(value)} getValue The function to determine what is compared - * @returns {Array} - */ -module.exports.uniqueBy = (arr, getValue) => { - return [...new Map(arr.map((i) => [getValue(i), i])).values()] -} From edaa67dfd0c08e6ff47554ff1493f2012742d899 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Sun, 8 Dec 2019 11:03:14 +0100 Subject: [PATCH 53/92] chore: remove unused packages --- package.json | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/package.json b/package.json index 9deef33de6..fdb501cda1 100644 --- a/package.json +++ b/package.json @@ -44,13 +44,11 @@ "abort-controller": "^3.0.0", "aggregate-error": "^3.0.1", "any-signal": "^1.1.0", - "async": "^2.6.2", "async-iterator-all": "^1.0.0", "bignumber.js": "^9.0.0", "class-is": "^1.1.0", "debug": "^4.1.1", "err-code": "^1.1.2", - "fsm-event": "^2.1.0", "hashlru": "^2.3.0", "it-handshake": "^1.0.1", "it-length-prefixed": "^3.0.0", @@ -64,19 +62,12 @@ "moving-average": "^1.0.0", "multiaddr": "^7.2.1", "multistream-select": "^0.15.0", - "once": "^1.4.0", "p-any": "^2.1.0", "p-fifo": "^1.0.0", - "p-map": "^3.0.0", - "p-queue": "^6.1.1", "p-settle": "^3.1.0", "peer-id": "^0.13.4", "peer-info": "^0.17.0", - "promisify-es6": "^1.0.3", "protons": "^1.0.1", - "pull-cat": "^1.1.11", - "pull-handshake": "^1.1.4", - "pull-stream": "^3.6.9", "retimer": "^2.0.0", "timeout-abort-controller": "^1.0.0", "xsalsa20": "^1.0.2" @@ -84,16 +75,12 @@ "devDependencies": { "@nodeutils/defaults-deep": "^1.1.0", "abortable-iterator": "^2.1.0", - "aegir": "^20.0.0", + "aegir": "^20.4.1", "chai": "^4.2.0", "chai-as-promised": "^7.1.1", - "chai-checkmark": "^1.0.1", "cids": "^0.7.1", "delay": "^4.3.0", "dirty-chai": "^2.0.1", - "electron-webrtc": "^0.3.0", - "glob": "^7.1.4", - "interface-datastore": "^0.6.0", "it-pair": "^1.0.0", "libp2p-bootstrap": "^0.10.3", "libp2p-delegated-content-routing": "^0.4.1", @@ -103,23 +90,13 @@ "libp2p-kad-dht": "^0.18.2", "libp2p-mdns": "^0.13.0", "libp2p-mplex": "^0.9.1", - "libp2p-pnet": "~0.1.0", "libp2p-secio": "^0.12.1", - "libp2p-spdy": "^0.13.2", "libp2p-tcp": "^0.14.1", "libp2p-websockets": "^0.13.1", - "lodash.times": "^4.3.2", "nock": "^10.0.6", "p-defer": "^3.0.0", "p-times": "^2.1.0", "p-wait-for": "^3.1.0", - "portfinder": "^1.0.20", - "pull-goodbye": "0.0.2", - "pull-length-prefixed": "^1.3.3", - "pull-mplex": "^0.1.2", - "pull-pair": "^1.1.0", - "pull-protocol-buffers": "~0.1.2", - "pull-serializer": "^0.3.2", "sinon": "^7.2.7", "streaming-iterables": "^4.1.0", "wrtc": "^0.4.1" From a2f31d99d2164993fcfa69c45ce60fdf32719012 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Sun, 8 Dec 2019 11:04:52 +0100 Subject: [PATCH 54/92] chore: disable pull dep check until ping is refactored --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7a128bda2b..5820e0a93d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,8 @@ jobs: - stage: check script: # - npx aegir build --bundlesize - - npx aegir dep-check -- -i wrtc -i electron-webrtc + # Remove pull libs once ping is async + - npx aegir dep-check -- -i wrtc -i electron-webrtc -i pull-handshake -i pull-stream - npm run lint - stage: test From 600f761009dfc36421e66b2b471724720dea5e50 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Sun, 8 Dec 2019 11:07:44 +0100 Subject: [PATCH 55/92] chore: remove uneeded dep check exclusions --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5820e0a93d..bcff5d7b11 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,7 @@ jobs: script: # - npx aegir build --bundlesize # Remove pull libs once ping is async - - npx aegir dep-check -- -i wrtc -i electron-webrtc -i pull-handshake -i pull-stream + - npx aegir dep-check -- -i pull-handshake -i pull-stream - npm run lint - stage: test From ad15d4ed09344728ac2bfbb5783f8ef4d0a99a91 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Sun, 8 Dec 2019 11:05:11 +0100 Subject: [PATCH 56/92] chore: add bundlesize check back to ci --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index bcff5d7b11..b4536c9887 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ jobs: include: - stage: check script: - # - npx aegir build --bundlesize + - npx aegir build --bundlesize # Remove pull libs once ping is async - npx aegir dep-check -- -i pull-handshake -i pull-stream - npm run lint From 7fc1900343c5b6dc31c492399ab0f47a7872dfac Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Tue, 10 Dec 2019 15:15:08 +0100 Subject: [PATCH 57/92] chore: it-all over async-iterator-all --- package.json | 2 +- src/content-routing.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index fdb501cda1..eaddaabba9 100644 --- a/package.json +++ b/package.json @@ -44,12 +44,12 @@ "abort-controller": "^3.0.0", "aggregate-error": "^3.0.1", "any-signal": "^1.1.0", - "async-iterator-all": "^1.0.0", "bignumber.js": "^9.0.0", "class-is": "^1.1.0", "debug": "^4.1.1", "err-code": "^1.1.2", "hashlru": "^2.3.0", + "it-all": "^1.0.1", "it-handshake": "^1.0.1", "it-length-prefixed": "^3.0.0", "it-pipe": "^1.1.0", diff --git a/src/content-routing.js b/src/content-routing.js index 022b8b2bac..87dfc64061 100644 --- a/src/content-routing.js +++ b/src/content-routing.js @@ -3,7 +3,7 @@ const errCode = require('err-code') const { messages, codes } = require('./errors') -const all = require('async-iterator-all') +const all = require('it-all') const pAny = require('p-any') module.exports = (node) => { From 64cbf90e02cc8a549bdc5ca0a87b340a508e13d3 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Tue, 10 Dec 2019 19:26:54 +0100 Subject: [PATCH 58/92] refactor: ping (#505) * refactor: ping * chore: ping is now a function * chore: address review --- doc/API.md | 26 +++++++++++++ package.json | 1 + src/identify/index.js | 2 +- src/index.js | 27 ++++++++------ src/ping/README.md | 13 ++----- src/ping/handler.js | 50 ------------------------- src/ping/index.js | 63 ++++++++++++++++++++++++++++++-- src/ping/ping.js | 83 ------------------------------------------ src/util/index.js | 16 -------- test/core/ping.node.js | 35 ++++++++++++++++++ 10 files changed, 142 insertions(+), 174 deletions(-) delete mode 100644 src/ping/handler.js delete mode 100644 src/ping/ping.js delete mode 100644 src/util/index.js create mode 100644 test/core/ping.node.js diff --git a/doc/API.md b/doc/API.md index 38886c69bc..b31996142b 100644 --- a/doc/API.md +++ b/doc/API.md @@ -10,6 +10,7 @@ * [`hangUp`](#hangUp) * [`handle`](#handle) * [`unhandle`](#unhandle) + * [`ping`](#ping) * [`peerRouting.findPeer`](#peerRouting.findPeer) * [`contentRouting.findProviders`](#contentRouting.findProviders) * [`contentRouting.provide`](#contentRouting.provide) @@ -307,6 +308,31 @@ Unregisters all handlers with the given protocols libp2p.unhandle(['/echo/1.0.0']) ``` +### ping + +Pings a given peer and get the operation's latency. + +`libp2p.ping(peer)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| peer | `PeerInfo|PeerId|Multiaddr|string` | peer to ping | + +#### Returns + +| Type | Description | +|------|-------------| +| `Promise` | Latency of the operation in ms | + +#### Example + +```js +// ... +const latency = await libp2p.ping(otherPeerId) +``` + ### peerRouting.findPeer Iterates over all peer routers in series to find the given peer. If the DHT is enabled, it will be tried first. diff --git a/package.json b/package.json index eaddaabba9..11e2be02fb 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "err-code": "^1.1.2", "hashlru": "^2.3.0", "it-all": "^1.0.1", + "it-buffer": "^0.1.1", "it-handshake": "^1.0.1", "it-length-prefixed": "^3.0.0", "it-pipe": "^1.1.0", diff --git a/src/identify/index.js b/src/identify/index.js index 6f3490379a..df51729597 100644 --- a/src/identify/index.js +++ b/src/identify/index.js @@ -9,7 +9,7 @@ const { collect, take } = require('streaming-iterables') const PeerInfo = require('peer-info') const PeerId = require('peer-id') const multiaddr = require('multiaddr') -const { toBuffer } = require('../util') +const { toBuffer } = require('it-buffer') const Message = require('./message') diff --git a/src/index.js b/src/index.js index c54e3fb8e5..4ba23ce562 100644 --- a/src/index.js +++ b/src/index.js @@ -21,6 +21,7 @@ const TransportManager = require('./transport-manager') const Upgrader = require('./upgrader') const PeerStore = require('./peer-store') const Registrar = require('./registrar') +const ping = require('./ping') const { IdentifyService, multicodecs: IDENTIFY_PROTOCOLS @@ -148,6 +149,9 @@ class Libp2p extends EventEmitter { this.peerRouting = peerRouting(this) this.contentRouting = contentRouting(this) + // Mount default protocols + ping.mount(this) + this._onDiscoveryPeer = this._onDiscoveryPeer.bind(this) } @@ -203,6 +207,8 @@ class Libp2p extends EventEmitter { await this.transportManager.close() await this.registrar.close() + + ping.unmount(this) } catch (err) { if (err) { log.error(err) @@ -280,17 +286,16 @@ class Libp2p extends EventEmitter { ) } - // TODO: Update ping - // /** - // * Pings the provided peer - // * - // * @param {PeerInfo|PeerId|Multiaddr|string} peer The peer to ping - // * @returns {Promise} - // */ - // ping (peer) { - // const peerInfo = await getPeerInfoRemote(peer, this) - // return new Ping(this._switch, peerInfo) - // } + /** + * Pings the given peer + * @param {PeerInfo|PeerId|Multiaddr|string} peer The peer to ping + * @returns {Promise} + */ + async ping (peer) { + const peerInfo = await getPeerInfo(peer, this.peerStore) + + return ping(this, peerInfo) + } /** * Registers the `handler` for each protocol diff --git a/src/ping/README.md b/src/ping/README.md index b0ace04389..079e956237 100644 --- a/src/ping/README.md +++ b/src/ping/README.md @@ -8,16 +8,11 @@ libp2p-ping JavaScript Implementation ## Usage ```javascript -var Ping = require('libp2p-ping') +var Ping = require('libp2p/src/ping') -Ping.mount(swarm) // Enable this peer to echo Ping requests +Ping.mount(libp2p) // Enable this peer to echo Ping requests -var p = new Ping(swarm, peerDst) // Ping peerDst, peerDst must be a peer-info object +const latency = await Ping(libp2p, peerDst) -p.on('ping', function (time) { - console.log(time + 'ms') - p.stop() // stop sending pings -}) - -p.start() +Ping.unmount(libp2p) ``` diff --git a/src/ping/handler.js b/src/ping/handler.js deleted file mode 100644 index 12f1793a23..0000000000 --- a/src/ping/handler.js +++ /dev/null @@ -1,50 +0,0 @@ -'use strict' - -const pull = require('pull-stream/pull') -const handshake = require('pull-handshake') -const constants = require('./constants') -const PROTOCOL = constants.PROTOCOL -const PING_LENGTH = constants.PING_LENGTH - -const debug = require('debug') -const log = debug('libp2p-ping') -log.error = debug('libp2p-ping:error') - -function mount (swarm) { - swarm.handle(PROTOCOL, (protocol, conn) => { - const stream = handshake({ timeout: 0 }) - const shake = stream.handshake - - // receive and echo back - function next () { - shake.read(PING_LENGTH, (err, buf) => { - if (err === true) { - // stream closed - return - } - if (err) { - return log.error(err) - } - - shake.write(buf) - return next() - }) - } - - pull( - conn, - stream, - conn - ) - - next() - }) -} - -function unmount (swarm) { - swarm.unhandle(PROTOCOL) -} - -exports = module.exports -exports.mount = mount -exports.unmount = unmount diff --git a/src/ping/index.js b/src/ping/index.js index f23b21ce9a..9a13bb351b 100644 --- a/src/ping/index.js +++ b/src/ping/index.js @@ -1,7 +1,62 @@ 'use strict' -const handler = require('./handler') +const debug = require('debug') +const log = debug('libp2p-ping') +log.error = debug('libp2p-ping:error') +const errCode = require('err-code') -exports = module.exports = require('./ping') -exports.mount = handler.mount -exports.unmount = handler.unmount +const crypto = require('libp2p-crypto') +const pipe = require('it-pipe') +const { toBuffer } = require('it-buffer') +const { collect } = require('streaming-iterables') + +const { PROTOCOL, PING_LENGTH } = require('./constants') + +/** + * Ping a given peer and wait for its response, getting the operation latency. + * @param {Libp2p} node + * @param {PeerInfo} peer + * @returns {Promise} + */ +async function ping (node, peer) { + log('dialing %s to %s', PROTOCOL, peer.id.toB58String()) + + const { stream } = await node.dialProtocol(peer, PROTOCOL) + + const start = new Date() + const data = crypto.randomBytes(PING_LENGTH) + + const [result] = await pipe( + [data], + stream, + toBuffer, + collect + ) + const end = Date.now() + + if (!data.equals(result)) { + throw errCode(new Error('Received wrong ping ack'), 'ERR_WRONG_PING_ACK') + } + + return end - start +} + +/** + * Subscribe ping protocol handler. + * @param {Libp2p} node + */ +function mount (node) { + node.handle(PROTOCOL, ({ stream }) => pipe(stream, stream)) +} + +/** + * Unsubscribe ping protocol handler. + * @param {Libp2p} node + */ +function unmount (node) { + node.unhandle(PROTOCOL) +} + +exports = module.exports = ping +exports.mount = mount +exports.unmount = unmount diff --git a/src/ping/ping.js b/src/ping/ping.js deleted file mode 100644 index 5c9f7f0337..0000000000 --- a/src/ping/ping.js +++ /dev/null @@ -1,83 +0,0 @@ -'use strict' - -const EventEmitter = require('events').EventEmitter -const pull = require('pull-stream/pull') -const empty = require('pull-stream/sources/empty') -const handshake = require('pull-handshake') -const constants = require('./constants') -const util = require('./util') -const rnd = util.rnd -const debug = require('debug') -const log = debug('libp2p-ping') -log.error = debug('libp2p-ping:error') - -const PROTOCOL = constants.PROTOCOL -const PING_LENGTH = constants.PING_LENGTH - -class Ping extends EventEmitter { - constructor (swarm, peer) { - super() - - this._stopped = false - this.peer = peer - this.swarm = swarm - } - - start () { - log('dialing %s to %s', PROTOCOL, this.peer.id.toB58String()) - - this.swarm.dial(this.peer, PROTOCOL, (err, conn) => { - if (err) { - return this.emit('error', err) - } - - const stream = handshake({ timeout: 0 }) - this.shake = stream.handshake - - pull( - stream, - conn, - stream - ) - - // write and wait to see ping back - const self = this - function next () { - const start = new Date() - const buf = rnd(PING_LENGTH) - self.shake.write(buf) - self.shake.read(PING_LENGTH, (err, bufBack) => { - const end = new Date() - if (err || !buf.equals(bufBack)) { - const err = new Error('Received wrong ping ack') - return self.emit('error', err) - } - - self.emit('ping', end - start) - - if (self._stopped) { - return - } - next() - }) - } - - next() - }) - } - - stop () { - if (this._stopped || !this.shake) { - return - } - - this._stopped = true - - pull( - empty(), - this.shake.rest() - ) - } -} - -module.exports = Ping diff --git a/src/util/index.js b/src/util/index.js deleted file mode 100644 index bca13a4530..0000000000 --- a/src/util/index.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict' - -/** - * Converts BufferList messages to Buffers - * @param {*} source - * @returns {AsyncGenerator} - */ -function toBuffer (source) { - return (async function * () { - for await (const chunk of source) { - yield Buffer.isBuffer(chunk) ? chunk : chunk.slice() - } - })() -} - -module.exports.toBuffer = toBuffer diff --git a/test/core/ping.node.js b/test/core/ping.node.js new file mode 100644 index 0000000000..97c786030a --- /dev/null +++ b/test/core/ping.node.js @@ -0,0 +1,35 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +const { expect } = chai + +const pTimes = require('p-times') + +const peerUtils = require('../utils/creators/peer') +const baseOptions = require('../utils/base-options') + +describe('ping', () => { + let nodes + + beforeEach(async () => { + nodes = await peerUtils.createPeer({ + number: 2, + config: baseOptions + }) + }) + + it('ping once from peer0 to peer1', async () => { + const latency = await nodes[0].ping(nodes[1].peerInfo) + + expect(latency).to.be.a('Number') + }) + + it('ping several times for getting an average', async () => { + const latencies = await pTimes(5, () => nodes[1].ping(nodes[0].peerInfo)) + + const averageLatency = latencies.reduce((p, c) => p + c, 0) / latencies.length + expect(averageLatency).to.be.a('Number') + }) +}) From 3d30cb18cde5241a98e79eeba26bbb804a4183b1 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Tue, 10 Dec 2019 21:45:56 +0100 Subject: [PATCH 59/92] docs: config (#495) * docs: new api * chore: new iteration * chore: apply suggestions from code review Co-Authored-By: Alan Shaw * chore: apply suggestions from code review Co-Authored-By: Jacob Heun * chore: address review * docs: add events * chore: apply suggestions from code review Co-Authored-By: Jacob Heun * docs: configuration * chore: apply suggestions from code review Co-Authored-By: Jacob Heun * chore: update peer routing description Co-Authored-By: Yusef Napora * chore: decouple examples * chore: address pr comment * fix: connection encryption is required * chore: apply review suggestion Co-Authored-By: Jacob Heun --- doc/CONFIGURATION.md | 384 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 doc/CONFIGURATION.md diff --git a/doc/CONFIGURATION.md b/doc/CONFIGURATION.md new file mode 100644 index 0000000000..577b9f5014 --- /dev/null +++ b/doc/CONFIGURATION.md @@ -0,0 +1,384 @@ +# Configuration + +* [Overview](#overview) +* [Modules](#modules) + * [Transport](#transport) + * [Stream Multiplexing](#stream-multiplexing) + * [Connection Encryption](#connection-encryption) + * [Peer Discovery](#peer-discovery) + * [Content Routing](#content-routing) + * [Peer Routing](#peer-routing) + * [DHT](#dht) + * [Pubsub](#pubsub) +* [Customizing Libp2p](#customizing-libp2p) + * [Examples](#examples) +* [Configuration examples](#configuration-examples) + +## Overview + +libp2p is a modular networking stack. It's designed to be able to suit a variety of project needs. The configuration of libp2p is a key part of its structure. It enables you to bring exactly what you need, and only what you need. This document is a guide on how to configure libp2p for your particular project. Check out the [Configuration examples](#configuration-examples) section if you're just looking to leverage an existing configuration. + +Regardless of how you configure libp2p, the top level [API](./API.md) will always remain the same. **Note**: if some modules are not configured, like Content Routing, using those methods will throw errors. + +## Modules + +`js-libp2p` acts as the composer for this modular p2p networking stack using libp2p compatible modules as its subsystems. For getting an instance of `js-libp2p` compliant with all types of networking requirements, it is possible to specify the following subsystems: + +- Transports +- Multiplexers +- Connection encryption mechanisms +- Peer discovery protocols +- Content routing protocols +- Peer routing protocols +- DHT implementation +- Pubsub router + +The libp2p ecosystem contains at least one module for each of these subsystems. The user should install and import the modules that are relevant for their requirements. Moreover, thanks to the existing interfaces it is easy to create a libp2p compatible module and use it. + +After selecting the modules to use, it is also possible to configure each one according to your needs. + +Bear in mind that only a **transport** and **connection encryption** are required, while all the other subsystems are optional. + +### Transport + +> In a p2p system, we need to interact with other peers in the network. Transports are used to establish connections between peers. The libp2p transports to use should be decided according to the environment where your node will live, as well as other requirements that you might have. + +Some available transports are: + +- [libp2p/js-libp2p-tcp](https://github.com/libp2p/js-libp2p-tcp) +- [libp2p/js-libp2p-webrtc-star](https://github.com/libp2p/js-libp2p-webrtc-star) +- [libp2p/js-libp2p-webrtc-direct](https://github.com/libp2p/js-libp2p-webrtc-direct) +- [libp2p/js-libp2p-websockets](https://github.com/libp2p/js-libp2p-websockets) +- [libp2p/js-libp2p-utp](https://github.com/libp2p/js-libp2p-utp) (Work in Progress) + +You should take into consideration that `js-libp2p-tcp` and `js-libp2p-utp` are not available in a **browser** environment. + +If none of the available transports fulfills your needs, you can create a libp2p compatible transport. A libp2p transport just needs to be compliant with the [Transport Interface](https://github.com/libp2p/js-interfaces/tree/master/src/transport). + +If you want to know more about libp2p transports, you should read the following content: + +- https://docs.libp2p.io/concepts/transport +- https://github.com/libp2p/specs/tree/master/connections + +### Stream Multiplexing + +> Libp2p peers will need to communicate with each other through several protocols during their life. Stream multiplexing allows multiple independent logical streams to share a common underlying transport medium, instead of creating a new connection with the same peer per needed protocol. + +Some available stream multiplexers are: + +- [libp2p/js-libp2p-mplex](https://github.com/libp2p/js-libp2p-mplex) + +If none of the available stream multiplexers fulfills your needs, you can create a libp2p compatible stream multiplexer. A libp2p multiplexer just needs to be compliant with the [Stream Muxer Interface](https://github.com/libp2p/js-interfaces/tree/master/src/stream-muxer). + +If you want to know more about libp2p stream multiplexing, you should read the following content: + +- https://docs.libp2p.io/concepts/stream-multiplexing +- https://github.com/libp2p/specs/tree/master/connections +- https://github.com/libp2p/specs/tree/master/mplex + +### Connection Encryption + +> A connection encryption mechanism must be used, in order to ensure all exchanged data between two peers is encrypted. + +Some available connection encryption protocols: + +- [libp2p/js-libp2p-secio](https://github.com/libp2p/js-libp2p-secio) + +If none of the available connection encryption mechanisms fulfills your needs, you can create a libp2p compatible one. A libp2p connection encryption protocol just needs to be compliant with the [Crypto Interface](https://github.com/libp2p/js-interfaces/tree/master/src/crypto). + +If you want to know more about libp2p connection encryption, you should read the following content: + +- https://docs.libp2p.io/concepts/secure-comms +- https://github.com/libp2p/specs/tree/master/connections + +### Peer Discovery + +> In a p2p network, peer discovery is critical to a functioning system. + +Some available peer discovery modules are: + +- [js-libp2p-mdns](https://github.com/libp2p/js-libp2p-mdns) +- [js-libp2p-bootstrap](https://github.com/libp2p/js-libp2p-bootstrap) +- [js-libp2p-kad-dht](https://github.com/libp2p/js-libp2p-kad-dht) +- [js-libp2p-webrtc-star](https://github.com/libp2p/js-libp2p-webrtc-star) + +If none of the available peer discovery protocols fulfills your needs, you can create a libp2p compatible one. A libp2p peer discovery protocol just needs to be compliant with the [Peer Discovery Interface](https://github.com/libp2p/js-interfaces/tree/master/src/peer-discovery). + +If you want to know more about libp2p peer discovery, you should read the following content: + +- https://github.com/libp2p/specs/blob/master/discovery/mdns.md + +### Content Routing + +> Content routing provides a way to find where content lives in the network. It works in two steps: 1) Peers provide (announce) to the network that they are holders of specific content and 2) Peers issue queries to find where that content lives. A Content Routing mechanism could be as complex as a DHT or as simple as a registry somewhere in the network. + +Some available content routing modules are: + +- [js-libp2p-kad-dht](https://github.com/libp2p/js-libp2p-kad-dht) +- [js-libp2p-delegated-peer-routing](https://github.com/libp2p/js-libp2p-delegated-peer-routing) + +If none of the available content routing protocols fulfills your needs, you can create a libp2p compatible one. A libp2p content routing protocol just needs to be compliant with the [Content Routing Interface](https://github.com/libp2p/js-interfaces/tree/master/src/content-routing). **(WIP: This module is not yet implemented)** + +If you want to know more about libp2p content routing, you should read the following content: + +- https://docs.libp2p.io/concepts/content-routing + +### Peer Routing + +> Peer Routing offers a way to find other peers in the network by issuing queries using a Peer Routing algorithm, which may be iterative or recursive. If the algorithm is unable to find the target peer, it will return the peers that are "closest" to the target peer, using a distance metric defined by the algorithm. + +Some available peer routing modules are: + +- [js-libp2p-kad-dht](https://github.com/libp2p/js-libp2p-kad-dht) +- [js-libp2p-delegated-peer-routing](https://github.com/libp2p/js-libp2p-delegated-peer-routing) + +If none of the available peer routing protocols fulfills your needs, you can create a libp2p compatible one. A libp2p peer routing protocol just needs to be compliant with the [Peer Routing Interface](https://github.com/libp2p/js-interfaces/tree/master/src/peer-routing). **(WIP: This module is not yet implemented)** + +If you want to know more about libp2p peer routing, you should read the following content: + +- https://docs.libp2p.io/concepts/peer-routing + +### DHT + +> A DHT can provide content and peer routing capabilities in a p2p system, as well as peer discovery capabilities. + +The DHT implementation currently available is [libp2p/js-libp2p-kad-dht](https://github.com/libp2p/js-libp2p-kad-dht). This implementation is largely based on the Kademlia whitepaper, augmented with notions from S/Kademlia, Coral and mainlineDHT. + +If this DHT implementation does not fulfill your needs and you want to create or use your own implementation, please get in touch with us through a github issue. We plan to work on improving the ability to bring your own DHT in a future release. + +If you want to know more about libp2p DHT, you should read the following content: + +- https://docs.libp2p.io/concepts/protocols/#kad-dht +- https://github.com/libp2p/specs/pull/108 + +#### Pubsub + +> Publish/Subscribe is a system where peers congregate around topics they are interested in. Peers interested in a topic are said to be subscribed to that topic and should receive the data published on it from other peers. + +Some available pubsub routers are: + +- [libp2p/js-libp2p-floodsub](https://github.com/libp2p/js-libp2p-floodsub) +- [ChainSafe/gossipsub-js](https://github.com/ChainSafe/gossipsub-js) + +If none of the available pubsub routers fulfills your needs, you can create a libp2p compatible one. A libp2p pubsub router just needs to be created on top of [libp2p/js-libp2p-pubsub](https://github.com/libp2p/js-libp2p-pubsub), which ensures `js-libp2p` API expectations. + +If you want to know more about libp2p pubsub, you should read the following content: + +- https://docs.libp2p.io/concepts/publish-subscribe +- https://github.com/libp2p/specs/tree/master/pubsub + +## Customizing libp2p + +When [creating a libp2p node](./API.md#create), the modules needed should be specified as follows: + +```js +const modules = { + transport: [], + streamMuxer: [], + connEncryption: [], + contentRouting: [], + peerRouting: [], + peerDiscovery: [], + dht: dhtImplementation, + pubsub: pubsubImplementation +} +``` + +Moreover, the majority of the modules can be customized via option parameters. This way, it is also possible to provide this options through a `config` object. This config object should have the property name of each building block to configure, the same way as the modules specification. + +Besides the `modules` and `config`, libp2p allows other internal options and configurations: +- `datastore`: an instance of [ipfs/interface-datastore](https://github.com/ipfs/interface-datastore/) modules. + - This is used in modules such as the DHT. If it is not provided, `js-libp2p` will use an in memory datastore. +- `peerInfo`: a previously created instance of [libp2p/js-peer-info](https://github.com/libp2p/js-peer-info). + - This is particularly useful if you want to reuse the same `peer-id`, as well as for modules like `libp2p-delegated-content-routing`, which need a `peer-id` in their instantiation. + +### Examples + +**1) Basic setup** + +```js +// Creating a libp2p node with: +// transport: websockets + tcp +// stream-muxing: mplex +// crypto-channel: secio +// discovery: multicast-dns +// dht: kad-dht +// pubsub: gossipsub + +const Libp2p = require('libp2p') +const TCP = require('libp2p-tcp') +const WS = require('libp2p-websockets') +const MPLEX = require('libp2p-mplex') +const SECIO = require('libp2p-secio') +const MulticastDNS = require('libp2p-mdns') +const DHT = require('libp2p-kad-dht') +const GossipSub = require('libp2p-gossipsub') + +const node = await Libp2p.create({ + modules: { + transport: [ + TCP, + new WS() // It can take instances too! + ], + streamMuxer: [MPLEX], + connEncryption: [SECIO], + peerDiscovery: [MulticastDNS], + dht: DHT, + pubsub: GossipSub + } +}) +``` + +**2) Customizing Peer Discovery** + +```js +const Libp2p = require('libp2p') +const TCP = require('libp2p-tcp') +const MPLEX = require('libp2p-mplex') +const SECIO = require('libp2p-secio') +const MulticastDNS = require('libp2p-mdns') + +const node = await Libp2p.create({ + modules: { + transport: [TCP], + streamMuxer: [MPLEX], + connEncryption: [SECIO], + peerDiscovery: [MulticastDNS] + }, + config: { + peerDiscovery: { + autoDial: true, // Auto connect to discovered peers (limited by ConnectionManager minPeers) + // The `tag` property will be searched when creating the instance of your Peer Discovery service. + // The associated object, will be passed to the service when it is instantiated. + [MulticastDNS.tag]: { + interval: 1000, + enabled: true + } + // .. other discovery module options. + } + } +}) +``` + +**3) Customizing Pubsub** + +```js +const Libp2p = require('libp2p') +const TCP = require('libp2p-tcp') +const MPLEX = require('libp2p-mplex') +const SECIO = require('libp2p-secio') +const GossipSub = require('libp2p-gossipsub') + +const node = await Libp2p.create({ + modules: { + transport: [TCP], + streamMuxer: [MPLEX], + connEncryption: [SECIO], + pubsub: GossipSub + }, + config: { + pubsub: { // The pubsub options (and defaults) can be found in the pubsub router documentation + enabled: true, + emitSelf: true, // whether the node should emit to self on publish + signMessages: true, // if messages should be signed + strictSigning: true // if message signing should be required + } + } +}) +``` + +**4) Customizing DHT** + +```js +const Libp2p = require('libp2p') +const TCP = require('libp2p-tcp') +const MPLEX = require('libp2p-mplex') +const SECIO = require('libp2p-secio') +const DHT = require('libp2p-kad-dht') + +const node = await Libp2p.create({ + modules: { + transport: [TCP], + streamMuxer: [MPLEX], + connEncryption: [SECIO], + dht: DHT + }, + config: { + dht: { // The DHT options (and defaults) can be found in its documentation + kBucketSize: 20, + enabled: true, + randomWalk: { + enabled: true, // Allows to disable discovery (enabled by default) + interval: 300e3, + timeout: 10e3 + } + } + } +}) +``` + +**5) Setup with Content and Peer Routing** + +```js +const Libp2p = require('libp2p') +const TCP = require('libp2p-tcp') +const MPLEX = require('libp2p-mplex') +const SECIO = require('libp2p-secio') +const DelegatedPeerRouter = require('libp2p-delegated-peer-routing') +const DelegatedContentRouter = require('libp2p-delegated-content-routing') +const PeerInfo = require('peer-info') + +// create a peerInfo +const peerInfo = await PeerInfo.create() + +const node = await Libp2p.create({ + modules: { + transport: [TCP], + streamMuxer: [MPLEX], + connEncryption: [SECIO], + contentRouting: [ + new DelegatedContentRouter(peerInfo.id) + ], + peerRouting: [ + new DelegatedPeerRouter() + ], + }, + peerInfo +}) +``` + +**6) Setup with Relay** + +```js +const Libp2p = require('libp2p') +const TCP = require('libp2p-tcp') +const MPLEX = require('libp2p-mplex') +const SECIO = require('libp2p-secio') + +const node = await Libp2p.create({ + modules: { + transport: [TCP], + streamMuxer: [MPLEX], + connEncryption: [SECIO] + }, + relay: { // Circuit Relay options (this config is part of libp2p core configurations) + enabled: true, + hop: { + enabled: true, + active: true + } + }, +}) +``` + +## Configuration examples + +As libp2p is designed to be a modular networking library, its usage will vary based on individual project needs. We've included links to some existing project configurations for your reference, in case you wish to replicate their configuration: + + +- [libp2p-ipfs-nodejs](https://github.com/ipfs/js-ipfs/tree/master/src/core/runtime/libp2p-nodejs.js) - libp2p configuration used by js-ipfs when running in Node.js +- [libp2p-ipfs-browser](https://github.com/ipfs/js-ipfs/tree/master/src/core/runtime/libp2p-browser.js) - libp2p configuration used by js-ipfs when running in a Browser (that supports WebRTC) + +If you have developed a project using `js-libp2p`, please consider submitting your configuration to this list so that it can be found easily by other users. + +The [examples](../examples) are also a good source of help for finding a configuration that suits your needs. From f5401128353f24ceb50bc98f41f32a86ecb078d6 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Wed, 11 Dec 2019 16:05:59 +0100 Subject: [PATCH 60/92] refactor: stats (#501) * docs: add initial notes on stats * feat: initial refactor of stats to metrics * feat: add support for placeholder metrics This is helpful for tracking metrics prior to knowing the remote peers id * fix: add metrics tests and fix issues * fix: always clear the dial timeout timer * docs: add metrics to api doc * chore: apply suggestions from code review Co-Authored-By: Vasco Santos * docs: update metrics docs * fix: call metrics.onDisconnect * docs(config): add example headers so they appear in the TOC * docs(config): add metrics configuration * docs(relay): fix relay configuration docs --- doc/API.md | 109 +++++++- doc/CONFIGURATION.md | 94 +++++-- doc/METRICS.md | 28 ++ package.json | 3 + src/config.js | 3 + src/constants.js | 12 +- src/dialer/index.js | 2 +- src/index.js | 27 +- src/metrics/index.js | 247 ++++++++++++++++++ src/{stats => metrics}/old-peers.js | 0 src/{stats/stat.js => metrics/stats.js} | 27 +- src/registrar.js | 1 + src/stats/index.js | 150 ----------- src/upgrader.js | 34 +++ test/content-routing/content-routing.node.js | 4 +- test/dialing/relay.node.js | 1 + test/metrics/index.node.js | 127 +++++++++ test/metrics/index.spec.js | 259 +++++++++++++++++++ test/utils/creators/peer.js | 2 +- 19 files changed, 939 insertions(+), 191 deletions(-) create mode 100644 doc/METRICS.md create mode 100644 src/metrics/index.js rename src/{stats => metrics}/old-peers.js (100%) rename src/{stats/stat.js => metrics/stats.js} (89%) delete mode 100644 src/stats/index.js create mode 100644 test/metrics/index.node.js create mode 100644 test/metrics/index.spec.js diff --git a/doc/API.md b/doc/API.md index b31996142b..33ffc685b6 100644 --- a/doc/API.md +++ b/doc/API.md @@ -21,6 +21,13 @@ * [`pubsub.getTopics`](#pubsub.getTopics) * [`pubsub.publish`](#pubsub.publish) * [`pubsub.subscribe`](#pubsub.subscribe) + * [`metrics.global`](#metrics.global) + * [`metrics.peers`](#metrics.peers) + * [`metrics.protocols`](#metrics.protocols) + * [`metrics.forPeer`](#metrics.forPeer) + * [`metrics.forProtocol`](#metrics.forProtocol) +* [Types](#types) + * [`Stats`](#stats) ## Static Functions @@ -117,7 +124,7 @@ unless they are performing a specific action. See [peer discovery and auto dial] -## Libp2p Instance Methods +## Libp2p Instance Methods ### start @@ -635,3 +642,103 @@ const handler = (msg) => { libp2p.pubsub.unsubscribe(topic, handler) ``` + +### metrics.global + +A [`Stats`](#stats) object of tracking the global bandwidth of the libp2p node. + +#### Example + +```js +const peerIdStrings = libp2p.metrics.peers +``` + +### metrics.peers + +An array of `PeerId` strings of each peer currently being tracked. + +#### Example + +```js +const peerIdStrings = libp2p.metrics.peers +``` + +### metrics.protocols + +An array of protocol strings that are currently being tracked. + +#### Example + +```js +const protocols = libp2p.metrics.protocols +``` + +### metrics.forPeer + +Returns the [`Stats`](#stats) object for a given `PeerId` if it is being tracked. + +`libp2p.metrics.forPeer(peerId)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| peerId | `PeerId` | The peer to get stats for | + +#### Returns + +| Type | Description | +|------|-------------| +| [`Stats`](#stats) | The bandwidth stats of the peer | + +#### Example + +```js +const peerStats = libp2p.metrics.forPeer(peerInfo) +console.log(peerStats.toJSON()) +``` + +### metrics.forProtocol + +Returns the [`Stats`](#stats) object for a given protocol if it is being tracked. + +`libp2p.metrics.forProtocol(protocol)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| protocol | `string` | The protocol to get stats for | + +#### Returns + +| Type | Description | +|------|-------------| +| [`Stats`](#stats) | The bandwidth stats of the protocol across all peers | + +#### Example + +```js +const peerStats = libp2p.metrics.forProtocol('/meshsub/1.0.0') +console.log(peerStats.toJSON()) +``` + +## Types + +### Stats + +- `Stats` + - `toJSON`: Returns a JSON snapshot of the stats. + - `dataReceived`: The stringified value of total incoming data for this stat. + - `dataSent`: The stringified value of total outgoing data for this stat. + - `movingAverages`: The properties are dependent on the configuration of the moving averages interval. Defaults are listed here. + - `['60000']`: The calculated moving average at a 1 minute interval. + - `['300000']`: The calculated moving average at a 5 minute interval. + - `['900000']`: The calculated moving average at a 15 minute interval. + - `snapshot`: A getter that returns a clone of the raw stats. + - `dataReceived`: A [`BigNumber`](https://github.com/MikeMcl/bignumber.js/) of the amount of incoming data + - `dataSent`: A [`BigNumber`](https://github.com/MikeMcl/bignumber.js/) of the amount of outgoing data + - `movingAverages`: A getter that returns a clone of the raw [moving averages](https://www.npmjs.com/package/moving-averages) stats. **Note**: The properties of this are dependent on configuration. The defaults are shown here. + - `['60000']`: The [MovingAverage](https://www.npmjs.com/package/moving-averages) at a 1 minute interval. + - `['300000']`: The [MovingAverage](https://www.npmjs.com/package/moving-averages) at a 5 minute interval. + - `['900000']`: The [MovingAverage](https://www.npmjs.com/package/moving-averages) at a 15 minute interval. diff --git a/doc/CONFIGURATION.md b/doc/CONFIGURATION.md index 577b9f5014..801e4f15fe 100644 --- a/doc/CONFIGURATION.md +++ b/doc/CONFIGURATION.md @@ -1,18 +1,26 @@ # Configuration -* [Overview](#overview) -* [Modules](#modules) - * [Transport](#transport) - * [Stream Multiplexing](#stream-multiplexing) - * [Connection Encryption](#connection-encryption) - * [Peer Discovery](#peer-discovery) - * [Content Routing](#content-routing) - * [Peer Routing](#peer-routing) - * [DHT](#dht) - * [Pubsub](#pubsub) -* [Customizing Libp2p](#customizing-libp2p) - * [Examples](#examples) -* [Configuration examples](#configuration-examples) +- [Configuration](#configuration) + - [Overview](#overview) + - [Modules](#modules) + - [Transport](#transport) + - [Stream Multiplexing](#stream-multiplexing) + - [Connection Encryption](#connection-encryption) + - [Peer Discovery](#peer-discovery) + - [Content Routing](#content-routing) + - [Peer Routing](#peer-routing) + - [DHT](#dht) + - [Pubsub](#pubsub) + - [Customizing libp2p](#customizing-libp2p) + - [Examples](#examples) + - [Basic setup](#basic-setup) + - [Customizing Peer Discovery](#customizing-peer-discovery) + - [Customizing Pubsub](#customizing-pubsub) + - [Customizing DHT](#customizing-dht) + - [Setup with Content and Peer Routing](#setup-with-content-and-peer-routing) + - [Setup with Relay](#setup-with-relay) + - [Configuring Metrics](#configuring-metrics) + - [Configuration examples](#configuration-examples) ## Overview @@ -45,7 +53,7 @@ Bear in mind that only a **transport** and **connection encryption** are require Some available transports are: -- [libp2p/js-libp2p-tcp](https://github.com/libp2p/js-libp2p-tcp) +- [libp2p/js-libp2p-tcp](https://github.com/libp2p/js-libp2p-tcp) - [libp2p/js-libp2p-webrtc-star](https://github.com/libp2p/js-libp2p-webrtc-star) - [libp2p/js-libp2p-webrtc-direct](https://github.com/libp2p/js-libp2p-webrtc-direct) - [libp2p/js-libp2p-websockets](https://github.com/libp2p/js-libp2p-websockets) @@ -151,7 +159,7 @@ If you want to know more about libp2p DHT, you should read the following content - https://docs.libp2p.io/concepts/protocols/#kad-dht - https://github.com/libp2p/specs/pull/108 -#### Pubsub +### Pubsub > Publish/Subscribe is a system where peers congregate around topics they are interested in. Peers interested in a topic are said to be subscribed to that topic and should receive the data published on it from other peers. @@ -194,7 +202,7 @@ Besides the `modules` and `config`, libp2p allows other internal options and con ### Examples -**1) Basic setup** +#### Basic setup ```js // Creating a libp2p node with: @@ -229,7 +237,7 @@ const node = await Libp2p.create({ }) ``` -**2) Customizing Peer Discovery** +#### Customizing Peer Discovery ```js const Libp2p = require('libp2p') @@ -248,7 +256,7 @@ const node = await Libp2p.create({ config: { peerDiscovery: { autoDial: true, // Auto connect to discovered peers (limited by ConnectionManager minPeers) - // The `tag` property will be searched when creating the instance of your Peer Discovery service. + // The `tag` property will be searched when creating the instance of your Peer Discovery service. // The associated object, will be passed to the service when it is instantiated. [MulticastDNS.tag]: { interval: 1000, @@ -260,7 +268,7 @@ const node = await Libp2p.create({ }) ``` -**3) Customizing Pubsub** +#### Customizing Pubsub ```js const Libp2p = require('libp2p') @@ -287,7 +295,7 @@ const node = await Libp2p.create({ }) ``` -**4) Customizing DHT** +#### Customizing DHT ```js const Libp2p = require('libp2p') @@ -317,7 +325,7 @@ const node = await Libp2p.create({ }) ``` -**5) Setup with Content and Peer Routing** +#### Setup with Content and Peer Routing ```js const Libp2p = require('libp2p') @@ -347,7 +355,7 @@ const node = await Libp2p.create({ }) ``` -**6) Setup with Relay** +#### Setup with Relay ```js const Libp2p = require('libp2p') @@ -361,13 +369,45 @@ const node = await Libp2p.create({ streamMuxer: [MPLEX], connEncryption: [SECIO] }, - relay: { // Circuit Relay options (this config is part of libp2p core configurations) - enabled: true, - hop: { - enabled: true, - active: true + config: { + relay: { // Circuit Relay options (this config is part of libp2p core configurations) + enabled: true, // Allows you to dial and accept relayed connections. Does not make you a relay. + hop: { + enabled: true, // Allows you to be a relay for other peers + active: true // You will attempt to dial destination peers if you are not connected to them + } } + } +}) +``` + +#### Configuring Metrics + +Metrics are disabled in libp2p by default. You can enable and configure them as follows. Aside from enabled being `false` by default, the configuration options listed here are the current defaults. + +```js +const Libp2p = require('libp2p') +const TCP = require('libp2p-tcp') +const MPLEX = require('libp2p-mplex') +const SECIO = require('libp2p-secio') + +const node = await Libp2p.create({ + modules: { + transport: [TCP], + streamMuxer: [MPLEX], + connEncryption: [SECIO] }, + metrics: { + enabled: true, + computeThrottleMaxQueueSize: 1000, // How many messages a stat will queue before processing + computeThrottleTimeout: 2000, // Time in milliseconds a stat will wait, after the last item was added, before processing + movingAverageIntervals: [ // The moving averages that will be computed + 60 * 1000, // 1 minute + 5 * 60 * 1000, // 5 minutes + 15 * 60 * 1000 // 15 minutes + ], + maxOldPeersRetention: 50 // How many disconnected peers we will retain stats for + } }) ``` diff --git a/doc/METRICS.md b/doc/METRICS.md new file mode 100644 index 0000000000..027691cc0c --- /dev/null +++ b/doc/METRICS.md @@ -0,0 +1,28 @@ +# Bandwidth Metrics + +- Metrics gathering is optional, as there is a performance hit to using it. +- Metrics do NOT currently contain OS level stats, only libp2p application level Metrics. For example, TCP messages (ACK, FIN, etc) are not accounted for. +- See the [API](./API.md) for Metrics usage. Metrics in libp2p do not emit events, as such applications wishing to read Metrics will need to do so actively. This ensures that the system is not unnecessarily firing update notifications. + +## Tracking +- When a transport hands off a connection for upgrading, Metrics are hooked up if enabled. +- When a stream is created, Metrics will be tracked on that stream and associated to that streams protocol. +- Tracked Metrics are associated to a specific peer, and count towards global bandwidth Metrics. + +### Metrics Processing +- The main Metrics object consists of individual `Stats` objects +- The following categories are tracked: + - Global stats; every byte in and out + - Peer stats; every byte in and out, per peer + - Protocol stats; every byte in and out, per protocol +- When a message goes through Metrics: + - It is added to the global stat + - It is added to the stats for the remote peer + - It is added to the protocol stats if there is one +- When data is pushed onto a `Stat` it is added to a queue + - The queue is processed at the earliest of either (configurable): + - every 2 seconds after the last item was added to the queue + - or once 1000 items have been queued + - When the queue is processed: + - The data length is added to either the `in` or `out` stat + - The moving averages is calculated since the last queue processing (based on most recently processed item timestamp) diff --git a/package.json b/package.json index 11e2be02fb..eb874cc2e8 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "moving-average": "^1.0.0", "multiaddr": "^7.2.1", "multistream-select": "^0.15.0", + "mutable-proxy": "^1.0.0", "p-any": "^2.1.0", "p-fifo": "^1.0.0", "p-settle": "^3.1.0", @@ -82,7 +83,9 @@ "cids": "^0.7.1", "delay": "^4.3.0", "dirty-chai": "^2.0.1", + "it-concat": "^1.0.0", "it-pair": "^1.0.0", + "it-pushable": "^1.4.0", "libp2p-bootstrap": "^0.10.3", "libp2p-delegated-content-routing": "^0.4.1", "libp2p-delegated-peer-routing": "^0.4.0", diff --git a/src/config.js b/src/config.js index 8f87b602b9..257ec65605 100644 --- a/src/config.js +++ b/src/config.js @@ -6,6 +6,9 @@ const DefaultConfig = { connectionManager: { minPeers: 25 }, + metrics: { + enabled: false + }, config: { dht: { enabled: false, diff --git a/src/constants.js b/src/constants.js index 9cfcb77661..a0d5cc7955 100644 --- a/src/constants.js +++ b/src/constants.js @@ -9,5 +9,15 @@ module.exports = { MAX_PER_PEER_DIALS: 4, // Allowed parallel dials per DialRequest QUARTER_HOUR: 15 * 60e3, PRIORITY_HIGH: 10, - PRIORITY_LOW: 20 + PRIORITY_LOW: 20, + METRICS: { + computeThrottleMaxQueueSize: 1000, + computeThrottleTimeout: 2000, + movingAverageIntervals: [ + 60 * 1000, // 1 minute + 5 * 60 * 1000, // 5 minutes + 15 * 60 * 1000 // 15 minutes + ], + maxOldPeersRetention: 50 + } } diff --git a/src/dialer/index.js b/src/dialer/index.js index 47bda2d436..224f4e309c 100644 --- a/src/dialer/index.js +++ b/src/dialer/index.js @@ -91,7 +91,6 @@ class Dialer { try { const dialResult = await dialRequest.run({ ...options, signal }) - timeoutController.clear() log('dial succeeded to %s', dialResult.remoteAddr) return dialResult } catch (err) { @@ -102,6 +101,7 @@ class Dialer { log.error(err) throw err } finally { + timeoutController.clear() this._pendingDials.delete(dial) } } diff --git a/src/index.js b/src/index.js index 4ba23ce562..aad21e288a 100644 --- a/src/index.js +++ b/src/index.js @@ -17,6 +17,7 @@ const { codes } = require('./errors') const Circuit = require('./circuit') const Dialer = require('./dialer') +const Metrics = require('./metrics') const TransportManager = require('./transport-manager') const Upgrader = require('./upgrader') const PeerStore = require('./peer-store') @@ -51,9 +52,14 @@ class Libp2p extends EventEmitter { this.peerStore = new PeerStore() + if (this._options.metrics.enabled) { + this.metrics = new Metrics(this._options.metrics) + } + // Setup the Upgrader this.upgrader = new Upgrader({ localPeer: this.peerInfo.id, + metrics: this.metrics, onConnection: (connection) => { const peerInfo = this.peerStore.put(new PeerInfo(connection.remotePeer)) this.registrar.onConnect(peerInfo, connection) @@ -67,9 +73,13 @@ class Libp2p extends EventEmitter { }, onConnectionEnd: (connection) => { const peerInfo = getPeerInfo(connection.remotePeer) - this.registrar.onDisconnect(peerInfo, connection) - this.emit('peer:disconnect', peerInfo) + + // If there are no connections to the peer, disconnect + if (!this.registrar.getConnection(peerInfo)) { + this.emit('peer:disconnect', peerInfo) + this.metrics && this.metrics.onPeerDisconnected(peerInfo.id) + } } }) @@ -200,15 +210,15 @@ class Libp2p extends EventEmitter { try { await Promise.all([ this.pubsub && this.pubsub.stop(), - this._dht && this._dht.stop() + this._dht && this._dht.stop(), + this.metrics && this.metrics.stop() ]) - this.dialer.destroy() - await this.transportManager.close() await this.registrar.close() ping.unmount(this) + this.dialer.destroy() } catch (err) { if (err) { log.error(err) @@ -356,6 +366,9 @@ class Libp2p extends EventEmitter { // the other discovery modules this._dht.on('peer', this._onDiscoveryPeer) } + + // Start metrics if present + this.metrics && this.metrics.start() } /** @@ -403,13 +416,13 @@ class Libp2p extends EventEmitter { */ async _maybeConnect (peerInfo) { // If auto dialing is on and we have no connection to the peer, check if we should dial - if (this._config.peerDiscovery.autoDial === true && !this.registrar.connections.get(peerInfo)) { + if (this._config.peerDiscovery.autoDial === true && !this.registrar.getConnection(peerInfo)) { const minPeers = this._options.connectionManager.minPeers || 0 // TODO: This does not account for multiple connections to a peer if (minPeers > this.registrar.connections.size) { log('connecting to discovered peer') try { - await this.dialer.connectToPeer(peerInfo) + await this.dialer.connectToPeer(peerInfo.id) } catch (err) { log.error('could not connect to discovered peer', err) } diff --git a/src/metrics/index.js b/src/metrics/index.js new file mode 100644 index 0000000000..9cbe9489ec --- /dev/null +++ b/src/metrics/index.js @@ -0,0 +1,247 @@ +'use strict' + +const mergeOptions = require('merge-options') +const pipe = require('it-pipe') +const { tap } = require('streaming-iterables') +const oldPeerLRU = require('./old-peers') +const { METRICS: defaultOptions } = require('../constants') +const Stats = require('./stats') + +const initialCounters = [ + 'dataReceived', + 'dataSent' +] + +const directionToEvent = { + in: 'dataReceived', + out: 'dataSent' +} + +class Metrics { + /** + * + * @param {object} options + * @param {number} options.computeThrottleMaxQueueSize + * @param {number} options.computeThrottleTimeout + * @param {Array} options.movingAverageIntervals + * @param {number} options.maxOldPeersRetention + */ + constructor (options) { + this._options = mergeOptions(defaultOptions, options) + this._globalStats = new Stats(initialCounters, this._options) + this._peerStats = new Map() + this._protocolStats = new Map() + this._oldPeers = oldPeerLRU(this._options.maxOldPeersRetention) + this._running = false + this._onMessage = this._onMessage.bind(this) + } + + /** + * Must be called for stats to saved. Any data pushed for tracking + * will be ignored. + */ + start () { + this._running = true + } + + /** + * Stops all averages timers and prevents new data from being tracked. + * Once `stop` is called, `start` must be called to resume stats tracking. + */ + stop () { + this._running = false + this._globalStats.stop() + for (const stats of this._peerStats.values()) { + stats.stop() + } + for (const stats of this._protocolStats.values()) { + stats.stop() + } + } + + /** + * Gets the global `Stats` object + * @returns {Stats} + */ + get global () { + return this._globalStats + } + + /** + * Returns a list of `PeerId` strings currently being tracked + * @returns {Array} + */ + get peers () { + return Array.from(this._peerStats.keys()) + } + + /** + * Returns the `Stats` object for the given `PeerId` whether it + * is a live peer, or in the disconnected peer LRU cache. + * @param {PeerId} peerId + * @returns {Stats} + */ + forPeer (peerId) { + const idString = peerId.toString() + return this._peerStats.get(idString) || this._oldPeers.get(idString) + } + + /** + * Returns a list of all protocol strings currently being tracked. + * @returns {Array} + */ + get protocols () { + return Array.from(this._protocolStats.keys()) + } + + /** + * Returns the `Stats` object for the given `protocol`. + * @param {string} protocol + * @returns {Stats} + */ + forProtocol (protocol) { + return this._protocolStats.get(protocol) + } + + /** + * Should be called when all connections to a given peer + * have closed. The `Stats` collection for the peer will + * be stopped and moved to an LRU for temporary retention. + * @param {PeerId} peerId + */ + onPeerDisconnected (peerId) { + const idString = peerId.toString() + const peerStats = this._peerStats.get(idString) + if (peerStats) { + peerStats.stop() + this._peerStats.delete(idString) + this._oldPeers.set(idString, peerStats) + } + } + + /** + * Takes the metadata for a message and tracks it in the + * appropriate categories. If the protocol is present, protocol + * stats will also be tracked. + * + * @private + * @param {object} params + * @param {PeerId} params.remotePeer Remote peer + * @param {string} [params.protocol] Protocol string the stream is running + * @param {string} params.direction One of ['in','out'] + * @param {number} params.dataLength Size of the message + * @returns {void} + */ + _onMessage ({ remotePeer, protocol, direction, dataLength }) { + if (!this._running) return + + const key = directionToEvent[direction] + + let peerStats = this.forPeer(remotePeer) + if (!peerStats) { + peerStats = new Stats(initialCounters, this._options) + this._peerStats.set(remotePeer.toString(), peerStats) + } + + // Peer and global stats + peerStats.push(key, dataLength) + this._globalStats.push(key, dataLength) + + // Protocol specific stats + if (protocol) { + let protocolStats = this.forProtocol(protocol) + if (!protocolStats) { + protocolStats = new Stats(initialCounters, this._options) + this._protocolStats.set(protocol, protocolStats) + } + protocolStats.push(key, dataLength) + } + } + + /** + * Replaces the `PeerId` string with the given `peerId`. + * If stats are already being tracked for the given `peerId`, the + * placeholder stats will be merged with the existing stats. + * @param {string} placeholder A peerId string + * @param {PeerId} peerId + */ + updatePlaceholder (placeholder, peerId) { + if (!this._running) return + const placeholderStats = this.forPeer(placeholder) + const peerIdString = peerId.toString() + const existingStats = this.forPeer(peerId) + let mergedStats = placeholderStats + + // If we already have stats, merge the two + if (existingStats) { + // If existing, merge + mergedStats = Metrics.mergeStats(existingStats, mergedStats) + // Attempt to delete from the old peers list just in case it was tracked there + this._oldPeers.delete(peerIdString) + } + + this._peerStats.delete(placeholder.toString()) + this._peerStats.set(peerIdString, mergedStats) + mergedStats.start() + } + + /** + * Tracks data running through a given Duplex Iterable `stream`. If + * the `peerId` is not provided, a placeholder string will be created and + * returned. This allows lazy tracking of a peer when the peer is not yet known. + * When the `PeerId` is known, `Metrics.updatePlaceholder` should be called + * with the placeholder string returned from here, and the known `PeerId`. + * + * @param {Object} options + * @param {{ sink: function(*), source: function() }} options.stream A duplex iterable stream + * @param {PeerId} [options.peerId] The id of the remote peer that's connected + * @param {string} [options.protocol] The protocol the stream is running + * @returns {string} The peerId string or placeholder string + */ + trackStream ({ stream, remotePeer, protocol }) { + const metrics = this + const _source = stream.source + stream.source = tap(chunk => metrics._onMessage({ + remotePeer, + protocol, + direction: 'in', + dataLength: chunk.length + }))(_source) + + const _sink = stream.sink + stream.sink = source => { + pipe( + source, + tap(chunk => metrics._onMessage({ + remotePeer, + protocol, + direction: 'out', + dataLength: chunk.length + })), + _sink + ) + } + + return stream + } + + /** + * Merges `other` into `target`. `target` will be modified + * and returned. + * @param {Stats} target + * @param {Stats} other + * @returns {Stats} + */ + static mergeStats (target, other) { + target.stop() + other.stop() + + // Merge queues + target._queue = [...target._queue, ...other._queue] + + // TODO: how to merge moving averages? + return target + } +} + +module.exports = Metrics diff --git a/src/stats/old-peers.js b/src/metrics/old-peers.js similarity index 100% rename from src/stats/old-peers.js rename to src/metrics/old-peers.js diff --git a/src/stats/stat.js b/src/metrics/stats.js similarity index 89% rename from src/stats/stat.js rename to src/metrics/stats.js index 9b9b4dec04..9a275c603e 100644 --- a/src/stats/stat.js +++ b/src/metrics/stats.js @@ -83,6 +83,31 @@ class Stats extends EventEmitter { return Object.assign({}, this._movingAverages) } + /** + * Returns a plain JSON object of the stats + * + * @returns {*} + */ + toJSON () { + const snapshot = this.snapshot + const movingAverages = this.movingAverages + const data = { + dataReceived: snapshot.dataReceived.toString(), + dataSent: snapshot.dataSent.toString(), + movingAverages: {} + } + + const counters = Object.keys(movingAverages) + for (const key of counters) { + data.movingAverages[key] = {} + for (const interval of Object.keys(movingAverages[key])) { + data.movingAverages[key][interval] = movingAverages[key][interval].movingAverage() + } + } + + return data + } + /** * Pushes the given operation data to the queue, along with the * current Timestamp, then resets the update timer. @@ -151,7 +176,7 @@ class Stats extends EventEmitter { } /** - * For each key in the stats, the frequncy and moving averages + * For each key in the stats, the frequency and moving averages * will be updated via Stats._updateFrequencyFor based on the time * difference between calls to this method. * diff --git a/src/registrar.js b/src/registrar.js index 666336ffe8..cf3b2894f4 100644 --- a/src/registrar.js +++ b/src/registrar.js @@ -100,6 +100,7 @@ class Registrar { if (storedConn && storedConn.length > 1) { storedConn = storedConn.filter((conn) => conn.id === connection.id) + this.connections.set(id, storedConn) } else if (storedConn) { for (const [, topology] of this.topologies) { topology.disconnect(peerInfo, error) diff --git a/src/stats/index.js b/src/stats/index.js deleted file mode 100644 index b79af19841..0000000000 --- a/src/stats/index.js +++ /dev/null @@ -1,150 +0,0 @@ -'use strict' - -const EventEmitter = require('events') - -const Stat = require('./stat') -const OldPeers = require('./old-peers') - -const defaultOptions = { - computeThrottleMaxQueueSize: 1000, - computeThrottleTimeout: 2000, - movingAverageIntervals: [ - 60 * 1000, // 1 minute - 5 * 60 * 1000, // 5 minutes - 15 * 60 * 1000 // 15 minutes - ], - maxOldPeersRetention: 50 -} - -const initialCounters = [ - 'dataReceived', - 'dataSent' -] - -const directionToEvent = { - in: 'dataReceived', - out: 'dataSent' -} - -/** - * Binds to message events on the given `observer` to generate stats - * based on the Peer, Protocol and Transport used for the message. Stat - * events will be emitted via the `update` event. - * - * @param {Observer} observer - * @param {any} _options - * @returns {Stats} - */ -module.exports = (observer, _options) => { - const options = Object.assign({}, defaultOptions, _options) - const globalStats = new Stat(initialCounters, options) - - const stats = Object.assign(new EventEmitter(), { - start: start, - stop: stop, - global: globalStats, - peers: () => Array.from(peerStats.keys()), - forPeer: (peerId) => { - return peerStats.get(peerId) || oldPeers.get(peerId) - }, - transports: () => Array.from(transportStats.keys()), - forTransport: (transport) => transportStats.get(transport), - protocols: () => Array.from(protocolStats.keys()), - forProtocol: (protocol) => protocolStats.get(protocol) - }) - - globalStats.on('update', propagateChange) - - const oldPeers = OldPeers(options.maxOldPeersRetention) - const peerStats = new Map() - const transportStats = new Map() - const protocolStats = new Map() - - observer.on('peer:closed', (peerId) => { - const peer = peerStats.get(peerId) - if (peer) { - peer.removeListener('update', propagateChange) - peer.stop() - peerStats.delete(peerId) - oldPeers.set(peerId, peer) - } - }) - - return stats - - function onMessage (peerId, transportTag, protocolTag, direction, bufferLength) { - const event = directionToEvent[direction] - - if (transportTag) { - // because it has a transport tag, this message is at the global level, so we account this - // traffic as global. - globalStats.push(event, bufferLength) - - // peer stats - let peer = peerStats.get(peerId) - if (!peer) { - peer = oldPeers.get(peerId) - if (peer) { - oldPeers.delete(peerId) - } else { - peer = new Stat(initialCounters, options) - } - peer.on('update', propagateChange) - peer.start() - peerStats.set(peerId, peer) - } - peer.push(event, bufferLength) - } - - // transport stats - if (transportTag) { - let transport = transportStats.get(transportTag) - if (!transport) { - transport = new Stat(initialCounters, options) - transport.on('update', propagateChange) - transportStats.set(transportTag, transport) - } - transport.push(event, bufferLength) - } - - // protocol stats - if (protocolTag) { - let protocol = protocolStats.get(protocolTag) - if (!protocol) { - protocol = new Stat(initialCounters, options) - protocol.on('update', propagateChange) - protocolStats.set(protocolTag, protocol) - } - protocol.push(event, bufferLength) - } - } - - function start () { - observer.on('message', onMessage) - - globalStats.start() - - for (const peerStat of peerStats.values()) { - peerStat.start() - } - for (const transportStat of transportStats.values()) { - transportStat.start() - } - } - - function stop () { - observer.removeListener('message', onMessage) - globalStats.stop() - - for (const peerStat of peerStats.values()) { - peerStat.stop() - } - for (const transportStat of transportStats.values()) { - transportStat.stop() - } - } - - function propagateChange () { - stats.emit('update') - } -} diff --git a/src/upgrader.js b/src/upgrader.js index 1699451a6d..afef9d192c 100644 --- a/src/upgrader.js +++ b/src/upgrader.js @@ -8,6 +8,7 @@ const { Connection } = require('libp2p-interfaces/src/connection') const PeerId = require('peer-id') const pipe = require('it-pipe') const errCode = require('err-code') +const mutableProxy = require('mutable-proxy') const { codes } = require('./errors') @@ -30,6 +31,7 @@ class Upgrader { /** * @param {object} options * @param {PeerId} options.localPeer + * @param {Metrics} options.metrics * @param {Map} options.cryptos * @param {Map} options.muxers * @param {function(Connection)} options.onConnection Called when a connection is upgraded @@ -37,12 +39,14 @@ class Upgrader { */ constructor ({ localPeer, + metrics, cryptos, muxers, onConnectionEnd = () => {}, onConnection = () => {} }) { this.localPeer = localPeer + this.metrics = metrics this.cryptos = cryptos || new Map() this.muxers = muxers || new Map() this.protector = null @@ -63,6 +67,15 @@ class Upgrader { let muxedConnection let Muxer let cryptoProtocol + let setPeer + let proxyPeer + + if (this.metrics) { + ({ setTarget: setPeer, proxy: proxyPeer } = mutableProxy()) + const idString = (parseInt(Math.random() * 1e9)).toString(36) + Date.now() + setPeer({ toString: () => idString }) + maConn = this.metrics.trackStream({ stream: maConn, remotePeer: proxyPeer }) + } log('Starting the inbound connection upgrade') @@ -89,6 +102,11 @@ class Upgrader { throw err } + if (this.metrics) { + this.metrics.updatePlaceholder(proxyPeer, remotePeer) + setPeer(remotePeer) + } + log('Successfully upgraded inbound connection') return this._createConnection({ @@ -120,6 +138,15 @@ class Upgrader { let muxedConnection let cryptoProtocol let Muxer + let setPeer + let proxyPeer + + if (this.metrics) { + ({ setTarget: setPeer, proxy: proxyPeer } = mutableProxy()) + const idString = (parseInt(Math.random() * 1e9)).toString(36) + Date.now() + setPeer({ toString: () => idString }) + maConn = this.metrics.trackStream({ stream: maConn, remotePeer: proxyPeer }) + } log('Starting the outbound connection upgrade') @@ -145,6 +172,11 @@ class Upgrader { throw err } + if (this.metrics) { + this.metrics.updatePlaceholder(proxyPeer, remotePeer) + setPeer(remotePeer) + } + log('Successfully upgraded outbound connection') return this._createConnection({ @@ -185,6 +217,7 @@ class Upgrader { try { const { stream, protocol } = await mss.handle(Array.from(this.protocols.keys())) log('%s: incoming stream opened on %s', direction, protocol) + if (this.metrics) this.metrics.trackStream({ stream, remotePeer, protocol }) connection.addStream(stream, protocol) this._onStream({ connection, stream, protocol }) } catch (err) { @@ -203,6 +236,7 @@ class Upgrader { const mss = new Multistream.Dialer(muxedStream) try { const { stream, protocol } = await mss.select(protocols) + if (this.metrics) this.metrics.trackStream({ stream, remotePeer, protocol }) return { stream: { ...muxedStream, ...stream }, protocol } } catch (err) { log.error('could not create new stream', err) diff --git a/test/content-routing/content-routing.node.js b/test/content-routing/content-routing.node.js index 8dd0265811..25511c8d3b 100644 --- a/test/content-routing/content-routing.node.js +++ b/test/content-routing/content-routing.node.js @@ -29,7 +29,7 @@ describe('content-routing', () => { it('.findProviders should return an error', async () => { try { - for await (const _ of node.contentRouting.findProviders('a cid')) {} // eslint-disable-line no-unused-vars + for await (const _ of node.contentRouting.findProviders('a cid')) {} // eslint-disable-line throw new Error('.findProviders should return an error') } catch (err) { expect(err).to.exist() @@ -222,7 +222,7 @@ describe('content-routing', () => { ]) try { - for await (const _ of node.contentRouting.findProviders(cid)) { } // eslint-disable-line no-unused-vars + for await (const _ of node.contentRouting.findProviders(cid)) { } // eslint-disable-line throw new Error('should handle errors when finding providers') } catch (err) { expect(err).to.exist() diff --git a/test/dialing/relay.node.js b/test/dialing/relay.node.js index b5ce59522f..4f477680a5 100644 --- a/test/dialing/relay.node.js +++ b/test/dialing/relay.node.js @@ -82,6 +82,7 @@ describe('Dialing (via relay, TCP)', () => { echoStream, collect ) + expect(output.slice()).to.eql(input) }) diff --git a/test/metrics/index.node.js b/test/metrics/index.node.js new file mode 100644 index 0000000000..ff25b1ad5f --- /dev/null +++ b/test/metrics/index.node.js @@ -0,0 +1,127 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +chai.use(require('chai-as-promised')) +const { expect } = chai +const sinon = require('sinon') + +const { randomBytes } = require('libp2p-crypto') +const pipe = require('it-pipe') +const concat = require('it-concat') +const delay = require('delay') + +const { createPeer } = require('../utils/creators/peer') +const baseOptions = require('../utils/base-options') + +describe('libp2p.metrics', () => { + let libp2p + + afterEach(async () => { + libp2p && await libp2p.stop() + }) + + it('should disable metrics by default', async () => { + [libp2p] = await createPeer({ + config: { + modules: baseOptions.modules + } + }) + + expect(libp2p.metrics).to.not.exist() + }) + + it('should start/stop metrics on startup/shutdown when enabled', async () => { + const config = { ...baseOptions } + config.metrics = { + enabled: true + } + ;[libp2p] = await createPeer({ started: false, config }) + + expect(libp2p.metrics).to.exist() + sinon.spy(libp2p.metrics, 'start') + sinon.spy(libp2p.metrics, 'stop') + + await libp2p.start() + expect(libp2p.metrics.start).to.have.property('callCount', 1) + + await libp2p.stop() + expect(libp2p.metrics.stop).to.have.property('callCount', 1) + }) + + it('should record metrics on connections and streams when enabled', async () => { + const config = { ...baseOptions } + config.metrics = { + enabled: true, + computeThrottleMaxQueueSize: 1, // compute after every message + movingAverageIntervals: [10] + } + let remoteLibp2p + ;[libp2p, remoteLibp2p] = await createPeer({ number: 2, config }) + + remoteLibp2p.handle('/echo/1.0.0', ({ stream }) => pipe(stream, stream)) + + const connection = await libp2p.dial(remoteLibp2p.peerInfo) + const { stream } = await connection.newStream('/echo/1.0.0') + + const bytes = randomBytes(512) + const result = await pipe( + [bytes], + stream, + concat + ) + + // Flush the call stack + await delay(0) + + expect(result).to.have.length(bytes.length) + // Protocol stats should equal the echo size + const protocolStats = libp2p.metrics.forProtocol('/echo/1.0.0').toJSON() + expect(Number(protocolStats.dataReceived)).to.equal(bytes.length) + expect(Number(protocolStats.dataSent)).to.equal(bytes.length) + + // A lot more traffic will be sent over the wire for the peer + const peerStats = libp2p.metrics.forPeer(connection.remotePeer).toJSON() + expect(Number(peerStats.dataReceived)).to.be.at.least(bytes.length) + await remoteLibp2p.stop() + }) + + it('should move disconnected peers to the old peers list', async () => { + const config = { ...baseOptions } + config.metrics = { + enabled: true, + computeThrottleMaxQueueSize: 1, // compute after every message + movingAverageIntervals: [10] + } + let remoteLibp2p + ;[libp2p, remoteLibp2p] = await createPeer({ number: 2, config }) + + remoteLibp2p.handle('/echo/1.0.0', ({ stream }) => pipe(stream, stream)) + + const connection = await libp2p.dial(remoteLibp2p.peerInfo) + const { stream } = await connection.newStream('/echo/1.0.0') + + const bytes = randomBytes(512) + await pipe( + [bytes], + stream, + concat + ) + + sinon.spy(libp2p.metrics, 'onPeerDisconnected') + await libp2p.hangUp(connection.remotePeer) + + // Flush call stack + await delay(0) + + expect(libp2p.metrics.onPeerDisconnected).to.have.property('callCount', 1) + expect(libp2p.metrics.peers).to.have.length(0) + + // forPeer should still give us the old peer stats, + // even though its not in the active peer list + const peerStats = libp2p.metrics.forPeer(connection.remotePeer).toJSON() + expect(Number(peerStats.dataReceived)).to.be.at.least(bytes.length) + await remoteLibp2p.stop() + }) +}) diff --git a/test/metrics/index.spec.js b/test/metrics/index.spec.js new file mode 100644 index 0000000000..d9018fdda4 --- /dev/null +++ b/test/metrics/index.spec.js @@ -0,0 +1,259 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +chai.use(require('chai-as-promised')) +const { expect } = chai +const sinon = require('sinon') + +const { randomBytes } = require('libp2p-crypto') +const duplexPair = require('it-pair/duplex') +const pipe = require('it-pipe') +const concat = require('it-concat') +const pushable = require('it-pushable') +const { consume } = require('streaming-iterables') +const delay = require('delay') + +const Metrics = require('../../src/metrics') +const Stats = require('../../src/metrics/stats') +const { createPeerId } = require('../utils/creators/peer') + +describe('Metrics', () => { + let peerId + let peerId2 + + before(async () => { + [peerId, peerId2] = await createPeerId({ number: 2 }) + }) + + afterEach(() => { + sinon.restore() + }) + + it('should not track data if not started', async () => { + const [local, remote] = duplexPair() + const metrics = new Metrics({ + computeThrottleMaxQueueSize: 1, // compute after every message + movingAverageIntervals: [10, 100, 1000] + }) + + metrics.trackStream({ + stream: local, + remotePeer: peerId + }) + + // Echo back + pipe(remote, remote) + + const bytes = randomBytes(1024) + + const results = await pipe( + [bytes], + local, + concat + ) + + // Flush the call stack + await delay(0) + + expect(results.length).to.eql(bytes.length) + + expect(metrics.forPeer(peerId)).to.equal(undefined) + expect(metrics.peers).to.eql([]) + const globalStats = metrics.global + expect(globalStats.snapshot.dataReceived.toNumber()).to.equal(0) + expect(globalStats.snapshot.dataSent.toNumber()).to.equal(0) + }) + + it('should be able to track a duplex stream', async () => { + const [local, remote] = duplexPair() + const metrics = new Metrics({ + computeThrottleMaxQueueSize: 1, // compute after every message + movingAverageIntervals: [10, 100, 1000] + }) + + metrics.trackStream({ + stream: local, + remotePeer: peerId + }) + metrics.start() + + // Echo back + pipe(remote, remote) + + const bytes = randomBytes(1024) + const input = (async function * () { + let i = 0 + while (i < 10) { + await delay(10) + yield bytes + i++ + } + })() + + const results = await pipe( + input, + local, + concat + ) + + // Flush the call stack + await delay(0) + + expect(results.length).to.eql(bytes.length * 10) + + const stats = metrics.forPeer(peerId) + expect(metrics.peers).to.eql([peerId.toString()]) + expect(stats.snapshot.dataReceived.toNumber()).to.equal(results.length) + expect(stats.snapshot.dataSent.toNumber()).to.equal(results.length) + + const globalStats = metrics.global + expect(globalStats.snapshot.dataReceived.toNumber()).to.equal(results.length) + expect(globalStats.snapshot.dataSent.toNumber()).to.equal(results.length) + }) + + it('should properly track global stats', async () => { + const [local, remote] = duplexPair() + const [local2, remote2] = duplexPair() + const metrics = new Metrics({ + computeThrottleMaxQueueSize: 1, // compute after every message + movingAverageIntervals: [10, 100, 1000] + }) + const protocol = '/echo/1.0.0' + metrics.start() + + // Echo back remotes + pipe(remote, remote) + pipe(remote2, remote2) + + metrics.trackStream({ + stream: local, + remotePeer: peerId, + protocol + }) + metrics.trackStream({ + stream: local2, + remotePeer: peerId2, + protocol + }) + + const bytes = randomBytes(1024) + + await Promise.all([ + pipe([bytes], local, consume), + pipe([bytes], local2, consume) + ]) + + // Flush the call stack + await delay(0) + + expect(metrics.peers).to.eql([peerId.toString(), peerId2.toString()]) + // Verify global metrics + const globalStats = metrics.global + expect(globalStats.snapshot.dataReceived.toNumber()).to.equal(bytes.length * 2) + expect(globalStats.snapshot.dataSent.toNumber()).to.equal(bytes.length * 2) + + // Verify individual metrics + for (const peer of [peerId, peerId2]) { + const stats = metrics.forPeer(peer) + + expect(stats.snapshot.dataReceived.toNumber()).to.equal(bytes.length) + expect(stats.snapshot.dataSent.toNumber()).to.equal(bytes.length) + } + + // Verify protocol metrics + const protocolStats = metrics.forProtocol(protocol) + expect(metrics.protocols).to.eql([protocol]) + expect(protocolStats.snapshot.dataReceived.toNumber()).to.equal(bytes.length * 2) + expect(protocolStats.snapshot.dataSent.toNumber()).to.equal(bytes.length * 2) + }) + + it('should be able to replace an existing peer', async () => { + const [local, remote] = duplexPair() + const metrics = new Metrics({ + computeThrottleMaxQueueSize: 1, // compute after every message + movingAverageIntervals: [10, 100, 1000] + }) + metrics.start() + + // Echo back remotes + pipe(remote, remote) + + const mockPeer = { + toString: () => 'a temporary id' + } + metrics.trackStream({ + stream: local, + remotePeer: mockPeer + }) + + const bytes = randomBytes(1024) + const input = pushable() + + const deferredPromise = pipe(input, local, consume) + + input.push(bytes) + + await delay(0) + + metrics.updatePlaceholder(mockPeer.toString(), peerId) + mockPeer.toString = peerId.toString.bind(peerId) + + input.push(bytes) + input.end() + + await deferredPromise + await delay(0) + + expect(metrics.peers).to.eql([peerId.toString()]) + // Verify global metrics + const globalStats = metrics.global + expect(globalStats.snapshot.dataReceived.toNumber()).to.equal(bytes.length * 2) + expect(globalStats.snapshot.dataSent.toNumber()).to.equal(bytes.length * 2) + + // Verify individual metrics + const stats = metrics.forPeer(peerId) + + expect(stats.snapshot.dataReceived.toNumber()).to.equal(bytes.length * 2) + expect(stats.snapshot.dataSent.toNumber()).to.equal(bytes.length * 2) + }) + + it('should only keep track of a set number of disconnected peers', () => { + const spies = [] + const trackedPeers = new Map([...new Array(50)].map((_, index) => { + const stat = new Stats([], { movingAverageIntervals: [] }) + spies.push(sinon.spy(stat, 'stop')) + return [String(index), stat] + })) + + const metrics = new Metrics({ + maxOldPeersRetention: 5 // Only keep track of 5 + }) + + // Clone so trackedPeers isn't modified + metrics._peerStats = new Map(trackedPeers) + + // Disconnect every peer + for (const id of trackedPeers.keys()) { + metrics.onPeerDisconnected({ + toString: () => id + }) + } + + // Verify only the last 5 have been retained + expect(metrics.peers).to.have.length(0) + const retainedPeers = [] + for (const id of trackedPeers.keys()) { + const stat = metrics.forPeer(id) + if (stat) retainedPeers.push(id) + } + expect(retainedPeers).to.eql(['45', '46', '47', '48', '49']) + + // Verify all stats were stopped + expect(spies).to.have.length(50) + for (const spy of spies) { + expect(spy).to.have.property('callCount', 1) + } + }) +}) diff --git a/test/utils/creators/peer.js b/test/utils/creators/peer.js index 9fb1c3746e..31c2653398 100644 --- a/test/utils/creators/peer.js +++ b/test/utils/creators/peer.js @@ -18,7 +18,7 @@ const listenAddr = multiaddr('/ip4/127.0.0.1/tcp/0') * @param {Object} [properties.config] * @param {number} [properties.number] number of peers (default: 1). * @param {boolean} [properties.fixture] use fixture for peer-id generation (default: true) - * @param {boolean} [properties.started] nodes should start (defaul: true) + * @param {boolean} [properties.started] nodes should start (default: true) * @return {Promise>} */ async function createPeer ({ number = 1, fixture = true, started = true, config = defaultOptions } = {}) { From af96dcc499aea9dcfb10594eea31dd01a0740895 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Wed, 11 Dec 2019 17:06:40 +0100 Subject: [PATCH 61/92] feat: discovery modules from transports should be added (#510) * feat: discovery modules from transports should be added * chore: apply suggestions from code review Co-Authored-By: Jacob Heun * chore: address review * chore: apply suggestions from code review Co-Authored-By: Jacob Heun --- doc/CONFIGURATION.md | 31 ++++++++ package.json | 1 + src/index.js | 23 ++++-- src/transport-manager.js | 8 ++ test/peer-discovery/index.spec.js | 126 +++++++++++++++++++++--------- 5 files changed, 147 insertions(+), 42 deletions(-) diff --git a/doc/CONFIGURATION.md b/doc/CONFIGURATION.md index 801e4f15fe..9904d1c68c 100644 --- a/doc/CONFIGURATION.md +++ b/doc/CONFIGURATION.md @@ -110,6 +110,8 @@ Some available peer discovery modules are: - [js-libp2p-kad-dht](https://github.com/libp2p/js-libp2p-kad-dht) - [js-libp2p-webrtc-star](https://github.com/libp2p/js-libp2p-webrtc-star) +**Note**: `peer-discovery` services within transports (such as `js-libp2p-webrtc-star`) are automatically gathered from the `transport`, via it's `discovery` property. As such, they do not need to be added in the discovery modules. However, these transports can also be configured and disabled as the other ones. + If none of the available peer discovery protocols fulfills your needs, you can create a libp2p compatible one. A libp2p peer discovery protocol just needs to be compliant with the [Peer Discovery Interface](https://github.com/libp2p/js-interfaces/tree/master/src/peer-discovery). If you want to know more about libp2p peer discovery, you should read the following content: @@ -268,6 +270,35 @@ const node = await Libp2p.create({ }) ``` +#### Setup webrtc transport and discovery + +```js + +const Libp2p = require('libp2p') +const WS = require('libp2p-websockets') +const WebRTCStar = require('libp2p-webrtc-star') +const MPLEX = require('libp2p-mplex') +const SECIO = require('libp2p-secio') + +const node = await Libp2p.create({ + modules: { + transport: [ + WS, + WebRTCStar + ], + streamMuxer: [MPLEX], + connEncryption: [SECIO], + }, + config: { + peerDiscovery: { + webRTCStar: { + enabled: true + } + } + } +}) +``` + #### Customizing Pubsub ```js diff --git a/package.json b/package.json index eb874cc2e8..6ba5bcd654 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "libp2p-mplex": "^0.9.1", "libp2p-secio": "^0.12.1", "libp2p-tcp": "^0.14.1", + "libp2p-webrtc-star": "^0.17.0", "libp2p-websockets": "^0.13.1", "nock": "^10.0.6", "p-defer": "^3.0.0", diff --git a/src/index.js b/src/index.js index aad21e288a..8ef3252c6c 100644 --- a/src/index.js +++ b/src/index.js @@ -48,7 +48,7 @@ class Libp2p extends EventEmitter { this._modules = this._options.modules this._config = this._options.config this._transport = [] // Transport instances/references - this._discovery = [] // Discovery service instances/references + this._discovery = new Map() // Discovery service instances/references this.peerStore = new PeerStore() @@ -437,7 +437,7 @@ class Libp2p extends EventEmitter { * @returns {Promise} */ _setupPeerDiscovery () { - for (const DiscoveryService of this._modules.peerDiscovery || []) { + const setupService = (DiscoveryService) => { let config = { enabled: true // on by default } @@ -448,7 +448,8 @@ class Libp2p extends EventEmitter { config = { ...config, ...this._config.peerDiscovery[DiscoveryService.tag] } } - if (config.enabled) { + if (config.enabled && + !this._discovery.has(DiscoveryService.tag)) { // not already added let discoveryService if (typeof DiscoveryService === 'function') { @@ -458,11 +459,23 @@ class Libp2p extends EventEmitter { } discoveryService.on('peer', this._onDiscoveryPeer) - this._discovery.push(discoveryService) + this._discovery.set(DiscoveryService.tag, discoveryService) + } + } + + // Discovery modules + for (const DiscoveryService of this._modules.peerDiscovery || []) { + setupService(DiscoveryService) + } + + // Transport modules with discovery + for (const Transport of this.transportManager.getTransports()) { + if (Transport.discovery) { + setupService(Transport.discovery) } } - return this._discovery.map(d => d.start()) + return Promise.all(Array.from(this._discovery.values(), d => d.start())) } } diff --git a/src/transport-manager.js b/src/transport-manager.js index 5e4a9201a7..d11189ab86 100644 --- a/src/transport-manager.js +++ b/src/transport-manager.js @@ -103,6 +103,14 @@ class TransportManager { return addrs } + /** + * Returns all the transports instances. + * @returns {Iterator} + */ + getTransports () { + return this._transports.values() + } + /** * Finds a transport that matches the given Multiaddr * @param {Multiaddr} ma diff --git a/test/peer-discovery/index.spec.js b/test/peer-discovery/index.spec.js index 3f220e47fa..66e08d37bd 100644 --- a/test/peer-discovery/index.spec.js +++ b/test/peer-discovery/index.spec.js @@ -10,57 +10,109 @@ const defer = require('p-defer') const mergeOptions = require('merge-options') const MulticastDNS = require('libp2p-mdns') +const WebRTCStar = require('libp2p-webrtc-star') const Libp2p = require('../../src') const baseOptions = require('../utils/base-options.browser') const { createPeerInfo } = require('../utils/creators/peer') describe('peer discovery', () => { - let peerInfo - let remotePeerInfo - let libp2p + describe('basic functions', () => { + let peerInfo + let remotePeerInfo + let libp2p - before(async () => { - [peerInfo, remotePeerInfo] = await createPeerInfo({ number: 2 }) - }) - - afterEach(async () => { - libp2p && await libp2p.stop() - sinon.reset() - }) + before(async () => { + [peerInfo, remotePeerInfo] = await createPeerInfo({ number: 2 }) + }) - it('should dial know peers on startup', async () => { - libp2p = new Libp2p({ - ...baseOptions, - peerInfo + afterEach(async () => { + libp2p && await libp2p.stop() + sinon.reset() }) - libp2p.peerStore.add(remotePeerInfo) - const deferred = defer() - sinon.stub(libp2p.dialer, 'connectToPeer').callsFake((remotePeerInfo) => { - expect(remotePeerInfo).to.equal(remotePeerInfo) - deferred.resolve() + + it('should dial know peers on startup', async () => { + libp2p = new Libp2p({ + ...baseOptions, + peerInfo + }) + libp2p.peerStore.add(remotePeerInfo) + const deferred = defer() + sinon.stub(libp2p.dialer, 'connectToPeer').callsFake((remotePeerInfo) => { + expect(remotePeerInfo).to.equal(remotePeerInfo) + deferred.resolve() + }) + const spy = sinon.spy() + libp2p.on('peer:discovery', spy) + + libp2p.start() + await deferred.promise + expect(spy.getCall(0).args).to.eql([remotePeerInfo]) }) - const spy = sinon.spy() - libp2p.on('peer:discovery', spy) - libp2p.start() - await deferred.promise - expect(spy.getCall(0).args).to.eql([remotePeerInfo]) + it('should ignore self on discovery', async () => { + libp2p = new Libp2p(mergeOptions(baseOptions, { + peerInfo, + modules: { + peerDiscovery: [MulticastDNS] + } + })) + + await libp2p.start() + const discoverySpy = sinon.spy() + libp2p.on('peer:discovery', discoverySpy) + libp2p._discovery.get('mdns').emit('peer', libp2p.peerInfo) + + expect(discoverySpy.called).to.eql(false) + }) }) - it('should ignore self on discovery', async () => { - libp2p = new Libp2p(mergeOptions(baseOptions, { - peerInfo, - modules: { - peerDiscovery: [MulticastDNS] - } - })) + describe('discovery modules from transports', () => { + let peerInfo, libp2p - await libp2p.start() - const discoverySpy = sinon.spy() - libp2p.on('peer:discovery', discoverySpy) - libp2p._discovery[0].emit('peer', libp2p.peerInfo) + before(async () => { + [peerInfo] = await createPeerInfo() + }) + + it('should add discovery module if present in transports and enabled', async () => { + libp2p = new Libp2p(mergeOptions(baseOptions, { + peerInfo, + modules: { + transport: [WebRTCStar] + }, + config: { + peerDiscovery: { + webRTCStar: { + enabled: true + } + } + } + })) + + await libp2p.start() + + expect(libp2p._discovery.size).to.eql(1) + expect(libp2p._discovery.has('webRTCStar')).to.eql(true) + }) + + it('should not add discovery module if present in transports but disabled', async () => { + libp2p = new Libp2p(mergeOptions(baseOptions, { + peerInfo, + modules: { + transport: [WebRTCStar] + }, + config: { + peerDiscovery: { + webRTCStar: { + enabled: false + } + } + } + })) - expect(discoverySpy.called).to.eql(false) + await libp2p.start() + + expect(libp2p._discovery.size).to.eql(0) + }) }) }) From 45f47023d25e5b8737e9e4012635c224ff91ae0d Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Thu, 12 Dec 2019 10:19:57 +0100 Subject: [PATCH 62/92] refactor: connection manager (#511) * refactor: initial refactor of the connection manager * fix: start/stop issues * fix: add tests and resolve pruning issues * chore: fix lint * test: move conn manager tests to node only for now * chore: apply suggestions from code review Co-Authored-By: Vasco Santos * fix: assert min max connection options * test: fix assertion check for browser * docs: add api and config docs for conn manager --- doc/API.md | 30 ++++ doc/CONFIGURATION.md | 33 ++++ doc/CONNECTION_MANAGER.md | 20 +++ src/connection-manager/README.md | 99 ----------- src/connection-manager/index.js | 242 ++++++++++++-------------- src/index.js | 12 +- src/registrar.js | 8 +- test/connection-manager/index.spec.js | 133 ++++++++++++++ test/metrics/index.node.js | 43 +++-- 9 files changed, 369 insertions(+), 251 deletions(-) create mode 100644 doc/CONNECTION_MANAGER.md delete mode 100644 src/connection-manager/README.md create mode 100644 test/connection-manager/index.spec.js diff --git a/doc/API.md b/doc/API.md index 33ffc685b6..ba17815fdf 100644 --- a/doc/API.md +++ b/doc/API.md @@ -643,6 +643,36 @@ const handler = (msg) => { libp2p.pubsub.unsubscribe(topic, handler) ``` +### connectionManager.setPeerValue + +Enables users to change the value of certain peers in a range of 0 to 1. Peers with the lowest values will have their Connections pruned first, if any Connection Manager limits are exceeded. See [./CONFIGURATION.md#configuring-connection-manager](./CONFIGURATION.md#configuring-connection-manager) for details on how to configure these limits. + +`libp2p.connectionManager.setPeerValue(peerId, value)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| peerId | `PeerId` | The peer to set the value for | +| value | `number` | The value of the peer from 0 to 1 | + +#### Returns + +| Type | Description | +|------|-------------| +| `void` | | + +#### Example + +```js +const topic = 'topic' +const handler = (msg) => { + // msg.data - pubsub data received +} + +libp2p.pubsub.unsubscribe(topic, handler) +``` + ### metrics.global A [`Stats`](#stats) object of tracking the global bandwidth of the libp2p node. diff --git a/doc/CONFIGURATION.md b/doc/CONFIGURATION.md index 9904d1c68c..c0c55b1348 100644 --- a/doc/CONFIGURATION.md +++ b/doc/CONFIGURATION.md @@ -15,10 +15,12 @@ - [Examples](#examples) - [Basic setup](#basic-setup) - [Customizing Peer Discovery](#customizing-peer-discovery) + - [Setup webrtc transport and discovery](#setup-webrtc-transport-and-discovery) - [Customizing Pubsub](#customizing-pubsub) - [Customizing DHT](#customizing-dht) - [Setup with Content and Peer Routing](#setup-with-content-and-peer-routing) - [Setup with Relay](#setup-with-relay) + - [Configuring Connection Manager](#configuring-connection-manager) - [Configuring Metrics](#configuring-metrics) - [Configuration examples](#configuration-examples) @@ -412,6 +414,37 @@ const node = await Libp2p.create({ }) ``` +#### Configuring Connection Manager + +The Connection Manager prunes Connections in libp2p whenever certain limits are exceeded. If Metrics are enabled, you can also configure the Connection Manager to monitor the bandwidth of libp2p and prune connections as needed. You can read more about what Connection Manager does at [./CONNECTION_MANAGER.md](./CONNECTION_MANAGER.md). The configuration values below show the defaults for Connection Manager. See [./CONNECTION_MANAGER.md](./CONNECTION_MANAGER.md#options) for a full description of the parameters. + +```js +const Libp2p = require('libp2p') +const TCP = require('libp2p-tcp') +const MPLEX = require('libp2p-mplex') +const SECIO = require('libp2p-secio') + +const node = await Libp2p.create({ + modules: { + transport: [TCP], + streamMuxer: [MPLEX], + connEncryption: [SECIO] + }, + connectionManager: { + maxConnections: Infinity, + minConnections: 0, + pollInterval: 2000, + defaultPeerValue: 1, + // The below values will only be taken into account when Metrics are enabled + maxData: Infinity, + maxSentData: Infinity, + maxReceivedData: Infinity, + maxEventLoopDelay: Infinity, + movingAverageInterval: 60000 + } +}) +``` + #### Configuring Metrics Metrics are disabled in libp2p by default. You can enable and configure them as follows. Aside from enabled being `false` by default, the configuration options listed here are the current defaults. diff --git a/doc/CONNECTION_MANAGER.md b/doc/CONNECTION_MANAGER.md new file mode 100644 index 0000000000..8d0a3aaf47 --- /dev/null +++ b/doc/CONNECTION_MANAGER.md @@ -0,0 +1,20 @@ +# Connection Manager + +The Connection Manager works with the Registrar to keep connections across libp2p within acceptable ranges, which can be configured. By default Connection Manager will monitor: +- The total number of open connections +- The latency/delay of the event loop + +If Metrics are enabled for libp2p, see [./CONFIGURATION.md#configuring-metrics](./CONFIGURATION.md#configuring-metrics) on how to configure metrics, the Connection Manager can be used to prune connections when certain limits are exceeded. + +The following is a list of available options for setting limits for the Connection Manager to enforce. + +## Options +- `maxConnections`: the maximum number of connections libp2p is willing to have before it starts disconnecting. Defaults to `Infinity` +- `minConnections`: the minimum number of connections below which libp2p not activate preemptive disconnections. Defaults to `0`. +- `maxData`: sets the maximum data — in bytes per second - (sent and received) this node is willing to endure before it starts disconnecting peers. Defaults to `Infinity`. +- `maxSentData`: sets the maximum sent data — in bytes per second - this node is willing to endure before it starts disconnecting peers. Defaults to `Infinity`. +- `maxReceivedData`: sets the maximum received data — in bytes per second - this node is willing to endure before it starts disconnecting peers. Defaults to `Infinity`. +- `maxEventLoopDelay`: sets the maximum event loop delay (measured in milliseconds) this node is willing to endure before it starts disconnecting peers. Defaults to `Infinity`. +- `pollInterval`: sets the poll interval (in milliseconds) for assessing the current state and determining if this peer needs to force a disconnect. Defaults to `2000` (2 seconds). +- `movingAverageInterval`: the interval used to calculate moving averages (in milliseconds). Defaults to `60000` (1 minute). This must be an available interval configured in `Metrics` +- `defaultPeerValue`: number between 0 and 1. Defaults to 1. diff --git a/src/connection-manager/README.md b/src/connection-manager/README.md deleted file mode 100644 index 80158ea906..0000000000 --- a/src/connection-manager/README.md +++ /dev/null @@ -1,99 +0,0 @@ -# libp2p-connection-manager - -> JavaScript connection manager for libp2p - -**Note**: git history prior to merging into js-libp2p can be found in the original repository, https://github.com/libp2p/js-libp2p-connection-manager. - -## Table of Contents - -- [Install](#install) - - [npm](#npm) - - [Use in Node.js, a browser with browserify, webpack or any other bundler](##use-in-nodejs-or-in-the-browser-with-browserify-webpack-or-any-other-bundler) -- [Usage](#usage) -- [API](#api) -- [Contribute](#contribute) -- [License](#license) - -## API - -A connection manager manages the peers you're connected to. The application provides one or more limits that will trigger the disconnection of peers. These limits can be any of the following: - -* number of connected peers -* maximum bandwidth (sent / received or both) -* maximum event loop delay - -The connection manager will disconnect peers (starting from the less important peers) until all the measures are withing the stated limits. - -A connection manager first disconnects the peers with the least value. By default all peers have the same importance (1), but the application can define otherwise. Once a peer disconnects the connection manager discards the peer importance. (If necessary, the application should redefine the peer state if the peer is again connected). - - -### Create a ConnectionManager - -```js -const libp2p = // … -const options = {…} -const connManager = new ConnManager(libp2p, options) -``` - -Options is an optional object with the following key-value pairs: - -* **`maxPeers`**: number identifying the maximum number of peers the current peer is willing to be connected to before is starts disconnecting. Defaults to `Infinity` -* **`maxPeersPerProtocol`**: Object with key-value pairs, where a key is the protocol tag (case-insensitive) and the value is a number, representing the maximum number of peers allowing to connect for each protocol. Defaults to `{}`. -* **`minPeers`**: number identifying the number of peers below which this node will not activate preemptive disconnections. Defaults to `0`. -* **`maxData`**: sets the maximum data — in bytes per second - (sent and received) this node is willing to endure before it starts disconnecting peers. Defaults to `Infinity`. -* **`maxSentData`**: sets the maximum sent data — in bytes per second - this node is willing to endure before it starts disconnecting peers. Defaults to `Infinity`. -* **`maxReceivedData`**: sets the maximum received data — in bytes per second - this node is willing to endure before it starts disconnecting peers. Defaults to `Infinity`. -* **`maxEventLoopDelay`**: sets the maximum event loop delay (measured in miliseconds) this node is willing to endure before it starts disconnecting peers. Defaults to `Infinity`. -* **`pollInterval`**: sets the poll interval (in miliseconds) for assessing the current state and determining if this peer needs to force a disconnect. Defaults to `2000` (2 seconds). -* **`movingAverageInterval`**: the interval used to calculate moving averages (in miliseconds). Defaults to `60000` (1 minute). -* **`defaultPeerValue`**: number between 0 and 1. Defaults to 1. - - -### `connManager.start()` - -Starts the connection manager. - -### `connManager.stop()` - -Stops the connection manager. - - -### `connManager.setPeerValue(peerId, value)` - -Sets the peer value for a given peer id. This is used to sort peers (in reverse order of value) to determine which to disconnect from first. - -Arguments: - -* peerId: B58-encoded string or [`peer-id`](https://github.com/libp2p/js-peer-id) instance. -* value: a number between 0 and 1, which represents a scale of how valuable this given peer id is to the application. - -### `connManager.peers()` - -Returns the peers this connection manager is connected to. - -Returns an array of [PeerInfo](https://github.com/libp2p/js-peer-info). - -### `connManager.emit('limit:exceeded', limitName, measured)` - -Emitted when a limit is exceeded. Limit names can be: - -* `maxPeers` -* `minPeers` -* `maxData` -* `maxSentData` -* `maxReceivedData` -* `maxEventLoopDelay` -* a protocol tag string (lower-cased) - - -### `connManager.emit('disconnect:preemptive', peerId)` - -Emitted when a peer is about to be preemptively disconnected. - -### `connManager.emit('disconnected', peerId)` - -Emitted when a peer is disconnected (preemptively or note). If this peer reconnects, you will need to reset it's value, since the connection manager does not remember it. - -### `connManager.emit('connected', peerId: String)` - -Emitted when a peer connects. This is a good event to set the peer value, so you can get some control over who gets banned once a maximum number of peers is reached. diff --git a/src/connection-manager/index.js b/src/connection-manager/index.js index 16c079d4c5..1d01a7560a 100644 --- a/src/connection-manager/index.js +++ b/src/connection-manager/index.js @@ -1,12 +1,14 @@ 'use strict' -const EventEmitter = require('events') +const assert = require('assert') +const mergeOptions = require('merge-options') const LatencyMonitor = require('latency-monitor').default const debug = require('debug')('libp2p:connection-manager') +const retimer = require('retimer') const defaultOptions = { - maxPeers: Infinity, - minPeers: 0, + maxConnections: Infinity, + minConnections: 0, maxData: Infinity, maxSentData: Infinity, maxReceivedData: Infinity, @@ -16,170 +18,171 @@ const defaultOptions = { defaultPeerValue: 1 } -class ConnectionManager extends EventEmitter { +class ConnectionManager { + /** + * @constructor + * @param {Libp2p} libp2p + * @param {object} options + * @param {Number} options.maxConnections The maximum number of connections allowed. Default=Infinity + * @param {Number} options.minConnections The minimum number of connections to avoid pruning. Default=0 + * @param {Number} options.maxData The max data (in and out), per average interval to allow. Default=Infinity + * @param {Number} options.maxSentData The max outgoing data, per average interval to allow. Default=Infinity + * @param {Number} options.maxReceivedData The max incoming data, per average interval to allow.. Default=Infinity + * @param {Number} options.maxEventLoopDelay The upper limit the event loop can take to run. Default=Infinity + * @param {Number} options.pollInterval How often, in milliseconds, metrics and latency should be checked. Default=2000 + * @param {Number} options.movingAverageInterval How often, in milliseconds, to compute averages. Default=60000 + * @param {Number} options.defaultPeerValue The value of the peer. Default=1 + */ constructor (libp2p, options) { - super() this._libp2p = libp2p - this._options = Object.assign({}, defaultOptions, options) - this._options.maxPeersPerProtocol = fixMaxPeersPerProtocol(this._options.maxPeersPerProtocol) + this._registrar = libp2p.registrar + this._peerId = libp2p.peerInfo.id.toString() + this._options = mergeOptions(defaultOptions, options) + assert( + this._options.maxConnections > this._options.minConnections, + 'Connection Manager maxConnections must be greater than minConnections' + ) debug('options: %j', this._options) - this._stats = libp2p.stats - if (options && !this._stats) { - throw new Error('No libp2p.stats') - } + this._metrics = libp2p.metrics this._peerValues = new Map() - this._peers = new Map() - this._peerProtocols = new Map() - this._peerCountPerProtocol = new Map() - this._onStatsUpdate = this._onStatsUpdate.bind(this) - this._onPeerConnect = this._onPeerConnect.bind(this) - this._onPeerDisconnect = this._onPeerDisconnect.bind(this) - - if (this._libp2p.isStarted()) { - this._onceStarted() - } else { - this._libp2p.once('start', this._onceStarted.bind(this)) - } + this._connections = new Map() + this._timer = null + this._checkMetrics = this._checkMetrics.bind(this) } + /** + * Starts the Connection Manager. If Metrics are not enabled on libp2p + * only event loop and connection limits will be monitored. + */ start () { - this._stats.on('update', this._onStatsUpdate) - this._libp2p.on('connection:start', this._onPeerConnect) - this._libp2p.on('connection:end', this._onPeerDisconnect) + if (this._metrics) { + this._timer = this._timer || retimer(this._checkMetrics, this._options.pollInterval) + } + // latency monitor this._latencyMonitor = new LatencyMonitor({ + latencyCheckIntervalMs: this._options.pollInterval, dataEmitIntervalMs: this._options.pollInterval }) this._onLatencyMeasure = this._onLatencyMeasure.bind(this) this._latencyMonitor.on('data', this._onLatencyMeasure) + debug('started') } + /** + * Stops the Connection Manager + */ stop () { - this._stats.removeListener('update', this._onStatsUpdate) - this._libp2p.removeListener('connection:start', this._onPeerConnect) - this._libp2p.removeListener('connection:end', this._onPeerDisconnect) - this._latencyMonitor.removeListener('data', this._onLatencyMeasure) + this._timer && this._timer.clear() + this._latencyMonitor && this._latencyMonitor.removeListener('data', this._onLatencyMeasure) + debug('stopped') } + /** + * Sets the value of the given peer. Peers with lower values + * will be disconnected first. + * @param {PeerId} peerId + * @param {number} value A number between 0 and 1 + */ setPeerValue (peerId, value) { if (value < 0 || value > 1) { throw new Error('value should be a number between 0 and 1') } - if (peerId.toB58String) { - peerId = peerId.toB58String() + if (peerId.toString) { + peerId = peerId.toString() } this._peerValues.set(peerId, value) } - _onceStarted () { - this._peerId = this._libp2p.peerInfo.id.toB58String() - } - - _onStatsUpdate () { - const movingAvgs = this._stats.global.movingAverages - const received = movingAvgs.dataReceived[this._options.movingAverageInterval].movingAverage() + /** + * Checks the libp2p metrics to determine if any values have exceeded + * the configured maximums. + * @private + */ + _checkMetrics () { + const movingAverages = this._metrics.global.movingAverages + const received = movingAverages.dataReceived[this._options.movingAverageInterval].movingAverage() this._checkLimit('maxReceivedData', received) - const sent = movingAvgs.dataSent[this._options.movingAverageInterval].movingAverage() + const sent = movingAverages.dataSent[this._options.movingAverageInterval].movingAverage() this._checkLimit('maxSentData', sent) const total = received + sent this._checkLimit('maxData', total) - debug('stats update', total) - } - - _onPeerConnect (peerInfo) { - const peerId = peerInfo.id.toB58String() - debug('%s: connected to %s', this._peerId, peerId) - this._peerValues.set(peerId, this._options.defaultPeerValue) - this._peers.set(peerId, peerInfo) - this.emit('connected', peerId) - this._checkLimit('maxPeers', this._peers.size) - - protocolsFromPeerInfo(peerInfo).forEach((protocolTag) => { - const protocol = this._peerCountPerProtocol[protocolTag] - if (!protocol) { - this._peerCountPerProtocol[protocolTag] = 0 - } - this._peerCountPerProtocol[protocolTag]++ - - let peerProtocols = this._peerProtocols[peerId] - if (!peerProtocols) { - peerProtocols = this._peerProtocols[peerId] = new Set() - } - peerProtocols.add(protocolTag) - this._checkProtocolMaxPeersLimit(protocolTag, this._peerCountPerProtocol[protocolTag]) - }) + debug('metrics update', total) + this._timer.reschedule(this._options.pollInterval) } - _onPeerDisconnect (peerInfo) { - const peerId = peerInfo.id.toB58String() - debug('%s: disconnected from %s', this._peerId, peerId) - this._peerValues.delete(peerId) - this._peers.delete(peerId) - - const peerProtocols = this._peerProtocols[peerId] - if (peerProtocols) { - Array.from(peerProtocols).forEach((protocolTag) => { - const peerCountForProtocol = this._peerCountPerProtocol[protocolTag] - if (peerCountForProtocol) { - this._peerCountPerProtocol[protocolTag]-- - } - }) + /** + * Tracks the incoming connection and check the connection limit + * @param {Connection} connection + */ + onConnect (connection) { + const peerId = connection.remotePeer.toString() + this._connections.set(connection.id, connection) + if (!this._peerValues.has(peerId)) { + this._peerValues.set(peerId, this._options.defaultPeerValue) } + this._checkLimit('maxConnections', this._connections.size) + } - this.emit('disconnected', peerId) + /** + * Removes the connection from tracking + * @param {Connection} connection + */ + onDisconnect (connection) { + this._connections.delete(connection.id) + this._peerValues.delete(connection.remotePeer.toString()) } + /** + * If the event loop is slow, maybe close a connection + * @private + * @param {*} summary The LatencyMonitor summary + */ _onLatencyMeasure (summary) { this._checkLimit('maxEventLoopDelay', summary.avgMs) } + /** + * If the `value` of `name` has exceeded its limit, maybe close a connection + * @private + * @param {string} name The name of the field to check limits for + * @param {number} value The current value of the field + */ _checkLimit (name, value) { const limit = this._options[name] debug('checking limit of %s. current value: %d of %d', name, value, limit) if (value > limit) { debug('%s: limit exceeded: %s, %d', this._peerId, name, value) - this.emit('limit:exceeded', name, value) - this._maybeDisconnectOne() - } - } - - _checkProtocolMaxPeersLimit (protocolTag, value) { - debug('checking protocol limit. current value of %s is %d', protocolTag, value) - const limit = this._options.maxPeersPerProtocol[protocolTag] - if (value > limit) { - debug('%s: protocol max peers limit exceeded: %s, %d', this._peerId, protocolTag, value) - this.emit('limit:exceeded', protocolTag, value) this._maybeDisconnectOne() } } + /** + * If we have more connections than our maximum, close a connection + * to the lowest valued peer. + * @private + */ _maybeDisconnectOne () { - if (this._options.minPeers < this._peerValues.size) { + if (this._options.minConnections < this._connections.size) { const peerValues = Array.from(this._peerValues).sort(byPeerValue) debug('%s: sorted peer values: %j', this._peerId, peerValues) const disconnectPeer = peerValues[0] if (disconnectPeer) { const peerId = disconnectPeer[0] debug('%s: lowest value peer is %s', this._peerId, peerId) - debug('%s: forcing disconnection from %j', this._peerId, peerId) - this._disconnectPeer(peerId) + debug('%s: closing a connection to %j', this._peerId, peerId) + for (const connection of this._connections.values()) { + if (connection.remotePeer.toString() === peerId) { + connection.close() + break + } + } } } } - - _disconnectPeer (peerId) { - debug('preemptively disconnecting peer', peerId) - this.emit('%s: disconnect:preemptive', this._peerId, peerId) - const peer = this._peers.get(peerId) - this._libp2p.hangUp(peer, (err) => { - if (err) { - this.emit('error', err) - } - }) - } } module.exports = ConnectionManager @@ -187,32 +190,3 @@ module.exports = ConnectionManager function byPeerValue (peerValueEntryA, peerValueEntryB) { return peerValueEntryA[1] - peerValueEntryB[1] } - -function fixMaxPeersPerProtocol (maxPeersPerProtocol) { - if (!maxPeersPerProtocol) { - maxPeersPerProtocol = {} - } - - Object.keys(maxPeersPerProtocol).forEach((transportTag) => { - const max = maxPeersPerProtocol[transportTag] - delete maxPeersPerProtocol[transportTag] - maxPeersPerProtocol[transportTag.toLowerCase()] = max - }) - - return maxPeersPerProtocol -} - -function protocolsFromPeerInfo (peerInfo) { - const protocolTags = new Set() - peerInfo.multiaddrs.forEach((multiaddr) => { - multiaddr.protos().map(protocolToProtocolTag).forEach((protocolTag) => { - protocolTags.add(protocolTag) - }) - }) - - return Array.from(protocolTags) -} - -function protocolToProtocolTag (protocol) { - return protocol.name.toLowerCase() -} diff --git a/src/index.js b/src/index.js index 8ef3252c6c..c84b0c6e3d 100644 --- a/src/index.js +++ b/src/index.js @@ -15,6 +15,7 @@ const { getPeerInfo, getPeerInfoRemote } = require('./get-peer-info') const { validate: validateConfig } = require('./config') const { codes } = require('./errors') +const ConnectionManager = require('./connection-manager') const Circuit = require('./circuit') const Dialer = require('./dialer') const Metrics = require('./metrics') @@ -63,6 +64,7 @@ class Libp2p extends EventEmitter { onConnection: (connection) => { const peerInfo = this.peerStore.put(new PeerInfo(connection.remotePeer)) this.registrar.onConnect(peerInfo, connection) + this.connectionManager.onConnect(connection) this.emit('peer:connect', peerInfo) // Run identify for every connection @@ -74,6 +76,7 @@ class Libp2p extends EventEmitter { onConnectionEnd: (connection) => { const peerInfo = getPeerInfo(connection.remotePeer) this.registrar.onDisconnect(peerInfo, connection) + this.connectionManager.onDisconnect(connection) // If there are no connections to the peer, disconnect if (!this.registrar.getConnection(peerInfo)) { @@ -88,6 +91,9 @@ class Libp2p extends EventEmitter { this.handle = this.handle.bind(this) this.registrar.handle = this.handle + // Create the Connection Manager + this.connectionManager = new ConnectionManager(this, this._options.connectionManager) + // Setup the transport manager this.transportManager = new TransportManager({ libp2p: this, @@ -208,6 +214,7 @@ class Libp2p extends EventEmitter { log('libp2p is stopping') try { + this.connectionManager.stop() await Promise.all([ this.pubsub && this.pubsub.stop(), this._dht && this._dht.stop(), @@ -225,6 +232,7 @@ class Libp2p extends EventEmitter { this.emit('error', err) } } + this._isStarted = false log('libp2p has stopped') } @@ -290,7 +298,7 @@ class Libp2p extends EventEmitter { */ hangUp (peer) { return Promise.all( - this.registrar.connections.get(peer.toB58String()).map(connection => { + this.registrar.connections.get(peer.toString()).map(connection => { return connection.close() }) ) @@ -378,6 +386,8 @@ class Libp2p extends EventEmitter { _onDidStart () { this._isStarted = true + this.connectionManager.start() + this.peerStore.on('peer', peerInfo => { this.emit('peer:discovery', peerInfo) this._maybeConnect(peerInfo) diff --git a/src/registrar.js b/src/registrar.js index cf3b2894f4..f4c3868069 100644 --- a/src/registrar.js +++ b/src/registrar.js @@ -74,7 +74,7 @@ class Registrar { assert(PeerInfo.isPeerInfo(peerInfo), 'peerInfo must be an instance of peer-info') assert(Connection.isConnection(conn), 'conn must be an instance of interface-connection') - const id = peerInfo.id.toB58String() + const id = peerInfo.id.toString() const storedConn = this.connections.get(id) if (storedConn) { @@ -95,7 +95,7 @@ class Registrar { onDisconnect (peerInfo, connection, error) { assert(PeerInfo.isPeerInfo(peerInfo), 'peerInfo must be an instance of peer-info') - const id = peerInfo.id.toB58String() + const id = peerInfo.id.toString() let storedConn = this.connections.get(id) if (storedConn && storedConn.length > 1) { @@ -106,7 +106,7 @@ class Registrar { topology.disconnect(peerInfo, error) } - this.connections.delete(peerInfo.id.toB58String()) + this.connections.delete(peerInfo.id.toString()) } } @@ -118,7 +118,7 @@ class Registrar { getConnection (peerInfo) { assert(PeerInfo.isPeerInfo(peerInfo), 'peerInfo must be an instance of peer-info') - const connections = this.connections.get(peerInfo.id.toB58String()) + const connections = this.connections.get(peerInfo.id.toString()) // TODO: what should we return return connections ? connections[0] : null } diff --git a/test/connection-manager/index.spec.js b/test/connection-manager/index.spec.js new file mode 100644 index 0000000000..bf8fa697fb --- /dev/null +++ b/test/connection-manager/index.spec.js @@ -0,0 +1,133 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +chai.use(require('chai-as-promised')) +const { expect } = chai +const sinon = require('sinon') + +const { createPeer } = require('../utils/creators/peer') +const mockConnection = require('../utils/mockConnection') +const baseOptions = require('../utils/base-options.browser') + +describe('Connection Manager', () => { + let libp2p + + afterEach(async () => { + sinon.restore() + libp2p && await libp2p.stop() + }) + + it('should be able to create without metrics', async () => { + [libp2p] = await createPeer({ + config: { + modules: baseOptions.modules + }, + started: false + }) + + const spy = sinon.spy(libp2p.connectionManager, 'start') + + await libp2p.start() + expect(spy).to.have.property('callCount', 1) + expect(libp2p.connectionManager._metrics).to.not.exist() + }) + + it('should be able to create with metrics', async () => { + [libp2p] = await createPeer({ + config: { + modules: baseOptions.modules, + metrics: { + enabled: true + } + }, + started: false + }) + + const spy = sinon.spy(libp2p.connectionManager, 'start') + + await libp2p.start() + expect(spy).to.have.property('callCount', 1) + expect(libp2p.connectionManager._metrics).to.exist() + }) + + it('should close lowest value peer connection when the maximum has been reached', async () => { + const max = 5 + ;[libp2p] = await createPeer({ + config: { + modules: baseOptions.modules, + connectionManager: { + maxConnections: max + } + }, + started: false + }) + + await libp2p.start() + + sinon.spy(libp2p.connectionManager, '_maybeDisconnectOne') + + // Add 1 too many connections + const spies = new Map() + await Promise.all([...new Array(max + 1)].map(async (_, index) => { + const connection = await mockConnection() + const spy = sinon.spy(connection, 'close') + // The connections have the same remote id, give them random ones + // so that we can verify the correct connection was closed + sinon.stub(connection.remotePeer, 'toString').returns(index) + const value = Math.random() + spies.set(value, spy) + libp2p.connectionManager.setPeerValue(connection.remotePeer, value) + libp2p.connectionManager.onConnect(connection) + })) + + // get the lowest value + const lowest = Array.from(spies.keys()).sort()[0] + const lowestSpy = spies.get(lowest) + + expect(libp2p.connectionManager._maybeDisconnectOne).to.have.property('callCount', 1) + expect(lowestSpy).to.have.property('callCount', 1) + }) + + it('should close connection when the maximum has been reached even without peer values', async () => { + const max = 5 + ;[libp2p] = await createPeer({ + config: { + modules: baseOptions.modules, + connectionManager: { + maxConnections: max + } + }, + started: false + }) + + await libp2p.start() + + sinon.spy(libp2p.connectionManager, '_maybeDisconnectOne') + + // Add 1 too many connections + const spy = sinon.spy() + await Promise.all([...new Array(max + 1)].map(async () => { + const connection = await mockConnection() + sinon.stub(connection, 'close').callsFake(() => spy()) + libp2p.connectionManager.onConnect(connection) + })) + + expect(libp2p.connectionManager._maybeDisconnectOne).to.have.property('callCount', 1) + expect(spy).to.have.property('callCount', 1) + }) + + it('should fail if the connection manager has mismatched connection limit options', async () => { + await expect(createPeer({ + config: { + modules: baseOptions.modules, + connectionManager: { + maxConnections: 5, + minConnections: 6 + } + }, + started: false + })).to.eventually.rejected('maxConnections must be greater') + }) +}) diff --git a/test/metrics/index.node.js b/test/metrics/index.node.js index ff25b1ad5f..f3fa8c7c87 100644 --- a/test/metrics/index.node.js +++ b/test/metrics/index.node.js @@ -33,9 +33,16 @@ describe('libp2p.metrics', () => { }) it('should start/stop metrics on startup/shutdown when enabled', async () => { - const config = { ...baseOptions } - config.metrics = { - enabled: true + const config = { + ...baseOptions, + connectionManager: { + movingAverageIntervals: [10] + }, + metrics: { + enabled: true, + computeThrottleMaxQueueSize: 1, // compute after every message + movingAverageIntervals: [10] + } } ;[libp2p] = await createPeer({ started: false, config }) @@ -51,11 +58,16 @@ describe('libp2p.metrics', () => { }) it('should record metrics on connections and streams when enabled', async () => { - const config = { ...baseOptions } - config.metrics = { - enabled: true, - computeThrottleMaxQueueSize: 1, // compute after every message - movingAverageIntervals: [10] + const config = { + ...baseOptions, + connectionManager: { + movingAverageIntervals: [10] + }, + metrics: { + enabled: true, + computeThrottleMaxQueueSize: 1, // compute after every message + movingAverageIntervals: [10] + } } let remoteLibp2p ;[libp2p, remoteLibp2p] = await createPeer({ number: 2, config }) @@ -88,11 +100,16 @@ describe('libp2p.metrics', () => { }) it('should move disconnected peers to the old peers list', async () => { - const config = { ...baseOptions } - config.metrics = { - enabled: true, - computeThrottleMaxQueueSize: 1, // compute after every message - movingAverageIntervals: [10] + const config = { + ...baseOptions, + connectionManager: { + movingAverageIntervals: [10] + }, + metrics: { + enabled: true, + computeThrottleMaxQueueSize: 1, // compute after every message + movingAverageIntervals: [10] + } } let remoteLibp2p ;[libp2p, remoteLibp2p] = await createPeer({ number: 2, config }) From 3ee1e22242fbb5c600bbb37a95f4402b4d52307f Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Thu, 12 Dec 2019 10:43:17 +0100 Subject: [PATCH 63/92] chore: update contributors --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 6ba5bcd654..baad9d2058 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "libp2p", - "version": "0.26.2", + "version": "0.27.0-pre.0", "description": "JavaScript implementation of libp2p, a modular peer to peer network stack", "leadMaintainer": "Jacob Heun ", "main": "src/index.js", @@ -116,6 +116,7 @@ "Chris Dostert ", "Daijiro Wachi ", "David Dias ", + "Didrik Nordström ", "Diogo Silva ", "Dmitriy Ryajov ", "Elven ", @@ -152,10 +153,12 @@ "Yusef Napora ", "Zane Starr ", "a1300 ", + "dirkmc ", "ebinks ", "greenkeeperio-bot ", "isan_rivkin ", "mayerwin ", + "phillmac ", "swedneck <40505480+swedneck@users.noreply.github.com>", "ᴠɪᴄᴛᴏʀ ʙᴊᴇʟᴋʜᴏʟᴍ " ] From 9c884a72b0165f186ebe333c30e372b3fd52c9a1 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Thu, 12 Dec 2019 10:43:18 +0100 Subject: [PATCH 64/92] chore: release version v0.27.0-pre.0 --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38e5e51856..c43036ee57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,32 @@ + +# [0.27.0-pre.0](https://github.com/libp2p/js-libp2p/compare/v0.26.2...v0.27.0-pre.0) (2019-12-12) + + +### Bug Fixes + +* clean up peer discovery flow ([#494](https://github.com/libp2p/js-libp2p/issues/494)) ([f3eb1f1](https://github.com/libp2p/js-libp2p/commit/f3eb1f1)) +* clean up pending dials abort per feedback ([7c3371b](https://github.com/libp2p/js-libp2p/commit/7c3371b)) +* correct release readme ([c4bc00b](https://github.com/libp2p/js-libp2p/commit/c4bc00b)) +* examples readme typos ([#481](https://github.com/libp2p/js-libp2p/issues/481)) ([35ac02d](https://github.com/libp2p/js-libp2p/commit/35ac02d)) +* performance bottleneck in stat.js ([#463](https://github.com/libp2p/js-libp2p/issues/463)) ([93a1e42](https://github.com/libp2p/js-libp2p/commit/93a1e42)) +* release tokens as soon as they are available ([43440aa](https://github.com/libp2p/js-libp2p/commit/43440aa)) +* replace peerInfo addresses with listen addresses ([#485](https://github.com/libp2p/js-libp2p/issues/485)) ([acbbc0f](https://github.com/libp2p/js-libp2p/commit/acbbc0f)) +* token release logic ([1838a64](https://github.com/libp2p/js-libp2p/commit/1838a64)) + + +### Features + +* abort all pending dials on stop ([754fbc2](https://github.com/libp2p/js-libp2p/commit/754fbc2)) +* add early token recycling in ([24c6037](https://github.com/libp2p/js-libp2p/commit/24c6037)) +* add token based dialer ([f8540fa](https://github.com/libp2p/js-libp2p/commit/f8540fa)) +* discovery modules ([#486](https://github.com/libp2p/js-libp2p/issues/486)) ([997ee16](https://github.com/libp2p/js-libp2p/commit/997ee16)) +* discovery modules from transports should be added ([#510](https://github.com/libp2p/js-libp2p/issues/510)) ([af96dcc](https://github.com/libp2p/js-libp2p/commit/af96dcc)) +* peer store ([#470](https://github.com/libp2p/js-libp2p/issues/470)) ([f3e276e](https://github.com/libp2p/js-libp2p/commit/f3e276e)) +* registrar ([#471](https://github.com/libp2p/js-libp2p/issues/471)) ([797d8f0](https://github.com/libp2p/js-libp2p/commit/797d8f0)) +* support peer-id instances in peer store operations ([#491](https://github.com/libp2p/js-libp2p/issues/491)) ([11ed6bd](https://github.com/libp2p/js-libp2p/commit/11ed6bd)) + + + ## [0.26.2](https://github.com/libp2p/js-libp2p/compare/v0.26.1...v0.26.2) (2019-09-24) From cc65a4b06f8d8895ac37ffe6d305a91b07cfd596 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Thu, 12 Dec 2019 13:08:06 +0100 Subject: [PATCH 65/92] chore: update readme (#513) --- README.md | 208 ++++++++++------------------------------------ package-list.json | 1 - 2 files changed, 45 insertions(+), 164 deletions(-) diff --git a/README.md b/README.md index cbac8cfc9c..ba8a0f0b8f 100644 --- a/README.md +++ b/README.md @@ -45,11 +45,11 @@ We've come a long way, but this project is still in Alpha, lots of development i ## Table of Contents - [Background](#background) -- [Bundles](#bundles) +- [Install](#install) - [Usage](#usage) - - [Install](#install) - - [Usage](#usage) + - [Configuration](#configuration) - [API](#api) + - [Tutorials and Examples](#tutorials-and-examples) - [Development](#development) - [Tests](#tests) - [Packages](#packages) @@ -73,142 +73,25 @@ We are in the process of writing better documentation, blog posts, tutorials and To sum up, libp2p is a "network stack" -- a protocol suite -- that cleanly separates concerns, and enables sophisticated applications to only use the protocols they absolutely need, without giving up interoperability and upgradeability. libp2p grew out of IPFS, but it is built so that lots of people can use it, for lots of different projects. -## Bundles - -With its modular nature, libp2p can be found being used in different projects with different sets of features, while preserving the same top level API. `js-libp2p` is only a skeleton and should not be installed directly, if you are looking for a prebundled libp2p stack, please check: - -- [libp2p-ipfs-nodejs](https://github.com/ipfs/js-ipfs/tree/master/src/core/runtime/libp2p-nodejs.js) - The libp2p build used by js-ipfs when run in Node.js -- [libp2p-ipfs-browser](https://github.com/ipfs/js-ipfs/tree/master/src/core/runtime/libp2p-browser.js) - The libp2p build used by js-ipfs when run in a Browser (that supports WebRTC) - -If you have developed a libp2p bundle, please consider submitting it to this list so that it can be found easily by the users of libp2p. - ## Install -Again, as noted above, this module is only a skeleton and should not be used directly other than libp2p bundle implementors that want to extend its code. - ```sh -npm install --save libp2p +npm install libp2p ``` ## Usage -**IMPORTANT NOTE**: We are currently on the way of migrating all our `libp2p` modules to use `async await` and `async iterators`, instead of callbacks and `pull-streams`. As a consequence, when you start a new libp2p project, we must check which versions of the modules you should use. For now, it is required to use the modules using callbacks with `libp2p`, while we are working on getting the remaining modules ready for a full migration. For more details, you can have a look at [libp2p/js-libp2p#266](https://github.com/libp2p/js-libp2p/issues/266). - -### [Tutorials and Examples](/examples) - -You can find multiple examples on the [examples folder](/examples) that will guide you through using libp2p for several scenarios. - -### Creating your own libp2p bundle - -The libp2p module acts as a glue for every libp2p module that you can use to create your own libp2p bundle. Creating your own libp2p bundle gives you a lot of freedom when it comes to customize it with features and default setup. We recommend creating your own libp2p bundle for the app you are developing that takes into account your needs (e.g. for a browser working version of libp2p that acts as the network layer of IPFS, we have built one that leverages the Browser transports). - -**Example:** - -```JavaScript -// Creating a bundle that adds: -// transport: websockets + tcp -// stream-muxing: spdy & mplex -// crypto-channel: secio -// discovery: multicast-dns - -const Libp2p = require('libp2p') -const TCP = require('libp2p-tcp') -const WS = require('libp2p-websockets') -const SPDY = require('libp2p-spdy') -const MPLEX = require('libp2p-mplex') -const SECIO = require('libp2p-secio') -const MulticastDNS = require('libp2p-mdns') -const DHT = require('libp2p-kad-dht') -const GossipSub = require('libp2p-gossipsub') -const defaultsDeep = require('@nodeutils/defaults-deep') -const Protector = require('libp2p/src/pnet') -const DelegatedPeerRouter = require('libp2p-delegated-peer-routing') -const DelegatedContentRouter = require('libp2p-delegated-content-routing') - -class Node extends Libp2p { - constructor (_options) { - const peerInfo = _options.peerInfo - const defaults = { - // The libp2p modules for this libp2p bundle - modules: { - transport: [ - TCP, - new WS() // It can take instances too! - ], - streamMuxer: [ - SPDY, - MPLEX - ], - connEncryption: [ - SECIO - ], - /** Encryption for private networks. Needs additional private key to work **/ - // connProtector: new Protector(/*protector specific opts*/), - /** Enable custom content routers, such as delegated routing **/ - // contentRouting: [ - // new DelegatedContentRouter(peerInfo.id) - // ], - /** Enable custom peer routers, such as delegated routing **/ - // peerRouting: [ - // new DelegatedPeerRouter() - // ], - peerDiscovery: [ - MulticastDNS - ], - dht: DHT, // DHT enables PeerRouting, ContentRouting and DHT itself components - pubsub: GossipSub - }, - - // libp2p config options (typically found on a config.json) - config: { // The config object is the part of the config that can go into a file, config.json. - peerDiscovery: { - autoDial: true, // Auto connect to discovered peers (limited by ConnectionManager minPeers) - mdns: { // mdns options - interval: 1000, // ms - enabled: true - }, - webrtcStar: { // webrtc-star options - interval: 1000, // ms - enabled: false - } - // .. other discovery module options. - }, - relay: { // Circuit Relay options - enabled: true, - hop: { - enabled: false, - active: false - } - }, - dht: { - kBucketSize: 20, - enabled: true, - randomWalk: { - enabled: true, // Allows to disable discovery (enabled by default) - interval: 300e3, - timeout: 10e3 - } - }, - pubsub: { - enabled: true, - emitSelf: true, // whether the node should emit to self on publish, in the event of the topic being subscribed - signMessages: true, // if messages should be signed - strictSigning: true // if message signing should be required - } - } - } - - // overload any defaults of your bundle using https://github.com/nodeutils/defaults-deep - super(defaultsDeep(_options, defaults)) - } -} - -// Now all the nodes you create, will have TCP, WebSockets, SPDY, MPLEX, SECIO and MulticastDNS support. -``` +### Configuration + +For all the information on how you can configure libp2p see [CONFIGURATION.md](./doc/CONFIGURATION.md). ### API -See [API.md](./doc/API.md). +The specification is available on [API.md](./doc/API.md). + +### Tutorials and Examples + +You can find multiple examples on the [examples folder](./examples) that will guide you through using libp2p for several scenarios. ## Development @@ -244,50 +127,49 @@ List of packages currently in existence for libp2p | Package | Version | Deps | CI | Coverage | Lead Maintainer | | ---------|---------|---------|---------|---------|--------- | | **libp2p** | -| [`libp2p`](//github.com/libp2p/js-libp2p) | [![npm](https://img.shields.io/npm/v/libp2p.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p) | [![Travis CI](https://travis-ci.com/libp2p/js-libp2p.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-libp2p) | [Jacob Heun](mailto:jacobheun@gmail.com) | -| [`libp2p-daemon`](//github.com/libp2p/js-libp2p-daemon) | [![npm](https://img.shields.io/npm/v/libp2p-daemon.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-daemon/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-daemon.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-daemon) | [![Travis CI](https://travis-ci.com/libp2p/js-libp2p-daemon.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-daemon) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-daemon/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-libp2p-daemon) | [Jacob Heun](mailto:jacobheun@gmail.com) | -| [`libp2p-daemon-client`](//github.com/libp2p/js-libp2p-daemon-client) | [![npm](https://img.shields.io/npm/v/libp2p-daemon-client.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-daemon-client/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-daemon-client.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-daemon-client) | [![Travis CI](https://travis-ci.com/libp2p/js-libp2p-daemon-client.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-daemon-client) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-daemon-client/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-libp2p-daemon-client) | N/A | -| [`libp2p-interfaces`](//github.com/libp2p/js-interfaces) | [![npm](https://img.shields.io/npm/v/libp2p-interfaces.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-interfaces/releases) | [![Deps](https://david-dm.org/libp2p/js-interfaces.svg?style=flat-square)](https://david-dm.org/libp2p/js-interfaces) | [![Travis CI](https://travis-ci.com/libp2p/js-interfaces.svg?branch=master)](https://travis-ci.com/libp2p/js-interfaces) | [![codecov](https://codecov.io/gh/libp2p/js-interfaces/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-interfaces) | N/A | -| [`interop-libp2p`](//github.com/libp2p/interop) | [![npm](https://img.shields.io/npm/v/interop-libp2p.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/interop/releases) | [![Deps](https://david-dm.org/libp2p/interop.svg?style=flat-square)](https://david-dm.org/libp2p/interop) | [![Travis CI](https://travis-ci.com/libp2p/interop.svg?branch=master)](https://travis-ci.com/libp2p/interop) | [![codecov](https://codecov.io/gh/libp2p/interop/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/interop) | N/A | +| [`libp2p`](//github.com/libp2p/js-libp2p) | [![npm](https://img.shields.io/npm/v/libp2p.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-libp2p.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) | [Jacob Heun](mailto:jacobheun@gmail.com) | +| [`libp2p-daemon`](//github.com/libp2p/js-libp2p-daemon) | [![npm](https://img.shields.io/npm/v/libp2p-daemon.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-daemon/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-daemon.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-daemon) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-libp2p-daemon.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-daemon) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-daemon/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-daemon) | [Jacob Heun](mailto:jacobheun@gmail.com) | +| [`libp2p-daemon-client`](//github.com/libp2p/js-libp2p-daemon-client) | [![npm](https://img.shields.io/npm/v/libp2p-daemon-client.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-daemon-client/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-daemon-client.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-daemon-client) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-libp2p-daemon-client.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-daemon-client) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-daemon-client/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-daemon-client) | [Vasco Santos](mailto:santos.vasco10@gmail.com) | +| [`libp2p-interfaces`](//github.com/libp2p/js-interfaces) | [![npm](https://img.shields.io/npm/v/libp2p-interfaces.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-interfaces/releases) | [![Deps](https://david-dm.org/libp2p/js-interfaces.svg?style=flat-square)](https://david-dm.org/libp2p/js-interfaces) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-interfaces.svg?branch=master)](https://travis-ci.com/libp2p/js-interfaces) | [![codecov](https://codecov.io/gh/libp2p/js-interfaces/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-interfaces) | [Jacob Heun](mailto:jacobheun@gmail.com) | +| [`interop-libp2p`](//github.com/libp2p/interop) | [![npm](https://img.shields.io/npm/v/interop-libp2p.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/interop/releases) | [![Deps](https://david-dm.org/libp2p/interop.svg?style=flat-square)](https://david-dm.org/libp2p/interop) | [![Travis CI](https://flat.badgen.net/travis/libp2p/interop.svg?branch=master)](https://travis-ci.com/libp2p/interop) | [![codecov](https://codecov.io/gh/libp2p/interop/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/interop) | [Vasco Santos](mailto:santos.vasco10@gmail.com) | | **transports** | -| [`libp2p-tcp`](//github.com/libp2p/js-libp2p-tcp) | [![npm](https://img.shields.io/npm/v/libp2p-tcp.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-tcp/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-tcp.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-tcp) | [![Travis CI](https://travis-ci.com/libp2p/js-libp2p-tcp.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-tcp) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-tcp/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-libp2p-tcp) | N/A | -| [`libp2p-utp`](//github.com/libp2p/js-libp2p-utp) | [![npm](https://img.shields.io/npm/v/libp2p-utp.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-utp/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-utp.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-utp) | [![Travis CI](https://travis-ci.com/libp2p/js-libp2p-utp.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-utp) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-utp/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-libp2p-utp) | N/A | -| [`libp2p-webrtc-direct`](//github.com/libp2p/js-libp2p-webrtc-direct) | [![npm](https://img.shields.io/npm/v/libp2p-webrtc-direct.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-webrtc-direct/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-webrtc-direct.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-webrtc-direct) | [![Travis CI](https://travis-ci.com/libp2p/js-libp2p-webrtc-direct.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-webrtc-direct) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-webrtc-direct/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-libp2p-webrtc-direct) | N/A | -| [`libp2p-webrtc-star`](//github.com/libp2p/js-libp2p-webrtc-star) | [![npm](https://img.shields.io/npm/v/libp2p-webrtc-star.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-webrtc-star/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-webrtc-star.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-webrtc-star) | [![Travis CI](https://travis-ci.com/libp2p/js-libp2p-webrtc-star.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-webrtc-star) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-webrtc-star/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-libp2p-webrtc-star) | [Vasco Santos](mailto:vasco.santos@moxy.studio) | -| [`libp2p-websockets`](//github.com/libp2p/js-libp2p-websockets) | [![npm](https://img.shields.io/npm/v/libp2p-websockets.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-websockets/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-websockets.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-websockets) | [![Travis CI](https://travis-ci.com/libp2p/js-libp2p-websockets.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-websockets) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-websockets/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-libp2p-websockets) | [Jacob Heun](mailto:jacobheun@gmail.com) | -| [`libp2p-websocket-star`](//github.com/libp2p/js-libp2p-websocket-star) | [![npm](https://img.shields.io/npm/v/libp2p-websocket-star.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-websocket-star/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-websocket-star.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-websocket-star) | [![Travis CI](https://travis-ci.com/libp2p/js-libp2p-websocket-star.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-websocket-star) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-websocket-star/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-libp2p-websocket-star) | [Jacob Heun](mailto:jacobheun@gmail.com) | +| [`libp2p-tcp`](//github.com/libp2p/js-libp2p-tcp) | [![npm](https://img.shields.io/npm/v/libp2p-tcp.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-tcp/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-tcp.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-tcp) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-libp2p-tcp.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-tcp) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-tcp/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-tcp) | [Jacob Heun](mailto:jacobheun@gmail.com) | +| [`libp2p-utp`](//github.com/libp2p/js-libp2p-utp) | [![npm](https://img.shields.io/npm/v/libp2p-utp.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-utp/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-utp.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-utp) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-libp2p-utp.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-utp) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-utp/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-utp) | [Vasco Santos](mailto:santos.vasco10@gmail.com) | +| [`libp2p-webrtc-direct`](//github.com/libp2p/js-libp2p-webrtc-direct) | [![npm](https://img.shields.io/npm/v/libp2p-webrtc-direct.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-webrtc-direct/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-webrtc-direct.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-webrtc-direct) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-libp2p-webrtc-direct.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-webrtc-direct) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-webrtc-direct/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-webrtc-direct) | [Vasco Santos](mailto:vasco.santos@moxy.studio) | +| [`libp2p-webrtc-star`](//github.com/libp2p/js-libp2p-webrtc-star) | [![npm](https://img.shields.io/npm/v/libp2p-webrtc-star.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-webrtc-star/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-webrtc-star.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-webrtc-star) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-libp2p-webrtc-star.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-webrtc-star) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-webrtc-star/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-webrtc-star) | [Vasco Santos](mailto:vasco.santos@moxy.studio) | +| [`libp2p-websockets`](//github.com/libp2p/js-libp2p-websockets) | [![npm](https://img.shields.io/npm/v/libp2p-websockets.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-websockets/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-websockets.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-websockets) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-libp2p-websockets.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-websockets) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-websockets/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-websockets) | [Jacob Heun](mailto:jacobheun@gmail.com) | +| [`libp2p-websocket-star`](//github.com/libp2p/js-libp2p-websocket-star) | [![npm](https://img.shields.io/npm/v/libp2p-websocket-star.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-websocket-star/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-websocket-star.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-websocket-star) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-libp2p-websocket-star.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-websocket-star) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-websocket-star/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-websocket-star) | [Jacob Heun](mailto:jacobheun@gmail.com) | | **secure channels** | -| [`libp2p-secio`](//github.com/libp2p/js-libp2p-secio) | [![npm](https://img.shields.io/npm/v/libp2p-secio.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-secio/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-secio.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-secio) | [![Travis CI](https://travis-ci.com/libp2p/js-libp2p-secio.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-secio) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-secio/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-libp2p-secio) | [Friedel Ziegelmayer](mailto:dignifiedquire@gmail.com) | +| [`libp2p-secio`](//github.com/libp2p/js-libp2p-secio) | [![npm](https://img.shields.io/npm/v/libp2p-secio.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-secio/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-secio.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-secio) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-libp2p-secio.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-secio) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-secio/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-secio) | [Friedel Ziegelmayer](mailto:dignifiedquire@gmail.com) | | **stream multiplexers** | -| [`libp2p-mplex`](//github.com/libp2p/js-libp2p-mplex) | [![npm](https://img.shields.io/npm/v/libp2p-mplex.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-mplex/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-mplex.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-mplex) | [![Travis CI](https://travis-ci.com/libp2p/js-libp2p-mplex.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-mplex) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-mplex/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-libp2p-mplex) | [Vasco Santos](mailto:vasco.santos@moxy.studio) | -| [`libp2p-spdy`](//github.com/libp2p/js-libp2p-spdy) | [![npm](https://img.shields.io/npm/v/libp2p-spdy.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-spdy/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-spdy.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-spdy) | [![Travis CI](https://travis-ci.com/libp2p/js-libp2p-spdy.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-spdy) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-spdy/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-libp2p-spdy) | [Jacob Heun](mailto:jacobheun@gmail.com) | +| [`libp2p-mplex`](//github.com/libp2p/js-libp2p-mplex) | [![npm](https://img.shields.io/npm/v/libp2p-mplex.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-mplex/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-mplex.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-mplex) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-libp2p-mplex.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-mplex) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-mplex/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-mplex) | [Vasco Santos](mailto:vasco.santos@moxy.studio) | +| [`libp2p-spdy`](//github.com/libp2p/js-libp2p-spdy) | [![npm](https://img.shields.io/npm/v/libp2p-spdy.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-spdy/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-spdy.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-spdy) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-libp2p-spdy.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-spdy) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-spdy/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-spdy) | [Jacob Heun](mailto:jacobheun@gmail.com) | | **peer discovery** | -| [`libp2p-bootstrap`](//github.com/libp2p/js-libp2p-bootstrap) | [![npm](https://img.shields.io/npm/v/libp2p-bootstrap.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-bootstrap/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-bootstrap.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-bootstrap) | [![Travis CI](https://travis-ci.com/libp2p/js-libp2p-bootstrap.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-bootstrap) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-bootstrap/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-libp2p-bootstrap) | [Vasco Santos](mailto:vasco.santos@moxy.studio) | -| [`libp2p-kad-dht`](//github.com/libp2p/js-libp2p-kad-dht) | [![npm](https://img.shields.io/npm/v/libp2p-kad-dht.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-kad-dht/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-kad-dht.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-kad-dht) | [![Travis CI](https://travis-ci.com/libp2p/js-libp2p-kad-dht.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-kad-dht) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-kad-dht/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-libp2p-kad-dht) | [Vasco Santos](mailto:vasco.santos@moxy.studio) | -| [`libp2p-mdns`](//github.com/libp2p/js-libp2p-mdns) | [![npm](https://img.shields.io/npm/v/libp2p-mdns.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-mdns/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-mdns.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-mdns) | [![Travis CI](https://travis-ci.com/libp2p/js-libp2p-mdns.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-mdns) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-mdns/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-libp2p-mdns) | [Jacob Heun](mailto:jacobheun@gmail.com) | -| [`libp2p-rendezvous`](//github.com/libp2p/js-libp2p-rendezvous) | [![npm](https://img.shields.io/npm/v/libp2p-rendezvous.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-rendezvous/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-rendezvous.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-rendezvous) | [![Travis CI](https://travis-ci.com/libp2p/js-libp2p-rendezvous.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-rendezvous) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-rendezvous/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-libp2p-rendezvous) | N/A | -| [`libp2p-webrtc-star`](//github.com/libp2p/js-libp2p-webrtc-star) | [![npm](https://img.shields.io/npm/v/libp2p-webrtc-star.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-webrtc-star/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-webrtc-star.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-webrtc-star) | [![Travis CI](https://travis-ci.com/libp2p/js-libp2p-webrtc-star.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-webrtc-star) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-webrtc-star/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-libp2p-webrtc-star) | [Vasco Santos](mailto:vasco.santos@moxy.studio) | -| [`libp2p-websocket-star`](//github.com/libp2p/js-libp2p-websocket-star) | [![npm](https://img.shields.io/npm/v/libp2p-websocket-star.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-websocket-star/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-websocket-star.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-websocket-star) | [![Travis CI](https://travis-ci.com/libp2p/js-libp2p-websocket-star.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-websocket-star) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-websocket-star/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-libp2p-websocket-star) | [Jacob Heun](mailto:jacobheun@gmail.com) | +| [`libp2p-bootstrap`](//github.com/libp2p/js-libp2p-bootstrap) | [![npm](https://img.shields.io/npm/v/libp2p-bootstrap.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-bootstrap/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-bootstrap.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-bootstrap) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-libp2p-bootstrap.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-bootstrap) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-bootstrap/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-bootstrap) | [Vasco Santos](mailto:vasco.santos@moxy.studio) | +| [`libp2p-kad-dht`](//github.com/libp2p/js-libp2p-kad-dht) | [![npm](https://img.shields.io/npm/v/libp2p-kad-dht.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-kad-dht/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-kad-dht.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-kad-dht) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-libp2p-kad-dht.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-kad-dht) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-kad-dht/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-kad-dht) | [Vasco Santos](mailto:vasco.santos@moxy.studio) | +| [`libp2p-mdns`](//github.com/libp2p/js-libp2p-mdns) | [![npm](https://img.shields.io/npm/v/libp2p-mdns.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-mdns/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-mdns.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-mdns) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-libp2p-mdns.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-mdns) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-mdns/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-mdns) | [Jacob Heun](mailto:jacobheun@gmail.com) | +| [`libp2p-rendezvous`](//github.com/libp2p/js-libp2p-rendezvous) | [![npm](https://img.shields.io/npm/v/libp2p-rendezvous.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-rendezvous/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-rendezvous.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-rendezvous) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-libp2p-rendezvous.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-rendezvous) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-rendezvous/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-rendezvous) | [Vasco Santos](mailto:santos.vasco10@gmail.com) | +| [`libp2p-webrtc-star`](//github.com/libp2p/js-libp2p-webrtc-star) | [![npm](https://img.shields.io/npm/v/libp2p-webrtc-star.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-webrtc-star/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-webrtc-star.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-webrtc-star) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-libp2p-webrtc-star.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-webrtc-star) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-webrtc-star/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-webrtc-star) | [Vasco Santos](mailto:vasco.santos@moxy.studio) | +| [`libp2p-websocket-star`](//github.com/libp2p/js-libp2p-websocket-star) | [![npm](https://img.shields.io/npm/v/libp2p-websocket-star.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-websocket-star/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-websocket-star.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-websocket-star) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-libp2p-websocket-star.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-websocket-star) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-websocket-star/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-websocket-star) | [Jacob Heun](mailto:jacobheun@gmail.com) | | **content routing** | -| [`libp2p-delegated-content-routing`](//github.com/libp2p/js-libp2p-delegated-content-routing) | [![npm](https://img.shields.io/npm/v/libp2p-delegated-content-routing.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-delegated-content-routing/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-delegated-content-routing.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-delegated-content-routing) | [![Travis CI](https://travis-ci.com/libp2p/js-libp2p-delegated-content-routing.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-delegated-content-routing) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-delegated-content-routing/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-libp2p-delegated-content-routing) | N/A | -| [`libp2p-kad-dht`](//github.com/libp2p/js-libp2p-kad-dht) | [![npm](https://img.shields.io/npm/v/libp2p-kad-dht.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-kad-dht/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-kad-dht.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-kad-dht) | [![Travis CI](https://travis-ci.com/libp2p/js-libp2p-kad-dht.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-kad-dht) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-kad-dht/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-libp2p-kad-dht) | [Vasco Santos](mailto:vasco.santos@moxy.studio) | +| [`libp2p-delegated-content-routing`](//github.com/libp2p/js-libp2p-delegated-content-routing) | [![npm](https://img.shields.io/npm/v/libp2p-delegated-content-routing.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-delegated-content-routing/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-delegated-content-routing.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-delegated-content-routing) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-libp2p-delegated-content-routing.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-delegated-content-routing) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-delegated-content-routing/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-delegated-content-routing) | [Jacob Heun](mailto:jacobheun@gmail.com) | +| [`libp2p-kad-dht`](//github.com/libp2p/js-libp2p-kad-dht) | [![npm](https://img.shields.io/npm/v/libp2p-kad-dht.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-kad-dht/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-kad-dht.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-kad-dht) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-libp2p-kad-dht.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-kad-dht) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-kad-dht/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-kad-dht) | [Vasco Santos](mailto:vasco.santos@moxy.studio) | | **peer routing** | -| [`libp2p-delegated-peer-routing`](//github.com/libp2p/js-libp2p-delegated-peer-routing) | [![npm](https://img.shields.io/npm/v/libp2p-delegated-peer-routing.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-delegated-peer-routing/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-delegated-peer-routing.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-delegated-peer-routing) | [![Travis CI](https://travis-ci.com/libp2p/js-libp2p-delegated-peer-routing.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-delegated-peer-routing) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-delegated-peer-routing/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-libp2p-delegated-peer-routing) | N/A | -| [`libp2p-kad-dht`](//github.com/libp2p/js-libp2p-kad-dht) | [![npm](https://img.shields.io/npm/v/libp2p-kad-dht.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-kad-dht/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-kad-dht.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-kad-dht) | [![Travis CI](https://travis-ci.com/libp2p/js-libp2p-kad-dht.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-kad-dht) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-kad-dht/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-libp2p-kad-dht) | [Vasco Santos](mailto:vasco.santos@moxy.studio) | +| [`libp2p-delegated-peer-routing`](//github.com/libp2p/js-libp2p-delegated-peer-routing) | [![npm](https://img.shields.io/npm/v/libp2p-delegated-peer-routing.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-delegated-peer-routing/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-delegated-peer-routing.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-delegated-peer-routing) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-libp2p-delegated-peer-routing.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-delegated-peer-routing) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-delegated-peer-routing/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-delegated-peer-routing) | [Jacob Heun](mailto:jacobheun@gmail.com) | +| [`libp2p-kad-dht`](//github.com/libp2p/js-libp2p-kad-dht) | [![npm](https://img.shields.io/npm/v/libp2p-kad-dht.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-kad-dht/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-kad-dht.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-kad-dht) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-libp2p-kad-dht.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-kad-dht) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-kad-dht/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-kad-dht) | [Vasco Santos](mailto:vasco.santos@moxy.studio) | | **utilities** | -| [`libp2p-crypto`](//github.com/libp2p/js-libp2p-crypto) | [![npm](https://img.shields.io/npm/v/libp2p-crypto.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-crypto/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-crypto.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-crypto) | [![Travis CI](https://travis-ci.com/libp2p/js-libp2p-crypto.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-crypto) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-crypto/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-libp2p-crypto) | [Jacob Heun](mailto:jacobheun@gmail.com) | -| [`libp2p-crypto-secp256k1`](//github.com/libp2p/js-libp2p-crypto-secp256k1) | [![npm](https://img.shields.io/npm/v/libp2p-crypto-secp256k1.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-crypto-secp256k1/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-crypto-secp256k1.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-crypto-secp256k1) | [![Travis CI](https://travis-ci.com/libp2p/js-libp2p-crypto-secp256k1.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-crypto-secp256k1) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-crypto-secp256k1/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-libp2p-crypto-secp256k1) | [Friedel Ziegelmayer](mailto:dignifiedquire@gmail.com) | +| [`libp2p-crypto`](//github.com/libp2p/js-libp2p-crypto) | [![npm](https://img.shields.io/npm/v/libp2p-crypto.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-crypto/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-crypto.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-crypto) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-libp2p-crypto.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-crypto) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-crypto/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-crypto) | [Jacob Heun](mailto:jacobheun@gmail.com) | +| [`libp2p-crypto-secp256k1`](//github.com/libp2p/js-libp2p-crypto-secp256k1) | [![npm](https://img.shields.io/npm/v/libp2p-crypto-secp256k1.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-crypto-secp256k1/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-crypto-secp256k1.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-crypto-secp256k1) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-libp2p-crypto-secp256k1.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-crypto-secp256k1) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-crypto-secp256k1/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-crypto-secp256k1) | [Friedel Ziegelmayer](mailto:dignifiedquire@gmail.com) | | **data types** | -| [`peer-book`](//github.com/libp2p/js-peer-book) | [![npm](https://img.shields.io/npm/v/peer-book.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-peer-book/releases) | [![Deps](https://david-dm.org/libp2p/js-peer-book.svg?style=flat-square)](https://david-dm.org/libp2p/js-peer-book) | [![Travis CI](https://travis-ci.com/libp2p/js-peer-book.svg?branch=master)](https://travis-ci.com/libp2p/js-peer-book) | [![codecov](https://codecov.io/gh/libp2p/js-peer-book/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-peer-book) | N/A | -| [`peer-id`](//github.com/libp2p/js-peer-id) | [![npm](https://img.shields.io/npm/v/peer-id.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-peer-id/releases) | [![Deps](https://david-dm.org/libp2p/js-peer-id.svg?style=flat-square)](https://david-dm.org/libp2p/js-peer-id) | [![Travis CI](https://travis-ci.com/libp2p/js-peer-id.svg?branch=master)](https://travis-ci.com/libp2p/js-peer-id) | [![codecov](https://codecov.io/gh/libp2p/js-peer-id/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-peer-id) | N/A | -| [`peer-info`](//github.com/libp2p/js-peer-info) | [![npm](https://img.shields.io/npm/v/peer-info.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-peer-info/releases) | [![Deps](https://david-dm.org/libp2p/js-peer-info.svg?style=flat-square)](https://david-dm.org/libp2p/js-peer-info) | [![Travis CI](https://travis-ci.com/libp2p/js-peer-info.svg?branch=master)](https://travis-ci.com/libp2p/js-peer-info) | [![codecov](https://codecov.io/gh/libp2p/js-peer-info/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-peer-info) | [Vasco Santos](mailto:vasco.santos@moxy.studio) | +| [`peer-id`](//github.com/libp2p/js-peer-id) | [![npm](https://img.shields.io/npm/v/peer-id.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-peer-id/releases) | [![Deps](https://david-dm.org/libp2p/js-peer-id.svg?style=flat-square)](https://david-dm.org/libp2p/js-peer-id) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-peer-id.svg?branch=master)](https://travis-ci.com/libp2p/js-peer-id) | [![codecov](https://codecov.io/gh/libp2p/js-peer-id/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-peer-id) | [Vasco Santos](mailto:santos.vasco10@gmail.com) | +| [`peer-info`](//github.com/libp2p/js-peer-info) | [![npm](https://img.shields.io/npm/v/peer-info.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-peer-info/releases) | [![Deps](https://david-dm.org/libp2p/js-peer-info.svg?style=flat-square)](https://david-dm.org/libp2p/js-peer-info) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-peer-info.svg?branch=master)](https://travis-ci.com/libp2p/js-peer-info) | [![codecov](https://codecov.io/gh/libp2p/js-peer-info/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-peer-info) | [Vasco Santos](mailto:vasco.santos@moxy.studio) | | **pubsub** | -| [`libp2p-pubsub`](//github.com/libp2p/js-libp2p-pubsub) | [![npm](https://img.shields.io/npm/v/libp2p-pubsub.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-pubsub/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-pubsub.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-pubsub) | [![Travis CI](https://travis-ci.com/libp2p/js-libp2p-pubsub.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-pubsub) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-pubsub/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-libp2p-pubsub) | [Vasco Santos](mailto:santos.vasco10@gmail.com) | -| [`libp2p-floodsub`](//github.com/libp2p/js-libp2p-floodsub) | [![npm](https://img.shields.io/npm/v/libp2p-floodsub.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-floodsub/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-floodsub.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-floodsub) | [![Travis CI](https://travis-ci.com/libp2p/js-libp2p-floodsub.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-floodsub) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-floodsub/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-libp2p-floodsub) | N/A | -| [`libp2p-gossipsub`](//github.com/ChainSafe/gossipsub-js) | [![npm](https://img.shields.io/npm/v/libp2p-gossipsub.svg?maxAge=86400&style=flat-square)](//github.com/ChainSafe/gossipsub-js/releases) | [![Deps](https://david-dm.org/ChainSafe/gossipsub-js.svg?style=flat-square)](https://david-dm.org/ChainSafe/gossipsub-js) | [![Travis CI](https://travis-ci.com/ChainSafe/gossipsub-js.svg?branch=master)](https://travis-ci.com/ChainSafe/gossipsub-js) | [![codecov](https://codecov.io/gh/ChainSafe/gossipsub-js/branch/master/graph/badge.svg)](https://codecov.io/gh/ChainSafe/gossipsub-js) | N/A | +| [`libp2p-pubsub`](//github.com/libp2p/js-libp2p-pubsub) | [![npm](https://img.shields.io/npm/v/libp2p-pubsub.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-pubsub/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-pubsub.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-pubsub) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-libp2p-pubsub.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-pubsub) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-pubsub/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-pubsub) | [Vasco Santos](mailto:santos.vasco10@gmail.com) | +| [`libp2p-floodsub`](//github.com/libp2p/js-libp2p-floodsub) | [![npm](https://img.shields.io/npm/v/libp2p-floodsub.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-floodsub/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-floodsub.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-floodsub) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-libp2p-floodsub.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-floodsub) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-floodsub/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-floodsub) | [Vasco Santos](mailto:vasco.santos@moxy.studio) | +| [`libp2p-gossipsub`](//github.com/ChainSafe/gossipsub-js) | [![npm](https://img.shields.io/npm/v/libp2p-gossipsub.svg?maxAge=86400&style=flat-square)](//github.com/ChainSafe/gossipsub-js/releases) | [![Deps](https://david-dm.org/ChainSafe/gossipsub-js.svg?style=flat-square)](https://david-dm.org/ChainSafe/gossipsub-js) | [![Travis CI](https://flat.badgen.net/travis/ChainSafe/gossipsub-js.svg?branch=master)](https://travis-ci.com/ChainSafe/gossipsub-js) | [![codecov](https://codecov.io/gh/ChainSafe/gossipsub-js/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/ChainSafe/gossipsub-js) | [Cayman Nava](mailto:caymannava@gmail.com) | | **extensions** | -| [`libp2p-nat-mgnr`](//github.com/libp2p/js-libp2p-nat-mgnr) | [![npm](https://img.shields.io/npm/v/libp2p-nat-mgnr.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-nat-mgnr/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-nat-mgnr.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-nat-mgnr) | [![Travis CI](https://travis-ci.com/libp2p/js-libp2p-nat-mgnr.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-nat-mgnr) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-nat-mgnr/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-libp2p-nat-mgnr) | N/A | -| [`libp2p-utils`](//github.com/libp2p/js-libp2p-utils) | [![npm](https://img.shields.io/npm/v/libp2p-utils.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-utils/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-utils.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-utils) | [![Travis CI](https://travis-ci.com/libp2p/js-libp2p-utils.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-utils) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-utils/branch/master/graph/badge.svg)](https://codecov.io/gh/libp2p/js-libp2p-utils) | [Vasco Santos](mailto:santos.vasco10@gmail.com) | +| [`libp2p-nat-mgnr`](//github.com/libp2p/js-libp2p-nat-mgnr) | [![npm](https://img.shields.io/npm/v/libp2p-nat-mgnr.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-nat-mgnr/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-nat-mgnr.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-nat-mgnr) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-libp2p-nat-mgnr.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-nat-mgnr) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-nat-mgnr/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-nat-mgnr) | N/A | +| [`libp2p-utils`](//github.com/libp2p/js-libp2p-utils) | [![npm](https://img.shields.io/npm/v/libp2p-utils.svg?maxAge=86400&style=flat-square)](//github.com/libp2p/js-libp2p-utils/releases) | [![Deps](https://david-dm.org/libp2p/js-libp2p-utils.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-utils) | [![Travis CI](https://flat.badgen.net/travis/libp2p/js-libp2p-utils.svg?branch=master)](https://travis-ci.com/libp2p/js-libp2p-utils) | [![codecov](https://codecov.io/gh/libp2p/js-libp2p-utils/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-utils) | [Vasco Santos](mailto:santos.vasco10@gmail.com) | ## Contribute diff --git a/package-list.json b/package-list.json index 658086c450..54e3437f70 100644 --- a/package-list.json +++ b/package-list.json @@ -51,7 +51,6 @@ ["libp2p/js-libp2p-crypto-secp256k1", "libp2p-crypto-secp256k1"], "data types", - ["libp2p/js-peer-book", "peer-book"], ["libp2p/js-peer-id", "peer-id"], ["libp2p/js-peer-info", "peer-info"], From 9bbe93c7720ab58555ff700534a0877705217bec Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Thu, 12 Dec 2019 17:40:40 +0000 Subject: [PATCH 66/92] docs: fix the ToC links (#515) --- doc/API.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/doc/API.md b/doc/API.md index ba17815fdf..b4eaf4eac9 100644 --- a/doc/API.md +++ b/doc/API.md @@ -6,26 +6,26 @@ * [`start`](#start) * [`stop`](#stop) * [`dial`](#dial) - * [`dialProtocol`](#dialProtocol) + * [`dialProtocol`](#dialprotocol) * [`hangUp`](#hangUp) * [`handle`](#handle) * [`unhandle`](#unhandle) * [`ping`](#ping) - * [`peerRouting.findPeer`](#peerRouting.findPeer) - * [`contentRouting.findProviders`](#contentRouting.findProviders) - * [`contentRouting.provide`](#contentRouting.provide) - * [`contentRouting.put`](#contentRouting.put) - * [`contentRouting.get`](#contentRouting.get) - * [`contentRouting.getMany`](#contentRouting.getMany) - * [`pubsub.getSubscribers`](#pubsub.getSubscribers) - * [`pubsub.getTopics`](#pubsub.getTopics) - * [`pubsub.publish`](#pubsub.publish) - * [`pubsub.subscribe`](#pubsub.subscribe) - * [`metrics.global`](#metrics.global) - * [`metrics.peers`](#metrics.peers) - * [`metrics.protocols`](#metrics.protocols) - * [`metrics.forPeer`](#metrics.forPeer) - * [`metrics.forProtocol`](#metrics.forProtocol) + * [`peerRouting.findPeer`](#peerRoutingfindPeer) + * [`contentRouting.findProviders`](#contentroutingfindproviders) + * [`contentRouting.provide`](#contentroutingprovide) + * [`contentRouting.put`](#contentroutingput) + * [`contentRouting.get`](#contentroutingget) + * [`contentRouting.getMany`](#contentroutinggetmany) + * [`pubsub.getSubscribers`](#pubsubgetsubscribers) + * [`pubsub.getTopics`](#pubsubgettopics) + * [`pubsub.publish`](#pubsubpublish) + * [`pubsub.subscribe`](#pubsubsubscribe) + * [`metrics.global`](#metricsglobal) + * [`metrics.peers`](#metricspeers) + * [`metrics.protocols`](#metricsprotocols) + * [`metrics.forPeer`](#metricsforpeer) + * [`metrics.forProtocol`](#metricsforprotocol) * [Types](#types) * [`Stats`](#stats) From a39889c4ea8f0eaa0be558a96cc1b1fe1cdd17cd Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Thu, 12 Dec 2019 22:41:32 +0000 Subject: [PATCH 67/92] docs: add ToC link to pubsub.unsubscribe --- doc/API.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/API.md b/doc/API.md index b4eaf4eac9..d0c5b604db 100644 --- a/doc/API.md +++ b/doc/API.md @@ -7,11 +7,11 @@ * [`stop`](#stop) * [`dial`](#dial) * [`dialProtocol`](#dialprotocol) - * [`hangUp`](#hangUp) + * [`hangUp`](#hangup) * [`handle`](#handle) * [`unhandle`](#unhandle) * [`ping`](#ping) - * [`peerRouting.findPeer`](#peerRoutingfindPeer) + * [`peerRouting.findPeer`](#peerroutingfindpeer) * [`contentRouting.findProviders`](#contentroutingfindproviders) * [`contentRouting.provide`](#contentroutingprovide) * [`contentRouting.put`](#contentroutingput) @@ -21,6 +21,7 @@ * [`pubsub.getTopics`](#pubsubgettopics) * [`pubsub.publish`](#pubsubpublish) * [`pubsub.subscribe`](#pubsubsubscribe) + * [`pubsub.unsubscribe`](#pubsubunsubscribe) * [`metrics.global`](#metricsglobal) * [`metrics.peers`](#metricspeers) * [`metrics.protocols`](#metricsprotocols) From 4a871bbf8b3de47251261a32ac9b5cd1726b7427 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Sun, 15 Dec 2019 17:33:16 +0100 Subject: [PATCH 68/92] feat: coalescing dial support (#518) * docs: fix spelling in api * fix: dont create peerstore twice * feat: add support for dial coalescing * doc(fix): add setPeerValue to API TOC * docs: add more jsdocs to dialer * chore: remove old comment * fix: ensure connections are closed * fix: registrar.getConnections returns first open conn * fix: directly set the closed status * chore: remove unneeded log * refactor: peerStore.put takes an options object --- doc/API.md | 11 +- doc/DIALER.md | 1 + src/circuit/index.js | 2 +- src/circuit/listener.js | 2 +- src/dialer/index.js | 158 +++++++++++++++++++++-------- src/index.js | 37 +++---- src/peer-store/index.js | 22 ++-- src/registrar.js | 7 +- src/upgrader.js | 9 +- test/dialing/direct.node.js | 131 +++++++++++++++++++++--- test/dialing/direct.spec.js | 52 +++++++--- test/dialing/relay.node.js | 10 +- test/identify/index.spec.js | 4 +- test/peer-store/peer-store.spec.js | 51 ---------- 14 files changed, 325 insertions(+), 172 deletions(-) diff --git a/doc/API.md b/doc/API.md index d0c5b604db..651be03d00 100644 --- a/doc/API.md +++ b/doc/API.md @@ -22,6 +22,7 @@ * [`pubsub.publish`](#pubsubpublish) * [`pubsub.subscribe`](#pubsubsubscribe) * [`pubsub.unsubscribe`](#pubsubunsubscribe) + * [`connectionManager.setPeerValue`](#connectionmanagersetpeervalue) * [`metrics.global`](#metricsglobal) * [`metrics.peers`](#metricspeers) * [`metrics.protocols`](#metricsprotocols) @@ -92,7 +93,7 @@ Required keys in the `options` object: -Once you have a libp2p instance, you are able to listen to several events it emmits, so that you can be noticed of relevant network events. +Once you have a libp2p instance, you are able to listen to several events it emits, so that you can be noticed of relevant network events.
Events @@ -666,12 +667,8 @@ Enables users to change the value of certain peers in a range of 0 to 1. Peers w #### Example ```js -const topic = 'topic' -const handler = (msg) => { - // msg.data - pubsub data received -} - -libp2p.pubsub.unsubscribe(topic, handler) +libp2p.connectionManager.setPeerValue(highPriorityPeerId, 1) +libp2p.connectionManager.setPeerValue(lowPriorityPeerId, 0) ``` ### metrics.global diff --git a/doc/DIALER.md b/doc/DIALER.md index 920372b1c9..bba39d5b70 100644 --- a/doc/DIALER.md +++ b/doc/DIALER.md @@ -1,6 +1,7 @@ # js-libp2p Dialer **Synopsis** +* Parallel dials to the same peer will yield the same connection/error when the first dial settles. * All Dial Requests in js-libp2p must request a token(s) from the Dialer. * The number of tokens requested should be between 1 and the MAX_PER_PEER_DIALS max set in the Dialer. * If the number of available tokens is less than requested, the Dialer may return less than requested. diff --git a/src/circuit/index.js b/src/circuit/index.js index d5d293bfef..e55846d68d 100644 --- a/src/circuit/index.js +++ b/src/circuit/index.js @@ -109,7 +109,7 @@ class Circuit { let disconnectOnFailure = false let relayConnection = this._registrar.getConnection(new PeerInfo(relayPeer)) if (!relayConnection) { - relayConnection = await this._dialer.connectToMultiaddr(relayAddr, options) + relayConnection = await this._dialer.connectToPeer(relayAddr, options) disconnectOnFailure = true } diff --git a/src/circuit/listener.js b/src/circuit/listener.js index 2569c50039..3d48d685fe 100644 --- a/src/circuit/listener.js +++ b/src/circuit/listener.js @@ -24,7 +24,7 @@ module.exports = (circuit) => { listener.listen = async (addr) => { const [addrString] = String(addr).split('/p2p-circuit').slice(-1) - const relayConn = await circuit._dialer.connectToMultiaddr(multiaddr(addrString)) + const relayConn = await circuit._dialer.connectToPeer(multiaddr(addrString)) const relayedAddr = relayConn.remoteAddr.encapsulate('/p2p-circuit') listeningAddrs.set(relayConn.remotePeer.toB58String(), relayedAddr) diff --git a/src/dialer/index.js b/src/dialer/index.js index 224f4e309c..b51938a17f 100644 --- a/src/dialer/index.js +++ b/src/dialer/index.js @@ -4,6 +4,8 @@ const multiaddr = require('multiaddr') const errCode = require('err-code') const TimeoutController = require('timeout-abort-controller') const anySignal = require('any-signal') +const PeerId = require('peer-id') +const PeerInfo = require('peer-info') const debug = require('debug') const log = debug('libp2p:dialer') log.error = debug('libp2p:dialer:error') @@ -38,7 +40,7 @@ class Dialer { this.timeout = timeout this.perPeerLimit = perPeerLimit this.tokens = [...new Array(concurrency)].map((_, index) => index) - this._pendingDials = new Set() + this._pendingDials = new Map() } /** @@ -56,72 +58,111 @@ class Dialer { } /** - * Connects to the first success of a given list of `Multiaddr`. `addrs` should - * include the id of the peer being dialed, it will be used for encryption verification. + * Connects to a given `PeerId` or `Multiaddr` by dialing all of its known addresses. + * The dial to the first address that is successfully able to upgrade a connection + * will be used. * - * @param {Array|Multiaddr} addrs + * @param {PeerInfo|Multiaddr} peer The peer to dial * @param {object} [options] * @param {AbortSignal} [options.signal] An AbortController signal * @returns {Promise} */ - async connectToMultiaddr (addrs, options = {}) { - if (!Array.isArray(addrs)) addrs = [multiaddr(addrs)] - - const dialAction = (addr, options) => { - if (options.signal.aborted) throw errCode(new Error('already aborted'), 'ERR_ALREADY_ABORTED') - return this.transportManager.dial(addr, options) + async connectToPeer (peer, options = {}) { + const dialTarget = this._createDialTarget(peer) + if (dialTarget.addrs.length === 0) { + throw errCode(new Error('The dial request has no addresses'), 'ERR_NO_DIAL_MULTIADDRS') } - const dialRequest = new DialRequest({ - addrs, - dialAction, - dialer: this - }) - - // Combine the timeout signal and options.signal, if provided - const timeoutController = new TimeoutController(this.timeout) - const signals = [timeoutController.signal] - options.signal && signals.push(options.signal) - const signal = anySignal(signals) - - const dial = { - dialRequest, - controller: timeoutController - } - this._pendingDials.add(dial) + const pendingDial = this._pendingDials.get(dialTarget.id) || this._createPendingDial(dialTarget, options) try { - const dialResult = await dialRequest.run({ ...options, signal }) - log('dial succeeded to %s', dialResult.remoteAddr) - return dialResult + const connection = await pendingDial.promise + log('dial succeeded to %s', dialTarget.id) + return connection } catch (err) { // Error is a timeout - if (timeoutController.signal.aborted) { + if (pendingDial.controller.signal.aborted) { err.code = codes.ERR_TIMEOUT } log.error(err) throw err } finally { - timeoutController.clear() - this._pendingDials.delete(dial) + pendingDial.destroy() } } /** - * Connects to a given `PeerInfo` or `PeerId` by dialing all of its known addresses. - * The dial to the first address that is successfully able to upgrade a connection - * will be used. - * - * @param {PeerId} peerId The remote peer id to dial + * @typedef DialTarget + * @property {string} id + * @property {Multiaddr[]} addrs + */ + + /** + * Creates a DialTarget. The DialTarget is used to create and track + * the DialRequest to a given peer. + * @private + * @param {PeerInfo|Multiaddr} peer A PeerId or Multiaddr + * @returns {DialTarget} + */ + _createDialTarget (peer) { + const dialable = Dialer.getDialable(peer) + if (multiaddr.isMultiaddr(dialable)) { + return { + id: dialable.toString(), + addrs: [dialable] + } + } + const addrs = this.peerStore.multiaddrsForPeer(dialable) + return { + id: dialable.id.toString(), + addrs + } + } + + /** + * @typedef PendingDial + * @property {DialRequest} dialRequest + * @property {TimeoutController} controller + * @property {Promise} promise + * @property {function():void} destroy + */ + + /** + * Creates a PendingDial that wraps the underlying DialRequest + * @private + * @param {DialTarget} dialTarget * @param {object} [options] * @param {AbortSignal} [options.signal] An AbortController signal - * @returns {Promise} + * @returns {PendingDial} */ - connectToPeer (peerId, options = {}) { - const addrs = this.peerStore.multiaddrsForPeer(peerId) + _createPendingDial (dialTarget, options) { + const dialAction = (addr, options) => { + if (options.signal.aborted) throw errCode(new Error('already aborted'), 'ERR_ALREADY_ABORTED') + return this.transportManager.dial(addr, options) + } - // TODO: ensure the peer id is on the multiaddr + const dialRequest = new DialRequest({ + addrs: dialTarget.addrs, + dialAction, + dialer: this + }) + + // Combine the timeout signal and options.signal, if provided + const timeoutController = new TimeoutController(this.timeout) + const signals = [timeoutController.signal] + options.signal && signals.push(options.signal) + const signal = anySignal(signals) - return this.connectToMultiaddr(addrs, options) + const pendingDial = { + dialRequest, + controller: timeoutController, + promise: dialRequest.run({ ...options, signal }), + destroy: () => { + timeoutController.clear() + this._pendingDials.delete(dialTarget.id) + } + } + this._pendingDials.set(dialTarget.id, pendingDial) + return pendingDial } getTokens (num) { @@ -137,6 +178,37 @@ class Dialer { log('token %d released', token) this.tokens.push(token) } + + /** + * Converts the given `peer` into a `PeerInfo` or `Multiaddr`. + * @static + * @param {PeerInfo|PeerId|Multiaddr|string} peer + * @returns {PeerInfo|Multiaddr} + */ + static getDialable (peer) { + if (PeerInfo.isPeerInfo(peer)) return peer + if (typeof peer === 'string') { + peer = multiaddr(peer) + } + + let addr + if (multiaddr.isMultiaddr(peer)) { + addr = peer + try { + peer = PeerId.createFromCID(peer.getPeerId()) + } catch (err) { + // Couldn't get the PeerId, just use the address + return peer + } + } + + if (PeerId.isPeerId(peer)) { + peer = new PeerInfo(peer) + } + + addr && peer.multiaddrs.add(addr) + return peer + } } module.exports = Dialer diff --git a/src/index.js b/src/index.js index c84b0c6e3d..3fc56692ac 100644 --- a/src/index.js +++ b/src/index.js @@ -6,12 +6,11 @@ const log = debug('libp2p') log.error = debug('libp2p:error') const PeerInfo = require('peer-info') -const multiaddr = require('multiaddr') const peerRouting = require('./peer-routing') const contentRouting = require('./content-routing') const pubsub = require('./pubsub') -const { getPeerInfo, getPeerInfoRemote } = require('./get-peer-info') +const { getPeerInfo } = require('./get-peer-info') const { validate: validateConfig } = require('./config') const { codes } = require('./errors') @@ -51,8 +50,6 @@ class Libp2p extends EventEmitter { this._transport = [] // Transport instances/references this._discovery = new Map() // Discovery service instances/references - this.peerStore = new PeerStore() - if (this._options.metrics.enabled) { this.metrics = new Metrics(this._options.metrics) } @@ -62,7 +59,7 @@ class Libp2p extends EventEmitter { localPeer: this.peerInfo.id, metrics: this.metrics, onConnection: (connection) => { - const peerInfo = this.peerStore.put(new PeerInfo(connection.remotePeer)) + const peerInfo = this.peerStore.put(new PeerInfo(connection.remotePeer), { silent: true }) this.registrar.onConnect(peerInfo, connection) this.connectionManager.onConnect(connection) this.emit('peer:connect', peerInfo) @@ -74,7 +71,7 @@ class Libp2p extends EventEmitter { } }, onConnectionEnd: (connection) => { - const peerInfo = getPeerInfo(connection.remotePeer) + const peerInfo = Dialer.getDialable(connection.remotePeer) this.registrar.onDisconnect(peerInfo, connection) this.connectionManager.onDisconnect(connection) @@ -266,27 +263,22 @@ class Libp2p extends EventEmitter { * @returns {Promise} */ async dialProtocol (peer, protocols, options) { + const dialable = Dialer.getDialable(peer) let connection - if (multiaddr.isMultiaddr(peer)) { - connection = await this.dialer.connectToMultiaddr(peer, options) - } else { - peer = await getPeerInfoRemote(peer, this) - connection = await this.dialer.connectToPeer(peer.id, options) + if (PeerInfo.isPeerInfo(dialable)) { + this.peerStore.put(dialable, { silent: true }) + connection = this.registrar.getConnection(dialable) } - const peerInfo = getPeerInfo(connection.remotePeer) + if (!connection) { + connection = await this.dialer.connectToPeer(dialable, options) + } // If a protocol was provided, create a new stream if (protocols) { - const stream = await connection.newStream(protocols) - - peerInfo.protocols.add(stream.protocol) - this.peerStore.put(peerInfo) - - return stream + return connection.newStream(protocols) } - this.peerStore.put(peerInfo) return connection } @@ -428,11 +420,10 @@ class Libp2p extends EventEmitter { // If auto dialing is on and we have no connection to the peer, check if we should dial if (this._config.peerDiscovery.autoDial === true && !this.registrar.getConnection(peerInfo)) { const minPeers = this._options.connectionManager.minPeers || 0 - // TODO: This does not account for multiple connections to a peer - if (minPeers > this.registrar.connections.size) { - log('connecting to discovered peer') + if (minPeers > this.connectionManager._connections.size) { + log('connecting to discovered peer %s', peerInfo.id.toString()) try { - await this.dialer.connectToPeer(peerInfo.id) + await this.dialer.connectToPeer(peerInfo) } catch (err) { log.error('could not connect to discovered peer', err) } diff --git a/src/peer-store/index.js b/src/peer-store/index.js index 321fd204e9..72a7667512 100644 --- a/src/peer-store/index.js +++ b/src/peer-store/index.js @@ -36,11 +36,16 @@ class PeerStore extends EventEmitter { /** * Stores the peerInfo of a new peer. - * If already exist, its info is updated. + * If already exist, its info is updated. If `silent` is set to + * true, no 'peer' event will be emitted. This can be useful if you + * are already in the process of dialing the peer. The peer is technically + * known, but may not have been added to the PeerStore yet. * @param {PeerInfo} peerInfo + * @param {object} [options] + * @param {boolean} [options.silent] (Default=false) * @return {PeerInfo} */ - put (peerInfo) { + put (peerInfo, options = { silent: false }) { assert(PeerInfo.isPeerInfo(peerInfo), 'peerInfo must be an instance of peer-info') let peer @@ -50,8 +55,8 @@ class PeerStore extends EventEmitter { } else { peer = this.add(peerInfo) - // Emit the new peer found - this.emit('peer', peerInfo) + // Emit the peer if silent = false + !options.silent && this.emit('peer', peerInfo) } return peer } @@ -219,13 +224,12 @@ class PeerStore extends EventEmitter { } /** - * Returns the known multiaddrs for a given `PeerId` - * @param {PeerId} peerId + * Returns the known multiaddrs for a given `PeerInfo` + * @param {PeerInfo} peer * @returns {Array} */ - multiaddrsForPeer (peerId) { - const peerInfo = this.get(peerId.toB58String()) - return peerInfo.multiaddrs.toArray() + multiaddrsForPeer (peer) { + return this.put(peer, true).multiaddrs.toArray() } } diff --git a/src/registrar.js b/src/registrar.js index f4c3868069..d91f2bba17 100644 --- a/src/registrar.js +++ b/src/registrar.js @@ -119,8 +119,11 @@ class Registrar { assert(PeerInfo.isPeerInfo(peerInfo), 'peerInfo must be an instance of peer-info') const connections = this.connections.get(peerInfo.id.toString()) - // TODO: what should we return - return connections ? connections[0] : null + // Return the first, open connection + if (connections) { + return connections.find(connection => connection.stat.status === 'open') + } + return null } /** diff --git a/src/upgrader.js b/src/upgrader.js index afef9d192c..146735b90f 100644 --- a/src/upgrader.js +++ b/src/upgrader.js @@ -98,7 +98,6 @@ class Upgrader { } catch (err) { log.error('Failed to upgrade inbound connection', err) await maConn.close(err) - // TODO: We shouldn't throw here, as there isn't anything to catch the failure throw err } @@ -248,9 +247,11 @@ class Upgrader { pipe(muxedConnection, muxer, muxedConnection) maConn.timeline.upgraded = Date.now() - const timelineProxy = new Proxy(maConn.timeline, { + const _timeline = maConn.timeline + maConn.timeline = new Proxy(_timeline, { set: (...args) => { - if (args[1] === 'close' && args[2]) { + if (args[1] === 'close' && args[2] && !_timeline.close) { + connection.stat.status = 'closed' this.onConnectionEnd(connection) } @@ -266,7 +267,7 @@ class Upgrader { remotePeer: remotePeer, stat: { direction, - timeline: timelineProxy, + timeline: maConn.timeline, multiplexer: Muxer.multicodec, encryption: cryptoProtocol }, diff --git a/test/dialing/direct.node.js b/test/dialing/direct.node.js index fd4ec73b78..d6b4588d09 100644 --- a/test/dialing/direct.node.js +++ b/test/dialing/direct.node.js @@ -14,8 +14,10 @@ const PeerId = require('peer-id') const PeerInfo = require('peer-info') const delay = require('delay') const pDefer = require('p-defer') +const pSettle = require('p-settle') const pipe = require('it-pipe') const AggregateError = require('aggregate-error') +const { Connection } = require('libp2p-interfaces/src/connection') const { AbortError } = require('libp2p-interfaces/src/transport/errors') const Libp2p = require('../../src') @@ -29,6 +31,7 @@ const swarmKeyBuffer = Buffer.from(require('../fixtures/swarm.key')) const mockUpgrader = require('../utils/mockUpgrader') const createMockConnection = require('../utils/mockConnection') const Peers = require('../fixtures/peers') +const { createPeerInfo } = require('../utils/creators/peer') const listenAddr = multiaddr('/ip4/127.0.0.1/tcp/0') const unsupportedAddr = multiaddr('/ip4/127.0.0.1/tcp/9999/ws') @@ -65,7 +68,7 @@ describe('Dialing (direct, TCP)', () => { it('should be able to connect to a remote node via its multiaddr', async () => { const dialer = new Dialer({ transportManager: localTM }) - const connection = await dialer.connectToMultiaddr(remoteAddr) + const connection = await dialer.connectToPeer(remoteAddr) expect(connection).to.exist() await connection.close() }) @@ -73,7 +76,8 @@ describe('Dialing (direct, TCP)', () => { it('should be able to connect to a remote node via its stringified multiaddr', async () => { const dialer = new Dialer({ transportManager: localTM }) - const connection = await dialer.connectToMultiaddr(remoteAddr.toString()) + const dialable = Dialer.getDialable(remoteAddr.toString()) + const connection = await dialer.connectToPeer(dialable) expect(connection).to.exist() await connection.close() }) @@ -81,7 +85,7 @@ describe('Dialing (direct, TCP)', () => { it('should fail to connect to an unsupported multiaddr', async () => { const dialer = new Dialer({ transportManager: localTM }) - await expect(dialer.connectToMultiaddr(unsupportedAddr)) + await expect(dialer.connectToPeer(unsupportedAddr)) .to.eventually.be.rejectedWith(AggregateError) .and.to.have.nested.property('._errors[0].code', ErrorCodes.ERR_TRANSPORT_UNAVAILABLE) }) @@ -113,7 +117,7 @@ describe('Dialing (direct, TCP)', () => { peerInfo.multiaddrs.add(remoteAddr) peerStore.put(peerInfo) - const connection = await dialer.connectToPeer(peerId) + const connection = await dialer.connectToPeer(peerInfo) expect(connection).to.exist() await connection.close() }) @@ -147,15 +151,23 @@ describe('Dialing (direct, TCP)', () => { throw new AbortError() }) - await expect(dialer.connectToMultiaddr(remoteAddr)) + await expect(dialer.connectToPeer(remoteAddr)) .to.eventually.be.rejectedWith(Error) .and.to.have.property('code', ErrorCodes.ERR_TIMEOUT) }) it('should dial to the max concurrency', async () => { + const addrs = [ + '/ip4/0.0.0.0/tcp/8000', + '/ip4/0.0.0.0/tcp/8001', + '/ip4/0.0.0.0/tcp/8002' + ] const dialer = new Dialer({ transportManager: localTM, - concurrency: 2 + concurrency: 2, + peerStore: { + multiaddrsForPeer: () => addrs + } }) expect(dialer.tokens).to.have.length(2) @@ -163,8 +175,10 @@ describe('Dialing (direct, TCP)', () => { const deferredDial = pDefer() sinon.stub(localTM, 'dial').callsFake(() => deferredDial.promise) + const [peerInfo] = await createPeerInfo() + // Perform 3 multiaddr dials - dialer.connectToMultiaddr([remoteAddr, remoteAddr, remoteAddr]) + dialer.connectToPeer(peerInfo) // Let the call stack run await delay(0) @@ -206,9 +220,10 @@ describe('Dialing (direct, TCP)', () => { connEncryption: [Crypto] } }) + remoteLibp2p.peerInfo.multiaddrs.add(listenAddr) remoteLibp2p.handle('/echo/1.0.0', ({ stream }) => pipe(stream, stream)) - await remoteLibp2p.transportManager.listen([listenAddr]) + await remoteLibp2p.start() remoteAddr = remoteLibp2p.transportManager.getAddrs()[0] }) @@ -230,7 +245,7 @@ describe('Dialing (direct, TCP)', () => { } }) - sinon.spy(libp2p.dialer, 'connectToMultiaddr') + sinon.spy(libp2p.dialer, 'connectToPeer') const connection = await libp2p.dial(remoteAddr) expect(connection).to.exist() @@ -238,7 +253,7 @@ describe('Dialing (direct, TCP)', () => { expect(stream).to.exist() expect(protocol).to.equal('/echo/1.0.0') await connection.close() - expect(libp2p.dialer.connectToMultiaddr.callCount).to.equal(1) + expect(libp2p.dialer.connectToPeer.callCount).to.equal(1) }) it('should use the dialer for connecting to a peer', async () => { @@ -251,7 +266,7 @@ describe('Dialing (direct, TCP)', () => { } }) - sinon.spy(libp2p.dialer, 'connectToMultiaddr') + sinon.spy(libp2p.dialer, 'connectToPeer') const remotePeer = new PeerInfo(remoteLibp2p.peerInfo.id) remotePeer.multiaddrs.add(remoteAddr) @@ -261,7 +276,7 @@ describe('Dialing (direct, TCP)', () => { expect(stream).to.exist() expect(protocol).to.equal('/echo/1.0.0') await connection.close() - expect(libp2p.dialer.connectToMultiaddr.callCount).to.equal(1) + expect(libp2p.dialer.connectToPeer.callCount).to.equal(1) }) it('should be able to use hangup to close connections', async () => { @@ -296,7 +311,7 @@ describe('Dialing (direct, TCP)', () => { sinon.spy(libp2p.upgrader.protector, 'protect') sinon.stub(remoteLibp2p.upgrader, 'protector').value(new Protector(swarmKeyBuffer)) - const connection = await libp2p.dialer.connectToMultiaddr(remoteAddr) + const connection = await libp2p.dialer.connectToPeer(remoteAddr) expect(connection).to.exist() const { stream, protocol } = await connection.newStream('/echo/1.0.0') expect(stream).to.exist() @@ -304,5 +319,95 @@ describe('Dialing (direct, TCP)', () => { await connection.close() expect(libp2p.upgrader.protector.protect.callCount).to.equal(1) }) + + it('should coalesce parallel dials to the same peer (no id in multiaddr)', async () => { + libp2p = new Libp2p({ + peerInfo, + modules: { + transport: [Transport], + streamMuxer: [Muxer], + connEncryption: [Crypto] + } + }) + const dials = 10 + + const dialResults = await Promise.all([...new Array(dials)].map((_, index) => { + if (index % 2 === 0) return libp2p.dial(remoteLibp2p.peerInfo) + return libp2p.dial(remoteLibp2p.peerInfo.multiaddrs.toArray()[0]) + })) + + // All should succeed and we should have ten results + expect(dialResults).to.have.length(10) + for (const connection of dialResults) { + expect(Connection.isConnection(connection)).to.equal(true) + } + + // We will have two connections, since the multiaddr dial doesn't have a peer id + expect(libp2p.connectionManager._connections.size).to.equal(2) + expect(remoteLibp2p.connectionManager._connections.size).to.equal(2) + }) + + it('should coalesce parallel dials to the same peer (id in multiaddr)', async () => { + libp2p = new Libp2p({ + peerInfo, + modules: { + transport: [Transport], + streamMuxer: [Muxer], + connEncryption: [Crypto] + } + }) + const dials = 10 + + const fullAddress = remoteAddr.encapsulate(`/p2p/${remoteLibp2p.peerInfo.id.toString()}`) + const dialResults = await Promise.all([...new Array(dials)].map((_, index) => { + if (index % 2 === 0) return libp2p.dial(remoteLibp2p.peerInfo) + return libp2p.dial(fullAddress) + })) + + // All should succeed and we should have ten results + expect(dialResults).to.have.length(10) + for (const connection of dialResults) { + expect(Connection.isConnection(connection)).to.equal(true) + } + + // 1 connection, because we know the peer in the multiaddr + expect(libp2p.connectionManager._connections.size).to.equal(1) + expect(remoteLibp2p.connectionManager._connections.size).to.equal(1) + }) + + it('should coalesce parallel dials to the same error on failure', async () => { + libp2p = new Libp2p({ + peerInfo, + modules: { + transport: [Transport], + streamMuxer: [Muxer], + connEncryption: [Crypto] + } + }) + const dials = 10 + const error = new Error('Boom') + sinon.stub(libp2p.transportManager, 'dial').callsFake(() => Promise.reject(error)) + + const fullAddress = remoteAddr.encapsulate(`/p2p/${remoteLibp2p.peerInfo.id.toString()}`) + const dialResults = await pSettle([...new Array(dials)].map((_, index) => { + if (index % 2 === 0) return libp2p.dial(remoteLibp2p.peerInfo) + return libp2p.dial(fullAddress) + })) + + // All should succeed and we should have ten results + expect(dialResults).to.have.length(10) + for (const result of dialResults) { + expect(result).to.have.property('isRejected', true) + expect(result.reason).to.be.an.instanceof(AggregateError) + // All errors should be the exact same as `error` + for (const err of result.reason) { + expect(err).to.equal(error) + } + } + + // 1 connection, because we know the peer in the multiaddr + expect(libp2p.connectionManager._connections.size).to.equal(0) + expect(remoteLibp2p.connectionManager._connections.size).to.equal(0) + }) }) }) diff --git a/test/dialing/direct.spec.js b/test/dialing/direct.spec.js index 5281acd574..e7cff9c979 100644 --- a/test/dialing/direct.spec.js +++ b/test/dialing/direct.spec.js @@ -28,6 +28,7 @@ const Peers = require('../fixtures/peers') const { MULTIADDRS_WEBSOCKETS } = require('../fixtures/browser') const mockUpgrader = require('../utils/mockUpgrader') const createMockConnection = require('../utils/mockConnection') +const { createPeerId } = require('../utils/creators/peer') const unsupportedAddr = multiaddr('/ip4/127.0.0.1/tcp/9999/ws') const remoteAddr = MULTIADDRS_WEBSOCKETS[0] @@ -80,17 +81,27 @@ describe('Dialing (direct, WebSockets)', () => { }) it('should be able to connect to a remote node via its multiaddr', async () => { - const dialer = new Dialer({ transportManager: localTM }) + const dialer = new Dialer({ + transportManager: localTM, + peerStore: { + multiaddrsForPeer: () => [remoteAddr] + } + }) - const connection = await dialer.connectToMultiaddr(remoteAddr) + const connection = await dialer.connectToPeer(remoteAddr) expect(connection).to.exist() await connection.close() }) it('should be able to connect to a remote node via its stringified multiaddr', async () => { - const dialer = new Dialer({ transportManager: localTM }) + const dialer = new Dialer({ + transportManager: localTM, + peerStore: { + multiaddrsForPeer: () => [remoteAddr] + } + }) - const connection = await dialer.connectToMultiaddr(remoteAddr.toString()) + const connection = await dialer.connectToPeer(remoteAddr.toString()) expect(connection).to.exist() await connection.close() }) @@ -98,7 +109,7 @@ describe('Dialing (direct, WebSockets)', () => { it('should fail to connect to an unsupported multiaddr', async () => { const dialer = new Dialer({ transportManager: localTM }) - await expect(dialer.connectToMultiaddr(unsupportedAddr)) + await expect(dialer.connectToPeer(unsupportedAddr)) .to.eventually.be.rejectedWith(AggregateError) .and.to.have.nested.property('._errors[0].code', ErrorCodes.ERR_TRANSPORT_DIAL_FAILED) }) @@ -134,7 +145,10 @@ describe('Dialing (direct, WebSockets)', () => { it('should abort dials on queue task timeout', async () => { const dialer = new Dialer({ transportManager: localTM, - timeout: 50 + timeout: 50, + peerStore: { + multiaddrsForPeer: () => [remoteAddr] + } }) sinon.stub(localTM, 'dial').callsFake(async (addr, options) => { expect(options.signal).to.exist() @@ -145,7 +159,7 @@ describe('Dialing (direct, WebSockets)', () => { throw new AbortError() }) - await expect(dialer.connectToMultiaddr(remoteAddr)) + await expect(dialer.connectToPeer(remoteAddr)) .to.eventually.be.rejected() .and.to.have.property('code', ErrorCodes.ERR_TIMEOUT) }) @@ -153,7 +167,10 @@ describe('Dialing (direct, WebSockets)', () => { it('should dial to the max concurrency', async () => { const dialer = new Dialer({ transportManager: localTM, - concurrency: 2 + concurrency: 2, + peerStore: { + multiaddrsForPeer: () => [remoteAddr, remoteAddr, remoteAddr] + } }) expect(dialer.tokens).to.have.length(2) @@ -161,8 +178,9 @@ describe('Dialing (direct, WebSockets)', () => { const deferredDial = pDefer() sinon.stub(localTM, 'dial').callsFake(() => deferredDial.promise) + const [peerId] = await createPeerId() // Perform 3 multiaddr dials - dialer.connectToMultiaddr([remoteAddr, remoteAddr, remoteAddr]) + dialer.connectToPeer(peerId) // Let the call stack run await delay(0) @@ -185,7 +203,10 @@ describe('Dialing (direct, WebSockets)', () => { it('.destroy should abort pending dials', async () => { const dialer = new Dialer({ transportManager: localTM, - concurrency: 2 + concurrency: 2, + peerStore: { + multiaddrsForPeer: () => [remoteAddr, remoteAddr, remoteAddr] + } }) expect(dialer.tokens).to.have.length(2) @@ -201,7 +222,8 @@ describe('Dialing (direct, WebSockets)', () => { }) // Perform 3 multiaddr dials - const dialPromise = dialer.connectToMultiaddr([remoteAddr, remoteAddr, remoteAddr]) + const [peerId] = await createPeerId() + const dialPromise = dialer.connectToPeer(peerId) // Let the call stack run await delay(0) @@ -265,7 +287,8 @@ describe('Dialing (direct, WebSockets)', () => { } }) - sinon.spy(libp2p.dialer, 'connectToMultiaddr') + sinon.spy(libp2p.dialer, 'connectToPeer') + sinon.spy(libp2p.peerStore, 'put') const connection = await libp2p.dial(remoteAddr) expect(connection).to.exist() @@ -273,7 +296,8 @@ describe('Dialing (direct, WebSockets)', () => { expect(stream).to.exist() expect(protocol).to.equal('/echo/1.0.0') await connection.close() - expect(libp2p.dialer.connectToMultiaddr.callCount).to.equal(1) + expect(libp2p.dialer.connectToPeer.callCount).to.equal(1) + expect(libp2p.peerStore.put.callCount).to.be.at.least(1) }) it('should run identify automatically after connecting', async () => { @@ -290,7 +314,7 @@ describe('Dialing (direct, WebSockets)', () => { sinon.spy(libp2p.peerStore, 'replace') sinon.spy(libp2p.upgrader, 'onConnection') - const connection = await libp2p.dialer.connectToMultiaddr(remoteAddr) + const connection = await libp2p.dial(remoteAddr) expect(connection).to.exist() // Wait for onConnection to be called diff --git a/test/dialing/relay.node.js b/test/dialing/relay.node.js index 4f477680a5..c1b42e8598 100644 --- a/test/dialing/relay.node.js +++ b/test/dialing/relay.node.js @@ -42,13 +42,19 @@ describe('Dialing (via relay, TCP)', () => { // Reset multiaddrs and start libp2p.peerInfo.multiaddrs.clear() libp2p.peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') - libp2p.start() + return libp2p.start() })) }) afterEach(() => { // Stop each node - return Promise.all([srcLibp2p, relayLibp2p, dstLibp2p].map(libp2p => libp2p.stop())) + return Promise.all([srcLibp2p, relayLibp2p, dstLibp2p].map(async libp2p => { + await libp2p.stop() + // Clear the peer stores + for (const peerId of libp2p.peerStore.peers.keys()) { + libp2p.peerStore.remove(peerId) + } + })) }) it('should be able to connect to a peer over a relay with active connections', async () => { diff --git a/test/identify/index.spec.js b/test/identify/index.spec.js index 5eede683db..70563566ca 100644 --- a/test/identify/index.spec.js +++ b/test/identify/index.spec.js @@ -200,7 +200,7 @@ describe('Identify', () => { sinon.spy(libp2p.identifyService, 'identify') sinon.spy(libp2p.peerStore, 'replace') - const connection = await libp2p.dialer.connectToMultiaddr(remoteAddr) + const connection = await libp2p.dialer.connectToPeer(remoteAddr) expect(connection).to.exist() // Wait for nextTick to trigger the identify call await delay(1) @@ -221,7 +221,7 @@ describe('Identify', () => { sinon.spy(libp2p.identifyService, 'push') sinon.spy(libp2p.peerStore, 'update') - const connection = await libp2p.dialer.connectToMultiaddr(remoteAddr) + const connection = await libp2p.dialer.connectToPeer(remoteAddr) expect(connection).to.exist() // Wait for nextTick to trigger the identify call await delay(1) diff --git a/test/peer-store/peer-store.spec.js b/test/peer-store/peer-store.spec.js index 95be069f99..5988615eff 100644 --- a/test/peer-store/peer-store.spec.js +++ b/test/peer-store/peer-store.spec.js @@ -7,18 +7,12 @@ const { expect } = chai const sinon = require('sinon') const pDefer = require('p-defer') -const mergeOptions = require('merge-options') -const Libp2p = require('../../src') const PeerStore = require('../../src/peer-store') const multiaddr = require('multiaddr') - -const baseOptions = require('../utils/base-options') const peerUtils = require('../utils/creators/peer') -const mockConnection = require('../utils/mockConnection') const addr = multiaddr('/ip4/127.0.0.1/tcp/8000') -const listenAddr = multiaddr('/ip4/127.0.0.1/tcp/0') describe('peer-store', () => { let peerStore @@ -168,51 +162,6 @@ describe('peer-store', () => { }) }) -describe('peer-store on dial', () => { - let peerInfo - let remotePeerInfo - let libp2p - let remoteLibp2p - - before(async () => { - [peerInfo, remotePeerInfo] = await peerUtils.createPeerInfo({ number: 2 }) - remoteLibp2p = new Libp2p(mergeOptions(baseOptions, { - peerInfo: remotePeerInfo - })) - }) - - after(async () => { - sinon.restore() - await remoteLibp2p.stop() - libp2p && await libp2p.stop() - }) - - it('should put the remote peerInfo after dial and emit event', async () => { - const remoteId = remotePeerInfo.id.toB58String() - - libp2p = new Libp2p(mergeOptions(baseOptions, { - peerInfo - })) - - sinon.spy(libp2p.peerStore, 'put') - sinon.spy(libp2p.peerStore, 'add') - sinon.spy(libp2p.peerStore, 'update') - sinon.stub(libp2p.dialer, 'connectToMultiaddr').returns(mockConnection({ - remotePeer: remotePeerInfo.id - })) - - const connection = await libp2p.dial(listenAddr) - await connection.close() - - expect(libp2p.peerStore.put.callCount).to.equal(1) - expect(libp2p.peerStore.add.callCount).to.equal(1) - expect(libp2p.peerStore.update.callCount).to.equal(0) - - const storedPeer = libp2p.peerStore.get(remoteId) - expect(storedPeer).to.exist() - }) -}) - describe('peer-store on discovery', () => { // TODO: implement with discovery }) From 9900beb24323c8014b5d7674c271432314db5ef0 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Sun, 15 Dec 2019 17:36:46 +0100 Subject: [PATCH 69/92] chore: update contributors --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index baad9d2058..759547b2aa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "libp2p", - "version": "0.27.0-pre.0", + "version": "0.27.0-pre.1", "description": "JavaScript implementation of libp2p, a modular peer to peer network stack", "leadMaintainer": "Jacob Heun ", "main": "src/index.js", From 92ed56657c8e70a0bd8e1e0262211937f64d6ac5 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Sun, 15 Dec 2019 17:36:47 +0100 Subject: [PATCH 70/92] chore: release version v0.27.0-pre.1 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c43036ee57..8b85327bd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ + +# [0.27.0-pre.1](https://github.com/libp2p/js-libp2p/compare/v0.27.0-pre.0...v0.27.0-pre.1) (2019-12-15) + + +### Features + +* coalescing dial support ([#518](https://github.com/libp2p/js-libp2p/issues/518)) ([4a871bb](https://github.com/libp2p/js-libp2p/commit/4a871bb)) + + + # [0.27.0-pre.0](https://github.com/libp2p/js-libp2p/compare/v0.26.2...v0.27.0-pre.0) (2019-12-12) From 56a18256396cebad6c1b936a2987d2375e48b547 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Mon, 16 Dec 2019 11:26:38 +0000 Subject: [PATCH 71/92] fix: upgrader should not need muxers (#517) * fix: upgrader should not need muxers * chore: address review * chore: apply suggestions from code review Co-Authored-By: Jacob Heun --- src/upgrader.js | 100 ++++++++++++++++++-------------- test/upgrading/upgrader.spec.js | 29 +++++++++ 2 files changed, 87 insertions(+), 42 deletions(-) diff --git a/src/upgrader.js b/src/upgrader.js index 146735b90f..6e178a4c70 100644 --- a/src/upgrader.js +++ b/src/upgrader.js @@ -64,7 +64,7 @@ class Upgrader { async upgradeInbound (maConn) { let encryptedConn let remotePeer - let muxedConnection + let upgradedConn let Muxer let cryptoProtocol let setPeer @@ -94,7 +94,11 @@ class Upgrader { } = await this._encryptInbound(this.localPeer, protectedConn, this.cryptos)) // Multiplex the connection - ;({ stream: muxedConnection, Muxer } = await this._multiplexInbound(encryptedConn, this.muxers)) + if (this.muxers.size) { + ({ stream: upgradedConn, Muxer } = await this._multiplexInbound(encryptedConn, this.muxers)) + } else { + upgradedConn = encryptedConn + } } catch (err) { log.error('Failed to upgrade inbound connection', err) await maConn.close(err) @@ -112,7 +116,7 @@ class Upgrader { cryptoProtocol, direction: 'inbound', maConn, - muxedConnection, + upgradedConn, Muxer, remotePeer }) @@ -134,7 +138,7 @@ class Upgrader { let encryptedConn let remotePeer - let muxedConnection + let upgradedConn let cryptoProtocol let Muxer let setPeer @@ -164,7 +168,11 @@ class Upgrader { } = await this._encryptOutbound(this.localPeer, protectedConn, remotePeerId, this.cryptos)) // Multiplex the connection - ;({ stream: muxedConnection, Muxer } = await this._multiplexOutbound(encryptedConn, this.muxers)) + if (this.muxers.size) { + ({ stream: upgradedConn, Muxer } = await this._multiplexOutbound(encryptedConn, this.muxers)) + } else { + upgradedConn = encryptedConn + } } catch (err) { log.error('Failed to upgrade outbound connection', err) await maConn.close(err) @@ -182,7 +190,7 @@ class Upgrader { cryptoProtocol, direction: 'outbound', maConn, - muxedConnection, + upgradedConn, Muxer, remotePeer }) @@ -195,7 +203,7 @@ class Upgrader { * @param {string} cryptoProtocol The crypto protocol that was negotiated * @param {string} direction One of ['inbound', 'outbound'] * @param {MultiaddrConnection} maConn The transport layer connection - * @param {*} muxedConnection A duplex connection returned from multiplexer selection + * @param {*} upgradedConn A duplex connection returned from multiplexer and/or crypto selection * @param {Muxer} Muxer The muxer to be used for muxing * @param {PeerId} remotePeer The peer the connection is with * @returns {Connection} @@ -204,49 +212,52 @@ class Upgrader { cryptoProtocol, direction, maConn, - muxedConnection, + upgradedConn, Muxer, remotePeer }) { - // Create the muxer - const muxer = new Muxer({ - // Run anytime a remote stream is created - onStream: async muxedStream => { - const mss = new Multistream.Listener(muxedStream) + let muxer, newStream + + if (Muxer) { + // Create the muxer + muxer = new Muxer({ + // Run anytime a remote stream is created + onStream: async muxedStream => { + const mss = new Multistream.Listener(muxedStream) + try { + const { stream, protocol } = await mss.handle(Array.from(this.protocols.keys())) + log('%s: incoming stream opened on %s', direction, protocol) + if (this.metrics) this.metrics.trackStream({ stream, remotePeer, protocol }) + connection.addStream(stream, protocol) + this._onStream({ connection, stream, protocol }) + } catch (err) { + log.error(err) + } + }, + // Run anytime a stream closes + onStreamEnd: muxedStream => { + connection.removeStream(muxedStream.id) + } + }) + + newStream = async protocols => { + log('%s: starting new stream on %s', direction, protocols) + const muxedStream = muxer.newStream() + const mss = new Multistream.Dialer(muxedStream) try { - const { stream, protocol } = await mss.handle(Array.from(this.protocols.keys())) - log('%s: incoming stream opened on %s', direction, protocol) + const { stream, protocol } = await mss.select(protocols) if (this.metrics) this.metrics.trackStream({ stream, remotePeer, protocol }) - connection.addStream(stream, protocol) - this._onStream({ connection, stream, protocol }) + return { stream: { ...muxedStream, ...stream }, protocol } } catch (err) { - log.error(err) + log.error('could not create new stream', err) + throw errCode(err, codes.ERR_UNSUPPORTED_PROTOCOL) } - }, - // Run anytime a stream closes - onStreamEnd: muxedStream => { - connection.removeStream(muxedStream.id) } - }) - const newStream = async protocols => { - log('%s: starting new stream on %s', direction, protocols) - const muxedStream = muxer.newStream() - const mss = new Multistream.Dialer(muxedStream) - try { - const { stream, protocol } = await mss.select(protocols) - if (this.metrics) this.metrics.trackStream({ stream, remotePeer, protocol }) - return { stream: { ...muxedStream, ...stream }, protocol } - } catch (err) { - log.error('could not create new stream', err) - throw errCode(err, codes.ERR_UNSUPPORTED_PROTOCOL) - } + // Pipe all data through the muxer + pipe(upgradedConn, muxer, upgradedConn) } - // Pipe all data through the muxer - pipe(muxedConnection, muxer, muxedConnection) - - maConn.timeline.upgraded = Date.now() const _timeline = maConn.timeline maConn.timeline = new Proxy(_timeline, { set: (...args) => { @@ -258,6 +269,11 @@ class Upgrader { return Reflect.set(...args) } }) + maConn.timeline.upgraded = Date.now() + + const errConnectionNotMultiplexed = () => { + throw errCode(new Error('connection is not multiplexed'), 'ERR_CONNECTION_NOT_MULTIPLEXED') + } // Create the connection const connection = new Connection({ @@ -268,11 +284,11 @@ class Upgrader { stat: { direction, timeline: maConn.timeline, - multiplexer: Muxer.multicodec, + multiplexer: Muxer && Muxer.multicodec, encryption: cryptoProtocol }, - newStream, - getStreams: () => muxer.streams, + newStream: newStream || errConnectionNotMultiplexed, + getStreams: () => muxer ? muxer.streams : errConnectionNotMultiplexed, close: err => maConn.close(err) }) diff --git a/test/upgrading/upgrader.spec.js b/test/upgrading/upgrader.spec.js index 35d870069b..ef0cb84c9a 100644 --- a/test/upgrading/upgrader.spec.js +++ b/test/upgrading/upgrader.spec.js @@ -116,6 +116,35 @@ describe('Upgrader', () => { expect(result).to.eql([hello]) }) + it('should upgrade with only crypto', async () => { + const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer }) + + // No available muxers + const muxers = new Map() + sinon.stub(localUpgrader, 'muxers').value(muxers) + sinon.stub(remoteUpgrader, 'muxers').value(muxers) + + const cryptos = new Map([[Crypto.protocol, Crypto]]) + sinon.stub(localUpgrader, 'cryptos').value(cryptos) + sinon.stub(remoteUpgrader, 'cryptos').value(cryptos) + + const connections = await Promise.all([ + localUpgrader.upgradeOutbound(outbound), + remoteUpgrader.upgradeInbound(inbound) + ]) + + expect(connections).to.have.length(2) + + await expect(connections[0].newStream('/echo/1.0.0')).to.be.rejected() + + // Verify the MultiaddrConnection close method is called + sinon.spy(inbound, 'close') + sinon.spy(outbound, 'close') + await Promise.all(connections.map(conn => conn.close())) + expect(inbound.close.callCount).to.equal(1) + expect(outbound.close.callCount).to.equal(1) + }) + it('should use a private connection protector when provided', async () => { const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer }) From 24c3ce6f8df6c1821b5bd02daa2f9ca79ec8cdd2 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Mon, 16 Dec 2019 16:49:29 +0100 Subject: [PATCH 72/92] fix: make dialer configurable (#521) docs: update configuration and api docs --- doc/API.md | 3 +++ doc/CONFIGURATION.md | 24 ++++++++++++++++++++++++ src/config.js | 6 ++++++ src/constants.js | 6 ------ src/index.js | 5 ++++- test/dialing/direct.spec.js | 25 +++++++++++++++++++++++++ 6 files changed, 62 insertions(+), 7 deletions(-) diff --git a/doc/API.md b/doc/API.md index 651be03d00..8d904e07e0 100644 --- a/doc/API.md +++ b/doc/API.md @@ -46,7 +46,10 @@ Creates an instance of Libp2p. | options | `Object` | libp2p options | | options.modules | `Array` | libp2p modules to use | | [options.config] | `Object` | libp2p modules configuration and core configuration | +| [options.connectionManager] | `Object` | libp2p Connection Manager configuration | | [options.datastore] | `Object` | must implement [ipfs/interface-datastore](https://github.com/ipfs/interface-datastore) (in memory datastore will be used if not provided) | +| [options.dialer] | `Object` | libp2p Dialer configuration +| [options.metrics] | `Object` | libp2p Metrics configuration | [options.peerInfo] | [PeerInfo](https://github.com/libp2p/js-peer-info) | peerInfo instance (it will be created if not provided) | For Libp2p configurations and modules details read the [Configuration Document](./CONFIGURATION.md). diff --git a/doc/CONFIGURATION.md b/doc/CONFIGURATION.md index c0c55b1348..8df6512f19 100644 --- a/doc/CONFIGURATION.md +++ b/doc/CONFIGURATION.md @@ -20,6 +20,7 @@ - [Customizing DHT](#customizing-dht) - [Setup with Content and Peer Routing](#setup-with-content-and-peer-routing) - [Setup with Relay](#setup-with-relay) + - [Configuring Dialing](#configuring-dialing) - [Configuring Connection Manager](#configuring-connection-manager) - [Configuring Metrics](#configuring-metrics) - [Configuration examples](#configuration-examples) @@ -414,6 +415,29 @@ const node = await Libp2p.create({ }) ``` +#### Configuring Dialing + +Dialing in libp2p can be configured to limit the rate of dialing, and how long dials are allowed to take. The below configuration example shows the default values for the dialer. + +```js +const Libp2p = require('libp2p') +const TCP = require('libp2p-tcp') +const MPLEX = require('libp2p-mplex') +const SECIO = require('libp2p-secio') + +const node = await Libp2p.create({ + modules: { + transport: [TCP], + streamMuxer: [MPLEX], + connEncryption: [SECIO] + }, + dialer: { + maxParallelDials: 100, // How many multiaddrs we can dial in parallel + maxDialsPerPeer: 4, // How many multiaddrs we can dial per peer, in parallel + dialTimeout: 30e3 // 30 second dial timeout per peer + } +``` + #### Configuring Connection Manager The Connection Manager prunes Connections in libp2p whenever certain limits are exceeded. If Metrics are enabled, you can also configure the Connection Manager to monitor the bandwidth of libp2p and prune connections as needed. You can read more about what Connection Manager does at [./CONNECTION_MANAGER.md](./CONNECTION_MANAGER.md). The configuration values below show the defaults for Connection Manager. See [./CONNECTION_MANAGER.md](./CONNECTION_MANAGER.md#options) for a full description of the parameters. diff --git a/src/config.js b/src/config.js index 257ec65605..6d365c07f8 100644 --- a/src/config.js +++ b/src/config.js @@ -1,11 +1,17 @@ 'use strict' const mergeOptions = require('merge-options') +const Constants = require('./constants') const DefaultConfig = { connectionManager: { minPeers: 25 }, + dialer: { + maxParallelDials: Constants.MAX_PARALLEL_DIALS, + maxDialsPerPeer: Constants.MAX_PER_PEER_DIALS, + dialTimeout: Constants.DIAL_TIMEOUT + }, metrics: { enabled: false }, diff --git a/src/constants.js b/src/constants.js index a0d5cc7955..08ad156971 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,15 +1,9 @@ 'use strict' module.exports = { - DENY_TTL: 5 * 60 * 1e3, // How long before an errored peer can be dialed again - DENY_ATTEMPTS: 5, // Num of unsuccessful dials before a peer is permanently denied DIAL_TIMEOUT: 30e3, // How long in ms a dial attempt is allowed to take - MAX_COLD_CALLS: 50, // How many dials w/o protocols that can be queued MAX_PARALLEL_DIALS: 100, // Maximum allowed concurrent dials MAX_PER_PEER_DIALS: 4, // Allowed parallel dials per DialRequest - QUARTER_HOUR: 15 * 60e3, - PRIORITY_HIGH: 10, - PRIORITY_LOW: 20, METRICS: { computeThrottleMaxQueueSize: 1000, computeThrottleTimeout: 2000, diff --git a/src/index.js b/src/index.js index 3fc56692ac..36aaa6abbf 100644 --- a/src/index.js +++ b/src/index.js @@ -107,7 +107,10 @@ class Libp2p extends EventEmitter { this.dialer = new Dialer({ transportManager: this.transportManager, - peerStore: this.peerStore + peerStore: this.peerStore, + concurrency: this._options.dialer.maxParallelDials, + perPeerLimit: this._options.dialer.maxDialsPerPeer, + timeout: this._options.dialer.dialTimeout }) this._modules.transport.forEach((Transport) => { diff --git a/test/dialing/direct.spec.js b/test/dialing/direct.spec.js index e7cff9c979..b886b4b700 100644 --- a/test/dialing/direct.spec.js +++ b/test/dialing/direct.spec.js @@ -273,10 +273,35 @@ describe('Dialing (direct, WebSockets)', () => { }) expect(libp2p.dialer).to.exist() + expect(libp2p.dialer.concurrency).to.equal(Constants.MAX_PARALLEL_DIALS) + expect(libp2p.dialer.perPeerLimit).to.equal(Constants.MAX_PER_PEER_DIALS) + expect(libp2p.dialer.timeout).to.equal(Constants.DIAL_TIMEOUT) // Ensure the dialer also has the transport manager expect(libp2p.transportManager).to.equal(libp2p.dialer.transportManager) }) + it('should be able to override dialer options', async () => { + const config = { + peerInfo, + modules: { + transport: [Transport], + streamMuxer: [Muxer], + connEncryption: [Crypto] + }, + dialer: { + maxParallelDials: 10, + maxDialsPerPeer: 1, + dialTimeout: 1e3 // 30 second dial timeout per peer + } + } + libp2p = await Libp2p.create(config) + + expect(libp2p.dialer).to.exist() + expect(libp2p.dialer.concurrency).to.equal(config.dialer.maxParallelDials) + expect(libp2p.dialer.perPeerLimit).to.equal(config.dialer.maxDialsPerPeer) + expect(libp2p.dialer.timeout).to.equal(config.dialer.dialTimeout) + }) + it('should use the dialer for connecting', async () => { libp2p = new Libp2p({ peerInfo, From 6ca19c5ef40123fc8951af8dee36196341c3a8d2 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Mon, 16 Dec 2019 19:24:35 +0100 Subject: [PATCH 73/92] feat: add libp2p.connections getter (#522) * fix: make hangup accept what the API says it does * feat: add libp2p.connections getter * chore: fix typo --- doc/API.md | 36 +++++++++++++++++++++++++++----- src/index.js | 14 +++++++++++-- test/dialing/direct.node.js | 17 +++++++++++++++ test/registrar/registrar.node.js | 4 ++-- 4 files changed, 62 insertions(+), 9 deletions(-) diff --git a/doc/API.md b/doc/API.md index 8d904e07e0..945c42d2dd 100644 --- a/doc/API.md +++ b/doc/API.md @@ -181,6 +181,29 @@ const libp2p = await Libp2p.create(options) await libp2p.stop() ``` +### connections + +A Getter that returns a Map of the current Connections libp2p has to other peers. + +`libp2p.connections` + +#### Returns + +| Type | Description | +|------|-------------| +| `Map>` | A map of [`PeerId`][peer-id] strings to [`Connection`][connection] Arrays | + +#### Example + +```js +for (const [peerId, connections] of libp2p.connections) { + for (const connection of connections) { + console.log(peerId, connection.remoteAddr.toString()) + // Logs the PeerId string and the observed remote multiaddr of each Connection + } +} +``` + ### dial Dials to another peer in the network and establishes the connection. @@ -191,7 +214,7 @@ Dials to another peer in the network and establishes the connection. | Name | Type | Description | |------|------|-------------| -| peer | [PeerInfo](https://github.com/libp2p/js-peer-info), [PeerId](https://github.com/libp2p/js-peer-id), [multiaddr](https://github.com/multiformats/js-multiaddr), `string` | peer to dial | +| peer | [PeerInfo](https://github.com/libp2p/js-peer-info), [PeerId][peer-id], [multiaddr](https://github.com/multiformats/js-multiaddr), `string` | peer to dial | | [options] | `Object` | dial options | | [options.signal] | [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) | An `AbortSignal` instance obtained from an [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) that can be used to abort the connection before it completes | @@ -199,7 +222,7 @@ Dials to another peer in the network and establishes the connection. | Type | Description | |------|-------------| -| `Promise` | Promise resolves with the [Connection](https://github.com/libp2p/js-interfaces/tree/master/src/connection) instance | +| `Promise` | Promise resolves with the [Connection][connection] instance | #### Example @@ -226,7 +249,7 @@ Dials to another peer in the network and selects a protocol to communicate with | Name | Type | Description | |------|------|-------------| -| peer | [PeerInfo](https://github.com/libp2p/js-peer-info), [PeerId](https://github.com/libp2p/js-peer-id), [multiaddr](https://github.com/multiformats/js-multiaddr), `string` | peer to dial | +| peer | [PeerInfo](https://github.com/libp2p/js-peer-info), [PeerId][peer-id], [multiaddr](https://github.com/multiformats/js-multiaddr), `string` | peer to dial | | protocols | `String|Array` | A list of protocols (or single protocol) to negotiate with. Protocols are attempted in order until a match is made. (e.g '/ipfs/bitswap/1.1.0') | | [options] | `Object` | dial options | | [options.signal] | [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) | An `AbortSignal` instance obtained from an [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) that can be used to abort the connection before it completes | @@ -259,7 +282,7 @@ Attempts to gracefully close an open connection to the given peer. If the connec | Name | Type | Description | |------|------|-------------| -| peer | [PeerInfo](https://github.com/libp2p/js-peer-info), [PeerId](https://github.com/libp2p/js-peer-id), [multiaddr](https://github.com/multiformats/js-multiaddr), `string` | peer to hang up | +| peer | [PeerInfo](https://github.com/libp2p/js-peer-info), [PeerId][peer-id], [multiaddr](https://github.com/multiformats/js-multiaddr), `string` | peer to hang up | #### Returns @@ -355,7 +378,7 @@ Iterates over all peer routers in series to find the given peer. If the DHT is e | Name | Type | Description | |------|------|-------------| -| peerId | [`PeerId`](https://github.com/libp2p/js-peer-id) | ID of the peer to find | +| peerId | [`PeerId`][peer-id] | ID of the peer to find | | options | `Object` | operation options | | options.timeout | `number` | maximum time the query should run | @@ -773,3 +796,6 @@ console.log(peerStats.toJSON()) - `['60000']`: The [MovingAverage](https://www.npmjs.com/package/moving-averages) at a 1 minute interval. - `['300000']`: The [MovingAverage](https://www.npmjs.com/package/moving-averages) at a 5 minute interval. - `['900000']`: The [MovingAverage](https://www.npmjs.com/package/moving-averages) at a 15 minute interval. + +[connection]: https://github.com/libp2p/js-interfaces/tree/master/src/connection +[peer-id]: https://github.com/libp2p/js-peer-id diff --git a/src/index.js b/src/index.js index 36aaa6abbf..3b39d86d71 100644 --- a/src/index.js +++ b/src/index.js @@ -240,6 +240,15 @@ class Libp2p extends EventEmitter { return this._isStarted } + /** + * Gets a Map of the current connections. The keys are the stringified + * `PeerId` of the peer. The value is an array of Connections to that peer. + * @returns {Map} + */ + get connections () { + return this.registrar.connections + } + /** * Dials to the provided peer. If successful, the `PeerInfo` of the * peer will be added to the nodes `peerStore` @@ -288,12 +297,13 @@ class Libp2p extends EventEmitter { /** * Disconnects all connections to the given `peer` * - * @param {PeerId} peer The PeerId to close connections to + * @param {PeerInfo|PeerId|multiaddr|string} peer the peer to close connections to * @returns {Promise} */ hangUp (peer) { + const peerInfo = getPeerInfo(peer, this.peerStore) return Promise.all( - this.registrar.connections.get(peer.toString()).map(connection => { + this.registrar.connections.get(peerInfo.id.toString()).map(connection => { return connection.close() }) ) diff --git a/test/dialing/direct.node.js b/test/dialing/direct.node.js index d6b4588d09..31685b4c9c 100644 --- a/test/dialing/direct.node.js +++ b/test/dialing/direct.node.js @@ -296,6 +296,23 @@ describe('Dialing (direct, TCP)', () => { expect(connection.stat.timeline.close).to.exist() }) + it('should be able to use hangup by address string to close connections', async () => { + libp2p = new Libp2p({ + peerInfo, + modules: { + transport: [Transport], + streamMuxer: [Muxer], + connEncryption: [Crypto] + } + }) + + const connection = await libp2p.dial(`${remoteAddr.toString()}/p2p/${remotePeerInfo.id.toString()}`) + expect(connection).to.exist() + expect(connection.stat.timeline.close).to.not.exist() + await libp2p.hangUp(connection.remotePeer) + expect(connection.stat.timeline.close).to.exist() + }) + it('should use the protectors when provided for connecting', async () => { const protector = new Protector(swarmKeyBuffer) libp2p = new Libp2p({ diff --git a/test/registrar/registrar.node.js b/test/registrar/registrar.node.js index 1f914a55ab..31ffeb1193 100644 --- a/test/registrar/registrar.node.js +++ b/test/registrar/registrar.node.js @@ -61,12 +61,12 @@ describe('registrar on dial', () => { })) await libp2p.dial(remoteAddr) - expect(libp2p.registrar.connections.size).to.equal(1) + expect(libp2p.connections.size).to.equal(1) sinon.spy(libp2p.registrar, 'close') await libp2p.stop() expect(libp2p.registrar.close.callCount).to.equal(1) - expect(libp2p.registrar.connections.size).to.equal(0) + expect(libp2p.connections.size).to.equal(0) }) }) From 9f0f08f58638a5e11318f3f1041ea5b5b0361e64 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Mon, 16 Dec 2019 22:28:35 +0000 Subject: [PATCH 74/92] refactor: examples-pubsub (#504) --- examples/pubsub/1.js | 118 +++++++++++--------------------------- examples/pubsub/README.md | 77 ++++++++++++------------- 2 files changed, 70 insertions(+), 125 deletions(-) diff --git a/examples/pubsub/1.js b/examples/pubsub/1.js index 5dfddb2efe..4e84678c32 100644 --- a/examples/pubsub/1.js +++ b/examples/pubsub/1.js @@ -1,103 +1,51 @@ /* eslint-disable no-console */ 'use strict' -const libp2p = require('../../') +const Libp2p = require('../../') const TCP = require('libp2p-tcp') const Mplex = require('libp2p-mplex') const SECIO = require('libp2p-secio') const PeerInfo = require('peer-info') -const MulticastDNS = require('libp2p-mdns') const Gossipsub = require('libp2p-gossipsub') -const defaultsDeep = require('@nodeutils/defaults-deep') -const waterfall = require('async/waterfall') -const parallel = require('async/parallel') -const series = require('async/series') -class MyBundle extends libp2p { - constructor (_options) { - const defaults = { - modules: { - transport: [ TCP ], - streamMuxer: [ Mplex ], - connEncryption: [ SECIO ], - peerDiscovery: [ MulticastDNS ], - pubsub: Gossipsub - }, - config: { - peerDiscovery: { - mdns: { - interval: 2000, - enabled: true - } - }, - pubsub: { - enabled: true, - emitSelf: true - } - } +const createNode = async () => { + const peerInfo = await PeerInfo.create() + peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') + + const node = await Libp2p.create({ + peerInfo, + modules: { + transport: [TCP], + streamMuxer: [Mplex], + connEncryption: [SECIO], + pubsub: Gossipsub } + }) - super(defaultsDeep(_options, defaults)) - } + await node.start() + return node } -function createNode (callback) { - let node - - waterfall([ - (cb) => PeerInfo.create(cb), - (peerInfo, cb) => { - peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') - node = new MyBundle({ - peerInfo - }) - node.start(cb) - } - ], (err) => callback(err, node)) -} +;(async () => { + const topic = 'news' -parallel([ - (cb) => createNode(cb), - (cb) => createNode(cb) -], (err, nodes) => { - if (err) { throw err } + const [node1, node2] = await Promise.all([ + createNode(), + createNode(), + ]) - const node1 = nodes[0] - const node2 = nodes[1] + await node1.dial(node2.peerInfo) - node1.once('peer:connect', (peer) => { - console.log('connected to %s', peer.id.toB58String()) + await node1.pubsub.subscribe(topic, (msg) => { + console.log(`node1 received: ${msg.data.toString()}`) + }) - series([ - // node1 subscribes to "news" - (cb) => node1.pubsub.subscribe( - 'news', - (msg) => console.log(`node1 received: ${msg.data.toString()}`), - cb - ), - (cb) => setTimeout(cb, 500), - // node2 subscribes to "news" - (cb) => node2.pubsub.subscribe( - 'news', - (msg) => console.log(`node2 received: ${msg.data.toString()}`), - cb - ), - (cb) => setTimeout(cb, 500), - // node2 publishes "news" every second - (cb) => { - setInterval(() => { - node2.pubsub.publish( - 'news', - Buffer.from('Bird bird bird, bird is the word!'), - (err) => { - if (err) { throw err } - } - ) - }, 1000) - cb() - }, - ], (err) => { - if (err) { throw err } - }) + await node2.pubsub.subscribe(topic, (msg) => { + console.log(`node2 received: ${msg.data.toString()}`) }) -}) + + // node2 publishes "news" every second + setInterval(() => { + node2.pubsub.publish(topic, Buffer.from('Bird bird bird, bird is the word!')) + }, 1000) +})(); diff --git a/examples/pubsub/README.md b/examples/pubsub/README.md index 1c1345cb85..43deefb3c7 100644 --- a/examples/pubsub/README.md +++ b/examples/pubsub/README.md @@ -14,42 +14,45 @@ For this example, we will use MulticastDNS for automatic Peer Discovery. This ex Using PubSub is super simple, you only need to provide the implementation of your choice and you are ready to go. No need for extra configuration. +First, let's update our libp2p configuration with a pubsub implementation. + +```JavaScript +const Libp2p = require('libp2p') +const Gossipsub = require('libp2p-gossipsub') + +const node = await Libp2p.create({ + modules: { + transport: [ TCP ], + streamMuxer: [ Mplex ], + connEncryption: [ SECIO ], + // we add the Pubsub module we want + pubsub: Gossipsub + } +}) +``` + +Once that is done, we only need to create a few libp2p nodes, connect them and everything is ready to start using pubsub. + ```JavaScript -node1.once('peer:connect', (peer) => { - console.log('connected to %s', peer.id.toB58String()) - - series([ - // node1 subscribes to "news" - (cb) => node1.pubsub.subscribe( - 'news', - (msg) => console.log(`node1 received: ${msg.data.toString()}`), - cb - ), - (cb) => setTimeout(cb, 500), - // node2 subscribes to "news" - (cb) => node2.pubsub.subscribe( - 'news', - (msg) => console.log(`node2 received: ${msg.data.toString()}`), - cb - ), - (cb) => setTimeout(cb, 500), - // node2 publishes "news" every second - (cb) => { - setInterval(() => { - node2.pubsub.publish( - 'news', - Buffer.from('Bird bird bird, bird is the word!'), - (err) => { - if (err) { throw err } - } - ) - }, 1000) - cb() - }, - ], (err) => { - if (err) { throw err } - }) +const topic = 'news' + +const node1 = nodes[0] +const node2 = nodes[1] + +await node1.dial(node2.peerInfo) + +await node1.pubsub.subscribe(topic, (msg) => { + console.log(`node1 received: ${msg.data.toString()}`) }) + +await node2.pubsub.subscribe(topic, (msg) => { + console.log(`node2 received: ${msg.data.toString()}`) +}) + +// node2 publishes "news" every second +setInterval(() => { + node2.pubsub.publish(topic, Buffer.from('Bird bird bird, bird is the word!')) +}, 1000) ``` The output of the program should look like: @@ -68,12 +71,6 @@ You can change the pubsub `emitSelf` option if you don't want the publishing nod ```JavaScript const defaults = { config: { - peerDiscovery: { - mdns: { - interval: 2000, - enabled: true - } - }, pubsub: { enabled: true, emitSelf: false From 506af15b6b5c035efefae51bd2b0d4ace287da32 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Wed, 18 Dec 2019 02:35:49 +0000 Subject: [PATCH 75/92] refactor: examples/encrypted-communications (#499) * refactor: examples/encrypted-communications * chore: address review --- examples/encrypted-communications/1.js | 87 +++++++++------------ examples/encrypted-communications/README.md | 25 +++--- 2 files changed, 49 insertions(+), 63 deletions(-) diff --git a/examples/encrypted-communications/1.js b/examples/encrypted-communications/1.js index d15b811d8c..055881cc2b 100644 --- a/examples/encrypted-communications/1.js +++ b/examples/encrypted-communications/1.js @@ -1,63 +1,52 @@ 'use strict' -const libp2p = require('../../') +const Libp2p = require('../../') const TCP = require('libp2p-tcp') -const SPDY = require('libp2p-spdy') +const Mplex = require('libp2p-mplex') const SECIO = require('libp2p-secio') const PeerInfo = require('peer-info') -const waterfall = require('async/waterfall') -const parallel = require('async/parallel') -const pull = require('pull-stream') -const defaultsDeep = require('@nodeutils/defaults-deep') - -class MyBundle extends libp2p { - constructor (_options) { - const defaults = { - modules: { - transport: [ TCP ], - streamMuxer: [ SPDY ], - connEncryption: [ SECIO ] - } - } - super(defaultsDeep(_options, defaults)) - } -} +const pipe = require('it-pipe') -function createNode (callback) { - let node - - waterfall([ - (cb) => PeerInfo.create(cb), - (peerInfo, cb) => { - peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') - node = new MyBundle({ - peerInfo - }) - node.start(cb) +const createNode = async () => { + const peerInfo = await PeerInfo.create() + peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') + + const node = await Libp2p.create({ + peerInfo, + modules: { + transport: [TCP], + streamMuxer: [Mplex], + connEncryption: [SECIO] } - ], (err) => callback(err, node)) -} + }) -parallel([ - (cb) => createNode(cb), - (cb) => createNode(cb) -], (err, nodes) => { - if (err) { throw err } + await node.start() - const node1 = nodes[0] - const node2 = nodes[1] + return node +} - node2.handle('/a-protocol', (protocol, conn) => { - pull( - conn, - pull.map((v) => v.toString()), - pull.log() +;(async () => { + const [node1, node2] = await Promise.all([ + createNode(), + createNode() + ]) + + node2.handle('/a-protocol', ({ stream }) => { + pipe( + stream, + async function (source) { + for await (const msg of source) { + console.log(msg.toString()) + } + } ) }) - node1.dialProtocol(node2.peerInfo, '/a-protocol', (err, conn) => { - if (err) { throw err } - pull(pull.values(['This information is sent out encrypted to the other peer']), conn) - }) -}) + const { stream } = await node1.dialProtocol(node2.peerInfo, '/a-protocol') + + await pipe( + ['This information is sent out encrypted to the other peer'], + stream + ) +})(); diff --git a/examples/encrypted-communications/README.md b/examples/encrypted-communications/README.md index 844f1be614..b0866a3543 100644 --- a/examples/encrypted-communications/README.md +++ b/examples/encrypted-communications/README.md @@ -4,7 +4,7 @@ libp2p can leverage the encrypted communications from the transports it uses (i. We call this usage a _connection upgrade_ where given a connection between peer A to peer B, a protocol handshake can be performed that gives that connection new properties. -A byproduct of having these encrypted communications modules is that we can authenticate the peers we are dialing to. You might have noticed that every time we dial to a peer in libp2p space, we always use its PeerId at the end (e.g /ip4/127.0.0.1/tcp/89765/ipfs/QmWCbVw1XZ8hiYBwwshPce2yaTDYTqTaP7GCHGpry3ykWb), this PeerId is generated by hashing the Public Key of the peer. With this, we can create a crypto challenge when dialing to another peer and prove that peer is the owner of a PrivateKey that matches the Public Key we know. +A byproduct of having these encrypted communications modules is that we can authenticate the peers we are dialing to. You might have noticed that every time we dial to a peer in libp2p space, we always use its PeerId at the end (e.g /ip4/127.0.0.1/tcp/89765/p2p/QmWCbVw1XZ8hiYBwwshPce2yaTDYTqTaP7GCHGpry3ykWb), this PeerId is generated by hashing the Public Key of the peer. With this, we can create a crypto challenge when dialing to another peer and prove that peer is the owner of a PrivateKey that matches the Public Key we know. # 1. Set up encrypted communications with SECIO @@ -12,24 +12,21 @@ We will build this example on top of example for [Protocol and Stream Multiplexi SECIO is the crypto channel developed for IPFS, it is a TLS 1.3 like crypto channel that established an encrypted communication channel between two peers. -To add it to your libp2p bundle, all you have to do is: +To add it to your libp2p configuration, all you have to do is: ```JavaScript +const Libp2p = require('libp2p') const SECIO = require('libp2p-secio') -class MyBundle extends libp2p { - constructor (peerInfo) { - const defaults = { - modules: { - transport: [ TCP ], - streamMuxer: [ SPDY ], - // Attach secio as the crypto channel to use - connEncryption: [ SECIO ] - } +const createNode = () => { + return Libp2p.create({ + modules: { + transport: [ TCP ], + streamMuxer: [ Mplex ], + // Attach secio as the crypto channel to use + connEncryption: [ SECIO ] } - - super(defaultsDeep(_options, defaults)) - } + }) } ``` From fdb48c8df567786c8801f01a9b445e58861ae634 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Wed, 18 Dec 2019 02:37:36 +0000 Subject: [PATCH 76/92] refactor: examples/peer-and-content-routing (#500) * refactor: examples-peer-and-content-routing * chore: address review * chore: review suggestions Co-Authored-By: Jacob Heun --- examples/peer-and-content-routing/1.js | 96 +++++++----------- examples/peer-and-content-routing/2.js | 104 ++++++++------------ examples/peer-and-content-routing/README.md | 79 +++++++-------- 3 files changed, 110 insertions(+), 169 deletions(-) diff --git a/examples/peer-and-content-routing/1.js b/examples/peer-and-content-routing/1.js index 605ec307a5..405a8a35aa 100644 --- a/examples/peer-and-content-routing/1.js +++ b/examples/peer-and-content-routing/1.js @@ -1,77 +1,55 @@ /* eslint-disable no-console */ 'use strict' -const libp2p = require('../../') +const Libp2p = require('../../') const TCP = require('libp2p-tcp') const Mplex = require('libp2p-mplex') const SECIO = require('libp2p-secio') const PeerInfo = require('peer-info') const KadDHT = require('libp2p-kad-dht') -const defaultsDeep = require('@nodeutils/defaults-deep') -const waterfall = require('async/waterfall') -const parallel = require('async/parallel') -class MyBundle extends libp2p { - constructor (_options) { - const defaults = { - modules: { - transport: [ TCP ], - streamMuxer: [ Mplex ], - connEncryption: [ SECIO ], - // we add the DHT module that will enable Peer and Content Routing - dht: KadDHT - }, - config: { - dht: { - enabled: true, - kBucketSize: 20 - } +const delay = require('delay') + +const createNode = async () => { + const peerInfo = await PeerInfo.create() + peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') + + const node = await Libp2p.create({ + peerInfo, + modules: { + transport: [TCP], + streamMuxer: [Mplex], + connEncryption: [SECIO], + dht: KadDHT + }, + config: { + dht: { + enabled: true } } + }) - super(defaultsDeep(_options, defaults)) - } -} - -function createNode (callback) { - let node - - waterfall([ - (cb) => PeerInfo.create(cb), - (peerInfo, cb) => { - peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') - node = new MyBundle({ - peerInfo - }) - node.start(cb) - } - ], (err) => callback(err, node)) + await node.start() + return node } -parallel([ - (cb) => createNode(cb), - (cb) => createNode(cb), - (cb) => createNode(cb) -], (err, nodes) => { - if (err) { throw err } +;(async () => { + const [node1, node2, node3] = await Promise.all([ + createNode(), + createNode(), + createNode() + ]) - const node1 = nodes[0] - const node2 = nodes[1] - const node3 = nodes[2] + await Promise.all([ + node1.dial(node2.peerInfo), + node2.dial(node3.peerInfo) + ]) - parallel([ - (cb) => node1.dial(node2.peerInfo, cb), - (cb) => node2.dial(node3.peerInfo, cb), - // Set up of the cons might take time - (cb) => setTimeout(cb, 300) - ], (err) => { - if (err) { throw err } + // The DHT routing tables need a moment to populate + await delay(100) - node1.peerRouting.findPeer(node3.peerInfo.id, (err, peer) => { - if (err) { throw err } + const peer = await node1.peerRouting.findPeer(node3.peerInfo.id) - console.log('Found it, multiaddrs are:') - peer.multiaddrs.forEach((ma) => console.log(ma.toString())) - }) - }) -}) + console.log('Found it, multiaddrs are:') + peer.multiaddrs.forEach((ma) => console.log(ma.toString())) +})(); diff --git a/examples/peer-and-content-routing/2.js b/examples/peer-and-content-routing/2.js index 78b187c5d3..b6346803e2 100644 --- a/examples/peer-and-content-routing/2.js +++ b/examples/peer-and-content-routing/2.js @@ -1,85 +1,61 @@ /* eslint-disable no-console */ 'use strict' -const libp2p = require('../../') +const Libp2p = require('../../') const TCP = require('libp2p-tcp') const Mplex = require('libp2p-mplex') const SECIO = require('libp2p-secio') const PeerInfo = require('peer-info') const CID = require('cids') const KadDHT = require('libp2p-kad-dht') -const defaultsDeep = require('@nodeutils/defaults-deep') -const waterfall = require('async/waterfall') -const parallel = require('async/parallel') -class MyBundle extends libp2p { - constructor (_options) { - const defaults = { - modules: { - transport: [ TCP ], - streamMuxer: [ Mplex ], - connEncryption: [ SECIO ], - // we add the DHT module that will enable Peer and Content Routing - dht: KadDHT - }, - config: { - dht: { - enabled: true, - kBucketSize: 20 - } +const all = require('it-all') +const delay = require('delay') + +const createNode = async () => { + const peerInfo = await PeerInfo.create() + peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') + + const node = await Libp2p.create({ + peerInfo, + modules: { + transport: [TCP], + streamMuxer: [Mplex], + connEncryption: [SECIO], + dht: KadDHT + }, + config: { + dht: { + enabled: true } } + }) - super(defaultsDeep(_options, defaults)) - } + await node.start() + return node } -function createNode (callback) { - let node - - waterfall([ - (cb) => PeerInfo.create(cb), - (peerInfo, cb) => { - peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') - node = new MyBundle({ - peerInfo - }) - node.start(cb) - } - ], (err) => callback(err, node)) -} +;(async () => { + const [node1, node2, node3] = await Promise.all([ + createNode(), + createNode(), + createNode() + ]) -parallel([ - (cb) => createNode(cb), - (cb) => createNode(cb), - (cb) => createNode(cb) -], (err, nodes) => { - if (err) { throw err } + await Promise.all([ + node1.dial(node2.peerInfo), + node2.dial(node3.peerInfo) + ]) - const node1 = nodes[0] - const node2 = nodes[1] - const node3 = nodes[2] + const cid = new CID('QmTp9VkYvnHyrqKQuFPiuZkiX9gPcqj6x5LJ1rmWuSySnL') + await node1.contentRouting.provide(cid) - parallel([ - (cb) => node1.dial(node2.peerInfo, cb), - (cb) => node2.dial(node3.peerInfo, cb), - // Set up of the cons might take time - (cb) => setTimeout(cb, 300) - ], (err) => { - if (err) { throw err } + console.log('Node %s is providing %s', node1.peerInfo.id.toB58String(), cid.toBaseEncodedString()) - const cid = new CID('QmTp9VkYvnHyrqKQuFPiuZkiX9gPcqj6x5LJ1rmWuSySnL') + // wait for propagation + await delay(300) - node1.contentRouting.provide(cid, (err) => { - if (err) { throw err } + const providers = await all(node3.contentRouting.findProviders(cid, { timeout: 3000 })) - console.log('Node %s is providing %s', node1.peerInfo.id.toB58String(), cid.toBaseEncodedString()) - - node3.contentRouting.findProviders(cid, 5000, (err, providers) => { - if (err) { throw err } - - console.log('Found provider:', providers[0].id.toB58String()) - }) - }) - }) -}) + console.log('Found provider:', providers[0].id.toB58String()) +})(); diff --git a/examples/peer-and-content-routing/README.md b/examples/peer-and-content-routing/README.md index e00903efd7..7c7653f3a0 100644 --- a/examples/peer-and-content-routing/README.md +++ b/examples/peer-and-content-routing/README.md @@ -10,31 +10,27 @@ Content Routing is the category of modules that offer a way to find where conten This example builds on top of the [Protocol and Stream Muxing](../protocol-and-stream-muxing). We need to install `libp2p-kad-dht`, go ahead and `npm install libp2p-kad-dht`. If you want to see the final version, open [1.js](./1.js). -First, let's update our bundle to support Peer Routing and Content Routing. +First, let's update our config to support Peer Routing and Content Routing. ```JavaScript -class MyBundle extends libp2p { - constructor (_options) { - const defaults = { - modules: { - transport: [ TCP ], - streamMuxer: [ Mplex ], - connEncryption: [ SECIO ], - // we add the DHT module that will enable Peer and Content Routing - dht: KadDHT - }, - config: { - dht: { - // dht must be enabled - enabled: true, - kBucketSize: 20 - } - } +const Libp2p = require('libp2p') +const KadDHT = require('libp2p-kad-dht') + +const node = await Libp2p.create({ + modules: { + transport: [ TCP ], + streamMuxer: [ Mplex ], + connEncryption: [ SECIO ], + // we add the DHT module that will enable Peer and Content Routing + dht: KadDHT + }, + config: { + dht: { + // dht must be enabled + enabled: true } - - super(defaultsDeep(_options, defaults)) } -} +}) ``` Once that is done, we can use the createNode function we developed in the previous example to create 3 nodes. Connect node 1 to node 2 and node 2 to node 3. We will use node 2 as a way to find the whereabouts of node 3 @@ -44,22 +40,18 @@ const node1 = nodes[0] const node2 = nodes[1] const node3 = nodes[2] -parallel([ - (cb) => node1.dial(node2.peerInfo, cb), - (cb) => node2.dial(node3.peerInfo, cb), - // Set up of the cons might take time - (cb) => setTimeout(cb, 100) -], (err) => { - if (err) { throw err } - - // - node1.peerRouting.findPeer(node3.peerInfo.id, (err, peer) => { - if (err) { throw err } - - console.log('Found it, multiaddrs are:') - peer.multiaddrs.forEach((ma) => console.log(ma.toString())) - }) -}) +await Promise.all([ + node1.dial(node2.peerInfo), + node2.dial(node3.peerInfo) +]) + +// Set up of the cons might take time +await delay(100) + +const peer = await node1.peerRouting.findPeer(node3.peerInfo.id) + +console.log('Found it, multiaddrs are:') +peer.multiaddrs.forEach((ma) => console.log(ma.toString())) ``` You should see the output being something like: @@ -82,17 +74,12 @@ You can find this example completed in [2.js](./2.js), however as you will see i Instead of calling `peerRouting.findPeer`, we will use `contentRouting.provide` and `contentRouting.findProviders`. ```JavaScript -node1.contentRouting.provide(cid, (err) => { - if (err) { throw err } +await node1.contentRouting.provide(cid) +console.log('Node %s is providing %s', node1.peerInfo.id.toB58String(), cid.toBaseEncodedString()) - console.log('Node %s is providing %s', node1.peerInfo.id.toB58String(), cid.toBaseEncodedString()) +const provs = await all(node3.contentRouting.findProviders(cid, { timeout: 5000 })) - node3.contentRouting.findProviders(cid, 5000, (err, providers) => { - if (err) { throw err } - - console.log('Found provider:', providers[0].id.toB58String()) - }) -}) +console.log('Found provider:', providers[0].id.toB58String()) ``` The output of your program should look like: From 0d4b2bd23df2f45b9b0cf5f9d2890fa6caaffdac Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Fri, 20 Dec 2019 08:32:46 -0800 Subject: [PATCH 77/92] feat: allow transport options to be passed on creation (#524) * feat: allow transport options to be passed on creation * fix: only add circuit transport if enabled * chore: fix lint --- doc/CONFIGURATION.md | 29 +++++++++++++++++++++++ src/config.js | 3 ++- src/index.js | 10 +++++--- src/transport-manager.js | 4 +++- test/transports/transport-manager.spec.js | 29 +++++++++++++++++++++++ 5 files changed, 70 insertions(+), 5 deletions(-) diff --git a/doc/CONFIGURATION.md b/doc/CONFIGURATION.md index 8df6512f19..92da7af7ff 100644 --- a/doc/CONFIGURATION.md +++ b/doc/CONFIGURATION.md @@ -23,6 +23,7 @@ - [Configuring Dialing](#configuring-dialing) - [Configuring Connection Manager](#configuring-connection-manager) - [Configuring Metrics](#configuring-metrics) + - [Customizing Transports](#customizing-transports) - [Configuration examples](#configuration-examples) ## Overview @@ -499,6 +500,34 @@ const node = await Libp2p.create({ }) ``` +#### Customizing Transports + +Some Transports can be passed additional options when they are created. For example, `libp2p-webrtc-star` accepts an optional, custom `wrtc` implementation. In addition to libp2p passing itself and an `Upgrader` to handle connection upgrading, libp2p will also pass the options, if they are provided, from `config.transport`. + +```js +const Libp2p = require('libp2p') +const WebRTCStar = require('libp2p-webrtc-star') +const MPLEX = require('libp2p-mplex') +const SECIO = require('libp2p-secio') +const wrtc = require('wrtc') + +const transportKey = WebRTCStar.prototype[Symbol.toStringTag] +const node = await Libp2p.create({ + modules: { + transport: [WebRTCStar], + streamMuxer: [MPLEX], + connEncryption: [SECIO] + }, + config: { + transport: { + [transportKey]: { + wrtc // You can use `wrtc` when running in Node.js + } + } + } +}) +``` + ## Configuration examples As libp2p is designed to be a modular networking library, its usage will vary based on individual project needs. We've included links to some existing project configurations for your reference, in case you wish to replicate their configuration: diff --git a/src/config.js b/src/config.js index 6d365c07f8..11b0ce4bb9 100644 --- a/src/config.js +++ b/src/config.js @@ -41,7 +41,8 @@ const DefaultConfig = { enabled: false, active: false } - } + }, + transport: {} } } diff --git a/src/index.js b/src/index.js index 3b39d86d71..7e635aa76b 100644 --- a/src/index.js +++ b/src/index.js @@ -114,10 +114,14 @@ class Libp2p extends EventEmitter { }) this._modules.transport.forEach((Transport) => { - this.transportManager.add(Transport.prototype[Symbol.toStringTag], Transport) + const key = Transport.prototype[Symbol.toStringTag] + const transportOptions = this._config.transport[key] + this.transportManager.add(key, Transport, transportOptions) }) - // TODO: enable relay if enabled - this.transportManager.add(Circuit.prototype[Symbol.toStringTag], Circuit) + + if (this._config.relay.enabled) { + this.transportManager.add(Circuit.prototype[Symbol.toStringTag], Circuit) + } // Attach stream multiplexers if (this._modules.streamMuxer) { diff --git a/src/transport-manager.js b/src/transport-manager.js index d11189ab86..ec7a958146 100644 --- a/src/transport-manager.js +++ b/src/transport-manager.js @@ -26,9 +26,10 @@ class TransportManager { * * @param {String} key * @param {Transport} Transport + * @param {*} transportOptions Additional options to pass to the transport * @returns {void} */ - add (key, Transport) { + add (key, Transport, transportOptions = {}) { log('adding %s', key) if (!key) { throw errCode(new Error(`Transport must have a valid key, was given '${key}'`), codes.ERR_INVALID_KEY) @@ -38,6 +39,7 @@ class TransportManager { } const transport = new Transport({ + ...transportOptions, libp2p: this.libp2p, upgrader: this.upgrader }) diff --git a/test/transports/transport-manager.spec.js b/test/transports/transport-manager.spec.js index ab08fc1759..4e71001e21 100644 --- a/test/transports/transport-manager.spec.js +++ b/test/transports/transport-manager.spec.js @@ -115,6 +115,35 @@ describe('libp2p.transportManager', () => { expect(libp2p.transportManager._transports.size).to.equal(2) }) + it('should be able to customize a transport', () => { + const spy = sinon.spy() + const key = spy.prototype[Symbol.toStringTag] = 'TransportSpy' + const customOptions = { + another: 'value' + } + libp2p = new Libp2p({ + peerInfo, + modules: { + transport: [spy] + }, + config: { + transport: { + [key]: customOptions + } + } + }) + + expect(libp2p.transportManager).to.exist() + // Our transport and circuit relay + expect(libp2p.transportManager._transports.size).to.equal(2) + expect(spy).to.have.property('callCount', 1) + expect(spy.getCall(0)).to.have.deep.property('args', [{ + ...customOptions, + libp2p, + upgrader: libp2p.upgrader + }]) + }) + it('starting and stopping libp2p should start and stop TransportManager', async () => { libp2p = new Libp2p({ peerInfo, From dec8dc450fd85713867d608a8274f71b3eb5afa1 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Thu, 2 Jan 2020 21:51:02 +0100 Subject: [PATCH 78/92] refactor: examples/protocol-and-stream-muxing (#502) --- examples/protocol-and-stream-muxing/1.js | 131 ++++++------- examples/protocol-and-stream-muxing/2.js | 116 +++++------ examples/protocol-and-stream-muxing/3.js | 121 +++++------- examples/protocol-and-stream-muxing/README.md | 182 +++++++++--------- 4 files changed, 242 insertions(+), 308 deletions(-) diff --git a/examples/protocol-and-stream-muxing/1.js b/examples/protocol-and-stream-muxing/1.js index a4475b01e5..c6689aeaba 100644 --- a/examples/protocol-and-stream-muxing/1.js +++ b/examples/protocol-and-stream-muxing/1.js @@ -1,102 +1,79 @@ 'use strict' -const libp2p = require('../../') +const Libp2p = require('../../') const TCP = require('libp2p-tcp') +const MPLEX = require('libp2p-mplex') +const SECIO = require('libp2p-secio') const PeerInfo = require('peer-info') -const waterfall = require('async/waterfall') -const parallel = require('async/parallel') -const pull = require('pull-stream') -const defaultsDeep = require('@nodeutils/defaults-deep') -class MyBundle extends libp2p { - constructor (_options) { - const defaults = { - modules: { - transport: [ TCP ] - } - } - - super(defaultsDeep(_options, defaults)) - } -} +const pipe = require('it-pipe') -function createNode (callback) { - let node +const createNode = async () => { + const peerInfo = await PeerInfo.create() + peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') - waterfall([ - (cb) => PeerInfo.create(cb), - (peerInfo, cb) => { - peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') - node = new MyBundle({ - peerInfo - }) - node.start(cb) + const node = await Libp2p.create({ + peerInfo, + modules: { + transport: [TCP], + streamMuxer: [MPLEX], + connEncryption: [SECIO] } - ], (err) => callback(err, node)) -} + }) -parallel([ - (cb) => createNode(cb), - (cb) => createNode(cb) -], (err, nodes) => { - if (err) { throw err } + await node.start() - const node1 = nodes[0] - const node2 = nodes[1] + return node +} + +;(async () => { + const [node1, node2] = await Promise.all([ + createNode(), + createNode() + ]) // exact matching - node2.handle('/your-protocol', (protocol, conn) => { - pull( - conn, - pull.map((v) => v.toString()), - pull.log() + node2.handle('/your-protocol', ({ stream }) => { + pipe( + stream, + async function (source) { + for await (const msg of source) { + console.log(msg.toString()) + } + } ) }) - // semver matching + // multiple protocols /* - node2.handle('/another-protocol/1.0.1', (protocol, conn) => { - pull( - conn, - pull.map((v) => v.toString()), - pull.log() - ) - }) - */ + node2.handle(['/another-protocol/1.0.0', '/another-protocol/2.0.0'], ({ protocol, stream }) => { + if (protocol === '/another-protocol/2.0.0') { + // handle backwards compatibility + } - // custom func matching - /* - node2.handle('/custom-match-func', (protocol, conn) => { - pull( - conn, - pull.map((v) => v.toString()), - pull.log() + pipe( + stream, + async function (source) { + for await (const msg of source) { + console.log(msg.toString()) + } + } ) - }, (myProtocol, requestedProtocol, callback) => { - if (myProtocol.indexOf(requestedProtocol)) { - callback(null, true) - } else { - callback(null, false) - } }) */ - node1.dialProtocol(node2.peerInfo, '/your-protocol', (err, conn) => { - if (err) { throw err } - pull(pull.values(['my own protocol, wow!']), conn) - }) + const { stream } = await node1.dialProtocol(node2.peerInfo, ['/your-protocol']) + await pipe( + ['my own protocol, wow!'], + stream + ) /* - node1.dialProtocol(node2.peerInfo, '/another-protocol/1.0.0', (err, conn) => { - if (err) { throw err } - pull(pull.values(['semver me please']), conn) - }) - */ + const { stream } = node1.dialProtocol(node2.peerInfo, ['/another-protocol/1.0.0']) - /* - node1.dialProtocol(node2.peerInfo, '/custom-match-func/some-query', (err, conn) => { - if (err) { throw err } - pull(pull.values(['do I fall into your criteria?']), conn) - }) + await pipe( + ['my own protocol, wow!'], + stream + ) */ -}) +})(); diff --git a/examples/protocol-and-stream-muxing/2.js b/examples/protocol-and-stream-muxing/2.js index 89e86d8c52..6ce1ab26c4 100644 --- a/examples/protocol-and-stream-muxing/2.js +++ b/examples/protocol-and-stream-muxing/2.js @@ -1,83 +1,63 @@ 'use strict' -const libp2p = require('../../') +const Libp2p = require('../../') const TCP = require('libp2p-tcp') -const SPDY = require('libp2p-spdy') +const MPLEX = require('libp2p-mplex') +const SECIO = require('libp2p-secio') const PeerInfo = require('peer-info') -const waterfall = require('async/waterfall') -const parallel = require('async/parallel') -const series = require('async/series') -const pull = require('pull-stream') -const defaultsDeep = require('@nodeutils/defaults-deep') -class MyBundle extends libp2p { - constructor (_options) { - const defaults = { - modules: { - transport: [ TCP ], - streamMuxer: [ SPDY ] - } - } - - super(defaultsDeep(_options, defaults)) - } -} +const pipe = require('it-pipe') -function createNode (callback) { - let node +const createNode = async () => { + const peerInfo = await PeerInfo.create() + peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') - waterfall([ - (cb) => PeerInfo.create(cb), - (peerInfo, cb) => { - peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') - node = new MyBundle({ - peerInfo - }) - node.start(cb) + const node = await Libp2p.create({ + peerInfo, + modules: { + transport: [TCP], + streamMuxer: [MPLEX], + connEncryption: [SECIO] } - ], (err) => callback(err, node)) -} + }) -parallel([ - (cb) => createNode(cb), - (cb) => createNode(cb) -], (err, nodes) => { - if (err) { throw err } + await node.start() - const node1 = nodes[0] - const node2 = nodes[1] + return node +} - node2.handle('/a', (protocol, conn) => { - pull( - conn, - pull.map((v) => v.toString()), - pull.log() - ) - }) +;(async () => { + const [node1, node2] = await Promise.all([ + createNode(), + createNode() + ]) - node2.handle('/b', (protocol, conn) => { - pull( - conn, - pull.map((v) => v.toString()), - pull.log() + node2.handle(['/a', '/b'], ({ protocol, stream }) => { + pipe( + stream, + async function (source) { + for await (const msg of source) { + console.log(`from: ${protocol}, msg: ${msg.toString()}`) + } + } ) }) - series([ - (cb) => node1.dialProtocol(node2.peerInfo, '/a', (err, conn) => { - if (err) { throw err } - pull(pull.values(['protocol (a)']), conn) - cb() - }), - (cb) => node1.dialProtocol(node2.peerInfo, '/b', (err, conn) => { - if (err) { throw err } - pull(pull.values(['protocol (b)']), conn) - cb() - }), - (cb) => node1.dialProtocol(node2.peerInfo, '/b', (err, conn) => { - if (err) { throw err } - pull(pull.values(['another conn on protocol (b)']), conn) - cb() - }) - ]) -}) + const { stream: stream1 } = await node1.dialProtocol(node2.peerInfo, ['/a']) + await pipe( + ['protocol (a)'], + stream1 + ) + + const { stream: stream2 } = await node1.dialProtocol(node2.peerInfo, ['/b']) + await pipe( + ['protocol (b)'], + stream2 + ) + + const { stream: stream3 } = await node1.dialProtocol(node2.peerInfo, ['/b']) + await pipe( + ['another stream on protocol (b)'], + stream3 + ) +})(); diff --git a/examples/protocol-and-stream-muxing/3.js b/examples/protocol-and-stream-muxing/3.js index 453c035ade..68bc1f36b6 100644 --- a/examples/protocol-and-stream-muxing/3.js +++ b/examples/protocol-and-stream-muxing/3.js @@ -1,88 +1,69 @@ /* eslint-disable no-console */ 'use strict' -const libp2p = require('../../') +const Libp2p = require('../../') const TCP = require('libp2p-tcp') -const SPDY = require('libp2p-spdy') +const MPLEX = require('libp2p-mplex') +const SECIO = require('libp2p-secio') const PeerInfo = require('peer-info') -const waterfall = require('async/waterfall') -const parallel = require('async/parallel') -const series = require('async/series') -const pull = require('pull-stream') -const defaultsDeep = require('@nodeutils/defaults-deep') -class MyBundle extends libp2p { - constructor (_options) { - const defaults = { - modules: { - transport: [ TCP ], - streamMuxer: [ SPDY ] - } - } - - super(defaultsDeep(_options, defaults)) - } -} +const pipe = require('it-pipe') -function createNode (callback) { - let node +const createNode = async () => { + const peerInfo = await PeerInfo.create() + peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') - waterfall([ - (cb) => PeerInfo.create(cb), - (peerInfo, cb) => { - peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') - node = new MyBundle({ - peerInfo - }) - node.start(cb) + const node = await Libp2p.create({ + peerInfo, + modules: { + transport: [TCP], + streamMuxer: [MPLEX], + connEncryption: [SECIO] } - ], (err) => callback(err, node)) -} + }) -parallel([ - (cb) => createNode(cb), - (cb) => createNode(cb) -], (err, nodes) => { - if (err) { throw err } + await node.start() - const node1 = nodes[0] - const node2 = nodes[1] + return node +} - node1.handle('/node-1', (protocol, conn) => { - pull( - conn, - pull.map((v) => v.toString()), - pull.log() +;(async () => { + const [node1, node2] = await Promise.all([ + createNode(), + createNode() + ]) + + node1.handle('/node-1', ({ stream }) => { + pipe( + stream, + async function (source) { + for await (const msg of source) { + console.log(msg.toString()) + } + } ) }) - node2.handle('/node-2', (protocol, conn) => { - pull( - conn, - pull.map((v) => v.toString()), - pull.log() + node2.handle('/node-2', ({ stream }) => { + pipe( + stream, + async function (source) { + for await (const msg of source) { + console.log(msg.toString()) + } + } ) }) - series([ - (cb) => node1.dialProtocol(node2.peerInfo, '/node-2', (err, conn) => { - if (err) { throw err } - pull(pull.values(['from 1 to 2']), conn) - cb() - }), - (cb) => node2.dialProtocol(node1.peerInfo, '/node-1', (err, conn) => { - if (err) { throw err } - pull(pull.values(['from 2 to 1']), conn) - cb() - }) - ], (err) => { - if (err) { throw err } - console.log('Addresses by which both peers are connected') - node1.peerBook - .getAllArray() - .forEach((peer) => console.log('node 1 to node 2:', peer.isConnected().toString())) - node2.peerBook - .getAllArray() - .forEach((peer) => console.log('node 2 to node 1:', peer.isConnected().toString())) - }) -}) + const { stream: stream1 } = await node1.dialProtocol(node2.peerInfo, ['/node-2']) + await pipe( + ['from 1 to 2'], + stream1 + ) + + const { stream: stream2 } = await node2.dialProtocol(node1.peerInfo, ['/node-1']) + await pipe( + ['from 2 to 1'], + stream2 + ) +})(); diff --git a/examples/protocol-and-stream-muxing/README.md b/examples/protocol-and-stream-muxing/README.md index 1f526e6274..b85c790353 100644 --- a/examples/protocol-and-stream-muxing/README.md +++ b/examples/protocol-and-stream-muxing/README.md @@ -6,23 +6,30 @@ The feature of agreeing on a protocol over an established connection is what we # 1. Handle multiple protocols -Let's see _protocol multiplexing_ in action! You will need the following modules for this example: `libp2p`, `libp2p-tcp`, `peer-info`, `async` and `pull-stream`. This example reuses the base left by the [Transports](../transports) example. You can see the complete solution at [1.js](./1.js). +Let's see _protocol multiplexing_ in action! You will need the following modules for this example: `libp2p`, `libp2p-tcp`, `peer-info`, `it-pipe`, `it-buffer` and `streaming-iterables`. This example reuses the base left by the [Transports](../transports) example. You can see the complete solution at [1.js](./1.js). After creating the nodes, we need to tell libp2p which protocols to handle. ```JavaScript +const pipe = require('it-pipe') +const { map } = require('streaming-iterables') +const { toBuffer } = require('it-buffer') + // ... const node1 = nodes[0] const node2 = nodes[1] -// Here we are telling libp2p that is someone dials this node to talk with the `/your-protocol` -// multicodec, the protocol identifier, please call this callback and give it the connection +// Here we are telling libp2p that if someone dials this node to talk with the `/your-protocol` +// multicodec, the protocol identifier, please call this handler and give it the stream // so that incomming data can be handled -node2.handle('/your-protocol', (protocol, conn) => { - pull( - conn, - pull.map((v) => v.toString()), - pull.log() +node2.handle('/your-protocol', ({ stream }) => { + pipe( + stream, + source => (async function () { + for await (const msg of source) { + console.log(msg.toString()) + } + })() ) }) ``` @@ -30,53 +37,54 @@ node2.handle('/your-protocol', (protocol, conn) => { After the protocol is _handled_, now we can dial to it. ```JavaScript -node1.dialProtocol(node2.peerInfo, '/your-protocol', (err, conn) => { - if (err) { throw err } - pull(pull.values(['my own protocol, wow!']), conn) -}) +const { stream } = await node1.dialProtocol(node2.peerInfo, ['/your-protocol']) + +await pipe( + ['my own protocol, wow!'], + stream +) ``` You might have seen this in the [Transports](../transports) examples. However, what it was not explained is that you can do more than exact string matching, for example, you can use semver. ```JavaScript -node2.handle('/another-protocol/1.0.1', (protocol, conn) => { - pull( - conn, - pull.map((v) => v.toString()), - pull.log() +node2.handle('/another-protocol/1.0.1', ({ stream }) => { + pipe( + stream, + async function (source) { + for await (const msg of source) { + console.log(msg.toString()) + } + } ) }) // ... -node1.dialProtocol(node2.peerInfo, '/another-protocol/1.0.0', (err, conn) => { - if (err) { throw err } - pull(pull.values(['semver me please']), conn) -}) +const { stream } = await node1.dialProtocol(node2.peerInfo, ['/another-protocol/1.0.0']) + +await pipe( + ['my own protocol, wow!'], + stream +) ``` This feature is super power for network protocols. It works in the same way as versioning your RPC/REST API, but for anything that goes in the wire. We had to use this feature to upgrade protocols within the IPFS Stack (i.e Bitswap) and we successfully managed to do so without any network splits. -There is still one last feature, you can create your custom match functions. +There is still one last feature, you can provide multiple protocols for the same handler. If you have a backwards incompatible change, but it only requires minor changes to the code, you may prefer to do protocol checking instead of having multiple handlers ```JavaScript -node2.handle('/custom-match-func', (protocol, conn) => { - pull( - conn, - pull.map((v) => v.toString()), - pull.log() - ) -}, (myProtocol, requestedProtocol, callback) => { - // This is all custom. I'm checking the base path matches, think of this - // as a HTTP routing table. - if (myProtocol.indexOf(requestedProtocol)) { - callback(null, true) - } else { - callback(null, false) +node2.handle(['/another-protocol/1.0.0', '/another-protocol/2.0.0'], ({ protocol, stream }) => { + if (protocol === '/another-protocol/2.0.0') { + // handle backwards compatibility } -}) -// ... -node1.dialProtocol(node2.peerInfo, '/custom-match-func/some-query', (err, conn) => { - if (err) { throw err } - pull(pull.values(['do I fall into your criteria?']), conn) + + pipe( + stream, + async function (source) { + for await (const msg of source) { + console.log(msg.toString()) + } + } + ) }) ``` @@ -84,77 +92,68 @@ Try all of this out by executing [1.js](./1.js). # 2. Reuse existing connection -The example above would require a node to create a whole new connection for every time it dials in one of the protocols, this is a waste of resources and also it might be simply not possible (e.g lack of file descriptors, not enough ports being open, etc). What we really want is to dial a connection once and then multiplex several virtual connections (stream) over a single connection, this is where _stream multiplexing_ comes into play. +The examples above would require a node to create a whole new connection for every time it dials in one of the protocols, this is a waste of resources and also it might be simply not possible (e.g lack of file descriptors, not enough ports being open, etc). What we really want is to dial a connection once and then multiplex several virtual connections (stream) over a single connection, this is where _stream multiplexing_ comes into play. -Stream multiplexing is a old concept, in fact it happens in many of the layers of the [OSI System](https://en.wikipedia.org/wiki/OSI_model), in libp2p we make this feature to our avail by letting the user pick which module for stream multiplexing to use. +Stream multiplexing is an old concept, in fact it happens in many of the layers of the [OSI System](https://en.wikipedia.org/wiki/OSI_model). In libp2p, we make this feature to our avail by letting the user pick which module for stream multiplexing to use. -Currently, we have two available [libp2p-spdy](https://github.com/libp2p/js-libp2p-spdy) and [libp2p-mplex](https://github.com/libp2p/js-libp2p-mplex) and pluging them in is as easy as adding another transport. Let's revisit our libp2p bundle. +Currently, we have [libp2p-mplex](https://github.com/libp2p/js-libp2p-mplex) and pluging it in is as easy as adding a transport. Let's revisit our libp2p configuration. ```JavaScript -const SPDY = require('libp2p-spdy') +const Libp2p = require('libp2p') +const TCP = require('libp2p-tcp') +const MPLEX = require('libp2p-mplex') //... -class MyBundle extends libp2p { - constructor (_options) { - const defaults = { - modules: { - transport: [ TCP ], - // Here we are adding the SPDY muxer to our libp2p bundle. - // Thanks to protocol muxing, a libp2p bundle can support multiple Stream Muxers at the same - // time and pick the right one when dialing to a node - streamMuxer: [ SPDY ] - } - } - super(defaultsDeep(_options, defaults)) - } +const createNode = () => { + return Libp2p.create({ + modules: { + transport: [ TCP ], + streamMuxer: [ Mplex ] + } + }) } ``` With this, we can dial as many times as we want to a peer and always reuse the same established underlying connection. ```JavaScript -node2.handle('/a', (protocol, conn) => { - pull( - conn, - pull.map((v) => v.toString()), - pull.log() - ) -}) - -node2.handle('/b', (protocol, conn) => { - pull( - conn, - pull.map((v) => v.toString()), - pull.log() +node2.handle(['/a', '/b'], ({ protocol, stream }) => { + pipe( + stream, + async function (source) { + for await (const msg of source) { + console.log(`from: ${protocol}, msg: ${msg.toString()}`) + } + } ) }) -series([ - (cb) => node1.dialProtocol(node2.peerInfo, '/a', (err, conn) => { - if (err) { throw err } - pull(pull.values(['protocol (a)']), conn) - cb() - }), - (cb) => node1.dialProtocol(node2.peerInfo, '/b', (err, conn) => { - if (err) { throw err } - pull(pull.values(['protocol (b)']), conn) - cb() - }), - (cb) => node1.dialProtocol(node2.peerInfo, '/b', (err, conn) => { - if (err) { throw err } - pull(pull.values(['another conn on protocol (b)']), conn) - cb() - }) -]) +const { stream } = await node1.dialProtocol(node2.peerInfo, ['/a']) +await pipe( + ['protocol (a)'], + stream +) + +const { stream: stream2 } = await node1.dialProtocol(node2.peerInfo, ['/b']) +await pipe( + ['protocol (b)'], + stream2 +) + +const { stream: stream3 } = await node1.dialProtocol(node2.peerInfo, ['/b']) +await pipe( + ['another stream on protocol (b)'], + stream3 +) ``` By running [2.js](./2.js) you should see the following result: ``` > node 2.js -protocol (a) -protocol (b) -another protocol (b) +from: /a, msg: protocol (a) +from: /b, msg: protocol (b) +from: /b, msg: another stream on protocol (b) ``` # 3. Bidirectional connections @@ -168,8 +167,5 @@ You can see this working on example [3.js](./3.js). The result should look like ```Bash > node 3.js from 1 to 2 -Addresses by which both peers are connected -node 1 to node 2: /ip4/127.0.0.1/tcp/50629/p2p/QmZwMKTo6wG4Te9A6M2eJnWDpR8uhsGed4YRegnV5DcKiv -node 2 to node 1: /ip4/127.0.0.1/tcp/50630/p2p/QmRgormJQeDyXhDKma11eUtksoh8vWmeBoxghVt4meauW9 from 2 to 1 ``` From a1717dac6a1625bb113cc5f048d32f05ea9143e9 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Mon, 6 Jan 2020 14:03:07 +0000 Subject: [PATCH 79/92] fix: conn mngr min/max connection values (#528) Fixes the case when options are passed with `maxConnections` and/or `minConnections` set to `undefined`: ```console { defaultOptions: { maxConnections: Infinity, minConnections: 0, maxData: Infinity, maxSentData: Infinity, maxReceivedData: Infinity, maxEventLoopDelay: Infinity, pollInterval: 2000, movingAverageInterval: 60000, defaultPeerValue: 1 }, options: { minPeers: 25, maxConnections: undefined, minConnections: undefined } } { maxConnections: undefined, minConnections: undefined } 1) "before all" hook in "custom config" (node:67176) UnhandledPromiseRejectionWarning: AssertionError [ERR_ASSERTION]: Connection Manager maxConnections must be greater than minConnections at new ConnectionManager (node_modules/libp2p/src/connection-manager/index.js:43:5) at new Libp2p (node_modules/libp2p/src/index.js:92:30) at Object.module.exports [as libp2p] (src/core/components/libp2p.js:27:10) at Proxy.start (src/core/components/start.js:48:31) at async Daemon.start (src/cli/daemon.js:63:31) at async startHttpAPI (test/http-api/routes.js:29:5) at async Context. (test/http-api/routes.js:48:7) ``` --- package.json | 2 +- src/connection-manager/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 759547b2aa..dc45ee50b8 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "libp2p-crypto": "^0.17.1", "libp2p-interfaces": "^0.1.5", "mafmt": "^7.0.0", - "merge-options": "^1.0.1", + "merge-options": "^2.0.0", "moving-average": "^1.0.0", "multiaddr": "^7.2.1", "multistream-select": "^0.15.0", diff --git a/src/connection-manager/index.js b/src/connection-manager/index.js index 1d01a7560a..15f630f241 100644 --- a/src/connection-manager/index.js +++ b/src/connection-manager/index.js @@ -37,7 +37,7 @@ class ConnectionManager { this._libp2p = libp2p this._registrar = libp2p.registrar this._peerId = libp2p.peerInfo.id.toString() - this._options = mergeOptions(defaultOptions, options) + this._options = mergeOptions.call({ ignoreUndefined: true }, defaultOptions, options) assert( this._options.maxConnections > this._options.minConnections, 'Connection Manager maxConnections must be greater than minConnections' From f182f5bcd9ca8c4d786bee1cd17c28efa8efb5c6 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Mon, 6 Jan 2020 18:01:28 +0100 Subject: [PATCH 80/92] refactor: examples/discovery-mechanisms (#498) * refactor: examples-discovery-mechanisms * chore: apply suggestions from code review Co-Authored-By: Jacob Heun * chore: suggestion interval Co-Authored-By: Jacob Heun * chore: add peer connected event Co-authored-by: Jacob Heun --- examples/discovery-mechanisms/1.js | 63 ++++----- examples/discovery-mechanisms/2.js | 74 ++++------- examples/discovery-mechanisms/README.md | 165 +++++++++++------------- 3 files changed, 122 insertions(+), 180 deletions(-) diff --git a/examples/discovery-mechanisms/1.js b/examples/discovery-mechanisms/1.js index f2cd89de5d..18ed24ef95 100644 --- a/examples/discovery-mechanisms/1.js +++ b/examples/discovery-mechanisms/1.js @@ -1,14 +1,11 @@ /* eslint-disable no-console */ 'use strict' -const libp2p = require('../../') +const Libp2p = require('../../') const TCP = require('libp2p-tcp') const Mplex = require('libp2p-mplex') const SECIO = require('libp2p-secio') -const PeerInfo = require('peer-info') const Bootstrap = require('libp2p-bootstrap') -const waterfall = require('async/waterfall') -const defaultsDeep = require('@nodeutils/defaults-deep') // Find this list at: https://github.com/ipfs/js-ipfs/blob/master/src/core/runtime/config-nodejs.json const bootstrapers = [ @@ -23,51 +20,35 @@ const bootstrapers = [ '/ip4/104.236.151.122/tcp/4001/p2p/QmSoLju6m7xTh3DuokvT3886QRYqxAzb1kShaanJgW36yx' ] -class MyBundle extends libp2p { - constructor (_options) { - const defaults = { - modules: { - transport: [ TCP ], - streamMuxer: [ Mplex ], - connEncryption: [ SECIO ], - peerDiscovery: [ Bootstrap ] - }, - config: { - peerDiscovery: { - autoDial: true, - bootstrap: { - interval: 20e3, - enabled: true, - list: bootstrapers - } +;(async () => { + const node = await Libp2p.create({ + modules: { + transport: [TCP], + streamMuxer: [Mplex], + connEncryption: [SECIO], + peerDiscovery: [Bootstrap] + }, + config: { + peerDiscovery: { + bootstrap: { + interval: 60e3, + enabled: true, + list: bootstrapers } } } + }) - super(defaultsDeep(_options, defaults)) - } -} - -let node + node.peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') -waterfall([ - (cb) => PeerInfo.create(cb), - (peerInfo, cb) => { - peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') - node = new MyBundle({ - peerInfo - }) - node.start(cb) - } -], (err) => { - if (err) { throw err } + node.on('peer:connect', (peer) => { + console.log('Connection established to:', peer.id.toB58String()) // Emitted when a peer has been found + }) node.on('peer:discovery', (peer) => { // No need to dial, autoDial is on console.log('Discovered:', peer.id.toB58String()) }) - node.on('peer:connect', (peer) => { - console.log('Connection established to:', peer.id.toB58String()) - }) -}) + await node.start() +})(); diff --git a/examples/discovery-mechanisms/2.js b/examples/discovery-mechanisms/2.js index 8c5c630e2b..b5ca50e662 100644 --- a/examples/discovery-mechanisms/2.js +++ b/examples/discovery-mechanisms/2.js @@ -1,63 +1,45 @@ /* eslint-disable no-console */ 'use strict' -const libp2p = require('../../') +const Libp2p = require('../../') const TCP = require('libp2p-tcp') const Mplex = require('libp2p-mplex') const SECIO = require('libp2p-secio') -const PeerInfo = require('peer-info') const MulticastDNS = require('libp2p-mdns') -const waterfall = require('async/waterfall') -const parallel = require('async/parallel') -const defaultsDeep = require('@nodeutils/defaults-deep') -class MyBundle extends libp2p { - constructor (_options) { - const defaults = { - modules: { - transport: [ TCP ], - streamMuxer: [ Mplex ], - connEncryption: [ SECIO ], - peerDiscovery: [ MulticastDNS ] - }, - config: { - peerDiscovery: { - mdns: { - interval: 20e3, - enabled: true - } +const createNode = async () => { + const node = await Libp2p.create({ + modules: { + transport: [TCP], + streamMuxer: [Mplex], + connEncryption: [SECIO], + peerDiscovery: [MulticastDNS] + }, + config: { + peerDiscovery: { + mdns: { + interval: 20e3, + enabled: true } } } + }) + node.peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') - super(defaultsDeep(_options, defaults)) - } + return node } -function createNode (callback) { - let node - - waterfall([ - (cb) => PeerInfo.create(cb), - (peerInfo, cb) => { - peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') - node = new MyBundle({ - peerInfo - }) - node.start(cb) - } - ], (err) => callback(err, node)) -} - -parallel([ - (cb) => createNode(cb), - (cb) => createNode(cb) -], (err, nodes) => { - if (err) { throw err } - - const node1 = nodes[0] - const node2 = nodes[1] +;(async () => { + const [node1, node2] = await Promise.all([ + createNode(), + createNode() + ]) node1.on('peer:discovery', (peer) => console.log('Discovered:', peer.id.toB58String())) node2.on('peer:discovery', (peer) => console.log('Discovered:', peer.id.toB58String())) -}) + + await Promise.all([ + node1.start(), + node2.start() + ]) +})(); diff --git a/examples/discovery-mechanisms/README.md b/examples/discovery-mechanisms/README.md index 52ec93d3fb..ac7e889735 100644 --- a/examples/discovery-mechanisms/README.md +++ b/examples/discovery-mechanisms/README.md @@ -10,36 +10,32 @@ These mechanisms save configuration and enable a node to operate without any exp For this demo, we will connect to IPFS default bootstrapper nodes and so, we will need to support the same set of features those nodes have, that are: TCP, mplex and SECIO. You can see the complete example at [1.js](./1.js). -First, we create our libp2p bundle. +First, we create our libp2p node. ```JavaScript -const Bootstrap = require('libp2p-railing') -class MyBundle extends libp2p { - constructor (peerInfo) { - const defaults = { - modules: { - transport: [ TCP ], - streamMuxer: [ Mplex ], - connEncryption: [ SECIO ], - peerDiscovery: [ Bootstrap ] - }, - config: { - peerDiscovery: { - bootstrap: { - interval: 2000, - enabled: true, - list: bootstrapers - } - } +const Libp2p = require('libp2p') +const Bootstrap = require('libp2p-bootstrap') + +const node = Libp2p.create({ + modules: { + transport: [ TCP ], + streamMuxer: [ Mplex ], + connEncryption: [ SECIO ], + peerDiscovery: [ Bootstrap ] + }, + config: { + peerDiscovery: { + bootstrap: { + interval: 60e3, + enabled: true, + list: bootstrapers } } - - super(defaultsDeep(_options, defaults)) } -} +}) ``` -In this bundle, we use a `bootstrappers` array listing peers to connect _on boot_. Here is the list used by js-ipfs and go-ipfs. +In this configuration, we use a `bootstrappers` array listing peers to connect _on boot_. Here is the list used by js-ipfs and go-ipfs. ```JavaScript const bootstrapers = [ @@ -58,34 +54,37 @@ const bootstrapers = [ Now, once we create and start the node, we can listen for events such as `peer:discovery` and `peer:connect`, these events tell us when we found a peer, independently of the discovery mechanism used and when we actually dialed to that peer. ```JavaScript -let node - -waterfall([ - (cb) => PeerInfo.create(cb), - (peerInfo, cb) => { - peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') - node = new MyBundle({ - peerInfo - }) - node.start(cb) +const node = await Libp2p.create({ + peerInfo, + modules: { + transport: [ TCP ], + streamMuxer: [ Mplex ], + connEncryption: [ SECIO ], + peerDiscovery: [ Bootstrap ] + }, + config: { + peerDiscovery: { + bootstrap: { + interval: 60e3, + enabled: true, + list: bootstrapers + } + } } -], (err) => { - if (err) { throw err } - - // Emitted when a peer has been found - node.on('peer:discovery', (peer) => { - console.log('Discovered:', peer.id.toB58String()) - // Note how we need to dial, even if just to warm up the Connection (by not - // picking any protocol) in order to get a full Connection. The Peer Discovery - // doesn't make any decisions for you. - node.dial(peer, () => {}) - }) +}) - // Once the dial is complete, this event is emitted. - node.on('peer:connect', (peer) => { - console.log('Connection established to:', peer.id.toB58String()) - }) +node.peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') + +node.on('peer:connect', (peer) => { + console.log('Connection established to:', peer.id.toB58String()) // Emitted when a peer has been found +}) + +// Emitted when a peer has been found +node.on('peer:discovery', (peer) => { + console.log('Discovered:', peer.id.toB58String()) }) + +await node.start() ``` From running [1.js](./1.js), you should see the following: @@ -101,76 +100,56 @@ Discovered: QmSoLV4Bbm51jM9C4gDYZQ9Cy3U6aXMJDAbzgu2fzaDs64 Discovered: QmSoLer265NRgSp2LA3dPaeykiS1J6DifTC88f5uVQKNAd Discovered: QmSoLMeWqB7YGVLJN3pNLQpmmEk35v6wYtsMGLzSr5QBU3 Discovered: QmSoLju6m7xTh3DuokvT3886QRYqxAzb1kShaanJgW36yx -Connection established to: QmSoLMeWqB7YGVLJN3pNLQpmmEk35v6wYtsMGLzSr5QBU3 -Connection established to: QmSoLer265NRgSp2LA3dPaeykiS1J6DifTC88f5uVQKNAd -Connection established to: QmSoLV4Bbm51jM9C4gDYZQ9Cy3U6aXMJDAbzgu2fzaDs64 -Connection established to: QmSoLueR4xBeUbY9WZ9xGUUxunbKWcrNFTDAadQJmocnWm -Connection established to: QmSoLPppuBtQSGwKDZT2M73ULpjvfd3aZ6ha4oFGL1KrGM -Connection established to: QmSoLju6m7xTh3DuokvT3886QRYqxAzb1kShaanJgW36yx -Connection established to: QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ -Connection established to: QmSoLnSGccFuZQJzRadHn95W2CrSFmZuTdDWP8HXaHca9z -Connection established to: QmSoLSafTMBsPKadTEgaXctDQVcqN88CNLHXMkTNwMKPnu ``` ## 2. MulticastDNS to find other peers in the network For this example, we need `libp2p-mdns`, go ahead and `npm install` it. You can find the complete solution at [2.js](./2.js). -Update your libp2p bundle to include MulticastDNS. +Update your libp2p configuration to include MulticastDNS. ```JavaScript -class MyBundle extends libp2p { - constructor (peerInfo) { - const defaults = { - modules: { - transport: [ TCP ], - streamMuxer: [ Mplex ], - connEncryption: [ SECIO ], - peerDiscovery: [ MulticastDNS ] - }, - config: { - peerDiscovery: { - mdns: { - // Run at 1s so we can observe more quickly, default is 10s - interval: 1000, - enabled: true - } +const Libp2p = require('libp2p') +const MulticastDNS = require('libp2p-mdns') + +const createNode = () => { + return Libp2p.create({ + modules: { + transport: [ TCP ], + streamMuxer: [ Mplex ], + connEncryption: [ SECIO ], + peerDiscovery: [ MulticastDNS ] + }, + config: { + peerDiscovery: { + mdns: { + interval: 20e3, + enabled: true } } } - - super(defaultsDeep(_options, defaults)) - } + }) } ``` To observe it working, spawn two nodes. ```JavaScript -parallel([ - (cb) => createNode(cb), - (cb) => createNode(cb) -], (err, nodes) => { - if (err) { throw err } - - const node1 = nodes[0] - const node2 = nodes[1] +const [node1, node2] = await Promise.all([ + createNode(), + createNode() +]) - node1.on('peer:discovery', (peer) => console.log('Discovered:', peer.id.toB58String())) - node2.on('peer:discovery', (peer) => console.log('Discovered:', peer.id.toB58String())) -}) +node1.on('peer:discovery', (peer) => console.log('Discovered:', peer.id.toB58String())) +node2.on('peer:discovery', (peer) => console.log('Discovered:', peer.id.toB58String())) ``` -If you run this example, you will see a continuous stream of each peer discovering each other. +If you run this example, you will see the other peers being discovered. ```bash > node 2.js Discovered: QmSSbQpuKrxkoXHm1v4Pi35hPN5hUHMZoBoawEs2Nhvi8m Discovered: QmRcXXhtG8vTqwVBRonKWtV4ovDoC1Fe56WYtcrw694eiJ -Discovered: QmSSbQpuKrxkoXHm1v4Pi35hPN5hUHMZoBoawEs2Nhvi8m -Discovered: QmRcXXhtG8vTqwVBRonKWtV4ovDoC1Fe56WYtcrw694eiJ -Discovered: QmSSbQpuKrxkoXHm1v4Pi35hPN5hUHMZoBoawEs2Nhvi8m -Discovered: QmRcXXhtG8vTqwVBRonKWtV4ovDoC1Fe56WYtcrw694eiJ ``` ## 3. Where to find other Peer Discovery Mechanisms From 0840739a00c5ee9f5c203d6beed112c6917dc76b Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Tue, 7 Jan 2020 14:53:27 +0100 Subject: [PATCH 81/92] refactor: examples/pnet (#523) * refactor: examples/pnet * chore: rename pnet-ipfs to pnet * chore: address review --- examples/pnet-ipfs/index.js | 145 ---------------------- examples/pnet-ipfs/libp2p-bundle.js | 60 --------- examples/{pnet-ipfs => pnet}/.gitignore | 0 examples/{pnet-ipfs => pnet}/README.md | 4 +- examples/pnet/index.js | 50 ++++++++ examples/pnet/libp2p-node.js | 36 ++++++ examples/{pnet-ipfs => pnet}/package.json | 9 +- examples/{pnet-ipfs => pnet}/utils.js | 0 8 files changed, 92 insertions(+), 212 deletions(-) delete mode 100644 examples/pnet-ipfs/index.js delete mode 100644 examples/pnet-ipfs/libp2p-bundle.js rename examples/{pnet-ipfs => pnet}/.gitignore (100%) rename examples/{pnet-ipfs => pnet}/README.md (90%) create mode 100644 examples/pnet/index.js create mode 100644 examples/pnet/libp2p-node.js rename examples/{pnet-ipfs => pnet}/package.json (70%) rename examples/{pnet-ipfs => pnet}/utils.js (100%) diff --git a/examples/pnet-ipfs/index.js b/examples/pnet-ipfs/index.js deleted file mode 100644 index 1df9417157..0000000000 --- a/examples/pnet-ipfs/index.js +++ /dev/null @@ -1,145 +0,0 @@ -/* eslint no-console: ["off"] */ -'use strict' - -const IPFS = require('ipfs') -const assert = require('assert').strict -const { generate: writeKey } = require('libp2p/src/pnet') -const path = require('path') -const fs = require('fs') -const privateLibp2pBundle = require('./libp2p-bundle') -const { mkdirp } = require('./utils') - -// Create two separate repo paths so we can run two nodes and check their output -const repo1 = path.resolve('./tmp', 'repo1', '.ipfs') -const repo2 = path.resolve('./tmp', 'repo2', '.ipfs') -mkdirp(repo1) -mkdirp(repo2) - -// Create a buffer and write the swarm key to it -const swarmKey = Buffer.alloc(95) -writeKey(swarmKey) - -// This key is for the `TASK` mentioned in the writeFileSync calls below -const otherSwarmKey = Buffer.alloc(95) -writeKey(otherSwarmKey) - -// Add the swarm key to both repos -const swarmKey1Path = path.resolve(repo1, 'swarm.key') -const swarmKey2Path = path.resolve(repo2, 'swarm.key') -fs.writeFileSync(swarmKey1Path, swarmKey) -// TASK: switch the commented out line below so we're using a different key, to see the nodes fail to connect -fs.writeFileSync(swarmKey2Path, swarmKey) -// fs.writeFileSync(swarmKey2Path, otherSwarmKey) - -// Create the first ipfs node -const node1 = new IPFS({ - repo: repo1, - libp2p: privateLibp2pBundle(swarmKey1Path), - config: { - Addresses: { - // Set the swarm address so we dont get port collision on the nodes - Swarm: ['/ip4/0.0.0.0/tcp/9101'] - } - } -}) - -// Create the second ipfs node -const node2 = new IPFS({ - repo: repo2, - libp2p: privateLibp2pBundle(swarmKey2Path), - config: { - Addresses: { - // Set the swarm address so we dont get port collision on the nodes - Swarm: ['/ip4/0.0.0.0/tcp/9102'] - } - } -}) - -console.log('auto starting the nodes...') - -// `nodesStarted` keeps track of how many of our nodes have started -let nodesStarted = 0 -/** - * Calls `connectAndTalk` when both nodes have started - * @returns {void} - */ -const didStartHandler = () => { - if (++nodesStarted === 2) { - // If both nodes are up, start talking - connectAndTalk() - } -} - -/** - * Exits the process when all started nodes have stopped - * @returns {void} - */ -const didStopHandler = () => { - if (--nodesStarted < 1) { - console.log('all nodes stopped, exiting.') - process.exit(0) - } -} - -/** - * Stops the running nodes - * @param {Error} err An optional error to log to the console - * @returns {void} - */ -const doStop = (err) => { - if (err) { - console.error(err) - } - - console.log('Shutting down...') - node1.stop() - node2.stop() -} - -/** - * Connects the IPFS nodes and transfers data between them - * @returns {void} - */ -const connectAndTalk = async () => { - console.log('connecting the nodes...') - const node2Id = await node2.id() - const dataToAdd = Buffer.from('Hello, private friend!') - - // Connect the nodes - // This will error when different private keys are used - try { - await node1.swarm.connect(node2Id.addresses[0]) - } catch (err) { - return doStop(err) - } - console.log('the nodes are connected, let\'s add some data') - - // Add some data to node 1 - let addedCID - try { - addedCID = await node1.add(dataToAdd) - } catch (err) { - return doStop(err) - } - console.log(`added ${addedCID[0].path} to the node1`) - - // Retrieve the data from node 2 - let cattedData - try { - cattedData = await node2.cat(addedCID[0].path) - } catch (err) { - return doStop(err) - } - assert.deepEqual(cattedData.toString(), dataToAdd.toString(), 'Should have equal data') - console.log(`successfully retrieved "${dataToAdd.toString()}" from node2`) - - doStop() -} - -// Wait for the nodes to boot -node1.once('start', didStartHandler) -node2.once('start', didStartHandler) - -// Listen for the nodes stopping so we can cleanup -node1.once('stop', didStopHandler) -node2.once('stop', didStopHandler) diff --git a/examples/pnet-ipfs/libp2p-bundle.js b/examples/pnet-ipfs/libp2p-bundle.js deleted file mode 100644 index ff4fd37e82..0000000000 --- a/examples/pnet-ipfs/libp2p-bundle.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict' - -const Libp2p = require('libp2p') -const TCP = require('libp2p-tcp') -const MPLEX = require('libp2p-mplex') -const SECIO = require('libp2p-secio') -const fs = require('fs') -const Protector = require('libp2p/src/pnet') - -/** - * Options for the libp2p bundle - * @typedef {Object} libp2pBundle~options - * @property {PeerInfo} peerInfo - The PeerInfo of the IPFS node - * @property {PeerBook} peerBook - The PeerBook of the IPFS node - * @property {Object} config - The config of the IPFS node - * @property {Object} options - The options given to the IPFS node - */ - -/** - * privateLibp2pBundle returns a libp2p bundle function that will use the swarm - * key at the given `swarmKeyPath` to create the Protector - * - * @param {string} swarmKeyPath The path to our swarm key - * @returns {libp2pBundle} Returns a libp2pBundle function for use in IPFS creation - */ -const privateLibp2pBundle = (swarmKeyPath) => { - /** - * This is the bundle we will use to create our fully customized libp2p bundle. - * - * @param {libp2pBundle~options} opts The options to use when generating the libp2p node - * @returns {Libp2p} Our new libp2p node - */ - const libp2pBundle = (opts) => { - // Set convenience variables to clearly showcase some of the useful things that are available - const peerInfo = opts.peerInfo - const peerBook = opts.peerBook - - // Build and return our libp2p node - return new Libp2p({ - peerInfo, - peerBook, - modules: { - transport: [TCP], // We're only using the TCP transport for this example - streamMuxer: [MPLEX], // We're only using mplex muxing - // Let's make sure to use identifying crypto in our pnet since the protector doesn't - // care about node identity, and only the presence of private keys - connEncryption: [SECIO], - // Leave peer discovery empty, we don't want to find peers. We could omit the property, but it's - // being left in for explicit readability. - // We should explicitly dial pnet peers, or use a custom discovery service for finding nodes in our pnet - peerDiscovery: [], - connProtector: new Protector(fs.readFileSync(swarmKeyPath)) - } - }) - } - - return libp2pBundle -} - -module.exports = privateLibp2pBundle diff --git a/examples/pnet-ipfs/.gitignore b/examples/pnet/.gitignore similarity index 100% rename from examples/pnet-ipfs/.gitignore rename to examples/pnet/.gitignore diff --git a/examples/pnet-ipfs/README.md b/examples/pnet/README.md similarity index 90% rename from examples/pnet-ipfs/README.md rename to examples/pnet/README.md index 3e822767f7..b7515e9619 100644 --- a/examples/pnet-ipfs/README.md +++ b/examples/pnet/README.md @@ -1,5 +1,5 @@ -# Private Networking with IPFS -This example shows how to set up a private network of IPFS nodes. +# Private Networking +This example shows how to set up a private network of libp2p nodes. ## Setup Install dependencies: diff --git a/examples/pnet/index.js b/examples/pnet/index.js new file mode 100644 index 0000000000..7e675930ac --- /dev/null +++ b/examples/pnet/index.js @@ -0,0 +1,50 @@ +/* eslint no-console: ["off"] */ +'use strict' + +const { generate } = require('libp2p/src/pnet') +const privateLibp2pNode = require('./libp2p-node') + +const pipe = require('it-pipe') + +// Create a buffer and write the swarm key to it +const swarmKey = Buffer.alloc(95) +generate(swarmKey) + +// This key is for testing a different key not working +const otherSwarmKey = Buffer.alloc(95) +generate(otherSwarmKey) + +;(async () => { + const node1 = await privateLibp2pNode(swarmKey) + + // TASK: switch the commented out line below so we're using a different key, to see the nodes fail to connect + const node2 = await privateLibp2pNode(swarmKey) + // const node2 = await privateLibp2pNode(otherSwarmKey) + + await Promise.all([ + node1.start(), + node2.start() + ]) + + console.log('nodes started...') + + await node1.dial(node2.peerInfo) + + node2.handle('/private', ({ stream }) => { + pipe( + stream, + async function (source) { + for await (const msg of source) { + console.log(msg.toString()) + } + } + ) + }) + + const { stream } = await node1.dialProtocol(node2.peerInfo, '/private') + + await pipe( + ['This message is sent on a private network'], + stream + ) +})(); diff --git a/examples/pnet/libp2p-node.js b/examples/pnet/libp2p-node.js new file mode 100644 index 0000000000..1c5ff79926 --- /dev/null +++ b/examples/pnet/libp2p-node.js @@ -0,0 +1,36 @@ +'use strict' + +const Libp2p = require('libp2p') +const TCP = require('libp2p-tcp') +const MPLEX = require('libp2p-mplex') +const SECIO = require('libp2p-secio') +const Protector = require('libp2p/src/pnet') + +/** + * privateLibp2pNode returns a libp2p node function that will use the swarm + * key at the given `swarmKeyPath` to create the Protector + * + * @param {Buffer} swarmKey + * @returns {Promise} Returns a libp2pNode function for use in IPFS creation + */ +const privateLibp2pNode = async (swarmKeyPath) => { + const node = await Libp2p.create({ + modules: { + transport: [TCP], // We're only using the TCP transport for this example + streamMuxer: [MPLEX], // We're only using mplex muxing + // Let's make sure to use identifying crypto in our pnet since the protector doesn't + // care about node identity, and only the presence of private keys + connEncryption: [SECIO], + // Leave peer discovery empty, we don't want to find peers. We could omit the property, but it's + // being left in for explicit readability. + // We should explicitly dial pnet peers, or use a custom discovery service for finding nodes in our pnet + peerDiscovery: [], + connProtector: new Protector(swarmKeyPath) + } + }) + + node.peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') + return node +} + +module.exports = privateLibp2pNode diff --git a/examples/pnet-ipfs/package.json b/examples/pnet/package.json similarity index 70% rename from examples/pnet-ipfs/package.json rename to examples/pnet/package.json index 649e2711d9..22f9fae1e4 100644 --- a/examples/pnet-ipfs/package.json +++ b/examples/pnet/package.json @@ -11,10 +11,9 @@ "author": "", "license": "ISC", "dependencies": { - "ipfs": "^0.38.0", - "libp2p": "^0.26.2", - "libp2p-mplex": "^0.8.5", - "libp2p-secio": "^0.11.1", - "libp2p-tcp": "^0.13.2" + "libp2p": "../..", + "libp2p-mplex": "^0.9.3", + "libp2p-secio": "^0.12.1", + "libp2p-tcp": "^0.14.2" } } diff --git a/examples/pnet-ipfs/utils.js b/examples/pnet/utils.js similarity index 100% rename from examples/pnet-ipfs/utils.js rename to examples/pnet/utils.js From 21362b5cbe9d39833cb021abf027ecce796f710e Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Tue, 7 Jan 2020 14:56:05 +0100 Subject: [PATCH 82/92] refactor: examples transports (#503) * refactor: examples-transports * chore: apply suggestions from code review Co-Authored-By: Jacob Heun * chore: address review * chore: address review Co-authored-by: Jacob Heun --- examples/transports/1.js | 44 +++-- examples/transports/2.js | 84 +++++---- examples/transports/3.js | 119 +++++++------ examples/transports/README.md | 310 ++++++++++++++-------------------- 4 files changed, 246 insertions(+), 311 deletions(-) diff --git a/examples/transports/1.js b/examples/transports/1.js index 8f334a1691..4b161c602f 100644 --- a/examples/transports/1.js +++ b/examples/transports/1.js @@ -1,39 +1,33 @@ /* eslint-disable no-console */ 'use strict' -const libp2p = require('../../') +const Libp2p = require('../..') const TCP = require('libp2p-tcp') +const SECIO = require('libp2p-secio') const PeerInfo = require('peer-info') -const waterfall = require('async/waterfall') -const defaultsDeep = require('@nodeutils/defaults-deep') -class MyBundle extends libp2p { - constructor (_options) { - const defaults = { - modules: { - transport: [ - TCP - ] - } +const createNode = async (peerInfo) => { + // To signall the addresses we want to be available, we use + // the multiaddr format, a self describable address + peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') + + const node = await Libp2p.create({ + peerInfo, + modules: { + transport: [TCP], + connEncryption: [SECIO] } + }) - super(defaultsDeep(_options, defaults)) - } + await node.start() + return node } -let node - -waterfall([ - (cb) => PeerInfo.create(cb), - (peerInfo, cb) => { - peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') - node = new MyBundle({ peerInfo: peerInfo }) - node.start(cb) - } -], (err) => { - if (err) { throw err } +;(async () => { + const peerInfo = await PeerInfo.create() + const node = await createNode(peerInfo) console.log('node has started (true/false):', node.isStarted()) console.log('listening on:') node.peerInfo.multiaddrs.forEach((ma) => console.log(ma.toString())) -}) +})(); diff --git a/examples/transports/2.js b/examples/transports/2.js index 64465c2206..32a063e09e 100644 --- a/examples/transports/2.js +++ b/examples/transports/2.js @@ -1,39 +1,31 @@ /* eslint-disable no-console */ 'use strict' -const libp2p = require('../../') +const Libp2p = require('../..') const TCP = require('libp2p-tcp') +const SECIO = require('libp2p-secio') +const MPLEX = require('libp2p-mplex') const PeerInfo = require('peer-info') -const waterfall = require('async/waterfall') -const defaultsDeep = require('@nodeutils/defaults-deep') -const parallel = require('async/parallel') -const pull = require('pull-stream') -class MyBundle extends libp2p { - constructor (_options) { - const defaults = { - modules: { - transport: [ - TCP - ] - } - } - - super(defaultsDeep(_options, defaults)) - } -} +const pipe = require('it-pipe') +const concat = require('it-concat') -function createNode (callback) { - let node +const createNode = async (peerInfo) => { + // To signall the addresses we want to be available, we use + // the multiaddr format, a self describable address + peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') - waterfall([ - (cb) => PeerInfo.create(cb), - (peerInfo, cb) => { - peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') - node = new MyBundle({ peerInfo: peerInfo }) - node.start(cb) + const node = await Libp2p.create({ + peerInfo, + modules: { + transport: [TCP], + connEncryption: [SECIO], + streamMuxer: [MPLEX] } - ], (err) => callback(err, node)) + }) + + await node.start() + return node } function printAddrs (node, number) { @@ -41,29 +33,31 @@ function printAddrs (node, number) { node.peerInfo.multiaddrs.forEach((ma) => console.log(ma.toString())) } -parallel([ - (cb) => createNode(cb), - (cb) => createNode(cb) -], (err, nodes) => { - if (err) { throw err } - - const node1 = nodes[0] - const node2 = nodes[1] +;(async () => { + const [peerInfo1, peerInfo2] = await Promise.all([ + PeerInfo.create(), + PeerInfo.create() + ]) + const [node1, node2] = await Promise.all([ + createNode(peerInfo1), + createNode(peerInfo2) + ]) printAddrs(node1, '1') printAddrs(node2, '2') - node2.handle('/print', (protocol, conn) => { - pull( - conn, - pull.map((v) => v.toString()), - pull.log() + node2.handle('/print', async ({ stream }) => { + const result = await pipe( + stream, + concat ) + console.log(result.toString()) }) - node1.dialProtocol(node2.peerInfo, '/print', (err, conn) => { - if (err) { throw err } + const { stream } = await node1.dialProtocol(node2.peerInfo, '/print') - pull(pull.values(['Hello', ' ', 'p2p', ' ', 'world', '!']), conn) - }) -}) + await pipe( + ['Hello', ' ', 'p2p', ' ', 'world', '!'], + stream + ) +})(); diff --git a/examples/transports/3.js b/examples/transports/3.js index c117d648d2..b5e11811c8 100644 --- a/examples/transports/3.js +++ b/examples/transports/3.js @@ -1,70 +1,62 @@ /* eslint-disable no-console */ 'use strict' -const libp2p = require('../../') +const Libp2p = require('../..') const TCP = require('libp2p-tcp') const WebSockets = require('libp2p-websockets') +const SECIO = require('libp2p-secio') +const MPLEX = require('libp2p-mplex') const PeerInfo = require('peer-info') -const waterfall = require('async/waterfall') -const defaultsDeep = require('@nodeutils/defaults-deep') -const parallel = require('async/parallel') -const pull = require('pull-stream') -class MyBundle extends libp2p { - constructor (_options) { - const defaults = { - modules: { - transport: [ - TCP, - WebSockets - ] - } - } - - super(defaultsDeep(_options, defaults)) - } -} +const pipe = require('it-pipe') -function createNode (addrs, callback) { - if (!Array.isArray(addrs)) { - addrs = [addrs] +const createNode = async (peerInfo, transports, multiaddrs = []) => { + if (!Array.isArray(multiaddrs)) { + multiaddrs = [multiaddrs] } - let node + multiaddrs.forEach((addr) => peerInfo.multiaddrs.add(addr)) - waterfall([ - (cb) => PeerInfo.create(cb), - (peerInfo, cb) => { - addrs.forEach((addr) => peerInfo.multiaddrs.add(addr)) - node = new MyBundle({ peerInfo: peerInfo }) - node.start(cb) + const node = await Libp2p.create({ + peerInfo, + modules: { + transport: transports, + connEncryption: [SECIO], + streamMuxer: [MPLEX] } - ], (err) => callback(err, node)) + }) + + await node.start() + return node } -function printAddrs (node, number) { +function printAddrs(node, number) { console.log('node %s is listening on:', number) node.peerInfo.multiaddrs.forEach((ma) => console.log(ma.toString())) } -function print (protocol, conn) { - pull( - conn, - pull.map((v) => v.toString()), - pull.log() +function print ({ stream }) { + pipe( + stream, + async function (source) { + for await (const msg of source) { + console.log(msg.toString()) + } + } ) } -parallel([ - (cb) => createNode('/ip4/0.0.0.0/tcp/0', cb), - (cb) => createNode(['/ip4/0.0.0.0/tcp/0', '/ip4/127.0.0.1/tcp/10000/ws'], cb), - (cb) => createNode('/ip4/127.0.0.1/tcp/20000/ws', cb) -], (err, nodes) => { - if (err) { throw err } - - const node1 = nodes[0] - const node2 = nodes[1] - const node3 = nodes[2] +;(async () => { + const [peerInfo1, peerInfo2, peerInfo3] = await Promise.all([ + PeerInfo.create(), + PeerInfo.create(), + PeerInfo.create() + ]) + const [node1, node2, node3] = await Promise.all([ + createNode(peerInfo1, [TCP], '/ip4/0.0.0.0/tcp/0'), + createNode(peerInfo2, [TCP, WebSockets], ['/ip4/0.0.0.0/tcp/0', '/ip4/127.0.0.1/tcp/10000/ws']), + createNode(peerInfo3, [WebSockets], '/ip4/127.0.0.1/tcp/20000/ws') + ]) printAddrs(node1, '1') printAddrs(node2, '2') @@ -74,21 +66,24 @@ parallel([ node2.handle('/print', print) node3.handle('/print', print) - node1.dialProtocol(node2.peerInfo, '/print', (err, conn) => { - if (err) { throw err } - - pull(pull.values(['node 1 dialed to node 2 successfully']), conn) - }) - - node2.dialProtocol(node3.peerInfo, '/print', (err, conn) => { - if (err) { throw err } + // node 1 (TCP) dials to node 2 (TCP+WebSockets) + const { stream } = await node1.dialProtocol(node2.peerInfo, '/print') + await pipe( + ['node 1 dialed to node 2 successfully'], + stream + ) - pull(pull.values(['node 2 dialed to node 3 successfully']), conn) - }) + // node 2 (TCP+WebSockets) dials to node 2 (WebSockets) + const { stream: stream2 } = await node2.dialProtocol(node3.peerInfo, '/print') + await pipe( + ['node 2 dialed to node 3 successfully'], + stream2 + ) - node3.dialProtocol(node1.peerInfo, '/print', (err, conn) => { - if (err) { - console.log('node 3 failed to dial to node 1 with:', err.message) - } - }) -}) + // node 3 (listening WebSockets) can dial node 1 (TCP) + try { + await node3.dialProtocol(node1.peerInfo, '/print') + } catch (err) { + console.log('node 3 failed to dial to node 1 with:', err.message) + } +})(); diff --git a/examples/transports/README.md b/examples/transports/README.md index 67531217f5..830e534dd0 100644 --- a/examples/transports/README.md +++ b/examples/transports/README.md @@ -1,85 +1,65 @@ # [Transports](http://libp2p.io/implementations/#transports) -libp2p doesn't make assumptions for you, instead, it enables you as the developer of the application to pick the modules you need to run your application, which can vary depending on the runtime you are executing. A libp2p node can use one or more Transports to dial and listen for Connections. These transports are modules that offer a clean interface for dialing and listening, defined by the [interface-transport](https://github.com/libp2p/interface-transport) specification. Some examples of possible transports are: TCP, UTP, WebRTC, QUIC, HTTP, Pigeon and so on. +libp2p doesn't make assumptions for you, instead, it enables you as the developer of the application to pick the modules you need to run your application, which can vary depending on the runtime you are executing. A libp2p node can use one or more Transports to dial and listen for Connections. These transports are modules that offer a clean interface for dialing and listening, defined by the [interface-transport](https://github.com/libp2p/js-interfaces/tree/master/src/transport) specification. Some examples of possible transports are: TCP, UTP, WebRTC, QUIC, HTTP, Pigeon and so on. -A more complete definition of what is a transport can be found on the [interface-transport](https://github.com/libp2p/interface-transport) specification. A way to recognize a candidate transport is through the badge: +A more complete definition of what is a transport can be found on the [interface-transport](https://github.com/libp2p/js-interfaces/tree/master/src/transport) specification. A way to recognize a candidate transport is through the badge: [![](https://raw.githubusercontent.com/diasdavid/interface-transport/master/img/badge.png)](https://raw.githubusercontent.com/diasdavid/interface-transport/master/img/badge.png) -## 1. Creating a libp2p Bundle with TCP +## 1. Creating a libp2p node with TCP -When using libp2p, you always want to create your own libp2p Bundle, that is, pick your set of modules and create your network stack with the properties you need. In this example, we will create a bundle with TCP. You can find the complete solution on the file [1.js](./1.js). +When using libp2p, you need properly configure it, that is, pick your set of modules and create your network stack with the properties you need. In this example, we will create a libp2p node TCP. You can find the complete solution on the file [1.js](./1.js). -You will need 5 deps total, so go ahead and install all of them with: +You will need 4 dependencies total, so go ahead and install all of them with: ```bash -> npm install libp2p libp2p-tcp peer-info async @nodeutils/defaults-deep +> npm install libp2p libp2p-tcp libp2p-secio peer-info ``` Then, on your favorite text editor create a file with the `.js` extension. I've called mine `1.js`. -First thing is to create our own bundle! Insert: +First thing is to create our own libp2p node! Insert: ```JavaScript 'use strict' -const libp2p = require('libp2p') +const Libp2p = require('libp2p') const TCP = require('libp2p-tcp') -const PeerInfo = require('peer-info') -const waterfall = require('async/waterfall') -const defaultsDeep = require('@nodeutils/defaults-deep') - -// This MyBundle class is your libp2p bundle packed with TCP -class MyBundle extends libp2p { - constructor (_options) { - const defaults = { - // modules is a JS object that will describe the components - // we want for our libp2p bundle - modules: { - transport: [ - TCP - ] - } +const SECIO = require('libp2p-secio') + +const createNode = async (peerInfo) => { + // To signall the addresses we want to be available, we use + // the multiaddr format, a self describable address + peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') + + const node = await Libp2p.create({ + peerInfo, + modules: { + transport: [ TCP ], + connEncryption: [ SECIO ] } + }) - super(defaultsDeep(_options, defaults)) - } + await node.start() + return node } ``` -Now that we have our own MyBundle class that extends libp2p, let's create a node with it. We will use `async/waterfall` just for code structure, but you don't need to. Append to the same file: +Now that we have a function to create our own libp2p node, let's create a node with it. ```JavaScript -let node - -waterfall([ - // First we create a PeerInfo object, which will pack the - // info about our peer. Creating a PeerInfo is an async - // operation because we use the WebCrypto API - // (yeei Universal JS) - (cb) => PeerInfo.create(cb), - (peerInfo, cb) => { - // To signall the addresses we want to be available, we use - // the multiaddr format, a self describable address - peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') - // Now we can create a node with that PeerInfo object - node = new MyBundle({ peerInfo: peerInfo }) - // Last, we start the node! - node.start(cb) - } -], (err) => { - if (err) { throw err } - - // At this point the node has started - console.log('node has started (true/false):', node.isStarted()) - // And we can print the now listening addresses. - // If you are familiar with TCP, you might have noticed - // that we specified the node to listen in 0.0.0.0 and port - // 0, which means "listen in any network interface and pick - // a port for me - console.log('listening on:') - node.peerInfo.multiaddrs.forEach((ma) => console.log(ma.toString())) -}) +const peerInfo = await PeerInfo.create() +const node = await createNode(peerInfo) + +// At this point the node has started +console.log('node has started (true/false):', node.isStarted()) +// And we can print the now listening addresses. +// If you are familiar with TCP, you might have noticed +// that we specified the node to listen in 0.0.0.0 and port +// 0, which means "listen in any network interface and pick +// a port for me +console.log('listening on:') +node.peerInfo.multiaddrs.forEach((ma) => console.log(ma.toString())) ``` Running this should result in something like: @@ -96,78 +76,64 @@ That `QmW2cKTakTYqbQkUzBTEGXgWYFj1YEPeUndE1YWs6CBzDQ` is the PeerId that was cre ## 2. Dialing from one node to another node -Now that we have our bundle, let's create two nodes and make them dial to each other! You can find the complete solution at [2.js](./2.js). +Now that we have our `createNode` function, let's create two nodes and make them dial to each other! You can find the complete solution at [2.js](./2.js). For this step, we will need one more dependency. ```bash -> npm install pull-stream +> npm install it-pipe it-buffer ``` And we also need to import the module on our .js file: ```js -const pull = require('pull-stream') +const pipe = require('it-pipe') +const { toBuffer } = require('it-buffer') ``` -We are going to reuse the MyBundle class from step 1, but this time to make things simpler, we will create two functions, one to create nodes and another to print the addrs to avoid duplicating code. +We are going to reuse the `createNode` function from step 1, but this time to make things simpler, we will create another function to print the addrs to avoid duplicating code. ```JavaScript -function createNode (callback) { - let node - - waterfall([ - (cb) => PeerInfo.create(cb), - (peerInfo, cb) => { - peerInfo.multiaddrs.add('/ip4/0.0.0.0/tcp/0') - node = new MyBundle({ peerInfo: peerInfo }) - node.start(cb) - } - ], (err) => callback(err, node)) -} - function printAddrs (node, number) { console.log('node %s is listening on:', number) node.peerInfo.multiaddrs.forEach((ma) => console.log(ma.toString())) } - -``` - -Now we are going to use `async/parallel` to create two nodes, print their addresses and dial from one node to the other. We already added `async` as a dependency, but still need to import `async/parallel`: - -```js -const parallel = require('async/parallel') ``` Then, ```js -parallel([ - (cb) => createNode(cb), - (cb) => createNode(cb) -], (err, nodes) => { - if (err) { throw err } - - const node1 = nodes[0] - const node2 = nodes[1] +;(async () => { + const [peerInfo1, peerInfo2] = await Promise.all([ + PeerInfo.create(), + PeerInfo.create() + ]) + const [node1, node2] = await Promise.all([ + createNode(), + createNode() + ]) printAddrs(node1, '1') printAddrs(node2, '2') - node2.handle('/print', (protocol, conn) => { - pull( - conn, - pull.map((v) => v.toString()), - pull.log() + node2.handle('/print', ({ stream }) => { + pipe( + stream, + async function (source) { + for await (const msg of source) { + console.log(msg.toString()) + } + } ) }) - node1.dialProtocol(node2.peerInfo, '/print', (err, conn) => { - if (err) { throw err } + const { stream } = await node1.dialProtocol(node2.peerInfo, '/print') - pull(pull.values(['Hello', ' ', 'p2p', ' ', 'world', '!']), conn) - }) -}) + await pipe( + ['Hello', ' ', 'p2p', ' ', 'world', '!'], + stream + ) +})(); ``` The result should be look like: @@ -195,46 +161,28 @@ In this example, we will need to also install `libp2p-websockets`, go ahead and > npm install libp2p-websockets ``` -We want to create 3 nodes, one with TCP, one with TCP+WebSockets and one with just WebSockets. We need to update our `MyBundle` class to contemplate WebSockets as well: +We want to create 3 nodes, one with TCP, one with TCP+WebSockets and one with just WebSockets. We need to update our `createNode` function to contemplate WebSockets as well. Moreover, let's upgrade our function to enable us to pick the addrs in which a node will start a listener: ```JavaScript -const WebSockets = require('libp2p-websockets') // ... -class MyBundle extends libp2p { - constructor (_options) { - const defaults = { - modules: { - transport: [ - TCP, - WebSockets - ] - } - } - - super(defaultsDeep(_options, defaults)) - } -} -``` - -Now that we have our bundle ready, let's upgrade our createNode function to enable us to pick the addrs in which a node will start a listener. - -```JavaScript -function createNode (addrs, callback) { - if (!Array.isArray(addrs)) { - addrs = [addrs] +const createNode = async (peerInfo, transports, multiaddrs = []) => { + if (!Array.isArray(multiaddrs)) { + multiaddrs = [multiaddrs] } - let node + multiaddrs.forEach((addr) => peerInfo.multiaddrs.add(addr)) - waterfall([ - (cb) => PeerInfo.create(cb), - (peerInfo, cb) => { - addrs.forEach((addr) => peerInfo.multiaddrs.add(addr)) - node = new MyBundle({ peerInfo: peerInfo }) - node.start(cb) + const node = await Libp2p.create({ + peerInfo, + modules: { + transport: transports, + connEncryption: [ SECIO ] } - ], (err) => callback(err, node)) + }) + + await node.start() + return node } ``` @@ -243,58 +191,61 @@ As a rule, a libp2p node will only be capable of using a transport if: a) it has Let's update our flow to create nodes and see how they behave when dialing to each other: ```JavaScript -parallel([ - (cb) => createNode('/ip4/0.0.0.0/tcp/0', cb), - // Here we add an extra multiaddr that has a /ws at the end, this means that we want - // to create a TCP socket, but mount it as WebSockets instead. - (cb) => createNode(['/ip4/0.0.0.0/tcp/0', '/ip4/127.0.0.1/tcp/10000/ws'], cb), - (cb) => createNode('/ip4/127.0.0.1/tcp/20000/ws', cb) -], (err, nodes) => { - if (err) { throw err } - - const node1 = nodes[0] - const node2 = nodes[1] - const node3 = nodes[2] - - printAddrs(node1, '1') - printAddrs(node2, '2') - printAddrs(node3, '3') - - node1.handle('/print', print) - node2.handle('/print', print) - node3.handle('/print', print) - - // node 1 (TCP) dials to node 2 (TCP+WebSockets) - node1.dialProtocol(node2.peerInfo, '/print', (err, conn) => { - if (err) { throw err } - - pull(pull.values(['node 1 dialed to node 2 successfully']), conn) - }) - - // node 2 (TCP+WebSockets) dials to node 2 (WebSockets) - node2.dialProtocol(node3.peerInfo, '/print', (err, conn) => { - if (err) { throw err } - - pull(pull.values(['node 2 dialed to node 3 successfully']), conn) - }) +const WebSockets = require('libp2p-websockets') +const TCP = require('libp2p-tcp') - // node 3 (WebSockets) attempts to dial to node 1 (TCP) - node3.dialProtocol(node1.peerInfo, '/print', (err, conn) => { - if (err) { - console.log('node 3 failed to dial to node 1 with:', err.message) - } - }) -}) +const [peerInfo1, peerInfo2, peerInfo3] = await Promise.all([ + PeerInfo.create(), + PeerInfo.create(), + PeerInfo.create() +]) +const [node1, node2, node3] = await Promise.all([ + createNode(peerInfo1, [TCP], '/ip4/0.0.0.0/tcp/0'), + createNode(peerInfo2, [TCP, WebSockets], ['/ip4/0.0.0.0/tcp/0', '/ip4/127.0.0.1/tcp/10000/ws']), + createNode(peerInfo3, [WebSockets], '/ip4/127.0.0.1/tcp/20000/ws') +]) + +printAddrs(node1, '1') +printAddrs(node2, '2') +printAddrs(node3, '3') + +node1.handle('/print', print) +node2.handle('/print', print) +node3.handle('/print', print) + +// node 1 (TCP) dials to node 2 (TCP+WebSockets) +const { stream } = await node1.dialProtocol(node2.peerInfo, '/print') +await pipe( + ['node 1 dialed to node 2 successfully'], + stream +) + +// node 2 (TCP+WebSockets) dials to node 2 (WebSockets) +const { stream: stream2 } = await node2.dialProtocol(node3.peerInfo, '/print') +await pipe( + ['node 2 dialed to node 3 successfully'], + stream2 +) + +// node 3 (WebSockets) attempts to dial to node 1 (TCP) +try { + await node3.dialProtocol(node1.peerInfo, '/print') +} catch (err) { + console.log('node 3 failed to dial to node 1 with:', err.message) +} ``` `print` is a function created using the code from 2.js, but factored into its own function to save lines, here it is: ```JavaScript -function print (protocol, conn) { - pull( - conn, - pull.map((v) => v.toString()), - pull.log() +function print ({ stream }) { + pipe( + stream, + async function (source) { + for await (const msg of source) { + console.log(msg.toString()) + } + } ) } ``` @@ -312,18 +263,19 @@ node 2 is listening on: /ip4/192.168.2.156/tcp/62619/p2p/QmWAQtWdzWXibgfyc7WRHhhv6MdqVKzXvyfSTnN2aAvixX node 3 is listening on: /ip4/127.0.0.1/tcp/20000/ws/p2p/QmVq1PWh3VSDYdFqYMtqp4YQyXcrH27N7968tGdM1VQPj1 -node 3 failed to dial to node 1 with: No available transport to dial to node 1 dialed to node 2 successfully node 2 dialed to node 3 successfully +node 3 failed to dial to node 1 with: + Error: No transport available for address /ip4/127.0.0.1/tcp/51482 ``` As expected, we created 3 nodes, node 1 with TCP, node 2 with TCP+WebSockets and node 3 with just WebSockets. node 1 -> node 2 and node 2 -> node 3 managed to dial correctly because they shared a common transport, however, node 3 -> node 1 failed because they didn't share any. ## 4. How to create a new libp2p transport -Today there are already 3 transports available, one in the works and plenty to come, you can find these at [interface-transport implementations](https://github.com/libp2p/interface-transport#modules-that-implement-the-interface) list. +Today there are already several transports available and plenty to come, you can find these at [interface-transport implementations](https://github.com/libp2p/js-interfaces/tree/master/src/transport#modules-that-implement-the-interface) list. -Adding more transports is done through the same way as you added TCP and WebSockets. Some transports might offer extra functionalities, but as far as libp2p is concerned, if it follows the interface defined at the [spec](https://github.com/libp2p/interface-transport#api) it will be able to use it. +Adding more transports is done through the same way as you added TCP and WebSockets. Some transports might offer extra functionalities, but as far as libp2p is concerned, if it follows the interface defined at the [spec](https://github.com/libp2p/js-interfaces/tree/master/src/transport#api) it will be able to use it. If you decide to implement a transport yourself, please consider adding to the list so that others can use it as well. From 122a114d969f82948ce862ddb44d7324a7df5c4f Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Tue, 7 Jan 2020 15:30:59 +0100 Subject: [PATCH 83/92] chore: update contributors --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dc45ee50b8..9561dd4c5f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "libp2p", - "version": "0.27.0-pre.1", + "version": "0.27.0-pre.2", "description": "JavaScript implementation of libp2p, a modular peer to peer network stack", "leadMaintainer": "Jacob Heun ", "main": "src/index.js", From 999d086278d26cee30d89a08d6b636e012868747 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Tue, 7 Jan 2020 15:31:13 +0100 Subject: [PATCH 84/92] chore: release version v0.27.0-pre.2 --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b85327bd2..82eb114145 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ + +# [0.27.0-pre.2](https://github.com/libp2p/js-libp2p/compare/v0.27.0-pre.1...v0.27.0-pre.2) (2020-01-07) + + +### Bug Fixes + +* conn mngr min/max connection values ([#528](https://github.com/libp2p/js-libp2p/issues/528)) ([a1717da](https://github.com/libp2p/js-libp2p/commit/a1717da)) +* make dialer configurable ([#521](https://github.com/libp2p/js-libp2p/issues/521)) ([24c3ce6](https://github.com/libp2p/js-libp2p/commit/24c3ce6)) +* upgrader should not need muxers ([#517](https://github.com/libp2p/js-libp2p/issues/517)) ([56a1825](https://github.com/libp2p/js-libp2p/commit/56a1825)) + + +### Features + +* add libp2p.connections getter ([#522](https://github.com/libp2p/js-libp2p/issues/522)) ([6ca19c5](https://github.com/libp2p/js-libp2p/commit/6ca19c5)) +* allow transport options to be passed on creation ([#524](https://github.com/libp2p/js-libp2p/issues/524)) ([0d4b2bd](https://github.com/libp2p/js-libp2p/commit/0d4b2bd)) + + + # [0.27.0-pre.1](https://github.com/libp2p/js-libp2p/compare/v0.27.0-pre.0...v0.27.0-pre.1) (2019-12-15) From c44e6e33ed9e5b561ac82730c149dc60648735f5 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Tue, 7 Jan 2020 15:27:32 +0000 Subject: [PATCH 85/92] fix: stop discoveries (#530) * fix: stop discoveries * test: add discovery stop test * chore: fix lint Co-authored-by: Jacob Heun --- src/index.js | 7 +++++++ test/peer-discovery/index.spec.js | 26 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/index.js b/src/index.js index 7e635aa76b..8c43dd749d 100644 --- a/src/index.js +++ b/src/index.js @@ -218,7 +218,14 @@ class Libp2p extends EventEmitter { log('libp2p is stopping') try { + for (const service of this._discovery.values()) { + service.removeListener('peer', this._onDiscoveryPeer) + } + + await Promise.all(Array.from(this._discovery.values(), s => s.stop())) + this.connectionManager.stop() + await Promise.all([ this.pubsub && this.pubsub.stop(), this._dht && this._dht.stop(), diff --git a/test/peer-discovery/index.spec.js b/test/peer-discovery/index.spec.js index 66e08d37bd..31fc38cdb1 100644 --- a/test/peer-discovery/index.spec.js +++ b/test/peer-discovery/index.spec.js @@ -65,6 +65,32 @@ describe('peer discovery', () => { expect(discoverySpy.called).to.eql(false) }) + + it('should stop discovery on libp2p start/stop', async () => { + const mockDiscovery = { + tag: 'mock', + start: () => {}, + stop: () => {}, + on: () => {}, + removeListener: () => {} + } + const startSpy = sinon.spy(mockDiscovery, 'start') + const stopSpy = sinon.spy(mockDiscovery, 'stop') + + libp2p = new Libp2p(mergeOptions(baseOptions, { + peerInfo, + modules: { + peerDiscovery: [mockDiscovery] + } + })) + + await libp2p.start() + expect(startSpy).to.have.property('callCount', 1) + expect(stopSpy).to.have.property('callCount', 0) + await libp2p.stop() + expect(startSpy).to.have.property('callCount', 1) + expect(stopSpy).to.have.property('callCount', 1) + }) }) describe('discovery modules from transports', () => { From 83409deaa6773a550d38b77bd486faf8b8b97d29 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Tue, 14 Jan 2020 12:26:24 +0100 Subject: [PATCH 86/92] fix: registrar should filter the disconnected conn (#532) * fix: registrar on disconnect only when no connections * chore: add test --- src/registrar.js | 2 +- test/registrar/registrar.spec.js | 40 ++++++++++++++++++++++++++++++++ test/registrar/utils.js | 3 ++- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/registrar.js b/src/registrar.js index d91f2bba17..1af54ac470 100644 --- a/src/registrar.js +++ b/src/registrar.js @@ -99,7 +99,7 @@ class Registrar { let storedConn = this.connections.get(id) if (storedConn && storedConn.length > 1) { - storedConn = storedConn.filter((conn) => conn.id === connection.id) + storedConn = storedConn.filter((conn) => conn.id !== connection.id) this.connections.set(id, storedConn) } else if (storedConn) { for (const [, topology] of this.topologies) { diff --git a/test/registrar/registrar.spec.js b/test/registrar/registrar.spec.js index 563762376a..043ec5a335 100644 --- a/test/registrar/registrar.spec.js +++ b/test/registrar/registrar.spec.js @@ -11,6 +11,7 @@ const Topology = require('libp2p-interfaces/src/topology/multicodec-topology') const PeerStore = require('../../src/peer-store') const Registrar = require('../../src/registrar') const { createMockConnection } = require('./utils') +const peerUtils = require('../utils/creators/peer') const multicodec = '/test/1.0.0' @@ -170,5 +171,44 @@ describe('registrar', () => { await onDisconnectDefer.promise }) + + it('should filter connections on disconnect, removing the closed one', async () => { + const onDisconnectDefer = pDefer() + + const topologyProps = new Topology({ + multicodecs: multicodec, + handlers: { + onConnect: () => {}, + onDisconnect: () => { + onDisconnectDefer.resolve() + } + } + }) + + // Register protocol + registrar.register(topologyProps) + + // Setup connections before registrar + const [localPeer, remotePeer] = await peerUtils.createPeerInfo({ number: 2 }) + + const conn1 = await createMockConnection({ localPeer: localPeer.id, remotePeer: remotePeer.id }) + const conn2 = await createMockConnection({ localPeer: localPeer.id, remotePeer: remotePeer.id }) + const peerInfo = await PeerInfo.create(remotePeer.id) + const id = peerInfo.id.toString() + + // Add connection to registrar + peerStore.put(peerInfo) + registrar.onConnect(peerInfo, conn1) + registrar.onConnect(peerInfo, conn2) + + expect(registrar.connections.get(id).length).to.eql(2) + + conn2._stat.status = 'closed' + registrar.onDisconnect(peerInfo, conn2) + + const peerConnections = registrar.connections.get(id) + expect(peerConnections.length).to.eql(1) + expect(peerConnections[0]._stat.status).to.eql('open') + }) }) }) diff --git a/test/registrar/utils.js b/test/registrar/utils.js index 4f18684fc2..5676827c22 100644 --- a/test/registrar/utils.js +++ b/test/registrar/utils.js @@ -27,7 +27,8 @@ module.exports.createMockConnection = async (properties = {}) => { }, direction: 'outbound', encryption: '/secio/1.0.0', - multiplexer: '/mplex/6.7.0' + multiplexer: '/mplex/6.7.0', + status: 'open' }, newStream: (protocols) => { const id = streamId++ From 31d1b2369a9ef93ccb42c04fce1b6d21ec25b0c5 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Wed, 22 Jan 2020 11:47:30 +0100 Subject: [PATCH 87/92] fix: use toB58String everywhere to be consistent (#537) * chore: update deps * fix: consistently use b58 peerid string The migration to base32 will happen at a later date --- package.json | 6 +++--- src/connection-manager/index.js | 12 ++++++------ src/dialer/index.js | 2 +- src/identify/index.js | 2 +- src/index.js | 6 +++--- src/metrics/index.js | 12 ++++++------ src/registrar.js | 8 ++++---- src/upgrader.js | 4 ++-- test/connection-manager/index.spec.js | 2 +- test/dialing/direct.node.js | 6 +++--- test/dialing/relay.node.js | 20 ++++++++++---------- test/identify/index.spec.js | 9 +++++++-- test/metrics/index.spec.js | 18 ++++++++++-------- test/registrar/registrar.spec.js | 2 +- 14 files changed, 58 insertions(+), 51 deletions(-) diff --git a/package.json b/package.json index 9561dd4c5f..29e01bcad1 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "it-protocol-buffers": "^0.2.0", "latency-monitor": "~0.2.1", "libp2p-crypto": "^0.17.1", - "libp2p-interfaces": "^0.1.5", + "libp2p-interfaces": "^0.2.3", "mafmt": "^7.0.0", "merge-options": "^2.0.0", "moving-average": "^1.0.0", @@ -77,7 +77,7 @@ "devDependencies": { "@nodeutils/defaults-deep": "^1.1.0", "abortable-iterator": "^2.1.0", - "aegir": "^20.4.1", + "aegir": "^20.5.1", "chai": "^4.2.0", "chai-as-promised": "^7.1.1", "cids": "^0.7.1", @@ -102,7 +102,7 @@ "p-defer": "^3.0.0", "p-times": "^2.1.0", "p-wait-for": "^3.1.0", - "sinon": "^7.2.7", + "sinon": "^8.1.0", "streaming-iterables": "^4.1.0", "wrtc": "^0.4.1" }, diff --git a/src/connection-manager/index.js b/src/connection-manager/index.js index 15f630f241..a35d36f512 100644 --- a/src/connection-manager/index.js +++ b/src/connection-manager/index.js @@ -36,7 +36,7 @@ class ConnectionManager { constructor (libp2p, options) { this._libp2p = libp2p this._registrar = libp2p.registrar - this._peerId = libp2p.peerInfo.id.toString() + this._peerId = libp2p.peerInfo.id.toB58String() this._options = mergeOptions.call({ ignoreUndefined: true }, defaultOptions, options) assert( this._options.maxConnections > this._options.minConnections, @@ -91,8 +91,8 @@ class ConnectionManager { if (value < 0 || value > 1) { throw new Error('value should be a number between 0 and 1') } - if (peerId.toString) { - peerId = peerId.toString() + if (peerId.toB58String) { + peerId = peerId.toB58String() } this._peerValues.set(peerId, value) } @@ -119,7 +119,7 @@ class ConnectionManager { * @param {Connection} connection */ onConnect (connection) { - const peerId = connection.remotePeer.toString() + const peerId = connection.remotePeer.toB58String() this._connections.set(connection.id, connection) if (!this._peerValues.has(peerId)) { this._peerValues.set(peerId, this._options.defaultPeerValue) @@ -133,7 +133,7 @@ class ConnectionManager { */ onDisconnect (connection) { this._connections.delete(connection.id) - this._peerValues.delete(connection.remotePeer.toString()) + this._peerValues.delete(connection.remotePeer.toB58String()) } /** @@ -175,7 +175,7 @@ class ConnectionManager { debug('%s: lowest value peer is %s', this._peerId, peerId) debug('%s: closing a connection to %j', this._peerId, peerId) for (const connection of this._connections.values()) { - if (connection.remotePeer.toString() === peerId) { + if (connection.remotePeer.toB58String() === peerId) { connection.close() break } diff --git a/src/dialer/index.js b/src/dialer/index.js index b51938a17f..61f770e420 100644 --- a/src/dialer/index.js +++ b/src/dialer/index.js @@ -113,7 +113,7 @@ class Dialer { } const addrs = this.peerStore.multiaddrsForPeer(dialable) return { - id: dialable.id.toString(), + id: dialable.id.toB58String(), addrs } } diff --git a/src/identify/index.js b/src/identify/index.js index df51729597..27694a0ddf 100644 --- a/src/identify/index.js +++ b/src/identify/index.js @@ -180,7 +180,7 @@ class IdentifyService { const id = await PeerId.createFromPubKey(publicKey) const peerInfo = new PeerInfo(id) - if (connection.remotePeer.toString() !== id.toString()) { + if (connection.remotePeer.toB58String() !== id.toB58String()) { throw errCode(new Error('identified peer does not match the expected peer'), codes.ERR_INVALID_PEER) } diff --git a/src/index.js b/src/index.js index 8c43dd749d..c07d70c2f6 100644 --- a/src/index.js +++ b/src/index.js @@ -314,7 +314,7 @@ class Libp2p extends EventEmitter { hangUp (peer) { const peerInfo = getPeerInfo(peer, this.peerStore) return Promise.all( - this.registrar.connections.get(peerInfo.id.toString()).map(connection => { + this.registrar.connections.get(peerInfo.id.toB58String()).map(connection => { return connection.close() }) ) @@ -426,7 +426,7 @@ class Libp2p extends EventEmitter { * @param {PeerInfo} peerInfo */ _onDiscoveryPeer (peerInfo) { - if (peerInfo.id.toString() === this.peerInfo.id.toString()) { + if (peerInfo.id.toB58String() === this.peerInfo.id.toB58String()) { log.error(new Error(codes.ERR_DISCOVERED_SELF)) return } @@ -445,7 +445,7 @@ class Libp2p extends EventEmitter { if (this._config.peerDiscovery.autoDial === true && !this.registrar.getConnection(peerInfo)) { const minPeers = this._options.connectionManager.minPeers || 0 if (minPeers > this.connectionManager._connections.size) { - log('connecting to discovered peer %s', peerInfo.id.toString()) + log('connecting to discovered peer %s', peerInfo.id.toB58String()) try { await this.dialer.connectToPeer(peerInfo) } catch (err) { diff --git a/src/metrics/index.js b/src/metrics/index.js index 9cbe9489ec..e687f87b4f 100644 --- a/src/metrics/index.js +++ b/src/metrics/index.js @@ -82,7 +82,7 @@ class Metrics { * @returns {Stats} */ forPeer (peerId) { - const idString = peerId.toString() + const idString = peerId.toB58String() return this._peerStats.get(idString) || this._oldPeers.get(idString) } @@ -110,7 +110,7 @@ class Metrics { * @param {PeerId} peerId */ onPeerDisconnected (peerId) { - const idString = peerId.toString() + const idString = peerId.toB58String() const peerStats = this._peerStats.get(idString) if (peerStats) { peerStats.stop() @@ -140,7 +140,7 @@ class Metrics { let peerStats = this.forPeer(remotePeer) if (!peerStats) { peerStats = new Stats(initialCounters, this._options) - this._peerStats.set(remotePeer.toString(), peerStats) + this._peerStats.set(remotePeer.toB58String(), peerStats) } // Peer and global stats @@ -162,13 +162,13 @@ class Metrics { * Replaces the `PeerId` string with the given `peerId`. * If stats are already being tracked for the given `peerId`, the * placeholder stats will be merged with the existing stats. - * @param {string} placeholder A peerId string + * @param {PeerId} placeholder A peerId string * @param {PeerId} peerId */ updatePlaceholder (placeholder, peerId) { if (!this._running) return const placeholderStats = this.forPeer(placeholder) - const peerIdString = peerId.toString() + const peerIdString = peerId.toB58String() const existingStats = this.forPeer(peerId) let mergedStats = placeholderStats @@ -180,7 +180,7 @@ class Metrics { this._oldPeers.delete(peerIdString) } - this._peerStats.delete(placeholder.toString()) + this._peerStats.delete(placeholder.toB58String()) this._peerStats.set(peerIdString, mergedStats) mergedStats.start() } diff --git a/src/registrar.js b/src/registrar.js index 1af54ac470..45118c099c 100644 --- a/src/registrar.js +++ b/src/registrar.js @@ -74,7 +74,7 @@ class Registrar { assert(PeerInfo.isPeerInfo(peerInfo), 'peerInfo must be an instance of peer-info') assert(Connection.isConnection(conn), 'conn must be an instance of interface-connection') - const id = peerInfo.id.toString() + const id = peerInfo.id.toB58String() const storedConn = this.connections.get(id) if (storedConn) { @@ -95,7 +95,7 @@ class Registrar { onDisconnect (peerInfo, connection, error) { assert(PeerInfo.isPeerInfo(peerInfo), 'peerInfo must be an instance of peer-info') - const id = peerInfo.id.toString() + const id = peerInfo.id.toB58String() let storedConn = this.connections.get(id) if (storedConn && storedConn.length > 1) { @@ -106,7 +106,7 @@ class Registrar { topology.disconnect(peerInfo, error) } - this.connections.delete(peerInfo.id.toString()) + this.connections.delete(peerInfo.id.toB58String()) } } @@ -118,7 +118,7 @@ class Registrar { getConnection (peerInfo) { assert(PeerInfo.isPeerInfo(peerInfo), 'peerInfo must be an instance of peer-info') - const connections = this.connections.get(peerInfo.id.toString()) + const connections = this.connections.get(peerInfo.id.toB58String()) // Return the first, open connection if (connections) { return connections.find(connection => connection.stat.status === 'open') diff --git a/src/upgrader.js b/src/upgrader.js index 6e178a4c70..7b3fa12313 100644 --- a/src/upgrader.js +++ b/src/upgrader.js @@ -73,7 +73,7 @@ class Upgrader { if (this.metrics) { ({ setTarget: setPeer, proxy: proxyPeer } = mutableProxy()) const idString = (parseInt(Math.random() * 1e9)).toString(36) + Date.now() - setPeer({ toString: () => idString }) + setPeer({ toB58String: () => idString }) maConn = this.metrics.trackStream({ stream: maConn, remotePeer: proxyPeer }) } @@ -147,7 +147,7 @@ class Upgrader { if (this.metrics) { ({ setTarget: setPeer, proxy: proxyPeer } = mutableProxy()) const idString = (parseInt(Math.random() * 1e9)).toString(36) + Date.now() - setPeer({ toString: () => idString }) + setPeer({ toB58String: () => idString }) maConn = this.metrics.trackStream({ stream: maConn, remotePeer: proxyPeer }) } diff --git a/test/connection-manager/index.spec.js b/test/connection-manager/index.spec.js index bf8fa697fb..ce4c6312dc 100644 --- a/test/connection-manager/index.spec.js +++ b/test/connection-manager/index.spec.js @@ -75,7 +75,7 @@ describe('Connection Manager', () => { const spy = sinon.spy(connection, 'close') // The connections have the same remote id, give them random ones // so that we can verify the correct connection was closed - sinon.stub(connection.remotePeer, 'toString').returns(index) + sinon.stub(connection.remotePeer, 'toB58String').returns(index) const value = Math.random() spies.set(value, spy) libp2p.connectionManager.setPeerValue(connection.remotePeer, value) diff --git a/test/dialing/direct.node.js b/test/dialing/direct.node.js index 31685b4c9c..436305c1b2 100644 --- a/test/dialing/direct.node.js +++ b/test/dialing/direct.node.js @@ -306,7 +306,7 @@ describe('Dialing (direct, TCP)', () => { } }) - const connection = await libp2p.dial(`${remoteAddr.toString()}/p2p/${remotePeerInfo.id.toString()}`) + const connection = await libp2p.dial(`${remoteAddr.toString()}/p2p/${remotePeerInfo.id.toB58String()}`) expect(connection).to.exist() expect(connection.stat.timeline.close).to.not.exist() await libp2p.hangUp(connection.remotePeer) @@ -375,7 +375,7 @@ describe('Dialing (direct, TCP)', () => { }) const dials = 10 - const fullAddress = remoteAddr.encapsulate(`/p2p/${remoteLibp2p.peerInfo.id.toString()}`) + const fullAddress = remoteAddr.encapsulate(`/p2p/${remoteLibp2p.peerInfo.id.toB58String()}`) const dialResults = await Promise.all([...new Array(dials)].map((_, index) => { if (index % 2 === 0) return libp2p.dial(remoteLibp2p.peerInfo) return libp2p.dial(fullAddress) @@ -405,7 +405,7 @@ describe('Dialing (direct, TCP)', () => { const error = new Error('Boom') sinon.stub(libp2p.transportManager, 'dial').callsFake(() => Promise.reject(error)) - const fullAddress = remoteAddr.encapsulate(`/p2p/${remoteLibp2p.peerInfo.id.toString()}`) + const fullAddress = remoteAddr.encapsulate(`/p2p/${remoteLibp2p.peerInfo.id.toB58String()}`) const dialResults = await pSettle([...new Array(dials)].map((_, index) => { if (index % 2 === 0) return libp2p.dial(remoteLibp2p.peerInfo) return libp2p.dial(fullAddress) diff --git a/test/dialing/relay.node.js b/test/dialing/relay.node.js index c1b42e8598..5b4c75cf7e 100644 --- a/test/dialing/relay.node.js +++ b/test/dialing/relay.node.js @@ -59,11 +59,11 @@ describe('Dialing (via relay, TCP)', () => { it('should be able to connect to a peer over a relay with active connections', async () => { const relayAddr = relayLibp2p.transportManager.getAddrs()[0] - const relayIdString = relayLibp2p.peerInfo.id.toString() + const relayIdString = relayLibp2p.peerInfo.id.toB58String() const dialAddr = relayAddr .encapsulate(`/p2p/${relayIdString}`) - .encapsulate(`/p2p-circuit/p2p/${dstLibp2p.peerInfo.id.toString()}`) + .encapsulate(`/p2p-circuit/p2p/${dstLibp2p.peerInfo.id.toB58String()}`) const tcpAddrs = dstLibp2p.transportManager.getAddrs() await dstLibp2p.transportManager.listen([multiaddr(`/p2p-circuit${relayAddr}/p2p/${relayIdString}`)]) @@ -94,11 +94,11 @@ describe('Dialing (via relay, TCP)', () => { it('should fail to connect to a peer over a relay with inactive connections', async () => { const relayAddr = relayLibp2p.transportManager.getAddrs()[0] - const relayIdString = relayLibp2p.peerInfo.id.toString() + const relayIdString = relayLibp2p.peerInfo.id.toB58String() const dialAddr = relayAddr .encapsulate(`/p2p/${relayIdString}`) - .encapsulate(`/p2p-circuit/p2p/${dstLibp2p.peerInfo.id.toString()}`) + .encapsulate(`/p2p-circuit/p2p/${dstLibp2p.peerInfo.id.toB58String()}`) await expect(srcLibp2p.dial(dialAddr)) .to.eventually.be.rejectedWith(AggregateError) @@ -107,11 +107,11 @@ describe('Dialing (via relay, TCP)', () => { it('should not stay connected to a relay when not already connected and HOP fails', async () => { const relayAddr = relayLibp2p.transportManager.getAddrs()[0] - const relayIdString = relayLibp2p.peerInfo.id.toString() + const relayIdString = relayLibp2p.peerInfo.id.toB58String() const dialAddr = relayAddr .encapsulate(`/p2p/${relayIdString}`) - .encapsulate(`/p2p-circuit/p2p/${dstLibp2p.peerInfo.id.toString()}`) + .encapsulate(`/p2p-circuit/p2p/${dstLibp2p.peerInfo.id.toB58String()}`) await expect(srcLibp2p.dial(dialAddr)) .to.eventually.be.rejectedWith(AggregateError) @@ -124,11 +124,11 @@ describe('Dialing (via relay, TCP)', () => { it('dialer should stay connected to an already connected relay on hop failure', async () => { const relayAddr = relayLibp2p.transportManager.getAddrs()[0] - const relayIdString = relayLibp2p.peerInfo.id.toString() + const relayIdString = relayLibp2p.peerInfo.id.toB58String() const dialAddr = relayAddr .encapsulate(`/p2p/${relayIdString}`) - .encapsulate(`/p2p-circuit/p2p/${dstLibp2p.peerInfo.id.toString()}`) + .encapsulate(`/p2p-circuit/p2p/${dstLibp2p.peerInfo.id.toB58String()}`) await srcLibp2p.dial(relayAddr) @@ -143,11 +143,11 @@ describe('Dialing (via relay, TCP)', () => { it('destination peer should stay connected to an already connected relay on hop failure', async () => { const relayAddr = relayLibp2p.transportManager.getAddrs()[0] - const relayIdString = relayLibp2p.peerInfo.id.toString() + const relayIdString = relayLibp2p.peerInfo.id.toB58String() const dialAddr = relayAddr .encapsulate(`/p2p/${relayIdString}`) - .encapsulate(`/p2p-circuit/p2p/${dstLibp2p.peerInfo.id.toString()}`) + .encapsulate(`/p2p-circuit/p2p/${dstLibp2p.peerInfo.id.toB58String()}`) // Connect the destination peer and the relay const tcpAddrs = dstLibp2p.transportManager.getAddrs() diff --git a/test/identify/index.spec.js b/test/identify/index.spec.js index 70563566ca..29e4ce40cf 100644 --- a/test/identify/index.spec.js +++ b/test/identify/index.spec.js @@ -84,7 +84,12 @@ describe('Identify', () => { it('should throw if identified peer is the wrong peer', async () => { const localIdentify = new IdentifyService({ peerInfo: localPeer, - protocols + protocols, + registrar: { + peerStore: { + replace: () => {} + } + } }) const remoteIdentify = new IdentifyService({ peerInfo: remotePeer, @@ -92,7 +97,7 @@ describe('Identify', () => { }) const observedAddr = multiaddr('/ip4/127.0.0.1/tcp/1234') - const localConnectionMock = { newStream: () => {}, remotePeer } + const localConnectionMock = { newStream: () => {}, remotePeer: localPeer.id } const remoteConnectionMock = { remoteAddr: observedAddr } const [local, remote] = duplexPair() diff --git a/test/metrics/index.spec.js b/test/metrics/index.spec.js index d9018fdda4..d84250ef4d 100644 --- a/test/metrics/index.spec.js +++ b/test/metrics/index.spec.js @@ -104,7 +104,7 @@ describe('Metrics', () => { expect(results.length).to.eql(bytes.length * 10) const stats = metrics.forPeer(peerId) - expect(metrics.peers).to.eql([peerId.toString()]) + expect(metrics.peers).to.eql([peerId.toB58String()]) expect(stats.snapshot.dataReceived.toNumber()).to.equal(results.length) expect(stats.snapshot.dataSent.toNumber()).to.equal(results.length) @@ -148,7 +148,7 @@ describe('Metrics', () => { // Flush the call stack await delay(0) - expect(metrics.peers).to.eql([peerId.toString(), peerId2.toString()]) + expect(metrics.peers).to.eql([peerId.toB58String(), peerId2.toB58String()]) // Verify global metrics const globalStats = metrics.global expect(globalStats.snapshot.dataReceived.toNumber()).to.equal(bytes.length * 2) @@ -181,7 +181,7 @@ describe('Metrics', () => { pipe(remote, remote) const mockPeer = { - toString: () => 'a temporary id' + toB58String: () => 'a temporary id' } metrics.trackStream({ stream: local, @@ -197,8 +197,8 @@ describe('Metrics', () => { await delay(0) - metrics.updatePlaceholder(mockPeer.toString(), peerId) - mockPeer.toString = peerId.toString.bind(peerId) + metrics.updatePlaceholder(mockPeer, peerId) + mockPeer.toB58String = peerId.toB58String.bind(peerId) input.push(bytes) input.end() @@ -206,7 +206,7 @@ describe('Metrics', () => { await deferredPromise await delay(0) - expect(metrics.peers).to.eql([peerId.toString()]) + expect(metrics.peers).to.eql([peerId.toB58String()]) // Verify global metrics const globalStats = metrics.global expect(globalStats.snapshot.dataReceived.toNumber()).to.equal(bytes.length * 2) @@ -237,7 +237,7 @@ describe('Metrics', () => { // Disconnect every peer for (const id of trackedPeers.keys()) { metrics.onPeerDisconnected({ - toString: () => id + toB58String: () => id }) } @@ -245,7 +245,9 @@ describe('Metrics', () => { expect(metrics.peers).to.have.length(0) const retainedPeers = [] for (const id of trackedPeers.keys()) { - const stat = metrics.forPeer(id) + const stat = metrics.forPeer({ + toB58String: () => id + }) if (stat) retainedPeers.push(id) } expect(retainedPeers).to.eql(['45', '46', '47', '48', '49']) diff --git a/test/registrar/registrar.spec.js b/test/registrar/registrar.spec.js index 043ec5a335..056c4b49ab 100644 --- a/test/registrar/registrar.spec.js +++ b/test/registrar/registrar.spec.js @@ -194,7 +194,7 @@ describe('registrar', () => { const conn1 = await createMockConnection({ localPeer: localPeer.id, remotePeer: remotePeer.id }) const conn2 = await createMockConnection({ localPeer: localPeer.id, remotePeer: remotePeer.id }) const peerInfo = await PeerInfo.create(remotePeer.id) - const id = peerInfo.id.toString() + const id = peerInfo.id.toB58String() // Add connection to registrar peerStore.put(peerInfo) From 461faca1cafa1600480113ae0a86b1c3095bd71a Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Thu, 23 Jan 2020 23:12:41 +0700 Subject: [PATCH 88/92] docs: getting started (#514) * docs: getting started * docs: review getting started (#520) * doc: initial review of getting started Co-Authored-By: Vasco Santos * chore: move multiplexing to basic setup * chore: add read more cta * chore: apply suggestions from code review Co-Authored-By: Jacob Heun * chore: just configure multiplexer * chore: apply suggestions from code review Co-Authored-By: Jacob Heun * chore: just use websockets and changed dht module introduction * chore: add reference for events in the API doc * docs: simplify getting started guide and clean up language * chore: apply suggestions from code review Co-Authored-By: Alan Shaw * docs(fix): address review comments Co-authored-by: Jacob Heun Co-authored-by: Alan Shaw --- README.md | 5 + doc/API.md | 69 ++++++------ doc/CONFIGURATION.md | 2 +- doc/GETTING_STARTED.md | 247 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 289 insertions(+), 34 deletions(-) create mode 100644 doc/GETTING_STARTED.md diff --git a/README.md b/README.md index ba8a0f0b8f..2bde70ea41 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ We've come a long way, but this project is still in Alpha, lots of development i - [Usage](#usage) - [Configuration](#configuration) - [API](#api) + - [Getting Started](#getting-started) - [Tutorials and Examples](#tutorials-and-examples) - [Development](#development) - [Tests](#tests) @@ -89,6 +90,10 @@ For all the information on how you can configure libp2p see [CONFIGURATION.md](. The specification is available on [API.md](./doc/API.md). +### Getting started + +If you are starting your journey with `js-libp2p`, read the [GETTING_STARTED.md](./doc/GETTING_STARTED.md) guide. + ### Tutorials and Examples You can find multiple examples on the [examples folder](./examples) that will guide you through using libp2p for several scenarios. diff --git a/doc/API.md b/doc/API.md index 945c42d2dd..b219331864 100644 --- a/doc/API.md +++ b/doc/API.md @@ -28,6 +28,7 @@ * [`metrics.protocols`](#metricsprotocols) * [`metrics.forPeer`](#metricsforpeer) * [`metrics.forProtocol`](#metricsforprotocol) +* [Events](#events) * [Types](#types) * [`Stats`](#stats) @@ -96,39 +97,6 @@ Required keys in the `options` object: -Once you have a libp2p instance, you are able to listen to several events it emits, so that you can be noticed of relevant network events. - -
Events - -#### An error has occurred - -`libp2p.on('error', (err) => {})` - -- `err`: instance of `Error` - -#### A peer has been discovered - -`libp2p.on('peer:discovery', (peer) => {})` - -If `autoDial` option is `true`, applications should **not** attempt to connect to the peer -unless they are performing a specific action. See [peer discovery and auto dial](./PEER_DISCOVERY.md) for more information. - -- `peer`: instance of [PeerInfo][https://github.com/libp2p/js-peer-info] - -#### We have a new connection to a peer - -`libp2p.on('peer:connect', (peer) => {})` - -- `peer`: instance of [PeerInfo][https://github.com/libp2p/js-peer-info] - -#### We have closed a connection to a peer - -`libp2p.on('peer:disconnect', (peer) => {})` - -- `peer`: instance of [PeerInfo][https://github.com/libp2p/js-peer-info] - -
- ## Libp2p Instance Methods ### start @@ -777,6 +745,41 @@ const peerStats = libp2p.metrics.forProtocol('/meshsub/1.0.0') console.log(peerStats.toJSON()) ``` +## Events + +Once you have a libp2p instance, you can listen to several events it emits, so that you can be notified of relevant network events. + +#### An error has occurred + +`libp2p.on('error', (err) => {})` + +- `err`: instance of `Error` + +#### A peer has been discovered + +`libp2p.on('peer:discovery', (peer) => {})` + +If `autoDial` option is `true`, applications should **not** attempt to connect to the peer +unless they are performing a specific action. See [peer discovery and auto dial](./PEER_DISCOVERY.md) for more information. + +- `peer`: instance of [PeerInfo][https://github.com/libp2p/js-peer-info] + +#### A new connection to a peer has been opened + +This event will be triggered anytime a new Connection is established to another peer. + +`libp2p.on('peer:connect', (peer) => {})` + +- `peer`: instance of [PeerInfo][https://github.com/libp2p/js-peer-info] + +#### An existing connection to a peer has been closed + +This event will be triggered anytime we are disconnected from another peer, regardless of the circumstances of that disconnection. If we happen to have multiple connections to a peer, this event will **only** be triggered when the last connection is closed. + +`libp2p.on('peer:disconnect', (peer) => {})` + +- `peer`: instance of [PeerInfo][https://github.com/libp2p/js-peer-info] + ## Types ### Stats diff --git a/doc/CONFIGURATION.md b/doc/CONFIGURATION.md index 92da7af7ff..0ba1c2bd41 100644 --- a/doc/CONFIGURATION.md +++ b/doc/CONFIGURATION.md @@ -129,7 +129,7 @@ If you want to know more about libp2p peer discovery, you should read the follow Some available content routing modules are: - [js-libp2p-kad-dht](https://github.com/libp2p/js-libp2p-kad-dht) -- [js-libp2p-delegated-peer-routing](https://github.com/libp2p/js-libp2p-delegated-peer-routing) +- [js-libp2p-delegated-content-routing](https://github.com/libp2p/js-libp2p-delegated-content-routing) If none of the available content routing protocols fulfills your needs, you can create a libp2p compatible one. A libp2p content routing protocol just needs to be compliant with the [Content Routing Interface](https://github.com/libp2p/js-interfaces/tree/master/src/content-routing). **(WIP: This module is not yet implemented)** diff --git a/doc/GETTING_STARTED.md b/doc/GETTING_STARTED.md new file mode 100644 index 0000000000..b05cc4f6e7 --- /dev/null +++ b/doc/GETTING_STARTED.md @@ -0,0 +1,247 @@ +# Getting Started + +Welcome to libp2p! This guide will walk you through setting up a fully functional libp2p node 🚀 + +- [Getting Started](#getting-started) + - [Install](#install) + - [Configuring libp2p](#configuring-libp2p) + - [Basic setup](#basic-setup) + - [Transports](#transports) + - [Connection Encryption](#connection-encryption) + - [Multiplexing](#multiplexing) + - [Running Libp2p](#running-libp2p) + - [Custom setup](#custom-setup) + - [Peer Discovery](#peer-discovery) + - [Pubsub](#pubsub) + - [What is next](#what-is-next) + +## Install + +The first step is to install libp2p in your project: + +```sh +npm install libp2p +``` + +## Configuring libp2p + +If you're new to libp2p, we recommend configuring your node in stages, as this can make troubleshooting configuration issues much easier. In this guide, we'll do just that. If you're more experienced with libp2p, you may wish to jump to the [Configuration readme](./CONFIGURATION.md). + +### Basic setup + +Now that we have libp2p installed, let's configure the minimum needed to get your node running. The only modules libp2p requires are a [**Transport**][transport] and [**Crypto**][crypto] module. However, we recommend that a basic setup should also have a [**Stream Multiplexer**](streamMuxer) configured, which we will explain shortly. Let's start by setting up a Transport. + +#### Transports + +Libp2p uses Transports to establish connections between peers over the network. Transports are the components responsible for performing the actual exchange of data between libp2p nodes. You can configure any number of Transports, but you only need 1 to start with. Supporting more Transports will improve the ability of your node to speak to a larger number of nodes on the network, as matching Transports are required for two nodes to communicate with one another. + +You should select Transports according to the runtime of your application; Node.js or the browser. You can see a list of some of the available Transports in the [configuration readme](./CONFIGURATION.md#transport). For this guide let's install `libp2p-websockets`, as it can be used in both Node.js and the browser. + +Start by installing `libp2p-websockets`: + +```sh +npm install libp2p-websockets +``` + +Now that we have the module installed, let's configure libp2p to use the Transport. We'll use the [`Libp2p.create`](./API.md#create) method, which takes a single configuration object as its only parameter. We can add the Transport by passing it into the `modules.transport` array: + +```js +const Libp2p = require('libp2p') +const WebSockets = require('libp2p-websockets') + +const node = await Libp2p.create({ + modules: { + transport: [WebSockets] + } +}) +``` + +There are multiple libp2p transports available, you should evaluate the needs of your application and select the Transport(s) that best suit your requirements. You can add as many transports as you like to `modules.transport` in order to establish connections with as many peers as possible. + +
Read More +If you want to know more about libp2p transports, you should read the following content: + +- https://docs.libp2p.io/concepts/transport +- https://github.com/libp2p/specs/tree/master/connections +
+ +#### Connection Encryption + +Encryption is an important part of communicating on the libp2p network. Every connection must be encrypted to help ensure security for everyone. As such, Connection Encryption (Crypto) is a required component of libp2p. + +There are a growing number of Crypto modules being developed for libp2p. As those are released they will be tracked in the [Connection Encryption section of the configuration readme](./CONFIGURATION.md#connection-encryption). For now, we are going to configure our node to use the `libp2p-secio` module, which is widely supported across the various libp2p implementations. + +```sh +npm install libp2p-secio +``` + +With `libp2p-secio` installed, we can add it to our existing configuration by importing it and adding it to the `modules.connEncryption` array: + +```js +const Libp2p = require('libp2p') +const WebSockets = require('libp2p-websockets') +const SECIO = require('libp2p-secio') + +const node = await Libp2p.create({ + modules: { + transport: [WebSockets], + connEncryption: [SECIO] + } +}) +``` + +
Read More +If you want to know more about libp2p connection encryption, you should read the following content: + +- https://docs.libp2p.io/concepts/secure-comms +- https://github.com/libp2p/specs/tree/master/connections +
+ +#### Multiplexing + +While multiplexers are not strictly required, they are highly recommended as they improve the effectiveness and efficiency of connections for the various protocols libp2p runs. Adding a multiplexer to your configuration will allow libp2p to run several of its internal protocols, like Identify, as well as allow your application to easily run any number of protocols over a single connection. + +Looking at the [available stream multiplexing](./CONFIGURATION.md#stream-multiplexing) modules, js-libp2p currently only supports `libp2p-mplex`, so we will use that here. Bear in mind that future libp2p Transports might have `multiplexing` capabilities already built-in (such as `QUIC`). + +You can install `libp2p-mplex` and add it to your libp2p node as follows in the next example. + +```sh +npm install libp2p-mplex +``` + +```js +const Libp2p = require('libp2p') +const WebSockets = require('libp2p-websockets') +const SECIO = require('libp2p-secio') +const MPLEX = require('libp2p-mplex') + +const node = await Libp2p.create({ + modules: { + transport: [WebSockets], + connEncryption: [SECIO], + streamMuxer: [MPLEX] + } +}) +``` + +
Read More +If you want to know more about libp2p stream multiplexing, you should read the following content: + +- https://docs.libp2p.io/concepts/stream-multiplexing +- https://github.com/libp2p/specs/tree/master/connections +- https://github.com/libp2p/specs/tree/master/mplex +
+ +#### Running Libp2p + +Now that you have configured a [**Transport**][transport], [**Crypto**][crypto] and [**Stream Multiplexer**](streamMuxer) module, you can start your libp2p node. We can start and stop libp2p using the [`libp2p.start()`](./API.md#start) and [`libp2p.stop()`](./API.md#stop) methods. + +```js +const Libp2p = require('libp2p') +const WebSockets = require('libp2p-websockets') +const SECIO = require('libp2p-secio') +const MPLEX = require('libp2p-mplex') + +const node = await Libp2p.create({ + modules: { + transport: [WebSockets], + connEncryption: [SECIO], + streamMuxer: [MPLEX] + } +}) + +// start libp2p +await node.start() +console.log('libp2p has started') + +// stop libp2p +await node.stop() +console.log('libp2p has stopped') +``` + +### Custom setup + +Once your libp2p node is running, it is time to get it connected to the public network. We can do this via peer discovery. + +#### Peer Discovery + +Peer discovery is an important part of creating a well connected libp2p node. A static list of peers will often be used to join the network, but it's useful to couple other discovery mechanisms to ensure you're able to discover other peers that are important to your application. + +For each discovered peer libp2p will emit a `peer:discovery` event which includes metadata about that peer. You can read the [Events](./API.md#events) in the API doc to learn more. + +Looking at the [available peer discovery](./CONFIGURATION.md#peer-discovery) protocols, there are several options to be considered: +- If you already know the addresses of some other network peers, you should consider using `libp2p-bootstrap` as this is the easiest way of getting your peer into the network. +- If it is likely that you will have other peers on your local network, `libp2p-mdns` is a must if you're node is not running in the browser. It allows peers to discover each other when on the same local network. +- If your application is browser based you can use the `libp2p-webrtc-star` Transport, which includes a rendezvous based peer sharing service. +- A random walk approach can be used via `libp2p-kad-dht`, to crawl the network and find new peers along the way. + +For this guide we will configure `libp2p-bootstrap` as this is useful for joining the public network. + +Let's install `libp2p-bootstrap`. + +```sh +npm install libp2p-bootstrap +``` + +We can provide specific configurations for each protocol within a `config.peerDiscovery` property in the options as shown below. + +```js +const Libp2p = require('libp2p') +const WebSockets = require('libp2p-websockets') +const SECIO = require('libp2p-secio') +const MPLEX = require('libp2p-mplex') + +const Bootstrap = require('libp2p-bootstrap') + +// Known peers addresses +const bootstrapMultiaddrs = [ + '/dns4/ams-1.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLer265NRgSp2LA3dPaeykiS1J6DifTC88f5uVQKNAd', + '/dns4/lon-1.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLMeWqB7YGVLJN3pNLQpmmEk35v6wYtsMGLzSr5QBU3' +] + +const node = await Libp2p.create({ + modules: { + transport: [WebSockets], + connEncryption: [SECIO], + streamMuxer: [MPLEX], + peerDiscovery: [Bootstrap] + }, + config: { + peerDiscovery: { + autoDial: true, // Auto connect to discovered peers (limited by ConnectionManager minPeers) + // The `tag` property will be searched when creating the instance of your Peer Discovery service. + // The associated object, will be passed to the service when it is instantiated. + [Bootstrap.tag]: { + enabled: true, + list: bootstrapMultiaddrs // provide array of multiaddrs + } + } + } +}) + +node.on('peer:discovery', (peer) => { + console.log('Discovered %s', peer.id.toB58String()) // Log discovered peer +}) + +node.on('peer:connect', (peer) => { + console.log('Connected to %s', peer.id.toB58String()) // Log connected peer + }) + +// start libp2p +await node.start() +``` + +
Read More +If you want to know more about libp2p peer discovery, you should read the following content: + +- https://github.com/libp2p/specs/blob/master/discovery/mdns.md +
+ +## What is next + +There are a lot of other concepts within `libp2p`, that are not covered in this guide. For additional configuration options we recommend checking out the [Configuration Readme](./CONFIGURATION.md) and the [examples folder](../examples). If you have any problems getting started, or if anything isn't clear, please let us know by submitting an issue! + + +[transport]: https://github.com/libp2p/js-interfaces/tree/master/src/transport +[crypto]: https://github.com/libp2p/js-interfaces/tree/master/src/crypto +[streamMuxer]: https://github.com/libp2p/js-interfaces/tree/master/src/stream-muxer From 751a22741f8a08b9c4e65cbd6f57c129214f4579 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Fri, 24 Jan 2020 17:43:56 +0700 Subject: [PATCH 89/92] refactor: examples-browser (#508) * refactor: examples-browser * chore: add information to use signalling server * chore: apply suggestions from code review Co-Authored-By: Jacob Heun * chore: update deps * docs: refactor libp2p browser example * docs(examples): add back websockets and boostrap nodes docs(examples): redo the browser readme * fix: handle edge case of connections closing early * chore: fix lint * chore: update example deps and readme * chore: update webrtc-star * chore: apply suggestions from code review Co-Authored-By: Alan Shaw Co-authored-by: Jacob Heun Co-authored-by: Alan Shaw --- .gitignore | 1 + examples/libp2p-in-the-browser/.babelrc | 4 + examples/libp2p-in-the-browser/1/.gitignore | 1 - examples/libp2p-in-the-browser/1/package.json | 32 ------- .../libp2p-in-the-browser/1/public/index.html | 14 --- .../1/src/browser-bundle.js | 94 ------------------- .../1/src/create-node.js | 28 ------ examples/libp2p-in-the-browser/1/src/index.js | 57 ----------- examples/libp2p-in-the-browser/README.md | 61 +++++++++--- examples/libp2p-in-the-browser/index.html | 23 +++++ examples/libp2p-in-the-browser/index.js | 73 ++++++++++++++ examples/libp2p-in-the-browser/package.json | 34 +++++++ src/transport-manager.js | 4 +- src/upgrader.js | 9 +- 14 files changed, 190 insertions(+), 245 deletions(-) create mode 100644 examples/libp2p-in-the-browser/.babelrc delete mode 100644 examples/libp2p-in-the-browser/1/.gitignore delete mode 100644 examples/libp2p-in-the-browser/1/package.json delete mode 100644 examples/libp2p-in-the-browser/1/public/index.html delete mode 100644 examples/libp2p-in-the-browser/1/src/browser-bundle.js delete mode 100644 examples/libp2p-in-the-browser/1/src/create-node.js delete mode 100644 examples/libp2p-in-the-browser/1/src/index.js create mode 100644 examples/libp2p-in-the-browser/index.html create mode 100644 examples/libp2p-in-the-browser/index.js create mode 100644 examples/libp2p-in-the-browser/package.json diff --git a/.gitignore b/.gitignore index 91bbb866a9..61b3b5aa3d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ docs **/*.log test/repo-tests* **/bundle.js +.cache # Logs logs diff --git a/examples/libp2p-in-the-browser/.babelrc b/examples/libp2p-in-the-browser/.babelrc new file mode 100644 index 0000000000..620e785799 --- /dev/null +++ b/examples/libp2p-in-the-browser/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["@babel/preset-env"], + "plugins": ["syntax-async-functions","transform-regenerator"] +} \ No newline at end of file diff --git a/examples/libp2p-in-the-browser/1/.gitignore b/examples/libp2p-in-the-browser/1/.gitignore deleted file mode 100644 index 4187d67743..0000000000 --- a/examples/libp2p-in-the-browser/1/.gitignore +++ /dev/null @@ -1 +0,0 @@ -bundle.js \ No newline at end of file diff --git a/examples/libp2p-in-the-browser/1/package.json b/examples/libp2p-in-the-browser/1/package.json deleted file mode 100644 index 75010eb3c4..0000000000 --- a/examples/libp2p-in-the-browser/1/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "libp2p-in-the-browser", - "version": "0.1.0", - "description": "See other nodes in the network using WebRTC Star discovery mechanism", - "main": "src/index.js", - "scripts": { - "bundle": "browserify src/index.js > public/bundle.js", - "serve": "static public -p 9090 -H '{\"Cache-Control\": \"no-cache, must-revalidate\"}'", - "start": "npm run bundle && npm run serve" - }, - "license": "MIT", - "devDependencies": { - "browserify": "^14.5.0", - "concat-stream": "^1.6.0", - "detect-dom-ready": "^1.0.2", - "node-static": "~0.7.10" - }, - "dependencies": { - "detect-dom-ready": "^1.0.2", - "libp2p": "../../../", - "libp2p-bootstrap": "~0.9.7", - "libp2p-gossipsub": "~0.0.4", - "libp2p-kad-dht": "^0.15.3", - "libp2p-mplex": "~0.8.5", - "libp2p-secio": "~0.11.1", - "libp2p-spdy": "~0.13.3", - "libp2p-webrtc-star": "~0.15.8", - "libp2p-websocket-star": "~0.10.2", - "libp2p-websockets": "~0.12.2", - "peer-info": "~0.15.1" - } -} diff --git a/examples/libp2p-in-the-browser/1/public/index.html b/examples/libp2p-in-the-browser/1/public/index.html deleted file mode 100644 index c13adfe989..0000000000 --- a/examples/libp2p-in-the-browser/1/public/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - libp2p in the browser - - -

libp2p node running \o/

-
-
- - - - diff --git a/examples/libp2p-in-the-browser/1/src/browser-bundle.js b/examples/libp2p-in-the-browser/1/src/browser-bundle.js deleted file mode 100644 index b9ddfd22c5..0000000000 --- a/examples/libp2p-in-the-browser/1/src/browser-bundle.js +++ /dev/null @@ -1,94 +0,0 @@ -'use strict' - -const WebRTCStar = require('libp2p-webrtc-star') -const WebSockets = require('libp2p-websockets') -const WebSocketStar = require('libp2p-websocket-star') -const Mplex = require('libp2p-mplex') -const SPDY = require('libp2p-spdy') -const SECIO = require('libp2p-secio') -const Bootstrap = require('libp2p-bootstrap') -const DHT = require('libp2p-kad-dht') -const Gossipsub = require('libp2p-gossipsub') -const libp2p = require('libp2p') - -// Find this list at: https://github.com/ipfs/js-ipfs/blob/master/src/core/runtime/config-browser.json -const bootstrapList = [ - '/dns4/ams-1.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLer265NRgSp2LA3dPaeykiS1J6DifTC88f5uVQKNAd', - '/dns4/sfo-1.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLju6m7xTh3DuokvT3886QRYqxAzb1kShaanJgW36yx', - '/dns4/lon-1.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLMeWqB7YGVLJN3pNLQpmmEk35v6wYtsMGLzSr5QBU3', - '/dns4/sfo-2.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLnSGccFuZQJzRadHn95W2CrSFmZuTdDWP8HXaHca9z', - '/dns4/sfo-3.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLPppuBtQSGwKDZT2M73ULpjvfd3aZ6ha4oFGL1KrGM', - '/dns4/sgp-1.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLSafTMBsPKadTEgaXctDQVcqN88CNLHXMkTNwMKPnu', - '/dns4/nyc-1.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLueR4xBeUbY9WZ9xGUUxunbKWcrNFTDAadQJmocnWm', - '/dns4/nyc-2.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLV4Bbm51jM9C4gDYZQ9Cy3U6aXMJDAbzgu2fzaDs64', - '/dns4/node0.preload.ipfs.io/tcp/443/wss/p2p/QmZMxNdpMkewiVZLMRxaNxUeZpDUb34pWjZ1kZvsd16Zic', - '/dns4/node0.preload.ipfs.io/tcp/443/wss/p2p/Qmbut9Ywz9YEDrz8ySBSgWyJk41Uvm2QJPhwDJzJyGFsD6' -] - -class Node extends libp2p { - constructor ({ peerInfo }) { - const wrtcStar = new WebRTCStar({ id: peerInfo.id }) - const wsstar = new WebSocketStar({ id: peerInfo.id }) - - const defaults = { - modules: { - transport: [ - wrtcStar, - WebSockets, - wsstar - ], - streamMuxer: [ - Mplex, - SPDY - ], - connEncryption: [ - SECIO - ], - peerDiscovery: [ - wrtcStar.discovery, - wsstar.discovery, - Bootstrap - ], - dht: DHT, - pubsub: Gossipsub - }, - config: { - peerDiscovery: { - autoDial: true, - webRTCStar: { - enabled: true - }, - websocketStar: { - enabled: true - }, - bootstrap: { - interval: 20e3, - enabled: true, - list: bootstrapList - } - }, - relay: { - enabled: true, - hop: { - enabled: false, - active: false - } - }, - dht: { - enabled: false - }, - pubsub: { - enabled: false - } - }, - connectionManager: { - minPeers: 10, - maxPeers: 50 - } - } - - super({ ...defaults, peerInfo }) - } -} - -module.exports = Node diff --git a/examples/libp2p-in-the-browser/1/src/create-node.js b/examples/libp2p-in-the-browser/1/src/create-node.js deleted file mode 100644 index 7cd2075d76..0000000000 --- a/examples/libp2p-in-the-browser/1/src/create-node.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict' - -const PeerInfo = require('peer-info') -const Node = require('./browser-bundle') - -function createNode (callback) { - PeerInfo.create((err, peerInfo) => { - if (err) { - return callback(err) - } - - const peerIdStr = peerInfo.id.toB58String() - const webrtcAddr = `/dns4/star-signal.cloud.ipfs.team/tcp/443/wss/p2p-webrtc-star/p2p/${peerIdStr}` - const wsAddr = `/dns4/ws-star.discovery.libp2p.io/tcp/443/wss/p2p-websocket-star` - - peerInfo.multiaddrs.add(webrtcAddr) - peerInfo.multiaddrs.add(wsAddr) - - const node = new Node({ - peerInfo - }) - - node.idStr = peerIdStr - callback(null, node) - }) -} - -module.exports = createNode diff --git a/examples/libp2p-in-the-browser/1/src/index.js b/examples/libp2p-in-the-browser/1/src/index.js deleted file mode 100644 index 81f85d0784..0000000000 --- a/examples/libp2p-in-the-browser/1/src/index.js +++ /dev/null @@ -1,57 +0,0 @@ -/* eslint no-console: ["error", { allow: ["log"] }] */ -/* eslint max-nested-callbacks: ["error", 5] */ -'use strict' - -const domReady = require('detect-dom-ready') -const createNode = require('./create-node') - -domReady(() => { - const myPeerDiv = document.getElementById('my-peer') - const swarmDiv = document.getElementById('swarm') - - createNode((err, node) => { - if (err) { - return console.log('Could not create the Node, check if your browser has WebRTC Support', err) - } - - node.on('peer:discovery', (peerInfo) => { - console.log('Discovered a peer:', peerInfo.id.toB58String()) - }) - - node.on('peer:connect', (peerInfo) => { - const idStr = peerInfo.id.toB58String() - console.log('Got connection to: ' + idStr) - const connDiv = document.createElement('div') - connDiv.innerHTML = 'Connected to: ' + idStr - connDiv.id = idStr - swarmDiv.append(connDiv) - }) - - node.on('peer:disconnect', (peerInfo) => { - const idStr = peerInfo.id.toB58String() - const el = document.getElementById(idStr) - el && el.remove() - }) - - node.start((err) => { - if (err) { - return console.log(err) - } - - const idStr = node.peerInfo.id.toB58String() - - const idDiv = document - .createTextNode('Node is ready. ID: ' + idStr) - - myPeerDiv.append(idDiv) - - console.log('Node is listening o/') - node.peerInfo.multiaddrs.toArray().forEach(ma => { - console.log(ma.toString()) - }) - - // NOTE: to stop the node - // node.stop((err) => {}) - }) - }) -}) diff --git a/examples/libp2p-in-the-browser/README.md b/examples/libp2p-in-the-browser/README.md index 466b0f492a..9cadcc1c04 100644 --- a/examples/libp2p-in-the-browser/README.md +++ b/examples/libp2p-in-the-browser/README.md @@ -1,22 +1,55 @@ -# libp2p running in the Browser +# libp2p in the browser -One of the primary goals with libp2p P2P was to get it fully working in the browser and interopable with the versions running in Go and in Node.js. +This example leverages the [Parcel.js bundler](https://parceljs.org/) to compile and serve the libp2p code in the browser. Parcel uses [Babel](https://babeljs.io/) to handle transpilation of the code. You can use other bundlers such as Webpack or Browserify, but we will not be covering them here. -# 1. Setting up a simple app that lists connections to other nodes +## Setup -Start by installing libp2p's dependencies. +In order to run the example, first install the dependencies from same directory as this README: -```bash -> cd ../../ -> npm install -> cd examples/libp2p-in-the-browser ``` +cd ./examples/libp2p-in-the-browser +npm install +``` + +## Signaling Server + +This example uses the `libp2p-webrtc-star` module, which enables libp2p browser nodes to establish direct connections to one another via a central signaling server. For this example, we are using the signaling server that ships with `libp2p-webrtc-star`. + +You can start the server by running `npm run server`. This will start a signaling server locally on port `9090`. If you'd like to run a signaling server outside of this example, you can see instructions on how to do so in the [`libp2p-webrtc-star` README](https://github.com/libp2p/js-libp2p-webrtc-star). + +When you run the server, you should see output that looks something like this: + +```log +$ npm run server + +> libp2p-in-browser@1.0.0 server +> star-signal + +Listening on: http://0.0.0.0:9090 +``` + +## Running the examples -Then simply go into the folder [1](./1) and execute the following +Once you have started the signaling server, you can run the Parcel server. -```bash -> cd 1 -> npm install -> npm start -# open your browser in port :9090 ``` +npm start +``` + +The output should look something like this: + +```log +$ npm start + +> libp2p-in-browser@1.0.0 start +> parcel index.html + +Server running at http://localhost:1234 +✨ Built in 1000ms. +``` + +This will compile the code and start a server listening on port [http://localhost:1234](http://localhost:1234). Now open your browser to `http://localhost:1234`. You should see a log of your node's Peer ID, the discovered peers from the Bootstrap module, and connections to those peers as they are created. + +Now, if you open a second browser tab to `http://localhost:1234`, you should discover your node from the previous tab. This is due to the fact that the `libp2p-webrtc-star` transport also acts as a Peer Discovery interface. Your node will be notified of any peer that connects to the same signaling server you are connected to. Once libp2p discovers this new peer, it will attempt to establish a direct WebRTC connection. + +**Note**: In the example we assign libp2p to `window.libp2p`, in case you would like to play around with the API directly in the browser. You can of course make changes to `index.js` and Parcel will automatically rebuild and reload the browser tabs. diff --git a/examples/libp2p-in-the-browser/index.html b/examples/libp2p-in-the-browser/index.html new file mode 100644 index 0000000000..1c8f462b5b --- /dev/null +++ b/examples/libp2p-in-the-browser/index.html @@ -0,0 +1,23 @@ + + + + + + + js-libp2p parcel.js browser example + + + +
+

Starting libp2p...

+
+ +
+

+    
+ + + + + + \ No newline at end of file diff --git a/examples/libp2p-in-the-browser/index.js b/examples/libp2p-in-the-browser/index.js new file mode 100644 index 0000000000..2db38cc3c4 --- /dev/null +++ b/examples/libp2p-in-the-browser/index.js @@ -0,0 +1,73 @@ +import 'babel-polyfill' +import Libp2p from 'libp2p' +import Websockets from 'libp2p-websockets' +import WebRTCStar from 'libp2p-webrtc-star' +import Secio from 'libp2p-secio' +import Mplex from 'libp2p-mplex' +import Boostrap from 'libp2p-bootstrap' + +document.addEventListener('DOMContentLoaded', async () => { + // Create our libp2p node + const libp2p = await Libp2p.create({ + modules: { + transport: [Websockets, WebRTCStar], + connEncryption: [Secio], + streamMuxer: [Mplex], + peerDiscovery: [Boostrap] + }, + config: { + peerDiscovery: { + bootstrap: { + enabled: true, + list: [ + '/dns4/ams-1.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLer265NRgSp2LA3dPaeykiS1J6DifTC88f5uVQKNAd', + '/dns4/lon-1.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLMeWqB7YGVLJN3pNLQpmmEk35v6wYtsMGLzSr5QBU3', + '/dns4/sfo-3.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLPppuBtQSGwKDZT2M73ULpjvfd3aZ6ha4oFGL1KrGM', + '/dns4/sgp-1.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLSafTMBsPKadTEgaXctDQVcqN88CNLHXMkTNwMKPnu', + '/dns4/nyc-1.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLueR4xBeUbY9WZ9xGUUxunbKWcrNFTDAadQJmocnWm', + '/dns4/nyc-2.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLV4Bbm51jM9C4gDYZQ9Cy3U6aXMJDAbzgu2fzaDs64' + ] + } + } + } + }) + + // UI elements + const status = document.getElementById('status') + const output = document.getElementById('output') + + output.textContent = '' + + function log (txt) { + console.info(txt) + output.textContent += `${txt.trim()}\n` + } + + // Add the signaling server address, along with our PeerId to our multiaddrs list + // libp2p will automatically attempt to dial to the signaling server so that it can + // receive inbound connections from other peers + const webrtcAddr = '/ip4/0.0.0.0/tcp/9090/wss/p2p-webrtc-star' + libp2p.peerInfo.multiaddrs.add(webrtcAddr) + + // Listen for new peers + libp2p.on('peer:discovery', (peerInfo) => { + log(`Found peer ${peerInfo.id.toB58String()}`) + }) + + // Listen for new connections to peers + libp2p.on('peer:connect', (peerInfo) => { + log(`Connected to ${peerInfo.id.toB58String()}`) + }) + + // Listen for peers disconnecting + libp2p.on('peer:disconnect', (peerInfo) => { + log(`Disconnected from ${peerInfo.id.toB58String()}`) + }) + + await libp2p.start() + status.innerText = 'libp2p started!' + log(`libp2p id is ${libp2p.peerInfo.id.toB58String()}`) + + // Export libp2p to the window so you can play with the API + window.libp2p = libp2p +}) diff --git a/examples/libp2p-in-the-browser/package.json b/examples/libp2p-in-the-browser/package.json new file mode 100644 index 0000000000..9aa78ac69d --- /dev/null +++ b/examples/libp2p-in-the-browser/package.json @@ -0,0 +1,34 @@ +{ + "name": "libp2p-in-browser", + "version": "1.0.0", + "description": "A libp2p node running in the browser", + "main": "index.js", + "browserslist": [ + "last 2 Chrome versions" + ], + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "parcel index.html", + "server": "star-signal" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@babel/preset-env": "^7.8.3", + "libp2p": "../../", + "libp2p-bootstrap": "^0.10.3", + "libp2p-mplex": "^0.9.3", + "libp2p-secio": "^0.12.2", + "libp2p-webrtc-star": "^0.17.3", + "libp2p-websockets": "^0.13.2" + }, + "devDependencies": { + "@babel/cli": "^7.8.3", + "@babel/core": "^7.8.3", + "babel-plugin-syntax-async-functions": "^6.13.0", + "babel-plugin-transform-regenerator": "^6.26.0", + "babel-polyfill": "^6.26.0", + "parcel-bundler": "^1.12.4" + } +} diff --git a/src/transport-manager.js b/src/transport-manager.js index ec7a958146..bcbaa45e1f 100644 --- a/src/transport-manager.js +++ b/src/transport-manager.js @@ -86,8 +86,8 @@ class TransportManager { try { return await transport.dial(ma, options) } catch (err) { - if (err.code) throw err - throw errCode(err, codes.ERR_TRANSPORT_DIAL_FAILED) + if (!err.code) err.code = codes.ERR_TRANSPORT_DIAL_FAILED + throw err } } diff --git a/src/upgrader.js b/src/upgrader.js index 7b3fa12313..b56992883f 100644 --- a/src/upgrader.js +++ b/src/upgrader.js @@ -216,7 +216,10 @@ class Upgrader { Muxer, remotePeer }) { - let muxer, newStream + let muxer + let newStream + // eslint-disable-next-line prefer-const + let connection if (Muxer) { // Create the muxer @@ -261,7 +264,7 @@ class Upgrader { const _timeline = maConn.timeline maConn.timeline = new Proxy(_timeline, { set: (...args) => { - if (args[1] === 'close' && args[2] && !_timeline.close) { + if (connection && args[1] === 'close' && args[2] && !_timeline.close) { connection.stat.status = 'closed' this.onConnectionEnd(connection) } @@ -276,7 +279,7 @@ class Upgrader { } // Create the connection - const connection = new Connection({ + connection = new Connection({ localAddr: maConn.localAddr, remoteAddr: maConn.remoteAddr, localPeer: this.localPeer, From 691e9b7f7b5c702906032d39d88abee852869ee9 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Fri, 24 Jan 2020 11:55:59 +0100 Subject: [PATCH 90/92] chore: update contributors --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 29e01bcad1..b0b3f6fb52 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "libp2p", - "version": "0.27.0-pre.2", + "version": "0.27.0-rc.0", "description": "JavaScript implementation of libp2p, a modular peer to peer network stack", "leadMaintainer": "Jacob Heun ", "main": "src/index.js", From 1ba1ca77144a04c51c23fececbb29b5a504515fd Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Fri, 24 Jan 2020 11:55:59 +0100 Subject: [PATCH 91/92] chore: release version v0.27.0-rc.0 --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82eb114145..ce78c60196 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ + +# [0.27.0-rc.0](https://github.com/libp2p/js-libp2p/compare/v0.27.0-pre.2...v0.27.0-rc.0) (2020-01-24) + + +### Bug Fixes + +* registrar should filter the disconnected conn ([#532](https://github.com/libp2p/js-libp2p/issues/532)) ([83409de](https://github.com/libp2p/js-libp2p/commit/83409de)) +* stop discoveries ([#530](https://github.com/libp2p/js-libp2p/issues/530)) ([c44e6e3](https://github.com/libp2p/js-libp2p/commit/c44e6e3)) +* use toB58String everywhere to be consistent ([#537](https://github.com/libp2p/js-libp2p/issues/537)) ([31d1b23](https://github.com/libp2p/js-libp2p/commit/31d1b23)) + + + # [0.27.0-pre.2](https://github.com/libp2p/js-libp2p/compare/v0.27.0-pre.1...v0.27.0-pre.2) (2020-01-07) From 7e1a9d677beb43c950768345722f6b6e56311c20 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Fri, 24 Jan 2020 14:31:38 +0100 Subject: [PATCH 92/92] test(fix): fix listening test for ci --- test/core/listening.node.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/test/core/listening.node.js b/test/core/listening.node.js index 0178add3ac..c51af16484 100644 --- a/test/core/listening.node.js +++ b/test/core/listening.node.js @@ -41,16 +41,13 @@ describe('Listening', () => { // Should get something like: // /ip4/127.0.0.1/tcp/50866 // /ip4/192.168.1.2/tcp/50866 - expect(addrs.length).to.equal(2) - - const opts = [addrs[0].toOptions(), addrs[1].toOptions()] - expect(opts[0].family).to.equal('ipv4') - expect(opts[1].family).to.equal('ipv4') - expect(opts[0].transport).to.equal('tcp') - expect(opts[1].transport).to.equal('tcp') - expect(opts[0].host).to.match(/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/) - expect(opts[1].host).to.match(/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/) - expect(opts[0].port).to.be.gt(0) - expect(opts[1].port).to.be.gt(0) + expect(addrs.length).to.be.at.least(2) + for (const addr of addrs) { + const opts = addr.toOptions() + expect(opts.family).to.equal('ipv4') + expect(opts.transport).to.equal('tcp') + expect(opts.host).to.match(/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/) + expect(opts.port).to.be.gt(0) + } }) })