From b8bf044e562349b002b8d4b4f6cb644240f0d17e Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Wed, 20 May 2020 14:13:08 +0200 Subject: [PATCH] feat: rendezvous protocol full implementation --- .aegir.js | 57 +++++ .travis.yml | 61 +++-- LIBP2P.md | 47 ++++ README.md | 113 ++++++++- appveyor.yml | 28 --- package.json | 76 ++++-- src/constants.js | 5 + src/errors.js | 8 + src/index.js | 340 ++++++++++++++++++++++---- src/proto.js | 4 +- src/rpc.js | 155 ------------ src/server/index.js | 171 +++++++++---- src/server/queue.js | 33 --- src/server/rpc.js | 159 ------------ src/server/rpc/handlers/discover.js | 64 +++++ src/server/rpc/handlers/index.js | 21 ++ src/server/rpc/handlers/register.js | 76 ++++++ src/server/rpc/handlers/unregister.js | 42 ++++ src/server/rpc/index.js | 65 +++++ src/server/store/basic/index.js | 60 ----- test/client-mode.spec.js | 47 ++++ test/client.id.json | 5 - test/client2.id.json | 5 - test/connectivity.spec.js | 67 +++++ test/discovery.spec.js | 68 ------ test/fixtures/browser.js | 7 + test/fixtures/peers.js | 27 ++ test/flows.spec.js | 101 ++++++++ test/rendezvous.spec.js | 224 +++++++++++++++++ test/server.id.json | 5 - test/utils.js | 122 ++++----- 31 files changed, 1509 insertions(+), 754 deletions(-) create mode 100644 .aegir.js create mode 100644 LIBP2P.md delete mode 100644 appveyor.yml create mode 100644 src/constants.js create mode 100644 src/errors.js delete mode 100644 src/rpc.js delete mode 100644 src/server/queue.js delete mode 100644 src/server/rpc.js create mode 100644 src/server/rpc/handlers/discover.js create mode 100644 src/server/rpc/handlers/index.js create mode 100644 src/server/rpc/handlers/register.js create mode 100644 src/server/rpc/handlers/unregister.js create mode 100644 src/server/rpc/index.js delete mode 100644 src/server/store/basic/index.js create mode 100644 test/client-mode.spec.js delete mode 100644 test/client.id.json delete mode 100644 test/client2.id.json create mode 100644 test/connectivity.spec.js delete mode 100644 test/discovery.spec.js create mode 100644 test/fixtures/browser.js create mode 100644 test/fixtures/peers.js create mode 100644 test/flows.spec.js create mode 100644 test/rendezvous.spec.js delete mode 100644 test/server.id.json diff --git a/.aegir.js b/.aegir.js new file mode 100644 index 0000000..e78b224 --- /dev/null +++ b/.aegir.js @@ -0,0 +1,57 @@ +'use strict' + +const Libp2p = require('libp2p') +const { MULTIADDRS_WEBSOCKETS } = require('./test/fixtures/browser') +const Peers = require('./test/fixtures/peers') +const PeerId = require('peer-id') +const WebSockets = require('libp2p-websockets') +const Muxer = require('libp2p-mplex') +const { NOISE: Crypto } = require('libp2p-noise') + +const Rendezvous = require('.') + +let libp2p, rendezvous + +const before = async () => { + // Use the last peer + const peerId = await PeerId.createFromJSON(Peers[Peers.length - 1]) + + libp2p = new Libp2p({ + addresses: { + listen: [MULTIADDRS_WEBSOCKETS[0]] + }, + peerId, + modules: { + transport: [WebSockets], + streamMuxer: [Muxer], + connEncryption: [Crypto] + }, + config: { + relay: { + enabled: true, + hop: { + enabled: true, + active: false + } + } + } + }) + + await libp2p.start() + + // rendezvous = new Rendezvous({ libp2p }) + // await rendezvous.start() +} + +const after = async () => { + // await rendezvous.stop() + await libp2p.stop() +} + +module.exports = { + bundlesize: { maxSize: '100kB' }, + hooks: { + pre: before, + post: after + } +} diff --git a/.travis.yml b/.travis.yml index 74f58e8..fdab469 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,31 +1,42 @@ -sudo: false language: node_js +cache: npm +stages: + - check + - test + - cov -matrix: - include: - - node_js: 6 - env: CXX=g++-4.8 - - node_js: 8 - env: CXX=g++-4.8 - # - node_js: stable - # env: CXX=g++-4.8 +node_js: + - '10' + - '12' + +os: + - linux + - osx + - windows -script: - - npm run lint - - npm run test - - npm run coverage +script: npx nyc -s npm run test:node -- --bail +after_success: npx nyc report --reporter=text-lcov > coverage.lcov && npx codecov + +jobs: + include: + - stage: check + script: + - npx aegir dep-check + - npm run lint -before_script: - - export DISPLAY=:99.0 - - sh -e /etc/init.d/xvfb start + - stage: test + name: chrome + addons: + chrome: stable + script: + - npx aegir test -t browser -t webworker -after_success: - - npm run coverage-publish + - stage: test + name: firefox + addons: + firefox: latest + script: + - npx aegir test -t browser -t webworker -- --browsers FirefoxHeadless -addons: - firefox: 'latest' - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - g++-4.8 +notifications: + email: false \ No newline at end of file diff --git a/LIBP2P.md b/LIBP2P.md new file mode 100644 index 0000000..f3cf41e --- /dev/null +++ b/LIBP2P.md @@ -0,0 +1,47 @@ +# Rendezvous Protocol in js-libp2p + +The rendezvous protocol can be used in different contexts across libp2p. For using it, the libp2p network needs to have well known libp2p nodes acting as rendezvous servers. These nodes will have an extra role in the network. They will collect and maintain a list of registrations per rendezvous namespace. Other peers in the network will act as rendezvous clients and will register themselves on given namespaces by messaging a rendezvous server node. Taking into account these registrations, a rendezvous client is able to discover other peers in a given namespace by querying a server. + +## Usage + +`js-libp2p` supports the usage of the rendezvous protocol through its configuration. It allows to enable the rendezvous protocol, as well as its server mode, enable automatic peer discover and to specify the topics to register from startup. + +The rendezvous comes with a discovery service that enables libp2p to automatically discover other peers in the provided namespaces and eventually connect to them. +**TODO: it should be compliant with the peer-discovery interface and configured as any other discovery service instead!!** + +You can configure it through libp2p as follows: + +```js +const Libp2p = require('libp2p') + +const node = await Libp2p.create({ + // ... required configurations + rendezvous: { + enabled: true, + namespaces: ['/namespace/1', '/namespace/2'], + discovery: { + enabled: true, + interval: 1000 + }, + server: { + enabled: true + } + } +}) +``` + +While `js-libp2p` supports the rendezvous protocol out of the box, it also provides a rendezvous API that users can interact with. This API should allow users to register new rendezvous namespaces, unregister from previously registered namespaces and to manually discover other peers. + +## Libp2p Flow + +When a libp2p node with the rendezvous protocol enabled starts, it should start by connecting to a rendezvous server and ask for nodes in the given namespaces. The rendezvous server can be added to the bootstrap nodes or manually dialed. An example of a namespace could be a relay namespace, so that undiable nodes can register themselves as reachable through that relay. + +If the discovery service is disabled, the rendezvous API should allow users to discover peers registered on provided namespaces. + +When a libp2p node running the rendezvous protocol is going to stop, it should unregister from all the namespaces previously registered. + +In the event of a rendezvous client getting connected to a second rendezvous server, it should propagate its registrations to it. The rendezvous server should clean its registrations for a peer when it is not connected with it anymore. + +## Other notes: + +After a query is made, who is responsible for determining if we need more records? (cookie reuse) diff --git a/README.md b/README.md index 1443992..995cc57 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,117 @@ -# libp2p-rendezvous +# js-libp2p-rendezvous -A javascript implementation of the rendezvous protocol for libp2p +[![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://protocol.ai) +[![](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![](https://img.shields.io/badge/freenode-%23libp2p-yellow.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23libp2p) +[![](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg)](https://discuss.libp2p.io) +> Javascript implementation of the rendezvous protocol for libp2p + +## Overview + +Libp2p rendezvous is a lightweight mechanism for generalized peer discovery. It can be used for bootstrap purposes, real time peer discovery, application specific routing, and so on. Any node implementing the rendezvous protocol can act as a rendezvous point, allowing the discovery of relevant peers in a decentralized fashion. + +See https://github.com/libp2p/specs/tree/master/rendezvous for more details ## Lead Maintainer [Vasco Santos](https://github.com/vasco-santos). -See https://github.com/libp2p/specs/pull/44 for more details +## API + +### rendezvous.register + +Registers the peer in a given namespace. + +`rendezvous.register(namespace, [ttl])` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| namespace | `string` | namespace to register | +| ttl | `number` | registration ttl in ms (default: `7200e3` and minimum `120`) | + +#### Returns + +| Type | Description | +|------|-------------| +| `Promise` | Remaining ttl value | + +#### Example + +```js +// ... +const ttl = await rendezvous.register(namespace) +``` + +### rendezvous.unregister + +Unregisters the peer from a given namespace. + +`rendezvous.unregister(namespace)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| namespace | `string` | namespace to unregister | + +#### Returns + +| Type | Description | +|------|-------------| +| `Promise` | Operation resolved | + +#### Example + +```js +// ... +await rendezvous.register(namespace) +await rendezvous.unregister(namespace) +``` + +### rendezvous.discover + +Discovers peers registered under a given namespace. + +`rendezvous.discover(namespace, [limit], [cookie])` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| namespace | `string` | namespace to discover | +| limit | `number` | limit of peers to discover | +| cookie | `Buffer` | | + +#### Returns + +| Type | Description | +|------|-------------| +| `AsyncIterable<{ id: PeerId, signedPeerRecord: Envelope, ns: string, ttl: number }>` | Async Iterable registrations | + +#### Example + +```js +// ... +await rendezvous.register(namespace) + +for await (const reg of rendezvous.discover(namespace)) { + console.log(reg.id, reg.signedPeerRecord, reg.ns, reg.ttl) +} +``` + +## Contribute + +Feel free to join in. All welcome. Open an [issue](https://github.com/libp2p/js-libp2p-pubsub-peer-discovery/issues)! + +This repository falls under the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). + +[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/contributing.md) + +## License + +MIT - Protocol Labs 2020 + +[multiaddr]: https://github.com/multiformats/js-multiaddr diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 58aef65..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,28 +0,0 @@ -version: "{build}" - -environment: - matrix: - - nodejs_version: "6" - - nodejs_version: "8" - -matrix: - fast_finish: true - -install: - # Install Node.js - - ps: Install-Product node $env:nodejs_version - - # Upgrade npm - - npm install -g npm - - # Output our current versions for debugging - - node --version - - npm --version - - # Install our package dependencies - - npm install - -test_script: - - npm run test:node - -build: off diff --git a/package.json b/package.json index 8e3af76..6218213 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,16 @@ { "name": "libp2p-rendezvous", "version": "0.0.0", - "description": "A javascript implementation of the rendezvous protocol for libp2p", - "leadMaintainer": "Vasco Santos ", - "main": "index.js", - "scripts": { - "test": "aegir test" + "description": "Javascript implementation of the rendezvous protocol for libp2p", + "leadMaintainer": "Vasco Santos ", + "main": "src/index.js", + "files": [ + "dist", + "src" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p-rendezvous.git" }, "keywords": [ "libp2p", @@ -13,28 +18,49 @@ "protocol", "discovery" ], - "author": "Maciej Krüger ", - "license": "MIT", - "dependencies": { - "chai": "^4.1.2", - "dirty-chai": "^2.0.1", - "protons": "^1.0.1", - "pull-protocol-buffers": "^0.1.2" + "bugs": { + "url": "https://github.com/libp2p/js-libp2p-rendezvous/issues" }, - "devDependencies": { - "aegir": "^13.1.0", - "libp2p": "^0.20.2", - "libp2p-mplex": "^0.7.0", - "libp2p-secio": "^0.10.0", - "libp2p-spdy": "^0.12.1", - "libp2p-tcp": "^0.12.0" + "homepage": "https://libp2p.io", + "license": "MIT", + "engines": { + "node": ">=10.0.0", + "npm": ">=6.0.0" }, - "repository": { - "type": "git", - "url": "git+https://github.com/mkg20001/libp2p-rendezvous.git" + "scripts": { + "lint": "aegir lint", + "build": "aegir build", + "test": "aegir test", + "test:node": "aegir test -t node", + "test:browser": "aegir test -t browser", + "release": "aegir release", + "release-minor": "aegir release --type minor", + "release-major": "aegir release --type major", + "coverage": "nyc --reporter=text --reporter=lcov npm test" }, - "bugs": { - "url": "https://github.com/mkg20001/libp2p-rendezvous/issues" + "dependencies": { + "debug": "^4.1.1", + "err-code": "^2.0.3", + "it-buffer": "^0.1.2", + "it-length-prefixed": "^3.0.1", + "it-pipe": "^1.1.0", + "libp2p-interfaces": "^0.3.0", + "multiaddr": "^7.5.0", + "peer-id": "^0.13.13", + "protons": "^1.2.0", + "streaming-iterables": "^4.1.2" }, - "homepage": "https://github.com/mkg20001/libp2p-rendezvous#readme" + "devDependencies": { + "aegir": "^23.0.0", + "chai": "^4.2.0", + "chai-as-promised": "^7.1.1", + "dirty-chai": "^2.0.1", + "libp2p": "^0.28.3", + "libp2p-mplex": "^0.9.5", + "libp2p-noise": "^1.1.2", + "libp2p-websockets": "^0.13.6", + "p-times": "^3.0.0", + "p-wait-for": "^3.1.0", + "sinon": "^9.0.2" + } } diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000..3f063b7 --- /dev/null +++ b/src/constants.js @@ -0,0 +1,5 @@ +'use strict' + +exports.PROTOCOL_MULTICODEC = '/rendezvous/1.0.0' +exports.MAX_NS_LENGTH = 255 // TODO: spec this +exports.MAX_LIMIT = 1000 // TODO: spec this diff --git a/src/errors.js b/src/errors.js new file mode 100644 index 0000000..d130f4b --- /dev/null +++ b/src/errors.js @@ -0,0 +1,8 @@ +'use strict' + +exports.codes = { + INVALID_NAMESPACE: 'ERR_INVALID_NAMESPACE', + INVALID_TTL: 'ERR_INVALID_TTL', + INVALID_MULTIADDRS: 'ERR_INVALID_MULTIADDRS', + NO_CONNECTED_RENDEZVOUS_SERVERS: 'ERR_NO_CONNECTED_RENDEZVOUS_SERVERS' +} diff --git a/src/index.js b/src/index.js index fcbf075..eea845d 100644 --- a/src/index.js +++ b/src/index.js @@ -1,78 +1,320 @@ 'use strict' -const RPC = require('./rpc') -const noop = () => {} +const debug = require('debug') +const log = debug('libp2p:redezvous') +log.error = debug('libp2p:redezvous:error') -class RendezvousDiscovery { - constructor (swarm) { - this.swarm = swarm - this.peers = [] +const errCode = require('err-code') +const pipe = require('it-pipe') +const lp = require('it-length-prefixed') +const { collect } = require('streaming-iterables') +const { toBuffer } = require('it-buffer') + +const MulticodecTopology = require('libp2p-interfaces/src/topology/multicodec-topology') +const multiaddr = require('multiaddr') +const PeerId = require('peer-id') + +const Server = require('./server') +const { codes: errCodes } = require('./errors') +const { PROTOCOL_MULTICODEC } = require('./constants') +const { Message } = require('./proto') +const MESSAGE_TYPE = Message.MessageType + +/** + * Libp2p Rendezvous. + * A lightweight mechanism for generalized peer discovery. + */ +class Rendezvous { + /** + * @constructor + * @param {object} params + * @param {Libp2p} params.libp2p + * @param {object} params.options + * @param {boolean} [params.options.isServer = true] + */ + constructor ({ libp2p, options = { isServer: true } }) { + this._libp2p = libp2p + this._peerId = libp2p.peerId + this._registrar = libp2p.registrar + this._options = options + this._server = undefined + + /** + * @type {Map} + */ + this._rendezvousConns = new Map() + + this._registrarId = undefined + this._onPeerConnected = this._onPeerConnected.bind(this) + this._onPeerDisconnected = this._onPeerDisconnected.bind(this) } - _dial (pi, cb) { - if (!cb) cb = noop - this.swarm.dialProtocol(pi, '/rendezvous/1.0.0', (err, conn) => { - if (err) return cb(err) - const rpc = new RPC() - rpc.setup(conn, err => { - if (err) return cb(err) - this.peers.push(rpc) - cb() - }) + /** + * Register the rendezvous protocol in the libp2p node. + * @returns {Promise} + */ + async start () { + if (this._registrarId) { + return + } + + log('starting') + + // Create Rendezvous point if enabled + if (this._options.isServer) { + this._server = new Server({ registrar: this._registrar }) + } + + // register protocol with topology + const topology = new MulticodecTopology({ + multicodecs: PROTOCOL_MULTICODEC, + handlers: { + onConnect: this._onPeerConnected, + onDisconnect: this._onPeerDisconnected + } }) + this._registrarId = await this._registrar.register(topology) + + log('started') + } + + /** + * Unregister the rendezvous protocol and the streams with other peers will be closed. + * @returns {Promise} + */ + async stop () { + if (!this._registrarId) { + return + } + + log('stopping') + + // unregister protocol and handlers + await this._registrar.unregister(this._registrarId) + + this._registrarId = undefined + log('stopped') } - _rpc (cmd, ...a) { // TODO: add. round-robin / multicast / anycast? - this.peers[0][cmd](...a) + /** + * Registrar notifies a connection successfully with rendezvous protocol. + * @private + * @param {PeerId} peerId remote peer-id + * @param {Connection} conn connection to the peer + */ + _onPeerConnected (peerId, conn) { + const idB58Str = peerId.toB58String() + log('connected', idB58Str) + + this._rendezvousConns.set(idB58Str, conn) } - register (ns, peer, cb) { - this._rpc('register', ns, peer, 0, cb) // TODO: interface does not expose ttl option?! + /** + * Registrar notifies a closing connection with rendezvous protocol. + * @private + * @param {PeerId} peerId peerId + */ + _onPeerDisconnected (peerId) { + const idB58Str = peerId.toB58String() + log('disconnected', idB58Str) + + this._rendezvousConns.delete(idB58Str) + + if (this._server) { + this._server.removePeerRegistrations(peerId) + } } - discover (ns, limit, cookie, cb) { - if (typeof cookie === 'function') { - cb = cookie - cookie = Buffer.from('') + /** + * Register the peer in a given namespace + * @param {string} ns + * @param {number} [ttl = 7200e3] registration ttl in ms (minimum 120) + * @returns {Promise} + */ + async register (ns, ttl = 7200e3) { + if (!ns) { + throw errCode(new Error('a namespace must be provided'), errCodes.INVALID_NAMESPACE) } - if (typeof limit === 'function') { - cookie = Buffer.from('') - cb = limit - limit = 0 + + if (ttl < 120) { + throw errCode(new Error('a valid ttl must be provided (bigger than 120)'), errCodes.INVALID_TTL) } - if (typeof ns === 'function') { - cookie = Buffer.from('') - limit = 0 - cb = ns - ns = null + + const addrs = [] + for (const m of this._libp2p.multiaddrs) { + if (!multiaddr.isMultiaddr(m)) { + throw errCode(new Error('one or more of the provided multiaddrs is not valid'), errCodes.INVALID_MULTIADDRS) + } + + addrs.push(m.buffer) + } + + // Are there available rendezvous servers? + if (!this._rendezvousConns.size) { + throw errCode(new Error('no rendezvous servers connected'), errCodes.NO_CONNECTED_RENDEZVOUS_SERVERS) } - this._rpc('discover', ns, limit, cookie, cb) + const message = Message.encode({ + type: MESSAGE_TYPE.REGISTER, + register: { + peer: { + id: this._peerId.toBytes(), + addrs + }, + ns, + ttl // TODO: convert to seconds + } + }) + + const registerTasks = [] + const taskFn = async (id) => { + const conn = this._rendezvousConns.get(id) + const { stream } = await conn.newStream(PROTOCOL_MULTICODEC) + + const [response] = await pipe( + [message], + lp.encode(), + stream, + lp.decode(), + toBuffer, + collect + ) + + const recMessage = Message.decode(response) + + if (!recMessage.type === MESSAGE_TYPE.REGISTER_RESPONSE) { + throw new Error('unexpected message received') + } + + return recMessage.registerResponse.ttl + } + + for (const id of this._rendezvousConns.keys()) { + registerTasks.push(taskFn(id)) + } + + // Return first ttl + const [returnTtl] = await Promise.all(registerTasks) + return returnTtl } - unregister (ns, id) { + /** + * Unregister peer from the nampesapce. + * @param {string} ns + * @returns {Promise} + */ + async unregister (ns) { if (!ns) { - id = this.swarm.peerInfo.id.toBytes() - ns = null + throw errCode(new Error('a namespace must be provided'), errCodes.INVALID_NAMESPACE) } - if (!id) { - id = this.swarm.peerInfo.id.toBytes() + + // Are there available rendezvous servers? + if (!this._rendezvousConns.size) { + throw errCode(new Error('no rendezvous servers connected'), errCodes.NO_CONNECTED_RENDEZVOUS_SERVERS) + } + + const message = Message.encode({ + type: MESSAGE_TYPE.UNREGISTER, + unregister: { + id: this._peerId.toBytes(), + ns + } + }) + + const unregisterTasks = [] + const taskFn = async (id) => { + const conn = this._rendezvousConns.get(id) + const { stream } = await conn.newStream(PROTOCOL_MULTICODEC) + + await pipe( + [message], + lp.encode(), + stream, + async (source) => { + for await (const _ of source) { } // eslint-disable-line + } + ) + } + + for (const id of this._rendezvousConns.keys()) { + unregisterTasks.push(taskFn(id)) } - this._rpc('unregister', ns, id) + await Promise.all(unregisterTasks) } - start (cb) { - this.swarm.on('peer:connect', peer => { - this._dial(peer) + /** + * Discover peers registered under a given namespace + * @param {string} ns + * @param {number} [limit] + * @param {Buffer} [cookie] + * @returns {AsyncIterable<{ id: PeerId, multiaddrs: Array, ns: string, ttl: number }>} + */ + async * discover (ns, limit, cookie) { + // Are there available rendezvous servers? + if (!this._rendezvousConns.size) { + throw errCode(new Error('no rendezvous servers connected'), errCodes.NO_CONNECTED_RENDEZVOUS_SERVERS) + } + + const registrationTransformer = (r) => ({ + id: PeerId.createFromBytes(r.peer.id), + multiaddrs: r.peer.addrs && r.peer.addrs.map((a) => multiaddr(a)), + ns: r.ns, + ttl: r.ttl }) - cb() - } - stop (cb) { - // TODO: shutdown all conns - cb() + // Local search if Server + if (this._server) { + const localRegistrations = this._server.getRegistrations(ns, limit) + for (const r of localRegistrations) { + yield registrationTransformer(r) + + limit-- + if (limit === 0) { + return + } + } + } + + const message = Message.encode({ + type: MESSAGE_TYPE.DISCOVER, + discover: { + ns, + limit, + cookie + } + }) + + for (const id of this._rendezvousConns.keys()) { + const conn = this._rendezvousConns.get(id) + const { stream } = await conn.newStream(PROTOCOL_MULTICODEC) + + const [response] = await pipe( + [message], + lp.encode(), + stream, + lp.decode(), + toBuffer, + collect + ) + + const recMessage = Message.decode(response) + + if (!recMessage.type === MESSAGE_TYPE.DISCOVER_RESPONSE) { + throw new Error('unexpected message received') + } + + for (const r of recMessage.discoverResponse.registrations) { + // track registrations and check if already provided + yield registrationTransformer(r) + + limit-- + if (limit === 0) { + return + } + } + } } } -module.exports = RendezvousDiscovery +module.exports = Rendezvous diff --git a/src/proto.js b/src/proto.js index b2c6d94..ed9574b 100644 --- a/src/proto.js +++ b/src/proto.js @@ -3,6 +3,7 @@ const protons = require('protons') module.exports = protons(` +message Message { enum MessageType { REGISTER = 0; REGISTER_RESPONSE = 1; @@ -19,6 +20,7 @@ module.exports = protons(` E_INVALID_COOKIE = 103; E_NOT_AUTHORIZED = 200; E_INTERNAL_ERROR = 300; + E_UNAVAILABLE = 400; } message PeerInfo { @@ -35,6 +37,7 @@ module.exports = protons(` message RegisterResponse { optional ResponseStatus status = 1; optional string statusText = 2; + optional int64 ttl = 3; // in seconds } message Unregister { @@ -55,7 +58,6 @@ module.exports = protons(` optional string statusText = 4; } -message Message { optional MessageType type = 1; optional Register register = 2; optional RegisterResponse registerResponse = 3; diff --git a/src/rpc.js b/src/rpc.js deleted file mode 100644 index 5d0781b..0000000 --- a/src/rpc.js +++ /dev/null @@ -1,155 +0,0 @@ -'use strict' - -const pull = require('pull-stream') -const ppb = require('pull-protocol-buffers') -const {Message, MessageType} = require('./proto') -const Pushable = require('pull-pushable') -const debug = require('debug') -const log = debug('libp2p-rendezvous:rpc') -const Peer = require('peer-info') -const Id = require('peer-id') -const once = require('once') - -const TIMEOUT = 1000 * 10 // TODO: spec this - -function wrap (f, t) { - let cb = once((...a) => { - clearTimeout(timeout) - f(...a) - }) - let timeout - timeout = setTimeout(() => cb(new Error('Timeout!')), t) - return cb -} - -class RPC { - constructor () { - this.source = Pushable() - this.cbs = { - discover: [], - register: [] - } - } - sink (read) { - const next = (end, msg, doend) => { - if (doend) { - log('crash@%s: %s', this.id, doend) - return read(doend, next) - } - if (end) { - this.online = false - log('end@%s: %s', this.id, end) - this.source.end() - return - } - let f - let pi - switch (msg.type) { - case MessageType.REGISTER_RESPONSE: - f = this.cbs.register.shift() - if (typeof f !== 'function') { - log('register@%s: response ignored, no cb found!', this.id) - return read(null, next) - } else { - let e - if (msg.registerResponse.status) { - e = new Error('Server returned error: ' + (msg.registerResponse.statusText || '(unknown code)')) - } - f(e) - } - break - case MessageType.DISCOVER_RESPONSE: - try { - f = this.cbs.discover.shift() - if (typeof f !== 'function') { - log('discover@%s: response ignored, no cb found!', this.id) - return read(null, next) - } else { - if (msg.discoverResponse.status) { - return setImmediate(() => f(new Error('Server returned error: ' + (msg.discoverResponse.statusText || '(unknown code)')))) - } - pi = msg.discoverResponse.registrations.map(p => { - try { - // TODO: use other values like ttl/ns in peer-info? - const pi = new Peer(new Id(p.peer.id)) - p.peer.addrs.forEach(a => pi.multiaddrs.add(a)) - return pi - } catch (e) { - log('discover@%s: invalid pi returned: %s', this.id, e) - } - }).filter(Boolean) - setImmediate(() => f(null, { - cookie: msg.discoverResponse.cookie, - peers: pi - })) - } - } catch (e) { - f(e) - return next(null, null, e) - } - break - default: // should that disconnect or just get ignored? - log('error@%s: sent wrong msg type %s', this.id, msg.type) - return next(null, null, true) - } - read(null, next) - } - read(null, next) - } - setup (conn, cb) { - conn.getPeerInfo((err, pi) => { - if (err) return cb(err) - this.pi = pi - this.id = pi.id.toB58String() - pull( - conn, - ppb.decode(Message), - this, - ppb.encode(Message), - conn - ) - - this.online = true - cb() - }) - } - - register (ns, peer, ttl, cb) { - this.source.push({ - type: MessageType.REGISTER, - register: { - ns, - peer: { - id: peer.id.toBytes(), - addrs: peer.multiaddrs.toArray().map(a => a.buffer) - }, - ttl - } - }) - this.cbs.register.push(wrap(cb, TIMEOUT)) - } - - discover (ns, limit, cookie, cb) { - this.source.push({ - type: MessageType.DISCOVER, - discover: { - ns, - limit, - cookie - } - }) - this.cbs.discover.push(wrap(cb, TIMEOUT)) - } - - unregister (ns, id) { - this.source.push({ - type: MessageType.UNREGISTER, - unregister: { - ns, - id - } - }) - } -} - -module.exports = RPC diff --git a/src/server/index.js b/src/server/index.js index e7d7ca7..0786ccb 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -1,73 +1,136 @@ 'use strict' -// const {waterfall} = require('async') -const RPC = require('./rpc') const debug = require('debug') -const log = debug('libp2p:rendezvous:server') -const AsyncQueue = require('./queue') -const BasicStore = require('./store/basic') - -class Server { - constructor (opt) { - if (!opt) opt = {} - this.node = opt.node - this.config = opt.config - this.que = new AsyncQueue() - this.table = { - NS: {}, - RPC: {} - } - const Store = opt.store || BasicStore - this.store = new Store(this) - this._stubNS = this.store.create(Buffer.alloc(256, '0').toString()) +const log = debug('libp2p:redezvous-server') +log.error = debug('libp2p:redezvous-server:error') + +const { PROTOCOL_MULTICODEC, MAX_LIMIT } = require('../constants') +const rpc = require('./rpc') + +/** +* Rendezvous registration. +* @typedef {Object} Registration +* @property {PeerId} peerId +* @property {Array} addrs +* @property {number} expiration +*/ + +/** + * Libp2p rendezvous server. + */ +class RendezvousServer { + /** + * @constructor + * @param {object} params + * @param {Registrar} params.registrar + */ + constructor ({ registrar }) { + this._registrar = registrar + + /** + * Registrations per namespace. + * @type {Map>} + */ + this.registrations = new Map() + + // Incoming streams handling + this._registrar.handle(PROTOCOL_MULTICODEC, rpc(this)) } - start () { - this.gcIntv = setInterval(this.gc.bind(this), 60 * 1000) - this.node.handle('/rendezvous/1.0.0', (proto, conn) => { - const rpc = new RPC(this) - rpc.setup(conn, err => { - if (err) return log(err) - this.storeRPC(rpc) - }) + // TODO: Should we have a start method to gv the expired registrations? + // I am removing them on discover, but it should be useful to have a gc too + + /** + * Add a peer registration to a namespace. + * @param {string} ns + * @param {PeerId} peerId + * @param {Array} addrs + * @param {number} ttl + * @returns {void} + */ + addRegistration (ns, peerId, addrs, ttl) { + const nsRegistrations = this.registrations.get(ns) || new Map() + + nsRegistrations.set(peerId.toB58String(), { + peerId, + addrs, + expiration: Date.now() + ttl }) - } - stop () { - clearInterval(this.gcIntv) - // TODO: clear vars, shutdown conns, etc. - this.node.unhandle('/rendezvous/1.0.0') + this.registrations.set(ns, nsRegistrations) } - storeRPC (rpc) { - // TODO: should a peer that's connected twice be overriden or rejected? - this.table.RPC[rpc.id] = rpc - // TODO: remove on disconnect + /** + * Remove rengistration of a given namespace to a peer + * @param {string} ns + * @param {PeerId} peerId + * @returns {void} + */ + removeRegistration (ns, peerId) { + const nsRegistrations = this.registrations.get(ns) + + if (nsRegistrations) { + nsRegistrations.delete(peerId.toB58String()) + + // Remove registrations map to namespace if empty + if (!nsRegistrations.size) { + this.registrations.delete(ns) + } + log('removed existing registrations for the namespace - peer pair:', ns, peerId.toB58String()) + } } - getNS (name, create) { - if (!this.table.NS[name]) { - if (create) { - return (this.table.NS[name] = this.store.create(name)) - } else { - return this._stubNS + /** + * Remove registrations of a given peer + * @param {PeerId} peerId + * @returns {void} + */ + removePeerRegistrations (peerId) { + for (const [ns, reg] of this.registrations.entries()) { + reg.delete(peerId.toB58String()) + + // Remove registrations map to namespace if empty + if (!reg.size) { + this.registrations.delete(ns) } } - return this.table.NS[name] + + log('removed existing registrations for peer', peerId.toB58String()) } - gc () { - Object.keys(this.table.NS).forEach(ns => { - const n = this.table.NS[ns] - const removed = n.gc() - if (n.useless) { - log('drop NS %s because it is empty', n.name) - delete this.table.NS[ns] - } else { - if (removed) n.update() + /** + * Get registrations for a namespace + * @param {string} ns + * @param {number} limit + * @returns {Array} + */ + getRegistrations (ns, limit = MAX_LIMIT) { + const nsRegistrations = this.registrations.get(ns) || new Map() + const registrations = [] + + for (const [idStr, reg] of nsRegistrations.entries()) { + if (reg.expiration <= Date.now()) { + // Clean outdated registration + nsRegistrations.delete(idStr) + continue } - }) + + registrations.push({ + ns, + peer: { + id: reg.peerId.toBytes(), + addrs: reg.addrs + }, + ttl: reg.expiration - Date.now() + }) + + // Stop if reached limit + if (registrations.length === limit) { + break + } + } + return registrations } } -module.exports = Server +module.exports = RendezvousServer diff --git a/src/server/queue.js b/src/server/queue.js deleted file mode 100644 index f12b5c6..0000000 --- a/src/server/queue.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict' - -const debug = require('debug') -const log = debug('libp2p:rendezvous:queue') - -class AsyncQueue { - constructor () { - this.tasks = [] - this.taskIds = {} - this.triggered = false - } - add (name, fnc) { - if (this.taskIds[name]) return - log('queueing %s', name) - this.taskIds[name] = true - this.tasks.push(fnc) - this.trigger() - } - trigger () { - if (this.triggered) return - this.triggered = true - setTimeout(() => { - log('exec') - this.tasks.forEach(f => f()) - this.tasks = [] - this.taskIds = {} - this.triggered = false - log('exec done') - }, 100).unref() - } -} - -module.exports = AsyncQueue diff --git a/src/server/rpc.js b/src/server/rpc.js deleted file mode 100644 index fd49d2d..0000000 --- a/src/server/rpc.js +++ /dev/null @@ -1,159 +0,0 @@ -'use strict' - -const pull = require('pull-stream') -const ppb = require('pull-protocol-buffers') -const {Message, MessageType, ResponseStatus} = require('../proto') -const Pushable = require('pull-pushable') -const debug = require('debug') -const log = debug('libp2p-rendezvous:server:rpc') -const Peer = require('peer-info') -const Id = require('peer-id') - -const MAX_NS_LENGTH = 255 // TODO: spec this -const MAX_LIMIT = 1000 // TODO: spec this - -const registerErrors = { - 100: 'Invalid namespace provided', - 101: 'Invalid peer-info provided', - 102: 'Invalid TTL provided', - 103: 'Invalid cookie provided', - 200: 'Not authorized', - 300: 'Internal Server Error' -} - -const craftStatus = (status) => { - return { - status, - statusText: registerErrors[status] - } -} - -class RPC { - constructor (main) { - this.main = main - this.source = Pushable() - } - sink (read) { - const next = (end, msg, doend) => { - if (doend) { - log('crash@%s: %s', this.id, doend) - return read(doend, next) - } - if (end) { - this.online = false - log('end@%s: %s', this.id, end) - this.source.end() - return - } - switch (msg.type) { - case MessageType.REGISTER: - try { - log('register@%s: trying register on %s', this.id, msg.register.ns) - if (msg.register.peer.id && new Id(msg.register.peer.id).toB58String() !== this.id) { - log('register@%s: auth err (want %s)', this.id, new Id(msg.register.peer.id).toB58String()) - this.source.push({ - type: MessageType.REGISTER_RESPONSE, - registerResponse: craftStatus(ResponseStatus.E_NOT_AUTHORIZED) - }) - return read(null, next) - } else if (!msg.register.peer.id) { - msg.register.peer.id = this.pi.id.toBytes() - } - if (msg.register.ns > MAX_NS_LENGTH) { - log('register@%s: ns err', this.id) - this.source.push({ - type: MessageType.REGISTER_RESPONSE, - registerResponse: craftStatus(ResponseStatus.E_INVALID_NAMESPACE) - }) - return read(null, next) - } - const pi = new Peer(new Id(msg.register.peer.id)) - msg.register.peer.addrs.forEach(a => pi.multiaddrs.add(a)) - this.main.getNS(msg.register.ns, true).addPeer(pi, Date.now(), msg.register.ttl, () => this.online) - log('register@%s: ok', this.id) - this.source.push({ - type: MessageType.REGISTER_RESPONSE, - registerResponse: craftStatus(ResponseStatus.OK) - }) - } catch (e) { - log('register@%s: internal error', this.id) - log(e) - this.source.push({ - type: MessageType.REGISTER_RESPONSE, - registerResponse: craftStatus(ResponseStatus.E_INTERNAL_ERROR) - }) - return read(null, next) - } - break - case MessageType.UNREGISTER: - try { - log('unregister@%s: unregister from %s', this.id, msg.unregister.ns) - // TODO: currently ignores id since there is no ownership error. change? - this.main.getNS(msg.unregister.ns).removePeer(this.id) - } catch (e) { - return next(null, null, e) - } - break - case MessageType.DISCOVER: - try { - // TODO: add more errors - log('discover@%s: discover on %s', this.id, msg.discover.ns) - if (msg.discover.limit <= 0 || msg.discover.limit > MAX_LIMIT) msg.discover.limit = MAX_LIMIT - const {peers, cookie} = this.main.getNS(msg.discover.ns).getPeers(msg.discover.cookie || Buffer.from(''), msg.discover.limit, this.id) - log('discover@%s: got %s peers', this.id, peers.length) - this.source.push({ - type: MessageType.DISCOVER_RESPONSE, - discoverResponse: { - registrations: peers.map(p => { - return { - ns: msg.discover.ns, - peer: { - id: p.pi.id.toBytes(), - addrs: p.pi.multiaddrs.toArray().map(a => a.buffer) - }, - ttl: p.ttl - } - }), - cookie - } - }) - } catch (e) { - log('discover@%s: internal error', this.id) - log(e) - this.source.push({ - type: MessageType.DISCOVER_RESPONSE, - registerResponse: craftStatus(ResponseStatus.E_INTERNAL_ERROR) - }) - return read(null, next) - } - break - // case MessageType.REGISTER_RESPONSE: - // case MessageType.DISCOVER_RESPONSE: - default: // should that disconnect or just get ignored? - log('error@%s: sent wrong msg type %s', this.id, msg.type) - return next(null, null, true) - } - read(null, next) - } - read(null, next) - } - setup (conn, cb) { - conn.getPeerInfo((err, pi) => { - if (err) return cb(err) - this.pi = pi - this.id = pi.id.toB58String() - pull( - conn, - ppb.decode(Message), - this, - ppb.encode(Message), - conn - ) - - this.online = true - cb() - }) - } -} - -module.exports = RPC diff --git a/src/server/rpc/handlers/discover.js b/src/server/rpc/handlers/discover.js new file mode 100644 index 0000000..a82ae7f --- /dev/null +++ b/src/server/rpc/handlers/discover.js @@ -0,0 +1,64 @@ + +'use strict' + +const debug = require('debug') +const log = debug('libp2p:redezvous:protocol:discover') +log.error = debug('libp2p:redezvous:protocol:discover:error') + +const { Message } = require('../../../proto') +const MESSAGE_TYPE = Message.MessageType +const RESPONSE_STATUS = Message.ResponseStatus + +const { MAX_NS_LENGTH, MAX_LIMIT } = require('../../../constants') + +module.exports = (rendezvousPoint) => { + /** + * Process `Discover` Rendezvous messages. + * + * @param {PeerId} peerId + * @param {Message} msg + * @returns {Message} + */ + return function discover (peerId, msg) { + try { + log(`discover ${peerId.toB58String()}: discover on ${msg.discover.ns}`) + + // Validate namespace + if (!msg.discover.ns || msg.discover.ns > MAX_NS_LENGTH) { + log.error(`invalid namespace received: ${msg.discover.ns}`) + + return { + type: MESSAGE_TYPE.DISCOVER_RESPONSE, + discoverResponse: { + status: RESPONSE_STATUS.E_INVALID_NAMESPACE + } + } + } + + if (!msg.discover.limit || msg.discover.limit <= 0 || msg.discover.limit > MAX_LIMIT) { + msg.discover.limit = MAX_LIMIT + } + + // Get registrations + const registrations = rendezvousPoint.getRegistrations(msg.discover.ns, msg.discover.limit) + + return { + type: MESSAGE_TYPE.DISCOVER_RESPONSE, + discoverResponse: { + cookie: undefined, // TODO + registrations, + status: RESPONSE_STATUS.OK + } + } + } catch (err) { + log.error(err) + } + + return { + type: MESSAGE_TYPE.REGISTER_RESPONSE, + discoverResponse: { + status: RESPONSE_STATUS.E_INTERNAL_ERROR + } + } + } +} diff --git a/src/server/rpc/handlers/index.js b/src/server/rpc/handlers/index.js new file mode 100644 index 0000000..89248ab --- /dev/null +++ b/src/server/rpc/handlers/index.js @@ -0,0 +1,21 @@ +'use strict' + +const { Message } = require('../../../proto') +const MESSAGE_TYPE = Message.MessageType + +module.exports = (server) => { + const handlers = { + [MESSAGE_TYPE.REGISTER]: require('./register')(server), + [MESSAGE_TYPE.UNREGISTER]: require('./unregister')(server), + [MESSAGE_TYPE.DISCOVER]: require('./discover')(server) + } + + /** + * Get the message handler matching the passed in type. + * @param {number} type + * @returns {function(PeerId, Message, function(Error, Message))} + */ + return function getMessageHandler (type) { + return handlers[type] + } +} diff --git a/src/server/rpc/handlers/register.js b/src/server/rpc/handlers/register.js new file mode 100644 index 0000000..e6ed495 --- /dev/null +++ b/src/server/rpc/handlers/register.js @@ -0,0 +1,76 @@ + +'use strict' + +const debug = require('debug') +const log = debug('libp2p:redezvous:protocol:register') +log.error = debug('libp2p:redezvous:protocol:register:error') + +const { Message } = require('../../../proto') +const MESSAGE_TYPE = Message.MessageType +const RESPONSE_STATUS = Message.ResponseStatus + +const { MAX_NS_LENGTH } = require('../../../constants') + +module.exports = (rendezvousPoint) => { + /** + * Process `Register` Rendezvous messages. + * + * @param {PeerId} peerId + * @param {Message} msg + * @returns {Message} + */ + return function register (peerId, msg) { + try { + log(`register ${peerId.toB58String()}: trying register on ${msg.register.ns}`) + + // Validate auth + if (!msg.register.peer.id.equals(peerId.toBytes())) { + log.error('unauthorized peer id to register') + + return { + type: MESSAGE_TYPE.REGISTER_RESPONSE, + registerResponse: { + status: RESPONSE_STATUS.E_NOT_AUTHORIZED + } + } + } + + // Validate namespace + if (!msg.register.ns || msg.register.ns > MAX_NS_LENGTH) { + log.error(`invalid namespace received: ${msg.register.ns}`) + + return { + type: MESSAGE_TYPE.REGISTER_RESPONSE, + registerResponse: { + status: RESPONSE_STATUS.E_INVALID_NAMESPACE + } + } + } + + // Add registration + rendezvousPoint.addRegistration( + msg.register.ns, + peerId, + msg.register.peer.addrs, + msg.register.ttl + ) + + return { + type: MESSAGE_TYPE.REGISTER_RESPONSE, + registerResponse: { + status: RESPONSE_STATUS.OK, + ttt: msg.register.ttl + } + } + } catch (err) { + log.error(err) + } + + return { + type: MESSAGE_TYPE.REGISTER_RESPONSE, + registerResponse: { + status: RESPONSE_STATUS.E_INTERNAL_ERROR + } + } + } +} diff --git a/src/server/rpc/handlers/unregister.js b/src/server/rpc/handlers/unregister.js new file mode 100644 index 0000000..936dc77 --- /dev/null +++ b/src/server/rpc/handlers/unregister.js @@ -0,0 +1,42 @@ + +'use strict' + +const debug = require('debug') +const log = debug('libp2p:redezvous:protocol:unregister') +log.error = debug('libp2p:redezvous:protocol:unregister:error') + +module.exports = (rendezvousPoint) => { + /** + * Process `Unregister` Rendezvous messages. + * + * @param {PeerId} peerId + * @param {Message} msg + */ + return function unregister (peerId, msg) { + try { + log(`unregister ${peerId.toB58String()}: trying unregister from ${msg.unregister.ns}`) + + if (!msg.unregister.id && !msg.unregister.ns) { + throw new Error('no peerId or namespace provided') + } + + // Validate auth + if (!msg.unregister.id.equals(peerId.toBytes())) { + log.error('unauthorized peer id to unregister') + + // TODO: auth validation of peerId? -- there is no answer + return + } + + // Remove registration + if (!msg.unregister.ns) { + rendezvousPoint.removePeerRegistrations(peerId) + } else { + rendezvousPoint.removeRegistration(msg.unregister.ns, peerId) + } + } catch (err) { + log.error(err) + } + // TODO: internal error? -- there is no answer + } +} diff --git a/src/server/rpc/index.js b/src/server/rpc/index.js new file mode 100644 index 0000000..d4f423e --- /dev/null +++ b/src/server/rpc/index.js @@ -0,0 +1,65 @@ +'use strict' + +const debug = require('debug') +const log = debug('libp2p:redezvous-point:rpc') +log.error = debug('libp2p:redezvous-point:rpc:error') + +const pipe = require('it-pipe') +const lp = require('it-length-prefixed') +const { toBuffer } = require('it-buffer') + +const handlers = require('./handlers') +const { Message } = require('../../proto') + +module.exports = (rendezvous) => { + const getMessageHandler = handlers(rendezvous) + + /** + * Process incoming Rendezvous messages. + * @param {PeerId} peerId + * @param {Message} msg + * @returns {Promise} + */ + function handleMessage (peerId, msg) { + const handler = getMessageHandler(msg.type) + + if (!handler) { + log.error(`no handler found for message type: ${msg.type}`) + return + } + + return handler(peerId, msg) + } + + /** + * Handle incoming streams on the rendezvous protocol. + * @param {Object} props + * @param {DuplexStream} props.stream + * @param {Connection} props.connection connection + * @returns {Promise} + */ + return async function onIncomingStream ({ stream, connection }) { + const peerId = connection.remotePeer + + log('incoming stream from: %s', peerId.toB58String()) + + await pipe( + stream.source, + lp.decode(), + toBuffer, + source => (async function * () { + for await (const msg of source) { + // handle the message + const desMessage = Message.decode(msg) + const res = await handleMessage(peerId, desMessage) + + if (res) { + yield Message.encode(res) + } + } + })(), + lp.encode(), + stream.sink + ) + } +} diff --git a/src/server/store/basic/index.js b/src/server/store/basic/index.js deleted file mode 100644 index 0095090..0000000 --- a/src/server/store/basic/index.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict' - -class NS { - constructor (name, que) { // name is a utf8 string - this.name = name - this.hexName = Buffer.from(name).toString('hex') // needed to prevent queue-dos attacks - this.que = que - this.id = {} - this.sorted = [] - } - addPeer (pi, ts, ttl, isOnline) { // isOnline returns a bool if the rpc connection still exists - const id = pi.id.toB58String() - this.id[id] = {pi, ts, ttl} - if (ttl) { - let expireAt = ts + ttl * 1000 - this.id[id].online = () => Date.now() >= expireAt - } else { - this.id[id].online = isOnline - } - this.update() - } - removePeer (pid) { - delete this.id[pid] - this.update() - } - update () { - this.que.add('sort@' + this.hexName, () => { - this.sorted = Object.keys(this.id).map(id => { return {id, ts: this.id[id].ts} }).sort((a, b) => a.ts - b.ts) - }) - } - getPeers (cookie, limit, ownId) { - cookie = cookie.length ? parseInt(cookie.toString(), 10) : 0 - let p = this.sorted.filter(p => p.ts > cookie && p.id !== ownId).slice(0, limit).map(p => this.id[p.id]) - let newCookie - if (p.length) { - newCookie = Buffer.from(p[p.length - 1].ts.toString()) - } else { - newCookie = Buffer.from('') - } - return {cookie: newCookie, peers: p} - } - gc () { - return Object.keys(this.id).filter(k => !this.id[k].online()).map(k => delete this.id[k]).length - } - get useless () { - return !Object.keys(this.id).length - } -} - -class BasicStore { - constructor (main) { - this.main = main - } - create (name) { - return new NS(name, this.main.que) - } -} - -module.exports = BasicStore -module.exports.NS = NS diff --git a/test/client-mode.spec.js b/test/client-mode.spec.js new file mode 100644 index 0000000..1dcb441 --- /dev/null +++ b/test/client-mode.spec.js @@ -0,0 +1,47 @@ +'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 Rendezvous = require('../src') + +const { createPeer } = require('./utils') + +describe('client mode', () => { + let peer, rendezvous + + afterEach(async () => { + peer && await peer.stop() + rendezvous && await rendezvous.stop() + }) + + it('registers a rendezvous handler by default', async () => { + [peer] = await createPeer() + rendezvous = new Rendezvous({ libp2p: peer }) + + const spyHandle = sinon.spy(peer.registrar, '_handle') + + await rendezvous.start() + + expect(spyHandle).to.have.property('callCount', 1) + }) + + it('can be started only in client mode', async () => { + [peer] = await createPeer() + rendezvous = new Rendezvous({ + libp2p: peer, + options: { + isRendezvousPoint: false + } + }) + + const spyHandle = sinon.spy(peer.registrar, '_handle') + + await rendezvous.start() + expect(spyHandle).to.have.property('callCount', 0) + }) +}) diff --git a/test/client.id.json b/test/client.id.json deleted file mode 100644 index 97b1858..0000000 --- a/test/client.id.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "QmVMx9YqSRYB75sGcYiTVtCpygxcfaxcuSJMBYoBadgJ7r", - "privKey": "CAASqQkwggSlAgEAAoIBAQCZPiah1KCGIIsMDXvxxk3djZfgCpckUDOAsG83FNwLx+3Z8Lg1LLAoArtula6/4LOaTaRZA9LKiSBw4yEgTMlinw77hxg6SLoHeMHi1AS/0MxCQuKxWZaeM5dtFkiUU/qJVwhTksIjmHtm/gWcBjmObAnRzeHIOLdlBL+6tcELYKH4OKcxD/VWMxFBbo5bjnPTddeQpSEtTVzsqX4kC4sBIHO3otEY8z9nefRXP8zIZ3TpfWcXMAhzbF7aJWHlUkDSblDCH41JlDXenvcerTlPN0Oqdj+8e5914qSTdSPAHbFyiGKeDc55thYZI2jDpNksSOZ2/HhDOjmNAE970VrRAgMBAAECggEBAJVnQdTvb521JruWfevHkezaemMFEDxoMP5bheKm5K5buuqLxZyaOBiaKVD0gE40bgaXgg8DKkUqkkVdO9O46XLMbpgOKzHP7AcS1b0nRoYYtLw5Z7jPBoiw9gZ1/kcW5SF3h/erEroPlOhh6ugmLYFMlfpGBsXlfe/wRFltkItcohvQKm45tDowmRNX7Gw/qE2m2Bcu8nF34OWNqSlhi/LccZWMHYXemy/MyYPeM2pMsebyEOKQb7cTKPUS7UqOYotvq8p8b/IRRi0vJZNtnON3qQDZhP3uPzZYFErWcrL3BIEgwMBNcJb2yNIvrc7LnH8xO0l7fLuYmJW1su8mQf0CgYEA5BlzcW24UPTvZdPjWHAGZBwK8RLHKxh615jfTjO4SI9ggdNVIOAWFQSKAyJT4hLyvg+4yR21WpZMj2pR3loO+Nn7djviB8a8SlWeqPSjpN/QT9nSl6JVcdoNp4vEyu+9O2MbNJp5oCZ/q6k9DovvZuxMc/2cNWo/YR4K6xcvehcCgYEAq/ywujkJ9fptpkJ5HzO7zt5sy+dodT2EDPTCeMQeLNXTC+P31th2FIl4/FzGrLJzVtWmP3kUS0es8hrRuLBy+Zg51czMliX8eJa36Vk305EAynkfIcGEzgmdbDVx6QyIWk1Y18WzVyuiVnLMbxT6ZfNXahwLNnV7umk0OH1bK1cCgYEAo9GTk7dVVO9UsDFJak6qiGOLiDAQUuc18nmchzGl/JbcnOEGlqHZuiaUaEPTMt6g79eiwu5PPUwMmEOnoKXVcuw7KWNApo0Y1dpAJN/uV49WsMKj+Lth2m7ct6QuJgGgSnKXK2R2TYrYzpSxgS0HN0gmcHeIJOS1uC43cTgppOkCgYAFTGSZaBZxeISWQaf/mRVpGxsY8Qkby4hc6dFv7QLM+M1mqWBCQyroGRAcHjOUsG6zNyPHAtDoPM4MK11Ypj70h4cImiWXXpY3lNUXoEMDBo2Sr0aRQKf5vPwXkFHxDwzIU2ewRgvvXI3EwgagSXIpX+TKhRCnXdkw9frA3sPHQwKBgQC4mCqbD3DhjiQFPOhhlZtHgruKs6b6L7npyfc4ZrDVwGFrOcbBQbuLD2f92wbB0FKWozx4FP0nlawigVHimMWG4qyJycDMBptfsUrqvxzRagmci+ZRY+cjyz2sA1Tox4nE4vVEz5ZxBGdmkRLY+hWRZMWDJrYLoZSBEKj0B336yw==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCZPiah1KCGIIsMDXvxxk3djZfgCpckUDOAsG83FNwLx+3Z8Lg1LLAoArtula6/4LOaTaRZA9LKiSBw4yEgTMlinw77hxg6SLoHeMHi1AS/0MxCQuKxWZaeM5dtFkiUU/qJVwhTksIjmHtm/gWcBjmObAnRzeHIOLdlBL+6tcELYKH4OKcxD/VWMxFBbo5bjnPTddeQpSEtTVzsqX4kC4sBIHO3otEY8z9nefRXP8zIZ3TpfWcXMAhzbF7aJWHlUkDSblDCH41JlDXenvcerTlPN0Oqdj+8e5914qSTdSPAHbFyiGKeDc55thYZI2jDpNksSOZ2/HhDOjmNAE970VrRAgMBAAE=" -} diff --git a/test/client2.id.json b/test/client2.id.json deleted file mode 100644 index 57c78fa..0000000 --- a/test/client2.id.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "QmcpjE4Mgs1kc31gN6DSRDPwQqY8C2x6iGuFo3gzTxJdSQ", - "privKey": "CAASpgkwggSiAgEAAoIBAQCZ2FkxB7wjzf1H8fqWGh843E5ZtmSQpQ4DtP4HYZAgNPNc66GmP9itAR44WziPgS3BC3gfXuWE7OTtZ0dhZj2e2BTpKZwXVoAwBaV9IZrbTntFsG50rHzoyCulGvJu2RPmG1PyE9+WLXM+oiaRqY2YshLoMkS88BsRw9+PBaA2jA75Bay/wm8AsmXEc/PloAGE6PiyM6nD/66WGpxScZ2Z1BO0MUy0MUBBCvA+D8fxTpzLqBGHrIJxIK+MebAkWNFWrWyvdnapABCxvL5pSjjjGb1SyUkeTr8Bn/IsFfnjjuZaszKTqYsNGZCxlDJdrL2rlEoP4MIEP2wM+WHYqFIBAgMBAAECggEAZV4mNqYwEy9xCeyo/iosFF0kyvvg+2Wl/E9PajGgs3fwOnOPyWkcLbIk5WFFvViSezZBafovJQyqMrrwT378byNVc+RU0xPN1taBmheAX6wwkVSVEw9sJj1udJVy1BL4h4/OGh16HwvHeaeB3kxn3grHZnNo000pqOT08tn0HLvZ1396SUXjyNahMco/dTOtUz8PQAKtu+MW0ekkczGi6mBPaiR/rzlzQZRresQ368azwjTTwe1T4YwQ7buPxrW2GRfnVfU5Uvp99acKnj5bKixQtyZDiKbHWK5PX3diMfYuPdgdz0R4EOIokE0akR+lxpk4Um7+gDHbI6ufvLQSAQKBgQDX0WGxpJg6nwHspxdS75syelyRuGWoS6PPxqhQws37RLSV6aoAZoAzvhxDPT3aDaR1VVJuxnDpVjidCTArk3vHuzOMriwKLDz2NifkWnSCiBdCjpY2aqbF+sQgwLAf+5RV8xQOkc2+4p0xXYV+21XXWLN8R2gezB7exXyrvH4CkQKBgQC2fSHOcvCM9ZmyLc99bfGJ0NbTC0s9G0EkFWni9V61vfQYxME4Mi1XPRwOrCP/dguATL/QRMD24xvZHYR1+vwY+A3MPZdOtrgeVdjia9VuZ8VLEdgFTFWOY9d3XlbLiiRSj0G8IjC+WEN5k5mWJn7XA+HdkQhuY+NyJ1nI5q4wcQKBgEUxqHTgJL6GxIMvf1bj44pnmM5PpKg0uCyhsM1T596rxIpcBFlkg64TQdR9ChujTBsiY++ISCNHtZcDnyIZgxIifwCXxx7r2A/IhTm9lqVTJMH+HUMNJrNLFx65KL7YVlLIQKH7NVACMAvnxClMAVWt5r3t1wAoyaz6/GHDaVNBAoGAJVtoWELfS3vbgsYt+5dOItBFqd5eAJxbsW9Qxc1FHh9MoOVmSIK9FWbFH5vNorYflJwhiBkLB39mbAPG4gAHK3VcHbteBhcRieQ5CeDZSEil8sAsYKlHumZl7WG6kuAsn1oEMucs40peRb0Za8tlm86HpjvSZga8wNmdX6sZbYECgYArG+pWYWjjXi1ithkCaLiL1816KPBqwXOjPHjXFuUSpbU+O7lBlEhMD6Fcj8IuxQzvbwn8L7TZRyZtN5xYBlPAblAU5PTgMKU1mQiqt0IyfifnyG0As+fbTcmGMAmd089UGBp/OLblgzoXfDhhMSo/ymrfjFYOQr88mPnbRagGWA==", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCZ2FkxB7wjzf1H8fqWGh843E5ZtmSQpQ4DtP4HYZAgNPNc66GmP9itAR44WziPgS3BC3gfXuWE7OTtZ0dhZj2e2BTpKZwXVoAwBaV9IZrbTntFsG50rHzoyCulGvJu2RPmG1PyE9+WLXM+oiaRqY2YshLoMkS88BsRw9+PBaA2jA75Bay/wm8AsmXEc/PloAGE6PiyM6nD/66WGpxScZ2Z1BO0MUy0MUBBCvA+D8fxTpzLqBGHrIJxIK+MebAkWNFWrWyvdnapABCxvL5pSjjjGb1SyUkeTr8Bn/IsFfnjjuZaszKTqYsNGZCxlDJdrL2rlEoP4MIEP2wM+WHYqFIBAgMBAAE=" -} diff --git a/test/connectivity.spec.js b/test/connectivity.spec.js new file mode 100644 index 0000000..0314f79 --- /dev/null +++ b/test/connectivity.spec.js @@ -0,0 +1,67 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +const { expect } = chai +const pWaitFor = require('p-wait-for') + +const multiaddr = require('multiaddr') + +const Rendezvous = require('../src') + +const { MULTIADDRS_WEBSOCKETS } = require('./fixtures/browser') +const relayAddr = MULTIADDRS_WEBSOCKETS[0] +const { createPeer } = require('./utils') + +describe('connectivity', () => { + let peers + + beforeEach(async () => { + // Create libp2p nodes + peers = await createPeer({ + number: 2 + }) + + // Create && start rendezvous + peers.map((libp2p) => { + const rendezvous = new Rendezvous({ libp2p }) + rendezvous.start() + libp2p.rendezvous = rendezvous + }) + + // Connect to testing relay node + await Promise.all(peers.map((libp2p) => libp2p.dial(relayAddr))) + }) + + afterEach(() => peers.map(async (libp2p) => { + await libp2p.rendezvous.stop() + await libp2p.stop() + })) + + it('updates known rendezvous points', async () => { + expect(peers[0].rendezvous._rendezvousConns.size).to.equal(0) + expect(peers[1].rendezvous._rendezvousConns.size).to.equal(0) + + // Connect each other via relay node + const m = multiaddr(`${relayAddr}/p2p-circuit/p2p/${peers[1].peerId.toB58String()}`) + const connection = await peers[0].dial(m) + + expect(peers[0].peerStore.peers.size).to.equal(2) + expect(peers[1].peerStore.peers.size).to.equal(2) + + // Wait event propagation + // Relay peer is not with rendezvous enabled + await pWaitFor(() => + peers[0].rendezvous._rendezvousConns.size === 1 && + peers[1].rendezvous._rendezvousConns.size === 1) + + expect(peers[0].rendezvous._rendezvousConns.get(peers[1].peerId.toB58String())).to.exist() + expect(peers[1].rendezvous._rendezvousConns.get(peers[0].peerId.toB58String())).to.exist() + + await connection.close() + + // Wait event propagation + await pWaitFor(() => peers[0].rendezvous._rendezvousConns.size === 0) + }) +}) diff --git a/test/discovery.spec.js b/test/discovery.spec.js deleted file mode 100644 index 687b40f..0000000 --- a/test/discovery.spec.js +++ /dev/null @@ -1,68 +0,0 @@ -'use strict' - -/* eslint-env mocha */ - -const {parallel} = require('async') -const Utils = require('./utils') - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) - -describe('discovery', () => { - let client - let client2 - let server - - before(done => { - Utils.default((err, _client, _server, _client2) => { - if (err) return done(err) - client = _client - client2 = _client2 - server = _server - parallel([client, client2].map(c => cb => c._dial(server.node.peerInfo, cb)), done) - }) - }) - - it('register', done => { - parallel( - [client, client2].map(c => cb => c.register('hello', c.swarm.peerInfo, cb)), - (...a) => setTimeout(() => done(...a), 100) // Queue is being processed every 100ms - ) - }) - - it('discover', done => { - client.discover('hello', (err, res) => { - if (err) return done(err) - expect(err).to.not.exist() - expect(res.peers).to.have.lengthOf(1) - expect(res.peers[0].id.toB58String()).to.equal(client2.swarm.peerInfo.id.toB58String()) - done() - }) - }) - - it('unregister', done => { - client2.unregister('hello') - setTimeout(() => done(), 100) // Queue is being processed every 100ms - }) - - it('discover (after unregister)', done => { - client.discover('hello', (err, res) => { - if (err) return done(err) - expect(err).to.not.exist() - expect(res.peers).to.have.lengthOf(0) - done() - }) - }) - - it('unregister other client', done => { - client.unregister('hello') - setTimeout(() => done(), 100) // Queue is being processed every 100ms - }) - - it('gc', () => { - server.gc() - expect(Object.keys(server.table.NS)).to.have.lengthOf(0) - }) -}) diff --git a/test/fixtures/browser.js b/test/fixtures/browser.js new file mode 100644 index 0000000..901c32e --- /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/p2p/QmckxVrJw1Yo8LqvmDJNUmdAsKtSbiKWmrXJFyKmUraBoN') +] diff --git a/test/fixtures/peers.js b/test/fixtures/peers.js new file mode 100644 index 0000000..fad0d23 --- /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/flows.spec.js b/test/flows.spec.js new file mode 100644 index 0000000..3a32042 --- /dev/null +++ b/test/flows.spec.js @@ -0,0 +1,101 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +const { expect } = chai + +const pWaitFor = require('p-wait-for') +const multiaddr = require('multiaddr') + +const Rendezvous = require('../src') + +const { MULTIADDRS_WEBSOCKETS } = require('./fixtures/browser') +const relayAddr = MULTIADDRS_WEBSOCKETS[0] +const { createPeer } = require('./utils') + +const namespace = 'ns' + +describe('flows', () => { + describe('3 rendezvous all acting as rendezvous point', () => { + let peers + + const connectPeers = async (peer, otherPeer) => { + // Connect each other via relay node + const m = multiaddr(`${relayAddr}/p2p-circuit/p2p/${otherPeer.peerId.toB58String()}`) + await peer.dial(m) + + // Wait event propagation + await pWaitFor(() => peer.rendezvous._rendezvousConns.size === 1) + } + + beforeEach(async () => { + // Create libp2p nodes + peers = await createPeer({ + number: 3 + }) + + // Create 3 rendezvous peers + peers.forEach((peer) => { + const rendezvous = new Rendezvous({ + libp2p: peer + }) + rendezvous.start() + peer.rendezvous = rendezvous + }) + + // Connect to testing relay node + await Promise.all(peers.map((libp2p) => libp2p.dial(relayAddr))) + }) + + afterEach(() => peers.map(async (libp2p) => { + await libp2p.rendezvous.stop() + await libp2p.stop() + })) + + it.skip('should not discover replicated peers?', () => { + // TODO + }) + + it('discover find registered peer for namespace only when registered', async () => { + await connectPeers(peers[0], peers[1]) + await connectPeers(peers[2], peers[1]) + + const registers = [] + + // Peer2 does not discovery any peer registered + for await (const reg of peers[2].rendezvous.discover(namespace)) { // eslint-disable-line + throw new Error('no registers should exist') + } + + // Peer0 register itself on namespace (connected to Peer1) + await peers[0].rendezvous.register(namespace) + + // Peer2 discovers Peer0 registered in Peer1 + for await (const reg of peers[2].rendezvous.discover(namespace)) { + registers.push(reg) + } + expect(registers).to.have.lengthOf(1) + expect(registers[0].id.toB58String()).to.eql(peers[0].peerId.toB58String()) + expect(registers[0].multiaddrs).to.eql(peers[0].multiaddrs) + expect(registers[0].ns).to.eql(namespace) + expect(registers[0].ttl).to.exist() + + // Peer0 unregister itself on namespace (connected to Peer1) + await peers[0].rendezvous.unregister(namespace) + + // Peer2 does not discovery any peer registered + for await (const reg of peers[2].rendezvous.discover(namespace)) { // eslint-disable-line + throw new Error('no registers should exist') + } + }) + + it('discovers locally first, and if limit achieved, not go to the network', async () => { + + }) + }) + + describe('3 rendezvous, one acting as rendezvous point', () => { + + }) +}) diff --git a/test/rendezvous.spec.js b/test/rendezvous.spec.js new file mode 100644 index 0000000..d9608fb --- /dev/null +++ b/test/rendezvous.spec.js @@ -0,0 +1,224 @@ +'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 pWaitFor = require('p-wait-for') + +const multiaddr = require('multiaddr') + +const Rendezvous = require('../src') +const { codes: errCodes } = require('../src/errors') + +const { createPeer } = require('./utils') +const { MULTIADDRS_WEBSOCKETS } = require('./fixtures/browser') +const relayAddr = MULTIADDRS_WEBSOCKETS[0] + +const namespace = 'ns' + +describe('rendezvous', () => { + describe('start and stop', () => { + let peer, rendezvous + + beforeEach(async () => { + [peer] = await createPeer() + rendezvous = new Rendezvous({ libp2p: peer }) + }) + + afterEach(async () => { + await peer.stop() + await rendezvous.stop() + }) + + it('can be started and stopped', async () => { + const spyRegister = sinon.spy(peer.registrar, 'register') + const spyUnregister = sinon.spy(peer.registrar, 'unregister') + + await rendezvous.start() + await rendezvous.stop() + + expect(spyRegister).to.have.property('callCount', 1) + expect(spyUnregister).to.have.property('callCount', 1) + }) + + it('registers the protocol once, if multiple starts', async () => { + const spyRegister = sinon.spy(peer.registrar, 'register') + + await rendezvous.start() + await rendezvous.start() + + expect(spyRegister).to.have.property('callCount', 1) + + await rendezvous.stop() + }) + + it('only unregisters on stop if already started', async () => { + const spyUnregister = sinon.spy(peer.registrar, 'unregister') + + await rendezvous.stop() + + expect(spyUnregister).to.have.property('callCount', 0) + }) + }) + + describe('api', () => { + let peers + + const connectPeers = async (peer, otherPeer) => { + // Connect to testing relay node + await peer.dial(relayAddr) + await otherPeer.dial(relayAddr) + + // Connect each other via relay node + const m = multiaddr(`${relayAddr}/p2p-circuit/p2p/${otherPeer.peerId.toB58String()}`) + await peer.dial(m) + + // Wait event propagation + await pWaitFor(() => peer.rendezvous._rendezvousConns.size === 1) + } + + beforeEach(async () => { + peers = await createPeer({ number: 3 }) + + // Create 3 rendezvous peers + // Peer0 will not be a server + peers.forEach((peer, index) => { + const rendezvous = new Rendezvous({ + libp2p: peer, + options: { + isServer: index !== 0 + } + }) + rendezvous.start() + peer.rendezvous = rendezvous + }) + }) + + afterEach(async () => { + for (const peer of peers) { + await peer.rendezvous.stop() + await peer.stop() + } + }) + + it('register throws error if a namespace is not provided', async () => { + await expect(peers[0].rendezvous.register()) + .to.eventually.rejected() + .and.have.property('code', errCodes.INVALID_NAMESPACE) + }) + + it('register throws error if ttl is too small', async () => { + await expect(peers[0].rendezvous.register(namespace, 10)) + .to.eventually.rejected() + .and.have.property('code', errCodes.INVALID_TTL) + }) + + it('register throws error if no connected rendezvous servers', async () => { + await expect(peers[0].rendezvous.register(namespace)) + .to.eventually.rejected() + .and.have.property('code', errCodes.NO_CONNECTED_RENDEZVOUS_SERVERS) + }) + + it('register to a connected rendezvous server node', async () => { + await connectPeers(peers[0], peers[1]) + + // Register + expect(peers[1].rendezvous._server.registrations.size).to.eql(0) + await peers[0].rendezvous.register(namespace) + + expect(peers[1].rendezvous._server.registrations.size).to.eql(1) + expect(peers[1].rendezvous._server.registrations.get(namespace)).to.exist() + + await peers[1].rendezvous.stop() + await peers[1].stop() + }) + + it('unregister throws if a namespace is not provided', async () => { + await expect(peers[0].rendezvous.unregister()) + .to.eventually.rejected() + .and.have.property('code', errCodes.INVALID_NAMESPACE) + }) + + it('register throws error if no connected rendezvous servers', async () => { + await expect(peers[0].rendezvous.unregister(namespace)) + .to.eventually.rejected() + .and.have.property('code', errCodes.NO_CONNECTED_RENDEZVOUS_SERVERS) + }) + + it('unregister to a connected rendezvous server node', async () => { + await connectPeers(peers[0], peers[1]) + + // Register + expect(peers[1].rendezvous._server.registrations.size).to.eql(0) + await peers[0].rendezvous.register(namespace) + + expect(peers[1].rendezvous._server.registrations.size).to.eql(1) + expect(peers[1].rendezvous._server.registrations.get(namespace)).to.exist() + + // Unregister + await peers[0].rendezvous.unregister(namespace) + expect(peers[1].rendezvous._server.registrations.size).to.eql(0) + + await peers[1].rendezvous.stop() + await peers[1].stop() + }) + + it('unregister to a connected rendezvous server node not fails if not registered', async () => { + await connectPeers(peers[0], peers[1]) + + // Unregister + await peers[0].rendezvous.unregister(namespace) + + await peers[1].rendezvous.stop() + }) + + it('discover throws error if a namespace is not provided', async () => { + try { + for await (const _ of peers[0].rendezvous.discover()) {} // eslint-disable-line + } catch (err) { + expect(err).to.exist() + expect(err.code).to.eql(errCodes.NO_CONNECTED_RENDEZVOUS_SERVERS) + return + } + throw new Error('discover should throw error if a namespace is not provided') + }) + + it('discover does not find any register if there is none', async () => { + await connectPeers(peers[0], peers[1]) + + for await (const reg of peers[0].rendezvous.discover(namespace)) { // eslint-disable-line + throw new Error('no registers should exist') + } + + await peers[1].rendezvous.stop() + }) + + it('discover find registered peer for namespace', async () => { + await connectPeers(peers[0], peers[1]) + await connectPeers(peers[2], peers[1]) + + const registers = [] + + // Peer2 does not discovery any peer registered + for await (const reg of peers[2].rendezvous.discover(namespace)) { // eslint-disable-line + throw new Error('no registers should exist') + } + + // Peer0 register itself on namespace (connected to Peer1) + await peers[0].rendezvous.register(namespace) + + // Peer2 discovers Peer0 registered in Peer1 + for await (const reg of peers[2].rendezvous.discover(namespace)) { + registers.push(reg) + } + expect(registers).to.have.lengthOf(1) + expect(registers[0].id.toB58String()).to.eql(peers[0].peerId.toB58String()) + expect(registers[0].multiaddrs).to.eql(peers[0].multiaddrs) + expect(registers[0].ns).to.eql(namespace) + expect(registers[0].ttl).to.exist() + }) + }) +}) diff --git a/test/server.id.json b/test/server.id.json deleted file mode 100644 index 51bad08..0000000 --- a/test/server.id.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "QmQ4eanHt2D1ye44ebGH9RB5XTrzMcZENsjdR4Zd2ELTig", - "privKey": "CAASqAkwggSkAgEAAoIBAQC9fPF9cVj8qtRJa57bmRfkb77ViZRG926fDQTAfzX9tICD3hiHYZLD/tb/0cr7z3Y2amZWrSyuCG3pkhEASk6bb6eND134EWH5wUPxGvWKaw1SmndlGUL8xy/EokH17ieoV1s2fGZ0V6GIeh9/z5REQ6rVNhNy3UOnEm2HcDQn9tmT5tALLbuIgvNA6otFq67/tB6PSiC4mP40kgUzO22E+n20f5HntSkWAS1WCsqL4nhRhKGToRIojpgzlEn6EkE45VkyGQvsgTxNAPGaSMHfS8+J4L8LK2nqTgQkrkUcpOAlg3gATaDHTJJyY2hiVAkAuJb8f+hi73f+cSmpbb9BAgMBAAECggEASR8P6YJ1/nrFlNeM49z+FU7x62E98OzGqWXSsZ3lbdPbzAdGm+eRRUTwHqQMmoOCcJk6iLQnC7mBAKM3IE+Mafr6Qzrs3i+HCWQFHeNzYUjSSVAGRuMqsHUE//JFVevjLdkX/7ydpMO0OAA4a4/k/TrHj6NgefDcjHpV/e/UkJ7MOsjN2QWpx2a4rvOHBDK12eNM9T99zw3MUfNDbw5BGPno9mqGN5uVP1csAZnzVLP3G9utr18veBxf06PZrdLnIakD9oMoXaNwrrQj0v1CQuVQO7tuYcm1r/SzkqGEslX8AmaOb1tiugod5n9dsShpfLrZiS91k8lfWFMgVIBw4QKBgQD4d8PF61VnbZaK4PUJXYrvgXY37tP8NkSrSZoJdpL4Y94FSdd7DHTR/CdNOyqqCXAhSgU6PC21+CqUMAxRfrvoe80vhkMNsBBQRIXUD1EFB20XOLlu+VNngMNWweDsRzwa/zkYOZViG2h8db0XYY2ST4G0CTJNniQO/LD6sa9hVQKBgQDDO3dF81njsXtME08IJ7iBVZ5MvOGSrX78nYPSqALZwWwivF8J0TY1gUJTqBDOoav61aqXWjxTRJ3/mab5DUyJzqU8Ho3KPQzoXgdT+HuppjaDBWt6IHW5eOyRQH7skGL5/EvdIpgTDIhlbDU5nVR6FPV4w6LPB3PHFtI3HC1WPQKBgQCv5zINY38R+w6SAZLYb4YV64SDMqyXKOBSl4fa3TxNZ35eJhnMPlRR+P7l+VZKDOZ6Wsn6oXIHGssiICYIZ/2mKEdqNtYv0Y6rFOfd6n4EXm6H+xukihTW+NzSBe4zuHa/8iI8mT+9tgOx4TTeYaz1gR4lFEGtm6CRj6nHwZWVBQKBgQCc/EIqU0XimyJDx/ry2c244fnKRs8zvKKxyo7nYwX3x1qGi+X35OysFWYaEriBDutVZV4pGfwMEM7jatAiz5jN7wZa0068Yl7wsjs+QD5f6jFHJaKIr3U6UIwZOD1XR7ruvPrbtCeImblLpLkfvOzixduk4dsWki1811Lt0ZB7GQKBgA+JFfb6aF+dnBub/sbogB0CGE7h1gHkZ01QlSuS2r7c9KKzyNahQhcv3BOXJaUEV3Atpkwca+P9cRtE72YgrjLeEbmKtlLVeMwO79bmLDsDm3oQlJlTowGnPmTU8QpMrtqxP41/y6t8VJvAmJiFd3Efq2Ojww7/u3IhecNd0hBo", - "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9fPF9cVj8qtRJa57bmRfkb77ViZRG926fDQTAfzX9tICD3hiHYZLD/tb/0cr7z3Y2amZWrSyuCG3pkhEASk6bb6eND134EWH5wUPxGvWKaw1SmndlGUL8xy/EokH17ieoV1s2fGZ0V6GIeh9/z5REQ6rVNhNy3UOnEm2HcDQn9tmT5tALLbuIgvNA6otFq67/tB6PSiC4mP40kgUzO22E+n20f5HntSkWAS1WCsqL4nhRhKGToRIojpgzlEn6EkE45VkyGQvsgTxNAPGaSMHfS8+J4L8LK2nqTgQkrkUcpOAlg3gATaDHTJJyY2hiVAkAuJb8f+hi73f+cSmpbb9BAgMBAAE=" -} diff --git a/test/utils.js b/test/utils.js index 177aef9..ffb5e38 100644 --- a/test/utils.js +++ b/test/utils.js @@ -1,87 +1,51 @@ 'use strict' -const Libp2p = require('libp2p') -const TCP = require('libp2p-tcp') -const MPLEX = require('libp2p-mplex') -const SPDY = require('libp2p-spdy') -const SECIO = require('libp2p-secio') - -const Id = require('peer-id') -const Peer = require('peer-info') - -const Server = require('../src/server') -const Client = require('../src') +const Transport = require('libp2p-websockets') +const Muxer = require('libp2p-mplex') +const { NOISE: Crypto } = require('libp2p-noise') +const PeerId = require('peer-id') -const Utils = module.exports = (id, addrs, cb) => { - Id.createFromJSON(require(id), (err, id) => { - if (err) return cb(err) - const peer = new Peer(id) - addrs.forEach(a => peer.multiaddrs.add(a)) +const pTimes = require('p-times') - const swarm = new Libp2p({ - transport: [ - new TCP() - ], - connection: { - muxer: [ - MPLEX, - SPDY - ], - crypto: [SECIO] - } - }, peer, null, { - relay: { - enabled: true, - hop: { - enabled: true, - active: false - } - } - }) - - swarm.start(err => { - if (err) return cb(err) - cb(null, swarm) - }) - }) -} - -Utils.id = (id, addrs, cb) => { - Id.createFromJSON(require(id), (err, id) => { - if (err) return cb(err) - const peer = new Peer(id) - addrs.forEach(a => peer.multiaddrs.add(a)) - cb(null, peer) - }) -} - -Utils.createServer = (id, addrs, opt, cb) => { - Utils(id, addrs, (err, swarm) => { - if (err) return cb(err) - const server = new Server(Object.assign(opt || {}, {node: swarm})) - server.start() - return cb(null, server, swarm) - }) +const Libp2p = require('libp2p') +const multiaddr = require('multiaddr') + +const Peers = require('./fixtures/peers') +const { MULTIADDRS_WEBSOCKETS } = require('./fixtures/browser') +const relayAddr = MULTIADDRS_WEBSOCKETS[0] + +const defaultConfig = { + modules: { + transport: [Transport], + streamMuxer: [Muxer], + connEncryption: [Crypto] + } } -Utils.createClient = (id, addrs, cb) => { - Utils(id, addrs, (err, swarm) => { - if (err) return cb(err) - const client = new Client(swarm) - client.start(err => { - if (err) return cb(err) - return cb(null, client, swarm) - }) - }) +/** + * Create libp2p nodes. + * @param {Object} [properties] + * @param {Object} [properties.config] + * @param {number} [properties.number] number of peers (default: 1). + * @param {boolean} [properties.started] nodes should start (default: true) + * @return {Promise>} + */ +async function createPeer ({ number = 1, started = true, config = {} } = {}) { + const peerIds = await pTimes(number, (i) => PeerId.createFromJSON(Peers[i])) + const peers = await pTimes(number, (i) => Libp2p.create({ + peerId: peerIds[i], + addresses: { + listen: [multiaddr(`${relayAddr}/p2p-circuit`)] + }, + ...defaultConfig, + ...config + })) + + if (started) { + await Promise.all(peers.map((p) => p.start())) + } + + return peers } -Utils.default = cb => Utils.createServer('./server.id.json', ['/ip4/0.0.0.0/tcp/0'], {}, (err, server) => { - if (err) return cb(err) - Utils.createClient('./client.id.json', ['/ip4/0.0.0.0/tcp/0'], (err, client) => { - if (err) return cb(err) - Utils.createClient('./client2.id.json', ['/ip4/0.0.0.0/tcp/0'], (err, client2) => { - if (err) return cb(err) - return cb(null, client, server, client2) - }) - }) -}) +module.exports.createPeer = createPeer