diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000000..ade1dcbcc0 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,67 @@ +name: ci +on: + push: + branches: + - master + pull_request: + branches: + - '**' + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: yarn + - run: yarn lint + - uses: gozala/typescript-error-reporter-action@v1.0.8 + - run: yarn build + - run: yarn aegir dep-check + - uses: ipfs/aegir/actions/bundle-size@master + name: size + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + test-node: + needs: check + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-latest, ubuntu-latest, macos-latest] + node: [12, 14] + fail-fast: true + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node }} + - run: yarn + - run: npx nyc --reporter=lcov aegir test -t node -- --bail + - uses: codecov/codecov-action@v1 + test-chrome: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: yarn + - run: npx aegir test -t browser -t webworker --bail + test-firefox: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: yarn + - run: npx aegir test -t browser -t webworker --bail -- --browsers FirefoxHeadless + test-interop: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: yarn + - run: cd node_modules/interop-libp2p && yarn && LIBP2P_JS=${GITHUB_WORKSPACE}/src/index.js npx aegir test -t node --bail + test-auto-relay-example: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: yarn + - run: cd examples && yarn && npm run test -- auto-relay diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index bb21bd4028..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,50 +0,0 @@ -language: node_js -cache: npm -stages: - - check - - test - - cov - -node_js: - - 'lts/*' - - '14' - -os: - - linux - - osx - -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 build --bundlesize - # Remove pull libs once ping is async - - npx aegir dep-check -- -i pull-handshake -i pull-stream - - npm run lint - - - stage: test - name: chrome - addons: - chrome: stable - script: - - npx aegir test -t browser -t webworker - - - stage: test - name: firefox - addons: - firefox: latest - script: - - npx aegir test -t browser -t webworker -- --browsers FirefoxHeadless - - - stage: test - name: interop - script: - - cd node_modules/interop-libp2p - - npm install - - LIBP2P_JS=${TRAVIS_BUILD_DIR}/src/index.js npx aegir test -t node --bail - -notifications: - email: false \ No newline at end of file diff --git a/doc/API.md b/doc/API.md index 69e0ca4f78..d907b7d851 100644 --- a/doc/API.md +++ b/doc/API.md @@ -13,14 +13,14 @@ * [`ping`](#ping) * [`multiaddrs`](#multiaddrs) * [`addressManager.getListenAddrs`](#addressmanagergetlistenaddrs) - * [`addressmger.getAnnounceAddrs`](#addressmanagergetannounceaddrs) - * [`addressManager.getNoAnnounceAddrs`](#addressmanagergetnoannounceaddrs) + * [`addressManager.getAnnounceAddrs`](#addressmanagergetannounceaddrs) * [`contentRouting.findProviders`](#contentroutingfindproviders) * [`contentRouting.provide`](#contentroutingprovide) * [`contentRouting.put`](#contentroutingput) * [`contentRouting.get`](#contentroutingget) * [`contentRouting.getMany`](#contentroutinggetmany) * [`peerRouting.findPeer`](#peerroutingfindpeer) + * [`peerRouting.getClosestPeers`](#peerroutinggetclosestpeers) * [`peerStore.addressBook.add`](#peerstoreaddressbookadd) * [`peerStore.addressBook.delete`](#peerstoreaddressbookdelete) * [`peerStore.addressBook.get`](#peerstoreaddressbookget) @@ -37,6 +37,7 @@ * [`peerStore.protoBook.add`](#peerstoreprotobookadd) * [`peerStore.protoBook.delete`](#peerstoreprotobookdelete) * [`peerStore.protoBook.get`](#peerstoreprotobookget) + * [`peerStore.protoBook.remove`](#peerstoreprotobookremove) * [`peerStore.protoBook.set`](#peerstoreprotobookset) * [`peerStore.delete`](#peerstoredelete) * [`peerStore.get`](#peerstoreget) @@ -90,7 +91,7 @@ Creates an instance of Libp2p. |------|------|-------------| | options | `object` | libp2p options | | options.modules | [`Array`](./CONFIGURATION.md#modules) | libp2p [modules](./CONFIGURATION.md#modules) to use | -| [options.addresses] | `{ listen: Array, announce: Array, noAnnounce: Array }` | Addresses for transport listening and to advertise to the network | +| [options.addresses] | `{ listen: Array, announce: Array, announceFilter: (ma: Array) => Array }` | Addresses for transport listening and to advertise to the network | | [options.config] | `object` | libp2p modules configuration and core configuration | | [options.connectionManager] | [`object`](./CONFIGURATION.md#configuring-connection-manager) | libp2p Connection Manager [configuration](./CONFIGURATION.md#configuring-connection-manager) | | [options.transportManager] | [`object`](./CONFIGURATION.md#configuring-transport-manager) | libp2p transport manager [configuration](./CONFIGURATION.md#configuring-transport-manager) | @@ -99,6 +100,7 @@ Creates an instance of Libp2p. | [options.keychain] | [`object`](./CONFIGURATION.md#setup-with-keychain) | keychain [configuration](./CONFIGURATION.md#setup-with-keychain) | | [options.metrics] | [`object`](./CONFIGURATION.md#configuring-metrics) | libp2p Metrics [configuration](./CONFIGURATION.md#configuring-metrics) | | [options.peerId] | [`PeerId`][peer-id] | peerId instance (it will be created if not provided) | +| [options.peerRouting] | [`object`](./CONFIGURATION.md#setup-with-content-and-peer-routing) | libp2p Peer routing service [configuration](./CONFIGURATION.md#setup-with-content-and-peer-routing) | | [options.peerStore] | [`object`](./CONFIGURATION.md#configuring-peerstore) | libp2p PeerStore [configuration](./CONFIGURATION.md#configuring-peerstore) | For Libp2p configurations and modules details read the [Configuration Document](./CONFIGURATION.md). @@ -482,26 +484,6 @@ const announceMa = libp2p.addressManager.getAnnounceAddrs() // [ ] ``` -### addressManager.getNoAnnounceAddrs - -Get the multiaddrs that were provided to not announce to the network. - -`libp2p.addressManager.getNoAnnounceAddrs()` - -#### Returns - -| Type | Description | -|------|-------------| -| `Array` | Provided noAnnounce multiaddrs | - -#### Example - -```js -// ... -const noAnnounceMa = libp2p.addressManager.getNoAnnounceAddrs() -// [ ] -``` - ### transportManager.getAddrs Get the multiaddrs that libp2p transports are using to listen on. @@ -695,6 +677,36 @@ Iterates over all peer routers in series to find the given peer. If the DHT is e const peer = await libp2p.peerRouting.findPeer(peerId, options) ``` +### peerRouting.getClosestPeers + +Iterates over all content routers in series to get the closest peers of the given key. +Once a content router succeeds, the iteration will stop. If the DHT is enabled, it will be queried first. + +`libp2p.peerRouting.getClosestPeers(cid, options)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| key | `Uint8Array` | A CID like key | +| options | `object` | operation options | +| options.timeout | `number` | How long the query can take (ms). | + +#### Returns + +| Type | Description | +|------|-------------| +| `AsyncIterable<{ id: PeerId, multiaddrs: Multiaddr[] }` | Async iterator for peer data | + +#### Example + +```js +// Iterate over the closest peers found for the given key +for await (const peer of libp2p.peerRouting.getClosestPeers(key)) { + console.log(peer.id, peer.multiaddrs) +} +``` + ### peerStore.addressBook.add Adds known `multiaddrs` of a given peer. If the peer is not known, it will be set with the provided multiaddrs. @@ -843,32 +855,6 @@ Consider using `addressBook.add()` if you're not sure this is what you want to d peerStore.addressBook.add(peerId, multiaddr) ``` -### peerStore.protoBook.add - -Add known `protocols` of a given peer. - -`peerStore.protoBook.add(peerId, protocols)` - -#### Parameters - -| Name | Type | Description | -|------|------|-------------| -| peerId | [`PeerId`][peer-id] | peerId to set | -| protocols | `Array` | protocols to add | - -#### Returns - -| Type | Description | -|------|-------------| -| `ProtoBook` | Returns the Proto Book component | - -#### Example - -```js -peerStore.protoBook.add(peerId, protocols) -``` - - ### peerStore.keyBook.delete Delete the provided peer from the book. @@ -1091,6 +1077,31 @@ Set known metadata of a given `peerId`. peerStore.metadataBook.set(peerId, 'location', uint8ArrayFromString('Berlin')) ``` +### peerStore.protoBook.add + +Add known `protocols` of a given peer. + +`peerStore.protoBook.add(peerId, protocols)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| peerId | [`PeerId`][peer-id] | peerId to set | +| protocols | `Array` | protocols to add | + +#### Returns + +| Type | Description | +|------|-------------| +| `ProtoBook` | Returns the Proto Book component | + +#### Example + +```js +peerStore.protoBook.add(peerId, protocols) +``` + ### peerStore.protoBook.delete Delete the provided peer from the book. @@ -1147,6 +1158,31 @@ peerStore.protoBook.get(peerId) // [ '/proto/1.0.0', '/proto/1.1.0' ] ``` +### peerStore.protoBook.remove + +Remove given `protocols` of a given peer. + +`peerStore.protoBook.remove(peerId, protocols)` + +#### Parameters + +| Name | Type | Description | +|------|------|-------------| +| peerId | [`PeerId`][peer-id] | peerId to set | +| protocols | `Array` | protocols to remove | + +#### Returns + +| Type | Description | +|------|-------------| +| `ProtoBook` | Returns the Proto Book component | + +#### Example + +```js +peerStore.protoBook.remove(peerId, protocols) +``` + ### peerStore.protoBook.set Set known `protocols` of a given peer. diff --git a/doc/CONFIGURATION.md b/doc/CONFIGURATION.md index 527ee35733..ff143ce74c 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) + - [Setup with Auto Relay](#setup-with-auto-relay) - [Setup with Keychain](#setup-with-keychain) - [Configuring Dialing](#configuring-dialing) - [Configuring Connection Manager](#configuring-connection-manager) @@ -210,10 +211,10 @@ Besides the `modules` and `config`, libp2p allows other internal options and con - This is used in modules such as the DHT. If it is not provided, `js-libp2p` will use an in memory datastore. - `peerId`: the identity of the node, an instance of [libp2p/js-peer-id](https://github.com/libp2p/js-peer-id). - 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. -- `addresses`: an object containing `listen`, `announce` and `noAnnounce` properties with `Array`: +- `addresses`: an object containing `listen`, `announce` and `announceFilter`: - `listen` addresses will be provided to the libp2p underlying transports for listening on them. - `announce` addresses will be used to compute the advertises that the node should advertise to the network. - - `noAnnounce` addresses will be used as a filter to compute the advertises that the node should advertise to the network. + - `announceFilter`: filter function used to filter announced addresses programmatically: `(ma: Array) => Array`. Default: returns all addresses. [`libp2p-utils`](https://github.com/libp2p/js-libp2p-utils) provides useful [multiaddr utilities](https://github.com/libp2p/js-libp2p-utils/blob/master/API.md#multiaddr-isloopbackma) to create your filters. ### Examples @@ -321,6 +322,8 @@ const MPLEX = require('libp2p-mplex') const { NOISE } = require('libp2p-noise') const GossipSub = require('libp2p-gossipsub') +const { SignaturePolicy } = require('libp2p-interfaces/src/pubsub/signature-policy') + const node = await Libp2p.create({ modules: { transport: [TCP], @@ -331,9 +334,8 @@ const node = await Libp2p.create({ 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 + emitSelf: false, // whether the node should emit to self on publish + globalSignaturePolicy: SignaturePolicy.StrictSign // message signing policy } } }) @@ -395,7 +397,14 @@ const node = await Libp2p.create({ new DelegatedPeerRouter() ], }, - peerId + peerId, + peerRouting: { // Peer routing configuration + refreshManager: { // Refresh known and connected closest peers + enabled: true, // Should find the closest peers. + interval: 6e5, // Interval for getting the new for closest peers of 10min + bootDelay: 10e3 // Delay for the initial query for closest peers + } + } }) ``` @@ -419,6 +428,37 @@ const node = await Libp2p.create({ 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 + }, + advertise: { + bootDelay: 15 * 60 * 1000, // Delay before HOP relay service is advertised on the network + enabled: true, // Allows you to disable the advertise of the Hop service + ttl: 30 * 60 * 1000 // Delay Between HOP relay service advertisements on the network + } + } + } +}) +``` + +#### Setup with Auto Relay + +```js +const Libp2p = require('libp2p') +const TCP = require('libp2p-tcp') +const MPLEX = require('libp2p-mplex') +const { NOISE } = require('libp2p-noise') + +const node = await Libp2p.create({ + modules: { + transport: [TCP], + streamMuxer: [MPLEX], + connEncryption: [NOISE] + }, + 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. + autoRelay: { + enabled: true, // Allows you to bind to relays with HOP enabled for improving node dialability + maxListeners: 2 // Configure maximum number of HOP relays to use } } } @@ -466,6 +506,7 @@ Dialing in libp2p can be configured to limit the rate of dialing, and how long d | maxDialsPerPeer | `number` | How many multiaddrs we can dial per peer, in parallel. | | dialTimeout | `number` | Second dial timeout per peer in ms. | | resolvers | `object` | Dial [Resolvers](https://github.com/multiformats/js-multiaddr/blob/master/src/resolvers/index.js) for resolving multiaddrs | +| addressSorter | `(Array
) => Array
` | Sort the known addresses of a peer before trying to dial. | The below configuration example shows how the dialer should be configured, with the current defaults: @@ -476,6 +517,7 @@ const MPLEX = require('libp2p-mplex') const { NOISE } = require('libp2p-noise') const { dnsaddrResolver } = require('multiaddr/src/resolvers') +const { publicAddressesFirst } = require('libp2p-utils/src/address-sort') const node = await Libp2p.create({ modules: { @@ -489,7 +531,8 @@ const node = await Libp2p.create({ dialTimeout: 30e3, resolvers: { dnsaddr: dnsaddrResolver - } + }, + addressSorter: publicAddressesFirst } ``` diff --git a/doc/migrations/v0.29-v0.30.md b/doc/migrations/v0.29-v0.30.md new file mode 100644 index 0000000000..ffec86f926 --- /dev/null +++ b/doc/migrations/v0.29-v0.30.md @@ -0,0 +1,185 @@ + +# Migrating to libp2p@30 + +A migration guide for refactoring your application code from libp2p v0.29.x to v0.30.0. + +## Table of Contents + +- [API](#api) +- [Development and Testing](#development-and-testing) +- [Module Updates](#module-updates) + +## API + +### Pubsub + +`js-libp2p` nodes prior to this version were emitting to self on publish by default. +This default value was changed on the pubsub router layer in the past, but we kept it overwritten in libp2p to avoid an upstream breaking change. +Now `js-libp2p` does not overwrite the pubsub router options anymore. Upstream projects that want this feature should enable it on their libp2p configuration. + +**Before** + +```js +const Gossipsub = require('libp2p-gossipsub') +const Libp2p = require('libp2p') + +const libp2p = await Libp2p.create({ + modules: { + // ... Add required modules according to the Configuration docs + pubsub: Gossipsub + } +}) +``` + +**After** + +```js +const Gossipsub = require('libp2p-gossipsub') +const Libp2p = require('libp2p') + +const libp2p = await Libp2p.create({ + modules: { + // ... Add required modules according to the Configuration docs + pubsub: Gossipsub + }, + config: { + pubsub: { + emitSelf: true + } + } +}) +``` + +The [Pubsub interface](https://github.com/libp2p/js-libp2p-interfaces/tree/master/src/pubsub) was updated on its message signing properties, taking into account the Gossipsub spec updates on [libp2p/specs#294](https://github.com/libp2p/specs/pull/294) and [libp2p/specs#299](https://github.com/libp2p/specs/pull/299) + +The signing property is now based on a `globalSignaturePolicy` option instead of the previous `signMessages` and `strictSigning` options. The default to strict signing pubsub messages was kept, but if you would like to disable it, the properties should be changed as follows: + +**Before** + +```js +const Gossipsub = require('libp2p-gossipsub') +const Libp2p = require('libp2p') + +const libp2p = await Libp2p.create({ + modules: { + // ... Add required modules according to the Configuration docs + pubsub: Gossipsub + }, + config: { + pubsub: { + signMessages: false, + strictSigning: false + } + } +}) +``` + +**After** + +```js +const Gossipsub = require('libp2p-gossipsub') +const { SignaturePolicy } = require('libp2p-interfaces/src/pubsub/signature-policy') +const Libp2p = require('libp2p') + +const libp2p = await Libp2p.create({ + modules: { + // ... Add required modules according to the Configuration docs + pubsub: Gossipsub + }, + config: { + pubsub: { + globalSignaturePolicy: SignaturePolicy.StrictNoSign + } + } +}) +``` + +### Addresses + +Libp2p has supported `noAnnounce` addresses configuration for some time now. However, it did not provide the best developer experience. In this release, we dropped the `noAnnounce` configuration property in favor of an `announceFilter` property function. + +**Before** + +```js +const Libp2p = require('libp2p') + +const libp2p = await Libp2p.create({ + addresses: { + listen: ['/ip4/127.0.0.1/tcp/8000/ws'], + noAnnounce: ['/ip4/127.0.0.1/tcp/8000/ws'], + }, + // ... additional configuration per the Configuration docs +}) +``` + +**After** + +```js +const Libp2p = require('libp2p') + +// Libp2p utils has several multiaddr utils you can leverage +const isPrivate = require('libp2p-utils/src/multiaddr/is-private') + +const libp2p = await Libp2p.create({ + addresses: { + listen: ['/ip4/127.0.0.1/tcp/8000/ws'], + // Filter function: (ma: Array) => Array + announceFilter: (multiaddrs) => multiaddrs.filter(m => !isPrivate(m)) + }, + // ... additional configuration per the Configuration docs +}) +``` + +It is important pointing out another change regarding address advertising. This is not an API breaking change, but it might have influence on your libp2p setup. +Previously, when using the addresses `announce` property, its multiaddrs were concatenated with the `listen` multiaddrs and then they were filtered out by the `noAnnounce` multiaddrs, in order to create the list of multiaddrs to advertise. +In `libp2p@0.30` the logic now operates as follows: + +- If `announce` addresses are provided, only they will be announced (no filters are applied) +- If `announce` is not provided, the transport addresses will be filtered (if a filter is provided) + - if the `announceFilter` is provide it will be passed the transport addresses + +## Development and Testing + +While this is not an API breaking change, there was a behavioral breaking change on the Websockets transport when in a browser environment. This change might create issues on local test setups. +`libp2p-websockets` has allowed `TCP` and `DNS` addresses, both with `ws` or `wss` to be used for dial purposes. Taking into account security (and browser policies), we are now restricting addresses to `DNS` + `wss` in the browser +With this new behavior, if you need to use non DNS addresses, you can configure your libp2p node as follows: + +```js +const Websockets = require('libp2p-websockets') +const filters = require('libp2p-websockets/src/filters') +const Libp2p = require('libp2p') + +const transportKey = Websockets.prototype[Symbol.toStringTag] +const libp2p = await Libp2p.create({ + modules: { + transport: [Websockets] + // ... Add required modules according to the Configuration docs + }, + config: { + transport: { + [transportKey]: { + filter: filters.all + } + } + } +}) +``` + +## Module Updates + +With this release you should update the following libp2p modules if you are relying on them: + + + +```json +"libp2p-delegated-content-routing": "^0.8.0", +"libp2p-delegated-peer-routing": "^0.8.0", +"libp2p-floodsub": "^0.24.0", +"libp2p-gossipsub": "^0.7.0", +"libp2p-websockets": "^0.15.0", +``` + +Note that some of them do not need to be updated for this libp2p version to work as expected, but we suggest you to keep them updated as part of this release. diff --git a/examples/auto-relay/README.md b/examples/auto-relay/README.md new file mode 100644 index 0000000000..fbb3b7a046 --- /dev/null +++ b/examples/auto-relay/README.md @@ -0,0 +1,192 @@ +# Auto relay + +Auto Relay enables libp2p nodes to dynamically find and bind to relays on the network. Once binding (listening) is done, the node can and should advertise its addresses on the network, allowing any other node to dial it over its bound relay(s). +While direct connections to nodes are preferable, it's not always possible to do so due to NATs or browser limitations. + +## 0. Setup the example + +Before moving into the examples, you should run `npm install` on the top level `js-libp2p` folder, in order to install all the dependencies needed for this example. Once the install finishes, you should move into the example folder with `cd examples/auto-relay`. + +This example comes with 3 main files. A `relay.js` file to be used in the first step, a `listener.js` file to be used in the second step and a `dialer.js` file to be used on the third step. All of these scripts will run their own libp2p node, which will interact with the previous ones. All nodes must be running in order for you to proceed. + +## 1. Set up a relay node + +In the first step of this example, we need to configure and run a relay node in order for our target node to bind to for accepting inbound connections. + +The relay node will need to have its relay subsystem enabled, as well as its HOP capability. It can be configured as follows: + +```js +const Libp2p = require('libp2p') +const Websockets = require('libp2p-websockets') +const { NOISE } = require('libp2p-noise') +const MPLEX = require('libp2p-mplex') + +const node = await Libp2p.create({ + modules: { + transport: [Websockets], + connEncryption: [NOISE], + streamMuxer: [MPLEX] + }, + addresses: { + listen: ['/ip4/0.0.0.0/tcp/0/ws'] + // TODO check "What is next?" section + // announce: ['/dns4/auto-relay.libp2p.io/tcp/443/wss/p2p/QmWDn2LY8nannvSWJzruUYoLZ4vV83vfCBwd8DipvdgQc3'] + }, + config: { + relay: { + enabled: true, + hop: { + enabled: true + }, + advertise: { + enabled: true, + } + } + } +}) + +await node.start() + +console.log(`Node started with id ${node.peerId.toB58String()}`) +console.log('Listening on:') +node.multiaddrs.forEach((ma) => console.log(`${ma.toString()}/p2p/${node.peerId.toB58String()}`)) +``` + +The Relay HOP advertise functionality is **NOT** required to be enabled. However, if you are interested in advertising on the network that this node is available to be used as a HOP Relay you can enable it. A content router module or Rendezvous needs to be configured to leverage this option. + +You should now run the following to start the relay node: + +```sh +node relay.js +``` + +This should print out something similar to the following: + +```sh +Node started with id QmWDn2LY8nannvSWJzruUYoLZ4vV83vfCBwd8DipvdgQc3 +Listening on: +/ip4/127.0.0.1/tcp/61592/ws/p2p/QmWDn2LY8nannvSWJzruUYoLZ4vV83vfCBwd8DipvdgQc3 +/ip4/192.168.1.120/tcp/61592/ws/p2p/QmWDn2LY8nannvSWJzruUYoLZ4vV83vfCBwd8DipvdgQc3 +``` + +## 2. Set up a listener node with Auto Relay Enabled + +One of the typical use cases for Auto Relay is nodes behind a NAT or browser nodes due to their inability to expose a public address. For running a libp2p node that automatically binds itself to connected HOP relays, you can see the following: + +```js +const Libp2p = require('libp2p') +const Websockets = require('libp2p-websockets') +const { NOISE } = require('libp2p-noise') +const MPLEX = require('libp2p-mplex') + +const relayAddr = process.argv[2] +if (!relayAddr) { + throw new Error('the relay address needs to be specified as a parameter') +} + +const node = await Libp2p.create({ + modules: { + transport: [Websockets], + connEncryption: [NOISE], + streamMuxer: [MPLEX] + }, + config: { + relay: { + enabled: true, + autoRelay: { + enabled: true, + maxListeners: 2 + } + } + } +}) + +await node.start() +console.log(`Node started with id ${node.peerId.toB58String()}`) + +const conn = await node.dial(relayAddr) + +// Wait for connection and relay to be bind for the example purpose +await new Promise((resolve) => { + node.peerStore.on('change:multiaddrs', ({ peerId }) => { + // Updated self multiaddrs? + if (peerId.equals(node.peerId)) { + resolve() + } + }) +}) + +console.log(`Connected to the HOP relay ${conn.remotePeer.toString()}`) +console.log(`Advertising with a relay address of ${node.multiaddrs[0].toString()}/p2p/${node.peerId.toB58String()}`) +``` + +As you can see in the code, we need to provide the relay address, `relayAddr`, as a process argument. This node will dial the provided relay address and automatically bind to it. + +You should now run the following to start the node running Auto Relay: + +```sh +node listener.js /ip4/192.168.1.120/tcp/58941/ws/p2p/QmQKCBm87HQMbFqy14oqC85pMmnRrj6iD46ggM6reqNpsd +``` + +This should print out something similar to the following: + +```sh +Node started with id QmerrWofKF358JE6gv3z74cEAyL7z1KqhuUoVfGEynqjRm +Connected to the HOP relay QmWDn2LY8nannvSWJzruUYoLZ4vV83vfCBwd8DipvdgQc3 +Advertising with a relay address of /ip4/192.168.1.120/tcp/61592/ws/p2p/QmWDn2LY8nannvSWJzruUYoLZ4vV83vfCBwd8DipvdgQc3/p2p-circuit/p2p/QmerrWofKF358JE6gv3z74cEAyL7z1KqhuUoVfGEynqjRm +``` + +Per the address, it is possible to verify that the auto relay node is listening on the circuit relay node address. + +Instead of dialing this relay manually, you could set up this node with the Bootstrap module and provide it in the bootstrap list. Moreover, you can use other `peer-discovery` modules to discover peers in the network and the node will automatically bind to the relays that support HOP until reaching the maximum number of listeners. + +## 3. Set up a dialer node for testing connectivity + +Now that you have a relay node and a node bound to that relay, you can test connecting to the auto relay node via the relay. + +```js +const Libp2p = require('libp2p') +const Websockets = require('libp2p-websockets') +const { NOISE } = require('libp2p-noise') +const MPLEX = require('libp2p-mplex') + +const autoRelayNodeAddr = process.argv[2] +if (!autoRelayNodeAddr) { + throw new Error('the auto relay node address needs to be specified') +} + +const node = await Libp2p.create({ + modules: { + transport: [Websockets], + connEncryption: [NOISE], + streamMuxer: [MPLEX] + } +}) + +await node.start() +console.log(`Node started with id ${node.peerId.toB58String()}`) + +const conn = await node.dial(autoRelayNodeAddr) +console.log(`Connected to the auto relay node via ${conn.remoteAddr.toString()}`) +``` + +You should now run the following to start the relay node using the listen address from step 2: + +```sh +node dialer.js /ip4/192.168.1.120/tcp/58941/ws/p2p/QmQKCBm87HQMbFqy14oqC85pMmnRrj6iD46ggM6reqNpsd +``` + +Once you start your test node, it should print out something similar to the following: + +```sh +Node started: Qme7iEzDxFoFhhkrsrkHkMnM11aPYjysaehP4NZeUfVMKG +Connected to the auto relay node via /ip4/192.168.1.120/tcp/61592/ws/p2p/QmWDn2LY8nannvSWJzruUYoLZ4vV83vfCBwd8DipvdgQc3/p2p-circuit/p2p/QmerrWofKF358JE6gv3z74cEAyL7z1KqhuUoVfGEynqjRm +``` + +As you can see from the output, the remote address of the established connection uses the relayed connection. + +## 4. What is next? + +Before moving into production, there are a few things that you should take into account. + +A relay node should not advertise its private address in a real world scenario, as the node would not be reachable by others. You should provide an array of public addresses in the libp2p `addresses.announce` option. If you are using websockets, bear in mind that due to browser’s security policies you cannot establish unencrypted connection from secure context. The simplest solution is to setup SSL with nginx and proxy to the node and setup a domain name for the certificate. diff --git a/examples/auto-relay/dialer.js b/examples/auto-relay/dialer.js new file mode 100644 index 0000000000..ebf5fd1249 --- /dev/null +++ b/examples/auto-relay/dialer.js @@ -0,0 +1,29 @@ +'use strict' + +const Libp2p = require('libp2p') +const Websockets = require('libp2p-websockets') +const { NOISE } = require('libp2p-noise') +const MPLEX = require('libp2p-mplex') + +async function main () { + const autoRelayNodeAddr = process.argv[2] + if (!autoRelayNodeAddr) { + throw new Error('the auto relay node address needs to be specified') + } + + const node = await Libp2p.create({ + modules: { + transport: [Websockets], + connEncryption: [NOISE], + streamMuxer: [MPLEX] + } + }) + + await node.start() + console.log(`Node started with id ${node.peerId.toB58String()}`) + + const conn = await node.dial(autoRelayNodeAddr) + console.log(`Connected to the auto relay node via ${conn.remoteAddr.toString()}`) +} + +main() diff --git a/examples/auto-relay/listener.js b/examples/auto-relay/listener.js new file mode 100644 index 0000000000..0f94374067 --- /dev/null +++ b/examples/auto-relay/listener.js @@ -0,0 +1,47 @@ +'use strict' + +const Libp2p = require('libp2p') +const Websockets = require('libp2p-websockets') +const { NOISE } = require('libp2p-noise') +const MPLEX = require('libp2p-mplex') + +async function main () { + const relayAddr = process.argv[2] + if (!relayAddr) { + throw new Error('the relay address needs to be specified as a parameter') + } + + const node = await Libp2p.create({ + modules: { + transport: [Websockets], + connEncryption: [NOISE], + streamMuxer: [MPLEX] + }, + config: { + relay: { + enabled: true, + autoRelay: { + enabled: true, + maxListeners: 2 + } + } + } + }) + + await node.start() + console.log(`Node started with id ${node.peerId.toB58String()}`) + + const conn = await node.dial(relayAddr) + + console.log(`Connected to the HOP relay ${conn.remotePeer.toString()}`) + + // Wait for connection and relay to be bind for the example purpose + node.peerStore.on('change:multiaddrs', ({ peerId }) => { + // Updated self multiaddrs? + if (peerId.equals(node.peerId)) { + console.log(`Advertising with a relay address of ${node.multiaddrs[0].toString()}/p2p/${node.peerId.toB58String()}`) + } + }) +} + +main() diff --git a/examples/auto-relay/relay.js b/examples/auto-relay/relay.js new file mode 100644 index 0000000000..2a18c3a769 --- /dev/null +++ b/examples/auto-relay/relay.js @@ -0,0 +1,40 @@ +'use strict' + +const Libp2p = require('libp2p') +const Websockets = require('libp2p-websockets') +const { NOISE } = require('libp2p-noise') +const MPLEX = require('libp2p-mplex') + +async function main () { + const node = await Libp2p.create({ + modules: { + transport: [Websockets], + connEncryption: [NOISE], + streamMuxer: [MPLEX] + }, + addresses: { + listen: ['/ip4/0.0.0.0/tcp/0/ws'] + // TODO check "What is next?" section + // announce: ['/dns4/auto-relay.libp2p.io/tcp/443/wss/p2p/QmWDn2LY8nannvSWJzruUYoLZ4vV83vfCBwd8DipvdgQc3'] + }, + config: { + relay: { + enabled: true, + hop: { + enabled: true + }, + advertise: { + enabled: true, + } + } + } + }) + + await node.start() + + console.log(`Node started with id ${node.peerId.toB58String()}`) + console.log('Listening on:') + node.multiaddrs.forEach((ma) => console.log(`${ma.toString()}/p2p/${node.peerId.toB58String()}`)) +} + +main() diff --git a/examples/auto-relay/test.js b/examples/auto-relay/test.js new file mode 100644 index 0000000000..0eaf8cc3ae --- /dev/null +++ b/examples/auto-relay/test.js @@ -0,0 +1,94 @@ +'use strict' + +const path = require('path') +const execa = require('execa') +const pDefer = require('p-defer') +const uint8ArrayToString = require('uint8arrays/to-string') + +function startProcess (name, args = []) { + return execa('node', [path.join(__dirname, name), ...args], { + cwd: path.resolve(__dirname), + all: true + }) +} + +async function test () { + let output1 = '' + let output2 = '' + let output3 = '' + let relayAddr + let autoRelayAddr + + const proc1Ready = pDefer() + const proc2Ready = pDefer() + + // Step 1 process + process.stdout.write('relay.js\n') + + const proc1 = startProcess('relay.js') + proc1.all.on('data', async (data) => { + process.stdout.write(data) + + output1 += uint8ArrayToString(data) + + if (output1.includes('Listening on:') && output1.includes('/p2p/')) { + relayAddr = output1.trim().split('Listening on:\n')[1].split('\n')[0] + proc1Ready.resolve() + } + }) + + await proc1Ready.promise + process.stdout.write('==================================================================\n') + + // Step 2 process + process.stdout.write('listener.js\n') + + const proc2 = startProcess('listener.js', [relayAddr]) + proc2.all.on('data', async (data) => { + process.stdout.write(data) + + output2 += uint8ArrayToString(data) + + if (output2.includes('Advertising with a relay address of') && output2.includes('/p2p/')) { + autoRelayAddr = output2.trim().split('Advertising with a relay address of ')[1] + proc2Ready.resolve() + } + }) + + await proc2Ready.promise + process.stdout.write('==================================================================\n') + + // Step 3 process + process.stdout.write('dialer.js\n') + + const proc3 = startProcess('dialer.js', [autoRelayAddr]) + proc3.all.on('data', async (data) => { + process.stdout.write(data) + + output3 += uint8ArrayToString(data) + + if (output3.includes('Connected to the auto relay node via')) { + const remoteAddr = output3.trim().split('Connected to the auto relay node via ')[1] + + if (remoteAddr === autoRelayAddr) { + proc3.kill() + proc2.kill() + proc1.kill() + } else { + throw new Error('dialer did not dial through the relay') + } + } + }) + + await Promise.all([ + proc1, + proc2, + proc3 + ]).catch((err) => { + if (err.signal !== 'SIGTERM') { + throw err + } + }) +} + +module.exports = test \ No newline at end of file diff --git a/examples/package.json b/examples/package.json new file mode 100644 index 0000000000..feaf656d1b --- /dev/null +++ b/examples/package.json @@ -0,0 +1,16 @@ +{ + "name": "libp2p-examples", + "version": "1.0.0", + "description": "Examples of how to use libp2p", + "scripts": { + "test": "node ./test.js", + "test:all": "node ./test-all.js" + }, + "license": "MIT", + "dependencies": { + "execa": "^2.1.0", + "fs-extra": "^8.1.0", + "p-defer": "^3.0.0", + "which": "^2.0.1" + } +} diff --git a/examples/pubsub/1.js b/examples/pubsub/1.js index 8c6fdfdb91..419838960b 100644 --- a/examples/pubsub/1.js +++ b/examples/pubsub/1.js @@ -43,6 +43,7 @@ const createNode = async () => { }) await node1.pubsub.subscribe(topic) + // Will not receive own published messages by default node2.pubsub.on(topic, (msg) => { console.log(`node2 received: ${uint8ArrayToString(msg.data)}`) }) diff --git a/examples/pubsub/README.md b/examples/pubsub/README.md index 17a896f3a6..2016b2c813 100644 --- a/examples/pubsub/README.md +++ b/examples/pubsub/README.md @@ -44,7 +44,6 @@ const node2 = nodes[1] // Add node's 2 data to the PeerStore node1.peerStore.addressBook.set(node2.peerId, node2.multiaddrs) - await node1.dial(node2.peerId) node1.pubsub.on(topic, (msg) => { @@ -52,6 +51,7 @@ node1.pubsub.on(topic, (msg) => { }) await node1.pubsub.subscribe(topic) +// Will not receive own published messages by default node2.pubsub.on(topic, (msg) => { console.log(`node2 received: ${uint8ArrayToString(msg.data)}`) }) @@ -68,25 +68,34 @@ The output of the program should look like: ``` > node 1.js connected to QmWpvkKm6qHLhoxpWrTswY6UMNWDyn8hN265Qp9ZYvgS82 -node2 received: Bird bird bird, bird is the word! node1 received: Bird bird bird, bird is the word! -node2 received: Bird bird bird, bird is the word! node1 received: Bird bird bird, bird is the word! ``` -You can change the pubsub `emitSelf` option if you don't want the publishing node to receive its own messages. +You can change the pubsub `emitSelf` option if you want the publishing node to receive its own messages. ```JavaScript const defaults = { config: { pubsub: { enabled: true, - emitSelf: false + emitSelf: true } } } ``` +The output of the program should look like: + +``` +> node 1.js +connected to QmWpvkKm6qHLhoxpWrTswY6UMNWDyn8hN265Qp9ZYvgS82 +node1 received: Bird bird bird, bird is the word! +node2 received: Bird bird bird, bird is the word! +node1 received: Bird bird bird, bird is the word! +node2 received: Bird bird bird, bird is the word! +``` + ## 2. Future work libp2p/IPFS PubSub is enabling a whole set of Distributed Real Time applications using CRDT (Conflict-Free Replicated Data Types). It is still going through heavy research (and hacking) and we invite you to join the conversation at [research-CRDT](https://github.com/ipfs/research-CRDT). Here is a list of some of the exciting examples: diff --git a/examples/pubsub/message-filtering/1.js b/examples/pubsub/message-filtering/1.js index 85d7bcf8c4..4d8a2c1803 100644 --- a/examples/pubsub/message-filtering/1.js +++ b/examples/pubsub/message-filtering/1.js @@ -44,6 +44,7 @@ const createNode = async () => { //subscribe node1.pubsub.on(topic, (msg) => { + // Will not receive own published messages by default console.log(`node1 received: ${uint8ArrayToString(msg.data)}`) }) await node1.pubsub.subscribe(topic) diff --git a/examples/pubsub/message-filtering/README.md b/examples/pubsub/message-filtering/README.md index a9c0dad26d..df99043051 100644 --- a/examples/pubsub/message-filtering/README.md +++ b/examples/pubsub/message-filtering/README.md @@ -97,15 +97,12 @@ Result ``` > node 1.js ############## fruit banana ############## -node1 received: banana node2 received: banana node3 received: banana ############## fruit apple ############## -node1 received: apple node2 received: apple node3 received: apple ############## fruit car ############## -node1 received: car ############## fruit orange ############## node1 received: orange node2 received: orange diff --git a/examples/test-all.js b/examples/test-all.js new file mode 100644 index 0000000000..3ee99e45fa --- /dev/null +++ b/examples/test-all.js @@ -0,0 +1,33 @@ +'use strict' + +process.on('unhandedRejection', (err) => { + console.error(err) + + process.exit(1) +}) + +const path = require('path') +const fs = require('fs') +const { + waitForOutput +} = require('./utils') + +async function testAll () { + for (const dir of fs.readdirSync(__dirname)) { + if (dir === 'node_modules' || dir === 'tests_output') { + continue + } + + const stats = fs.statSync(path.join(__dirname, dir)) + + if (!stats.isDirectory()) { + continue + } + + await waitForOutput('npm info ok', 'npm', ['test', '--', dir], { + cwd: __dirname + }) + } +} + +testAll() diff --git a/examples/test.js b/examples/test.js new file mode 100644 index 0000000000..3da6eccdb9 --- /dev/null +++ b/examples/test.js @@ -0,0 +1,95 @@ +'use strict' + +process.env.NODE_ENV = 'test' +process.env.CI = true // needed for some "clever" build tools + +const fs = require('fs-extra') +const path = require('path') +const execa = require('execa') +const dir = path.join(__dirname, process.argv[2]) + +testExample(dir) + .then(() => {}, (err) => { + if (err.exitCode) { + process.exit(err.exitCode) + } + + console.error(err) + process.exit(1) + }) + +async function testExample (dir) { + await installDeps(dir) + await build(dir) + await runTest(dir) + // TODO: add browser test setup +} + +async function installDeps (dir) { + if (!fs.existsSync(path.join(dir, 'package.json'))) { + console.info('Nothing to install in', dir) + return + } + + if (fs.existsSync(path.join(dir, 'node_modules'))) { + console.info('Dependencies already installed in', dir) + return + } + + const proc = execa.command('npm install', { + cwd: dir + }) + proc.all.on('data', (data) => { + process.stdout.write(data) + }) + + await proc +} + +async function build (dir) { + const pkgJson = path.join(dir, 'package.json') + + if (!fs.existsSync(pkgJson)) { + console.info('Nothing to build in', dir) + return + } + + const pkg = require(pkgJson) + let build + + if (pkg.scripts.bundle) { + build = 'bundle' + } + + if (pkg.scripts.build) { + build = 'build' + } + + if (!build) { + console.info('No "build" or "bundle" script in', pkgJson) + return + } + + const proc = execa('npm', ['run', build], { + cwd: dir + }) + proc.all.on('data', (data) => { + process.stdout.write(data) + }) + + await proc +} + +async function runTest (dir) { + console.info('Running node tests in', dir) + const testFile = path.join(dir, 'test.js') + + if (!fs.existsSync(testFile)) { + console.info('Nothing to test in', dir) + return + } + + const runTest = require(testFile) + + await runTest() +} \ No newline at end of file diff --git a/examples/utils.js b/examples/utils.js new file mode 100644 index 0000000000..aec6df5418 --- /dev/null +++ b/examples/utils.js @@ -0,0 +1,61 @@ +'use strict' + +const execa = require('execa') +const fs = require('fs-extra') +const which = require('which') + +async function isExecutable (command) { + try { + await fs.access(command, fs.constants.X_OK) + + return true + } catch (err) { + if (err.code === 'ENOENT') { + return isExecutable(await which(command)) + } + + if (err.code === 'EACCES') { + return false + } + + throw err + } +} + +async function waitForOutput (expectedOutput, command, args = [], opts = {}) { + if (!await isExecutable(command)) { + args.unshift(command) + command = 'node' + } + + const proc = execa(command, args, opts) + let output = '' + let time = 120000 + + let timeout = setTimeout(() => { + throw new Error(`Did not see "${expectedOutput}" in output from "${[command].concat(args).join(' ')}" after ${time/1000}s`) + }, time) + + proc.all.on('data', (data) => { + process.stdout.write(data) + + output += data.toString('utf8') + + if (output.includes(expectedOutput)) { + clearTimeout(timeout) + proc.kill() + } + }) + + try { + await proc + } catch (err) { + if (!err.killed) { + throw err + } + } +} + +module.exports = { + waitForOutput +} diff --git a/package.json b/package.json index 41587182fa..a45e0d2235 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,15 @@ "description": "JavaScript implementation of libp2p, a modular peer to peer network stack", "leadMaintainer": "Jacob Heun ", "main": "src/index.js", + "types": "dist/src/index.d.ts", + "typesVersions": { + "*": { + "src/*": [ + "dist/src/*", + "dist/src/*/index" + ] + } + }, "files": [ "dist", "src" @@ -14,6 +23,7 @@ "test": "npm run test:node && npm run test:browser", "test:node": "aegir test -t node -f \"./test/**/*.{node,spec}.js\"", "test:browser": "aegir test -t browser", + "test:examples": "cd examples && npm run test:all", "release": "aegir release -t node -t browser", "release-minor": "aegir release --type minor -t node -t browser", "release-major": "aegir release --type major -t node -t browser", @@ -45,13 +55,14 @@ "aggregate-error": "^3.0.1", "any-signal": "^1.1.0", "bignumber.js": "^9.0.0", + "cids": "^1.0.0", "class-is": "^1.1.0", "debug": "^4.1.1", "err-code": "^2.0.0", "events": "^3.1.0", "hashlru": "^2.3.0", "interface-datastore": "^2.0.0", - "ipfs-utils": "^2.2.0", + "ipfs-utils": "^5.0.1", "it-all": "^1.0.1", "it-buffer": "^0.1.2", "it-handshake": "^1.0.1", @@ -59,13 +70,14 @@ "it-pipe": "^1.1.0", "it-protocol-buffers": "^0.2.0", "libp2p-crypto": "^0.18.0", - "libp2p-interfaces": "^0.5.1", - "libp2p-utils": "^0.2.0", + "libp2p-interfaces": "^0.8.0", + "libp2p-utils": "^0.2.2", "mafmt": "^8.0.0", "merge-options": "^2.0.0", "moving-average": "^1.0.0", "multiaddr": "^8.1.0", "multicodec": "^2.0.0", + "multihashing-async": "^2.0.1", "multistream-select": "^1.0.0", "mutable-proxy": "^1.0.0", "node-forge": "^0.9.1", @@ -76,6 +88,7 @@ "protons": "^2.0.0", "retimer": "^2.0.0", "sanitize-filename": "^1.6.3", + "set-delayed-interval": "^1.0.0", "streaming-iterables": "^5.0.2", "timeout-abort-controller": "^1.1.1", "varint": "^5.0.0", @@ -84,25 +97,22 @@ "devDependencies": { "@nodeutils/defaults-deep": "^1.1.0", "abortable-iterator": "^3.0.0", - "aegir": "^27.0.0", - "chai": "^4.2.0", - "chai-as-promised": "^7.1.1", + "aegir": "git://github.com/ipfs/aegir#feat/ts-4.1.x", "chai-bytes": "^0.1.2", "chai-string": "^1.5.0", - "cids": "^1.0.0", "delay": "^4.3.0", - "dirty-chai": "^2.0.1", "interop-libp2p": "^0.3.0", + "into-stream": "^6.0.0", "ipfs-http-client": "^47.0.1", "it-concat": "^1.0.0", "it-pair": "^1.0.0", "it-pushable": "^1.4.0", "libp2p": ".", "libp2p-bootstrap": "^0.12.0", - "libp2p-delegated-content-routing": "^0.7.0", - "libp2p-delegated-peer-routing": "^0.7.0", - "libp2p-floodsub": "^0.23.0", - "libp2p-gossipsub": "^0.6.0", + "libp2p-delegated-content-routing": "^0.8.0", + "libp2p-delegated-peer-routing": "^0.8.0", + "libp2p-floodsub": "^0.24.0", + "libp2p-gossipsub": "^0.7.0", "libp2p-kad-dht": "^0.20.0", "libp2p-mdns": "^0.15.0", "libp2p-mplex": "^0.10.1", @@ -110,7 +120,7 @@ "libp2p-secio": "^0.13.1", "libp2p-tcp": "^0.15.1", "libp2p-webrtc-star": "^0.20.0", - "libp2p-websockets": "^0.14.0", + "libp2p-websockets": "^0.15.0", "multihashes": "^3.0.1", "nock": "^13.0.3", "p-defer": "^3.0.0", diff --git a/src/address-manager/README.md b/src/address-manager/README.md index 53f9324802..44b107c892 100644 --- a/src/address-manager/README.md +++ b/src/address-manager/README.md @@ -1,6 +1,6 @@ # Address Manager -The Address manager is responsible for keeping an updated register of the peer's addresses. It includes 3 different types of Addresses: `Listen Addresses`, `Announce Addresses` and `No Announce Addresses`. +The Address manager is responsible for keeping an updated register of the peer's addresses. It includes 2 different types of Addresses: `Listen Addresses` and `Announce Addresses`. These Addresses should be specified in your libp2p [configuration](../../doc/CONFIGURATION.md) when you create your node. @@ -20,17 +20,11 @@ Scenarios for Announce Addresses include: - when you setup a libp2p node in your private network at home, but you need to announce your public IP Address to the outside world; - when you want to announce a DNS address, which maps to your public IP Address. -## No Announce Addresses - -While we need to add Announce Addresses to enable peers' connectivity, we should also avoid announcing addresses that will not be reachable. No Announce Addresses should be specified so that they are filtered from the advertised multiaddrs. - -As stated in the Listen Addresses section, Listen Addresses might be modified by libp2p transports after the successfully bind to those addresses. Libp2p should also take these changes into account so that they can be matched when No Announce Addresses are being filtered out of the advertised multiaddrs. - ## Implementation When a libp2p node is created, the Address Manager will be populated from the provided addresses through the libp2p configuration. Once the node is started, the Transport Manager component will gather the listen addresses from the Address Manager, so that the libp2p transports can attempt to bind to them. -Libp2p will use the the Address Manager as the source of truth when advertising the peers addresses. After all transports are ready, other libp2p components/subsystems will kickoff, namely the Identify Service and the DHT. Both of them will announce the node addresses to the other peers in the network. The announce and noAnnounce addresses will have an important role here and will be gathered by libp2p to compute its current addresses to advertise everytime it is needed. +Libp2p will use the the Address Manager as the source of truth when advertising the peers addresses. After all transports are ready, other libp2p components/subsystems will kickoff, namely the Identify Service and the DHT. Both of them will announce the node addresses to the other peers in the network. The announce addresses will have an important role here and will be gathered by libp2p to compute its current addresses to advertise everytime it is needed. ## Future Considerations diff --git a/src/address-manager/index.js b/src/address-manager/index.js index 0ba0df03c3..5c9874af33 100644 --- a/src/address-manager/index.js +++ b/src/address-manager/index.js @@ -1,36 +1,35 @@ 'use strict' -const debug = require('debug') -const log = debug('libp2p:addresses') -log.error = debug('libp2p:addresses:error') - const multiaddr = require('multiaddr') /** - * Responsible for managing this peers addresses. - * Peers can specify their listen, announce and noAnnounce addresses. - * The listen addresses will be used by the libp2p transports to listen for new connections, - * while the announce an noAnnounce addresses will be combined with the listen addresses for - * address adverstising to other peers in the network. + * @typedef {import('multiaddr')} Multiaddr + */ + +/** + * @typedef {Object} AddressManagerOptions + * @property {string[]} [listen = []] - list of multiaddrs string representation to listen. + * @property {string[]} [announce = []] - list of multiaddrs string representation to announce. */ class AddressManager { /** + * Responsible for managing the peer addresses. + * Peers can specify their listen and announce addresses. + * The listen addresses will be used by the libp2p transports to listen for new connections, + * while the announce addresses will be used for the peer addresses' to other peers in the network. + * * @class - * @param {object} [options] - * @param {Array} [options.listen = []] - list of multiaddrs string representation to listen. - * @param {Array} [options.announce = []] - list of multiaddrs string representation to announce. - * @param {Array} [options.noAnnounce = []] - list of multiaddrs string representation to not announce. + * @param {AddressManagerOptions} [options] */ - constructor ({ listen = [], announce = [], noAnnounce = [] } = {}) { + constructor ({ listen = [], announce = [] } = {}) { this.listen = new Set(listen) this.announce = new Set(announce) - this.noAnnounce = new Set(noAnnounce) } /** * Get peer listen multiaddrs. * - * @returns {Array} + * @returns {Multiaddr[]} */ getListenAddrs () { return Array.from(this.listen).map((a) => multiaddr(a)) @@ -39,20 +38,11 @@ class AddressManager { /** * Get peer announcing multiaddrs. * - * @returns {Array} + * @returns {Multiaddr[]} */ getAnnounceAddrs () { return Array.from(this.announce).map((a) => multiaddr(a)) } - - /** - * Get peer noAnnouncing multiaddrs. - * - * @returns {Array} - */ - getNoAnnounceAddrs () { - return Array.from(this.noAnnounce).map((a) => multiaddr(a)) - } } module.exports = AddressManager diff --git a/src/circuit/auto-relay.js b/src/circuit/auto-relay.js new file mode 100644 index 0000000000..122ac979fb --- /dev/null +++ b/src/circuit/auto-relay.js @@ -0,0 +1,268 @@ +'use strict' + +const debug = require('debug') +const log = Object.assign(debug('libp2p:auto-relay'), { + error: debug('libp2p:auto-relay:err') +}) + +const uint8ArrayFromString = require('uint8arrays/from-string') +const uint8ArrayToString = require('uint8arrays/to-string') +const multiaddr = require('multiaddr') +const PeerId = require('peer-id') + +const { relay: multicodec } = require('./multicodec') +const { canHop } = require('./circuit/hop') +const { namespaceToCid } = require('./utils') +const { + CIRCUIT_PROTO_CODE, + HOP_METADATA_KEY, + HOP_METADATA_VALUE, + RELAY_RENDEZVOUS_NS +} = require('./constants') + +/** + * @typedef {import('libp2p-interfaces/src/connection').Connection} Connection + * @typedef {import('../peer-store/address-book').Address} Address + */ + +/** + * @typedef {Object} AutoRelayProperties + * @property {import('../')} libp2p + * + * @typedef {Object} AutoRelayOptions + * @property {number} [maxListeners = 1] - maximum number of relays to listen. + */ + +class AutoRelay { + /** + * Creates an instance of AutoRelay. + * + * @class + * @param {AutoRelayProperties & AutoRelayOptions} props + */ + constructor ({ libp2p, maxListeners = 1 }) { + this._libp2p = libp2p + this._peerId = libp2p.peerId + this._peerStore = libp2p.peerStore + this._connectionManager = libp2p.connectionManager + this._transportManager = libp2p.transportManager + this._addressSorter = libp2p.dialer.addressSorter + + this.maxListeners = maxListeners + + /** + * @type {Set} + */ + this._listenRelays = new Set() + + this._onProtocolChange = this._onProtocolChange.bind(this) + this._onPeerDisconnected = this._onPeerDisconnected.bind(this) + + this._peerStore.on('change:protocols', this._onProtocolChange) + this._connectionManager.on('peer:disconnect', this._onPeerDisconnected) + } + + /** + * Check if a peer supports the relay protocol. + * If the protocol is not supported, check if it was supported before and remove it as a listen relay. + * If the protocol is supported, check if the peer supports **HOP** and add it as a listener if + * inside the threshold. + * + * @param {Object} props + * @param {PeerId} props.peerId + * @param {string[]} props.protocols + * @returns {Promise} + */ + async _onProtocolChange ({ peerId, protocols }) { + const id = peerId.toB58String() + + // Check if it has the protocol + const hasProtocol = protocols.find(protocol => protocol === multicodec) + + // If no protocol, check if we were keeping the peer before as a listenRelay + if (!hasProtocol && this._listenRelays.has(id)) { + this._removeListenRelay(id) + return + } else if (!hasProtocol || this._listenRelays.has(id)) { + return + } + + // If protocol, check if can hop, store info in the metadataBook and listen on it + try { + const connection = this._connectionManager.get(peerId) + if (!connection) { + return + } + + // Do not hop on a relayed connection + if (connection.remoteAddr.protoCodes().includes(CIRCUIT_PROTO_CODE)) { + log(`relayed connection to ${id} will not be used to hop on`) + return + } + + const supportsHop = await canHop({ connection }) + + if (supportsHop) { + this._peerStore.metadataBook.set(peerId, HOP_METADATA_KEY, uint8ArrayFromString(HOP_METADATA_VALUE)) + await this._addListenRelay(connection, id) + } + } catch (err) { + log.error(err) + } + } + + /** + * Peer disconnects. + * + * @param {Connection} connection - connection to the peer + * @returns {void} + */ + _onPeerDisconnected (connection) { + const peerId = connection.remotePeer + const id = peerId.toB58String() + + // Not listening on this relay + if (!this._listenRelays.has(id)) { + return + } + + this._removeListenRelay(id) + } + + /** + * Attempt to listen on the given relay connection. + * + * @private + * @param {Connection} connection - connection to the peer + * @param {string} id - peer identifier string + * @returns {Promise} + */ + async _addListenRelay (connection, id) { + // Check if already listening on enough relays + if (this._listenRelays.size >= this.maxListeners) { + return + } + + // Get peer known addresses and sort them per public addresses first + const remoteAddrs = this._peerStore.addressBook.getMultiaddrsForPeer( + connection.remotePeer, this._addressSorter + ) + + if (!remoteAddrs || !remoteAddrs.length) { + return + } + + const listenAddr = `${remoteAddrs[0].toString()}/p2p-circuit` + this._listenRelays.add(id) + + // Attempt to listen on relay + try { + await this._transportManager.listen([multiaddr(listenAddr)]) + // Announce multiaddrs will update on listen success by TransportManager event being triggered + } catch (err) { + log.error(err) + this._listenRelays.delete(id) + } + } + + /** + * Remove listen relay. + * + * @private + * @param {string} id - peer identifier string. + * @returns {void} + */ + _removeListenRelay (id) { + if (this._listenRelays.delete(id)) { + // TODO: this should be responsibility of the connMgr + this._listenOnAvailableHopRelays([id]) + } + } + + /** + * Try to listen on available hop relay connections. + * The following order will happen while we do not have enough relays. + * 1. Check the metadata store for known relays, try to listen on the ones we are already connected. + * 2. Dial and try to listen on the peers we know that support hop but are not connected. + * 3. Search the network. + * + * @param {string[]} [peersToIgnore] + * @returns {Promise} + */ + async _listenOnAvailableHopRelays (peersToIgnore = []) { + // TODO: The peer redial issue on disconnect should be handled by connection gating + // Check if already listening on enough relays + if (this._listenRelays.size >= this.maxListeners) { + return + } + + const knownHopsToDial = [] + + // Check if we have known hop peers to use and attempt to listen on the already connected + for (const [id, metadataMap] of this._peerStore.metadataBook.data.entries()) { + // Continue to next if listening on this or peer to ignore + if (this._listenRelays.has(id) || peersToIgnore.includes(id)) { + continue + } + + const supportsHop = metadataMap.get(HOP_METADATA_KEY) + + // Continue to next if it does not support Hop + if (!supportsHop || uint8ArrayToString(supportsHop) !== HOP_METADATA_VALUE) { + continue + } + + const peerId = PeerId.createFromCID(id) + const connection = this._connectionManager.get(peerId) + + // If not connected, store for possible later use. + if (!connection) { + knownHopsToDial.push(peerId) + continue + } + + await this._addListenRelay(connection, id) + + // Check if already listening on enough relays + if (this._listenRelays.size >= this.maxListeners) { + return + } + } + + // Try to listen on known peers that are not connected + for (const peerId of knownHopsToDial) { + const connection = await this._libp2p.dial(peerId) + await this._addListenRelay(connection, peerId.toB58String()) + + // Check if already listening on enough relays + if (this._listenRelays.size >= this.maxListeners) { + return + } + } + + // Try to find relays to hop on the network + try { + const cid = await namespaceToCid(RELAY_RENDEZVOUS_NS) + for await (const provider of this._libp2p.contentRouting.findProviders(cid)) { + if (!provider.multiaddrs.length) { + continue + } + const peerId = provider.id + + this._peerStore.addressBook.add(peerId, provider.multiaddrs) + const connection = await this._libp2p.dial(peerId) + + await this._addListenRelay(connection, peerId.toB58String()) + + // Check if already listening on enough relays + if (this._listenRelays.size >= this.maxListeners) { + return + } + } + } catch (err) { + log.error(err) + } + } +} + +module.exports = AutoRelay diff --git a/src/circuit/circuit/hop.js b/src/circuit/circuit/hop.js index f497f33a32..32ab8b984c 100644 --- a/src/circuit/circuit/hop.js +++ b/src/circuit/circuit/hop.js @@ -1,22 +1,41 @@ 'use strict' const debug = require('debug') -const log = debug('libp2p:circuit:hop') -log.error = debug('libp2p:circuit:hop:error') +const log = Object.assign(debug('libp2p:circuit:hop'), { + error: debug('libp2p:circuit:hop:err') +}) +const errCode = require('err-code') const PeerId = require('peer-id') const { validateAddrs } = require('./utils') const StreamHandler = require('./stream-handler') const { CircuitRelay: CircuitPB } = require('../protocol') -const pipe = require('it-pipe') -const errCode = require('err-code') +const { pipe } = require('it-pipe') const { codes: Errors } = require('../../errors') const { stop } = require('./stop') const multicodec = require('./../multicodec') -module.exports.handleHop = async function handleHop ({ +/** + * @typedef {import('../../types').CircuitRequest} CircuitRequest + * @typedef {import('libp2p-interfaces/src/connection').Connection} Connection + * @typedef {import('../transport')} Transport + */ + +/** + * @typedef {Object} HopRequest + * @property {Connection} connection + * @property {CircuitRequest} request + * @property {StreamHandler} streamHandler + * @property {Transport} circuit + */ + +/** + * @param {HopRequest} options + * @returns {Promise} + */ +async function handleHop ({ connection, request, streamHandler, @@ -51,6 +70,9 @@ module.exports.handleHop = async function handleHop ({ } // TODO: Handle being an active relay + if (!destinationConnection) { + return + } // Handle the incoming HOP request by performing a STOP request const stopRequest = { @@ -63,8 +85,7 @@ module.exports.handleHop = async function handleHop ({ try { destinationStream = await stop({ connection: destinationConnection, - request: stopRequest, - circuit + request: stopRequest }) } catch (err) { return log.error(err) @@ -91,10 +112,10 @@ module.exports.handleHop = async function handleHop ({ * * @param {object} options * @param {Connection} options.connection - Connection to the relay - * @param {*} options.request + * @param {CircuitRequest} options.request * @returns {Promise} */ -module.exports.hop = async function hop ({ +async function hop ({ connection, request }) { @@ -106,14 +127,42 @@ module.exports.hop = async function hop ({ const response = await streamHandler.read() - if (response.code === CircuitPB.Status.SUCCESS) { + if (response && response.code === CircuitPB.Status.SUCCESS) { log('hop request was successful') return streamHandler.rest() } - log('hop request failed with code %d, closing stream', response.code) + log('hop request failed with code %d, closing stream', response && response.code) streamHandler.close() - throw errCode(new Error(`HOP request failed with code ${response.code}`), Errors.ERR_HOP_REQUEST_FAILED) + throw errCode(new Error(`HOP request failed with code ${response && response.code}`), Errors.ERR_HOP_REQUEST_FAILED) +} + +/** + * Performs a CAN_HOP request to a relay peer, in order to understand its capabilities. + * + * @param {object} options + * @param {Connection} options.connection - Connection to the relay + * @returns {Promise} + */ +async function canHop ({ + connection +}) { + // 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({ + type: CircuitPB.Type.CAN_HOP + }) + + const response = await streamHandler.read() + await streamHandler.close() + + if (!response || response.code !== CircuitPB.Status.SUCCESS) { + return false + } + + return true } /** @@ -122,10 +171,10 @@ module.exports.hop = async function hop ({ * @param {Object} options * @param {Connection} options.connection * @param {StreamHandler} options.streamHandler - * @param {Circuit} options.circuit + * @param {Transport} options.circuit * @private */ -module.exports.handleCanHop = function handleCanHop ({ +function handleCanHop ({ connection, streamHandler, circuit @@ -137,3 +186,10 @@ module.exports.handleCanHop = function handleCanHop ({ code: canHop ? CircuitPB.Status.SUCCESS : CircuitPB.Status.HOP_CANT_SPEAK_RELAY }) } + +module.exports = { + handleHop, + hop, + canHop, + handleCanHop +} diff --git a/src/circuit/circuit/stop.js b/src/circuit/circuit/stop.js index 77eaa1fcc2..15ded78eaf 100644 --- a/src/circuit/circuit/stop.js +++ b/src/circuit/circuit/stop.js @@ -1,23 +1,30 @@ 'use strict' +const debug = require('debug') +const log = Object.assign(debug('libp2p:circuit:stop'), { + error: debug('libp2p:circuit:stop:err') +}) + 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.error = debug('libp2p:circuit:stop:error') +/** + * @typedef {import('libp2p-interfaces/src/connection').Connection} Connection + * @typedef {import('libp2p-interfaces/src/stream-muxer/types').MuxedStream} MuxedStream + * @typedef {import('../../types').CircuitRequest} CircuitRequest + */ /** * Handles incoming STOP requests * * @private - * @param {*} options + * @param {Object} options * @param {Connection} options.connection - * @param {*} options.request - The CircuitRelay protobuf request (unencoded) + * @param {CircuitRequest} options.request - The CircuitRelay protobuf request (unencoded) * @param {StreamHandler} options.streamHandler - * @returns {Promise<*>} Resolves a duplex iterable + * @returns {Promise|void} Resolves a duplex iterable */ module.exports.handleStop = function handleStop ({ connection, @@ -44,10 +51,10 @@ module.exports.handleStop = function handleStop ({ * Creates a STOP request * * @private - * @param {*} options + * @param {Object} options * @param {Connection} options.connection - * @param {*} options.request - The CircuitRelay protobuf request (unencoded) - * @returns {Promise<*>} Resolves a duplex iterable + * @param {CircuitRequest} options.request - The CircuitRelay protobuf request (unencoded) + * @returns {Promise} Resolves a duplex iterable */ module.exports.stop = async function stop ({ connection, @@ -60,11 +67,15 @@ module.exports.stop = async function stop ({ 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() - } + if (response) { + 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) + log('stop request failed with code %d', response.code) + } else { + log('stop request was not received') + } streamHandler.close() } diff --git a/src/circuit/circuit/stream-handler.js b/src/circuit/circuit/stream-handler.js index 8b8ecf89bc..1610666b82 100644 --- a/src/circuit/circuit/stream-handler.js +++ b/src/circuit/circuit/stream-handler.js @@ -1,20 +1,31 @@ 'use strict' +const debug = require('debug') +const log = Object.assign(debug('libp2p:circuit:stream-handler'), { + error: debug('libp2p:circuit:stream-handler:err') +}) + 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.error = debug('libp2p:circuit:stream-handler:error') +/** + * @typedef {import('../../types').CircuitRequest} CircuitRequest + * @typedef {import('../../types').CircuitMessage} CircuitMessage + * @typedef {import('libp2p-interfaces/src/stream-muxer/types').MuxedStream} MuxedStream + */ +/** + * @template T + */ class StreamHandler { /** * Create a stream handler for connection * + * @class * @param {object} options - * @param {*} options.stream - A duplex iterable - * @param {number} options.maxLength - max bytes length of message + * @param {MuxedStream} options.stream - A duplex iterable + * @param {number} [options.maxLength = 4096] - max bytes length of message */ constructor ({ stream, maxLength = 4096 }) { this.stream = stream @@ -27,14 +38,14 @@ class StreamHandler { * Read and decode message * * @async - * @returns {void} + * @returns {Promise} */ 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 + return /** @type {CircuitRequest} */(value) } log('read received no value, closing stream') @@ -45,10 +56,12 @@ class StreamHandler { /** * Encode and write array of buffers * - * @param {*} msg - An unencoded CircuitRelay protobuf message + * @param {CircuitMessage} msg - An unencoded CircuitRelay protobuf message + * @returns {void} */ write (msg) { log('write message type %s', msg.type) + // @ts-ignore lp.encode expects type type 'Buffer | BufferList', not 'Uint8Array' this.shake.write(lp.encode.single(CircuitPB.encode(msg))) } @@ -62,6 +75,9 @@ class StreamHandler { return this.shake.stream } + /** + * @param {CircuitMessage} msg + */ end (msg) { this.write(msg) this.close() diff --git a/src/circuit/circuit/utils.js b/src/circuit/circuit/utils.js index be7ab35a73..80ef01c7a4 100644 --- a/src/circuit/circuit/utils.js +++ b/src/circuit/circuit/utils.js @@ -3,11 +3,18 @@ const multiaddr = require('multiaddr') const { CircuitRelay } = require('../protocol') +/** + * @typedef {import('./stream-handler')} StreamHandler + * @typedef {import('../../types').CircuitStatus} CircuitStatus + * @typedef {import('../../types').CircuitMessage} CircuitMessage + * @typedef {import('../../types').CircuitRequest} CircuitRequest + */ + /** * Write a response * * @param {StreamHandler} streamHandler - * @param {CircuitRelay.Status} status + * @param {CircuitStatus} status */ function writeResponse (streamHandler, status) { streamHandler.write({ @@ -19,12 +26,13 @@ function writeResponse (streamHandler, status) { /** * Validate incomming HOP/STOP message * - * @param {*} msg - A CircuitRelay unencoded protobuf message + * @param {CircuitRequest} msg - A CircuitRelay unencoded protobuf message * @param {StreamHandler} streamHandler */ function validateAddrs (msg, streamHandler) { + const { srcPeer, dstPeer } = /** @type {CircuitRequest} */(msg) try { - msg.dstPeer.addrs.forEach((addr) => { + dstPeer.addrs.forEach((addr) => { return multiaddr(addr) }) } catch (err) { @@ -35,7 +43,7 @@ function validateAddrs (msg, streamHandler) { } try { - msg.srcPeer.addrs.forEach((addr) => { + srcPeer.addrs.forEach((addr) => { return multiaddr(addr) }) } catch (err) { diff --git a/src/circuit/constants.js b/src/circuit/constants.js new file mode 100644 index 0000000000..b4de629c63 --- /dev/null +++ b/src/circuit/constants.js @@ -0,0 +1,12 @@ +'use strict' + +const minute = 60 * 1000 + +module.exports = { + ADVERTISE_BOOT_DELAY: 15 * minute, // Delay before HOP relay service is advertised on the network + ADVERTISE_TTL: 30 * minute, // Delay Between HOP relay service advertisements on the network + CIRCUIT_PROTO_CODE: 290, // Multicodec code + HOP_METADATA_KEY: 'hop_relay', // PeerStore metadaBook key for HOP relay service + HOP_METADATA_VALUE: 'true', // PeerStore metadaBook value for HOP relay service + RELAY_RENDEZVOUS_NS: '/libp2p/relay' // Relay HOP relay service namespace for discovery +} diff --git a/src/circuit/index.js b/src/circuit/index.js index 15746c907c..447d829ac5 100644 --- a/src/circuit/index.js +++ b/src/circuit/index.js @@ -1,187 +1,109 @@ 'use strict' -const mafmt = require('mafmt') -const multiaddr = require('multiaddr') -const PeerId = require('peer-id') -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 toConnection = require('libp2p-utils/src/stream-to-ma-conn') +const log = Object.assign(debug('libp2p:relay'), { + error: debug('libp2p:relay:err') +}) + +const { + setDelayedInterval, + clearDelayedInterval +} = require('set-delayed-interval') + +const AutoRelay = require('./auto-relay') +const { namespaceToCid } = require('./utils') +const { + ADVERTISE_BOOT_DELAY, + ADVERTISE_TTL, + RELAY_RENDEZVOUS_NS +} = require('./constants') -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') +/** + * @typedef {import('../')} Libp2p + * + * @typedef {Object} RelayAdvertiseOptions + * @property {number} [bootDelay = ADVERTISE_BOOT_DELAY] + * @property {boolean} [enabled = true] + * @property {number} [ttl = ADVERTISE_TTL] + * + * @typedef {Object} HopOptions + * @property {boolean} [enabled = false] + * @property {boolean} [active = false] + * + * @typedef {Object} AutoRelayOptions + * @property {number} [maxListeners = 2] - maximum number of relays to listen. + * @property {boolean} [enabled = false] + */ -class Circuit { +class Relay { /** - * Creates an instance of Circuit. + * Creates an instance of Relay. * * @class - * @param {object} options - * @param {Libp2p} options.libp2p - * @param {Upgrader} options.upgrader + * @param {Libp2p} libp2p */ - constructor ({ libp2p, upgrader }) { - this._dialer = libp2p.dialer - this._registrar = libp2p.registrar - this._connectionManager = libp2p.connectionManager - this._upgrader = upgrader - this._options = libp2p._config.relay + constructor (libp2p) { this._libp2p = libp2p - this.peerId = libp2p.peerId - 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) - } + this._options = { + advertise: { + bootDelay: ADVERTISE_BOOT_DELAY, + enabled: true, + ttl: ADVERTISE_TTL, + ...libp2p._config.relay.advertise + }, + ...libp2p._config.relay } - 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) + // Create autoRelay if enabled + this._autoRelay = this._options.autoRelay.enabled && new AutoRelay({ libp2p, ...this._options.autoRelay }) - const conn = await this._upgrader.upgradeInbound(maConn) - log('%s connection %s upgraded', type, maConn.remoteAddr) - this.handler && this.handler(conn) - } + this._advertiseService = this._advertiseService.bind(this) } /** - * Dial a peer over a relay + * Start Relay service. * - * @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 + * @returns {void} */ - 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._connectionManager.get(relayPeer) - if (!relayConnection) { - relayConnection = await this._dialer.connectToPeer(relayAddr, options) - disconnectOnFailure = true - } - - try { - const virtualConnection = await hop({ - connection: relayConnection, - circuit: this, - request: { - type: CircuitPB.Type.HOP, - srcPeer: { - id: this.peerId.toBytes(), - addrs: this._libp2p.multiaddrs.map(addr => addr.bytes) - }, - dstPeer: { - id: destinationPeer.toBytes(), - addrs: [multiaddr(destinationAddr).bytes] - } - } - }) - - const localAddr = relayAddr.encapsulate(`/p2p-circuit/p2p/${this.peerId.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 + start () { + // Advertise service if HOP enabled + const canHop = this._options.hop.enabled + + if (canHop && this._options.advertise.enabled) { + this._timeout = setDelayedInterval( + this._advertiseService, this._options.advertise.ttl, this._options.advertise.bootDelay + ) } } /** - * Create a listener + * Stop Relay service. * - * @param {any} options - * @param {Function} handler - * @returns {listener} + * @returns {void} */ - createListener (options, handler) { - if (typeof options === 'function') { - handler = options - options = {} - } - - // Called on successful HOP and STOP requests - this.handler = handler - - return createListener(this, options) + stop () { + clearDelayedInterval(this._timeout) } /** - * Filter check for all Multiaddrs that this transport can dial on + * Advertise hop relay service in the network. * - * @param {Array} multiaddrs - * @returns {Array} + * @returns {Promise} */ - filter (multiaddrs) { - multiaddrs = Array.isArray(multiaddrs) ? multiaddrs : [multiaddrs] - - return multiaddrs.filter((ma) => { - return mafmt.Circuit.matches(ma) - }) + async _advertiseService () { + try { + const cid = await namespaceToCid(RELAY_RENDEZVOUS_NS) + await this._libp2p.contentRouting.provide(cid) + } catch (err) { + if (err.code === 'NO_ROUTERS_AVAILABLE') { + log.error('a content router, such as a DHT, must be provided in order to advertise the relay service', err) + // Stop the advertise + this.stop() + } else { + log.error(err) + } + } } } -/** - * @type {Circuit} - */ -module.exports = withIs(Circuit, { className: 'Circuit', symbolName: '@libp2p/js-libp2p-circuit/circuit' }) +module.exports = Relay diff --git a/src/circuit/listener.js b/src/circuit/listener.js index 76870501dc..d19cca5e46 100644 --- a/src/circuit/listener.js +++ b/src/circuit/listener.js @@ -1,43 +1,36 @@ 'use strict' -const EventEmitter = require('events') +const { EventEmitter } = require('events') const multiaddr = require('multiaddr') -const debug = require('debug') -const log = debug('libp2p:circuit:listener') -log.err = debug('libp2p:circuit:error:listener') +/** + * @typedef {import('multiaddr')} Multiaddr + * @typedef {import('libp2p-interfaces/src/transport/types').Listener} Listener + */ /** - * @param {*} circuit + * @param {import('../')} libp2p * @returns {Listener} a transport listener */ -module.exports = (circuit) => { - const listener = new EventEmitter() +module.exports = (libp2p) => { const listeningAddrs = new Map() /** * Add swarm handler and listen for incoming connections * * @param {Multiaddr} addr - * @returns {void} + * @returns {Promise} */ - listener.listen = async (addr) => { + async function listen (addr) { const addrString = String(addr).split('/p2p-circuit').find(a => a !== '') - const relayConn = await circuit._dialer.connectToPeer(multiaddr(addrString)) + const relayConn = await libp2p.dial(multiaddr(addrString)) const relayedAddr = relayConn.remoteAddr.encapsulate('/p2p-circuit') listeningAddrs.set(relayConn.remotePeer.toB58String(), relayedAddr) listener.emit('listening') } - /** - * TODO: Remove the peers from our topology - * - * @returns {void} - */ - listener.close = () => {} - /** * Get fixed up multiaddrs * @@ -54,7 +47,7 @@ module.exports = (circuit) => { * * @returns {Multiaddr[]} */ - listener.getAddrs = () => { + function getAddrs () { const addrs = [] for (const addr of listeningAddrs.values()) { addrs.push(addr) @@ -62,5 +55,22 @@ module.exports = (circuit) => { return addrs } + /** @type Listener */ + const listener = Object.assign(new EventEmitter(), { + close: () => Promise.resolve(), + listen, + getAddrs + }) + + // Remove listeningAddrs when a peer disconnects + libp2p.connectionManager.on('peer:disconnect', (connection) => { + const deleted = listeningAddrs.delete(connection.remotePeer.toB58String()) + + if (deleted) { + // Announce listen addresses change + listener.emit('close') + } + }) + return listener } diff --git a/src/circuit/protocol/index.js b/src/circuit/protocol/index.js index f217cb4262..f25c1c38df 100644 --- a/src/circuit/protocol/index.js +++ b/src/circuit/protocol/index.js @@ -1,5 +1,10 @@ 'use strict' const protobuf = require('protons') + +/** + * @type {{CircuitRelay: import('../../types').CircuitMessageProto}} + */ + module.exports = protobuf(` message CircuitRelay { diff --git a/src/circuit/transport.js b/src/circuit/transport.js new file mode 100644 index 0000000000..ad29938574 --- /dev/null +++ b/src/circuit/transport.js @@ -0,0 +1,218 @@ +'use strict' + +const debug = require('debug') +const log = Object.assign(debug('libp2p:circuit'), { + error: debug('libp2p:circuit:err') +}) + +const mafmt = require('mafmt') +const multiaddr = require('multiaddr') +const PeerId = require('peer-id') +const { CircuitRelay: CircuitPB } = require('./protocol') + +const toConnection = require('libp2p-utils/src/stream-to-ma-conn') + +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 transportSymbol = Symbol.for('@libp2p/js-libp2p-circuit/circuit') + +/** + * @typedef {import('multiaddr')} Multiaddr + * @typedef {import('libp2p-interfaces/src/connection').Connection} Connection + * @typedef {import('libp2p-interfaces/src/stream-muxer/types').MuxedStream} MuxedStream + * @typedef {import('../types').CircuitRequest} CircuitRequest + */ + +class Circuit { + /** + * Creates an instance of the Circuit Transport. + * + * @class + * @param {object} options + * @param {import('../')} options.libp2p + * @param {import('../upgrader')} options.upgrader + */ + constructor ({ libp2p, upgrader }) { + this._dialer = libp2p.dialer + this._registrar = libp2p.registrar + this._connectionManager = libp2p.connectionManager + this._upgrader = upgrader + this._options = libp2p._config.relay + this._libp2p = libp2p + this.peerId = libp2p.peerId + + this._registrar.handle(multicodec, this._onProtocol.bind(this)) + } + + /** + * @param {Object} props + * @param {Connection} props.connection + * @param {MuxedStream} props.stream + */ + async _onProtocol ({ connection, stream }) { + /** @type {import('./circuit/stream-handler')} */ + const streamHandler = new StreamHandler({ stream }) + const request = await streamHandler.read() + + if (!request) { + return + } + + 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 + }) + 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 = request.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 {Promise} - 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._connectionManager.get(relayPeer) + if (!relayConnection) { + relayConnection = await this._dialer.connectToPeer(relayAddr, options) + disconnectOnFailure = true + } + + try { + const virtualConnection = await hop({ + connection: relayConnection, + request: { + type: CircuitPB.Type.HOP, + srcPeer: { + id: this.peerId.toBytes(), + addrs: this._libp2p.multiaddrs.map(addr => addr.bytes) + }, + dstPeer: { + id: destinationPeer.toBytes(), + addrs: [multiaddr(destinationAddr).bytes] + } + } + }) + + const localAddr = relayAddr.encapsulate(`/p2p-circuit/p2p/${this.peerId.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 + * @returns {import('libp2p-interfaces/src/transport/types').Listener} + */ + createListener (options, handler) { + if (typeof options === 'function') { + handler = options + options = {} + } + + // Called on successful HOP and STOP requests + this.handler = handler + + return createListener(this._libp2p) + } + + /** + * Filter check for all Multiaddrs that this transport can dial on + * + * @param {Multiaddr[]} multiaddrs + * @returns {Multiaddr[]} + */ + filter (multiaddrs) { + multiaddrs = Array.isArray(multiaddrs) ? multiaddrs : [multiaddrs] + + return multiaddrs.filter((ma) => { + return mafmt.Circuit.matches(ma) + }) + } + + get [Symbol.toStringTag] () { + return 'Circuit' + } + + /** + * Checks if the given value is a Transport instance. + * + * @param {any} other + * @returns {other is Transport} + */ + static isTransport (other) { + return Boolean(other && other[transportSymbol]) + } +} + +module.exports = Circuit diff --git a/src/circuit/utils.js b/src/circuit/utils.js new file mode 100644 index 0000000000..f75e13386a --- /dev/null +++ b/src/circuit/utils.js @@ -0,0 +1,19 @@ +'use strict' + +const CID = require('cids') +const multihashing = require('multihashing-async') + +const TextEncoder = require('ipfs-utils/src/text-encoder') + +/** + * Convert a namespace string into a cid. + * + * @param {string} namespace + * @returns {Promise} + */ +module.exports.namespaceToCid = async (namespace) => { + const bytes = new TextEncoder('utf8').encode(namespace) + const hash = await multihashing(bytes, 'sha2-256') + + return new CID(hash) +} diff --git a/src/config.js b/src/config.js index 1cc0f097b4..b609d2aad1 100644 --- a/src/config.js +++ b/src/config.js @@ -4,7 +4,9 @@ const mergeOptions = require('merge-options') const { dnsaddrResolver } = require('multiaddr/src/resolvers') const Constants = require('./constants') +const RelayConstants = require('./circuit/constants') +const { publicAddressesFirst } = require('libp2p-utils/src/address-sort') const { FaultTolerance } = require('./transport-manager') const DefaultConfig = { @@ -25,7 +27,8 @@ const DefaultConfig = { dialTimeout: Constants.DIAL_TIMEOUT, resolvers: { dnsaddr: dnsaddrResolver - } + }, + addressSorter: publicAddressesFirst }, metrics: { enabled: false @@ -34,6 +37,13 @@ const DefaultConfig = { persistence: false, threshold: 5 }, + peerRouting: { + refreshManager: { + enabled: true, + interval: 6e5, + bootDelay: 10e3 + } + }, config: { dht: { enabled: false, @@ -49,16 +59,22 @@ const DefaultConfig = { autoDial: true }, pubsub: { - enabled: true, - emitSelf: true, - signMessages: true, - strictSigning: true + enabled: true }, relay: { enabled: true, + advertise: { + bootDelay: RelayConstants.ADVERTISE_BOOT_DELAY, + enabled: false, + ttl: RelayConstants.ADVERTISE_TTL + }, hop: { enabled: false, active: false + }, + autoRelay: { + enabled: false, + maxListeners: 2 } }, transport: {} diff --git a/src/connection-manager/index.js b/src/connection-manager/index.js index 1b1d807cb5..4add4840e1 100644 --- a/src/connection-manager/index.js +++ b/src/connection-manager/index.js @@ -1,8 +1,9 @@ 'use strict' const debug = require('debug') -const log = debug('libp2p:connection-manager') -log.error = debug('libp2p:connection-manager:error') +const log = Object.assign(debug('libp2p:connection-manager'), { + error: debug('libp2p:connection-manager:err') +}) const errcode = require('err-code') const mergeOptions = require('merge-options') @@ -14,7 +15,7 @@ const { EventEmitter } = require('events') const PeerId = require('peer-id') const { - ERR_INVALID_PARAMETERS + codes: { ERR_INVALID_PARAMETERS } } = require('../errors') const defaultOptions = { @@ -31,29 +32,39 @@ const defaultOptions = { } /** - * Responsible for managing known connections. + * @typedef {import('../')} Libp2p + * @typedef {import('libp2p-interfaces/src/connection').Connection} Connection + */ + +/** + * @typedef {Object} ConnectionManagerOptions + * @property {number} [maxConnections = Infinity] - The maximum number of connections allowed. + * @property {number} [minConnections = 0] - The minimum number of connections to avoid pruning. + * @property {number} [maxData = Infinity] - The max data (in and out), per average interval to allow. + * @property {number} [maxSentData = Infinity] - The max outgoing data, per average interval to allow. + * @property {number} [maxReceivedData = Infinity] - The max incoming data, per average interval to allow. + * @property {number} [maxEventLoopDelay = Infinity] - The upper limit the event loop can take to run. + * @property {number} [pollInterval = 2000] - How often, in milliseconds, metrics and latency should be checked. + * @property {number} [movingAverageInterval = 60000] - How often, in milliseconds, to compute averages. + * @property {number} [defaultPeerValue = 1] - The value of the peer. + * @property {boolean} [autoDial = true] - Should preemptively guarantee connections are above the low watermark. + * @property {number} [autoDialInterval = 10000] - How often, in milliseconds, it should preemptively guarantee connections are above the low watermark. + */ + +/** * * @fires ConnectionManager#peer:connect Emitted when a new peer is connected. * @fires ConnectionManager#peer:disconnect Emitted when a peer is disconnected. */ class ConnectionManager extends EventEmitter { /** + * Responsible for managing known connections. + * * @class * @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 - * @param {boolean} options.autoDial - Should preemptively guarantee connections are above the low watermark. Default=true - * @param {number} options.autoDialInterval - How often, in milliseconds, it should preemptively guarantee connections are above the low watermark. Default=10000 + * @param {ConnectionManagerOptions} options */ - constructor (libp2p, options) { + constructor (libp2p, options = {}) { super() this._libp2p = libp2p @@ -66,8 +77,6 @@ class ConnectionManager extends EventEmitter { log('options: %j', this._options) - this._libp2p = libp2p - /** * Map of peer identifiers to their peer value for pruning connections. * @@ -78,7 +87,7 @@ class ConnectionManager extends EventEmitter { /** * Map of connections per peer * - * @type {Map>} + * @type {Map} */ this.connections = new Map() @@ -159,15 +168,13 @@ class ConnectionManager extends EventEmitter { * * @param {PeerId} peerId * @param {number} value - A number between 0 and 1 + * @returns {void} */ 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() - } - this._peerValues.set(peerId, value) + this._peerValues.set(peerId.toB58String(), value) } /** @@ -177,21 +184,24 @@ class ConnectionManager extends EventEmitter { * @private */ _checkMetrics () { - const movingAverages = this._libp2p.metrics.global.movingAverages - const received = movingAverages.dataReceived[this._options.movingAverageInterval].movingAverage() - this._checkMaxLimit('maxReceivedData', received) - const sent = movingAverages.dataSent[this._options.movingAverageInterval].movingAverage() - this._checkMaxLimit('maxSentData', sent) - const total = received + sent - this._checkMaxLimit('maxData', total) - log('metrics update', total) - this._timer = retimer(this._checkMetrics, this._options.pollInterval) + if (this._libp2p.metrics) { + const movingAverages = this._libp2p.metrics.global.movingAverages + const received = movingAverages.dataReceived[this._options.movingAverageInterval].movingAverage() + this._checkMaxLimit('maxReceivedData', received) + const sent = movingAverages.dataSent[this._options.movingAverageInterval].movingAverage() + this._checkMaxLimit('maxSentData', sent) + const total = received + sent + this._checkMaxLimit('maxData', total) + log('metrics update', total) + this._timer = retimer(this._checkMetrics, this._options.pollInterval) + } } /** * Tracks the incoming connection and check the connection limit * * @param {Connection} connection + * @returns {void} */ onConnect (connection) { const peerId = connection.remotePeer @@ -218,6 +228,7 @@ class ConnectionManager extends EventEmitter { * Removes the connection from tracking * * @param {Connection} connection + * @returns {void} */ onDisconnect (connection) { const peerId = connection.remotePeer.toB58String() @@ -237,7 +248,7 @@ class ConnectionManager extends EventEmitter { * Get a connection with a peer. * * @param {PeerId} peerId - * @returns {Connection} + * @returns {Connection|null} */ get (peerId) { const connections = this.getAll(peerId) @@ -251,7 +262,7 @@ class ConnectionManager extends EventEmitter { * Get all open connections with a peer. * * @param {PeerId} peerId - * @returns {Array} + * @returns {Connection[]} */ getAll (peerId) { if (!PeerId.isPeerId(peerId)) { diff --git a/src/connection-manager/latency-monitor.js b/src/connection-manager/latency-monitor.js index db299890b4..c9301ee142 100644 --- a/src/connection-manager/latency-monitor.js +++ b/src/connection-manager/latency-monitor.js @@ -1,3 +1,4 @@ +// @ts-nocheck 'use strict' /** @@ -6,7 +7,7 @@ /* global window */ const globalThis = require('ipfs-utils/src/globalthis') -const EventEmitter = require('events') +const { EventEmitter } = require('events') const VisibilityChangeEmitter = require('./visibility-change-emitter') const debug = require('debug')('latency-monitor:LatencyMonitor') @@ -17,6 +18,12 @@ const debug = require('debug')('latency-monitor:LatencyMonitor') * @property {number} maxMS What was the max time for a cb to be called * @property {number} avgMs What was the average time for a cb to be called * @property {number} lengthMs How long this interval was in ms + * + * @typedef {Object} LatencyMonitorOptions + * @property {number} [latencyCheckIntervalMs=500] - How often to add a latency check event (ms) + * @property {number} [dataEmitIntervalMs=5000] - How often to summarize latency check events. null or 0 disables event firing + * @property {Function} [asyncTestFn] - What cb-style async function to use + * @property {number} [latencyRandomPercentage=5] - What percent (+/-) of latencyCheckIntervalMs should we randomly use? This helps avoid alignment to other events. */ /** @@ -24,6 +31,8 @@ const debug = require('debug')('latency-monitor:LatencyMonitor') * the asyncTestFn and timing how long it takes the callback to be called. It can also periodically emit stats about this. * This can be disabled and stats can be pulled via setting dataEmitIntervalMs = 0. * + * @extends {EventEmitter} + * * The default implementation is an event loop latency monitor. This works by firing periodic events into the event loop * and timing how long it takes to get back. * @@ -37,11 +46,8 @@ const debug = require('debug')('latency-monitor:LatencyMonitor') */ class LatencyMonitor extends EventEmitter { /** - * @param {object} [options] - * @param {number} [options.latencyCheckIntervalMs=500] - How often to add a latency check event (ms) - * @param {number} [options.dataEmitIntervalMs=5000] - How often to summarize latency check events. null or 0 disables event firing - * @param {Function} [options.asyncTestFn] - What cb-style async function to use - * @param {number} [options.latencyRandomPercentage=5] - What percent (+/-) of latencyCheckIntervalMs should we randomly use? This helps avoid alignment to other events. + * @class + * @param {LatencyMonitorOptions} [options] */ constructor ({ latencyCheckIntervalMs, dataEmitIntervalMs, asyncTestFn, latencyRandomPercentage } = {}) { super() @@ -91,6 +97,7 @@ class LatencyMonitor extends EventEmitter { // See: http://stackoverflow.com/questions/6032429/chrome-timeouts-interval-suspended-in-background-tabs if (isBrowser()) { that._visibilityChangeEmitter = new VisibilityChangeEmitter() + that._visibilityChangeEmitter.on('visibilityChange', (pageInFocus) => { if (pageInFocus) { that._startTimers() diff --git a/src/connection-manager/visibility-change-emitter.js b/src/connection-manager/visibility-change-emitter.js index baece0ec17..ebe5e7d076 100644 --- a/src/connection-manager/visibility-change-emitter.js +++ b/src/connection-manager/visibility-change-emitter.js @@ -1,10 +1,12 @@ +// @ts-nocheck /* global document */ /** * This code is based on `latency-monitor` (https://github.com/mlucool/latency-monitor) by `mlucool` (https://github.com/mlucool), available under Apache License 2.0 (https://github.com/mlucool/latency-monitor/blob/master/LICENSE) */ 'use strict' -const EventEmitter = require('events') + +const { EventEmitter } = require('events') const debug = require('debug')('latency-monitor:VisibilityChangeEmitter') @@ -29,12 +31,12 @@ const debug = require('debug')('latency-monitor:VisibilityChangeEmitter') * }); * // To access the visibility state directly, call: * console.log('Am I focused now? ' + myVisibilityEmitter.isVisible()); - * - * @class VisibilityChangeEmitter */ -module.exports = class VisibilityChangeEmitter extends EventEmitter { +class VisibilityChangeEmitter extends EventEmitter { /** * Creates a VisibilityChangeEmitter + * + * @class */ constructor () { super() @@ -119,3 +121,5 @@ module.exports = class VisibilityChangeEmitter extends EventEmitter { this.emit('visibilityChange', visible) } } + +module.exports = VisibilityChangeEmitter diff --git a/src/content-routing.js b/src/content-routing.js index d7e160e79e..cba4a38bce 100644 --- a/src/content-routing.js +++ b/src/content-routing.js @@ -6,111 +6,130 @@ const { messages, codes } = require('./errors') const all = require('it-all') const pAny = require('p-any') -module.exports = (node) => { - const routers = node._modules.contentRouting || [] - const dht = node._dht +/** + * @typedef {import('peer-id')} PeerId + * @typedef {import('multiaddr')} Multiaddr + * @typedef {import('cids')} CID + */ + +/** + * @typedef {Object} GetData + * @property {PeerId} from + * @property {Uint8Array} val + */ + +class ContentRouting { + /** + * @class + * @param {import('./')} libp2p + */ + constructor (libp2p) { + this.libp2p = libp2p + this.routers = libp2p._modules.contentRouting || [] + this.dht = libp2p._dht + + // If we have the dht, make it first + if (this.dht) { + this.routers.unshift(this.dht) + } + } + + /** + * Iterates over all content routers in series to find providers of the given key. + * 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.timeout] - How long the query should run + * @param {number} [options.maxNumProviders] - maximum number of providers to find + * @returns {AsyncIterable<{ id: PeerId, multiaddrs: Multiaddr[] }>} + */ + async * findProviders (key, options) { + if (!this.routers.length) { + throw errCode(new Error('No content this.routers available'), 'NO_ROUTERS_AVAILABLE') + } + + const result = await pAny( + this.routers.map(async (router) => { + const provs = await all(router.findProviders(key, options)) + + if (!provs || !provs.length) { + throw errCode(new Error('not found'), 'NOT_FOUND') + } + return provs + }) + ) - // If we have the dht, make it first - if (dht) { - routers.unshift(dht) + for (const peer of result) { + yield peer + } } - return { - /** - * Iterates over all content routers in series to find providers of the given key. - * 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.timeout] - How long the query should run - * @param {number} [options.maxNumProviders] - maximum number of providers to find - * @returns {AsyncIterable<{ id: PeerId, multiaddrs: Multiaddr[] }>} - */ - async * findProviders (key, options) { - if (!routers.length) { - throw errCode(new Error('No content routers available'), 'NO_ROUTERS_AVAILABLE') - } - - const result = await pAny( - routers.map(async (router) => { - const provs = await all(router.findProviders(key, options)) - - if (!provs || !provs.length) { - throw errCode(new Error('not found'), 'NOT_FOUND') - } - return provs - }) - ) - - for (const peer of result) { - yield peer - } - }, - - /** - * 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 - * @returns {Promise} - */ - async provide (key) { // eslint-disable-line require-await - if (!routers.length) { - 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 {Uint8Array} key - * @param {Uint8Array} 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 {Uint8Array} key - * @param {Object} [options] - get options - * @param {number} [options.timeout] - optional timeout (default: 60000) - * @returns {Promise<{from: PeerId, val: Uint8Array}>} - */ - 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 {Uint8Array} 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) - } - - return dht.getMany(key, nVals, options) + /** + * 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 + * @returns {Promise} + */ + async provide (key) { + if (!this.routers.length) { + throw errCode(new Error('No content routers available'), 'NO_ROUTERS_AVAILABLE') } + + await Promise.all(this.routers.map((router) => router.provide(key))) + } + + /** + * Store the given key/value pair in the DHT. + * + * @param {Uint8Array} key + * @param {Uint8Array} 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 (!this.libp2p.isStarted() || !this.dht.isStarted) { + throw errCode(new Error(messages.NOT_STARTED_YET), codes.DHT_NOT_STARTED) + } + + return this.dht.put(key, value, options) + } + + /** + * Get the value to the given key. + * Times out after 1 minute by default. + * + * @param {Uint8Array} key + * @param {Object} [options] - get options + * @param {number} [options.timeout] - optional timeout (default: 60000) + * @returns {Promise} + */ + get (key, options) { + if (!this.libp2p.isStarted() || !this.dht.isStarted) { + throw errCode(new Error(messages.NOT_STARTED_YET), codes.DHT_NOT_STARTED) + } + + return this.dht.get(key, options) + } + + /** + * Get the `n` values to the given key without sorting. + * + * @param {Uint8Array} 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 (!this.libp2p.isStarted() || !this.dht.isStarted) { + throw errCode(new Error(messages.NOT_STARTED_YET), codes.DHT_NOT_STARTED) + } + + return this.dht.getMany(key, nVals, options) } } + +module.exports = ContentRouting diff --git a/src/dialer/dial-request.js b/src/dialer/dial-request.js index dc427e1bbf..62bab31be6 100644 --- a/src/dialer/dial-request.js +++ b/src/dialer/dial-request.js @@ -1,14 +1,27 @@ 'use strict' -const AbortController = require('abort-controller') -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 AbortController = require('abort-controller').default +const anySignal = require('any-signal') const FIFO = require('p-fifo') const pAny = require('p-any') +/** + * @typedef {import('libp2p-interfaces/src/connection').Connection} Connection + * @typedef {import('./')} Dialer + * @typedef {import('multiaddr')} Multiaddr + */ + +/** + * @typedef {Object} DialOptions + * @property {AbortSignal} signal + * + * @typedef {Object} DialRequestOptions + * @property {Multiaddr[]} addrs + * @property {(m: Multiaddr, options: DialOptions) => Promise} dialAction + * @property {Dialer} dialer + */ + class DialRequest { /** * Manages running the `dialAction` on multiple provided `addrs` in parallel @@ -17,10 +30,8 @@ class DialRequest { * 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 - * @param {Dialer} options.dialer + * @class + * @param {DialRequestOptions} options */ constructor ({ addrs, @@ -34,11 +45,11 @@ class DialRequest { /** * @async - * @param {object} options - * @param {AbortSignal} options.signal - An AbortController signal - * @returns {Connection} + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An AbortController signal + * @returns {Promise} */ - async run (options) { + async run (options = {}) { const tokens = this.dialer.getTokens(this.addrs.length) // If no tokens are available, throw if (tokens.length < 1) { @@ -78,4 +89,4 @@ class DialRequest { } } -module.exports.DialRequest = DialRequest +module.exports = DialRequest diff --git a/src/dialer/index.js b/src/dialer/index.js index 49d77e467d..09ae2627ad 100644 --- a/src/dialer/index.js +++ b/src/dialer/index.js @@ -1,14 +1,16 @@ 'use strict' -const multiaddr = require('multiaddr') +const debug = require('debug') +const log = Object.assign(debug('libp2p:dialer'), { + error: debug('libp2p:dialer:err') +}) const errCode = require('err-code') +const multiaddr = require('multiaddr') const TimeoutController = require('timeout-abort-controller') const anySignal = require('any-signal') -const debug = require('debug') -const log = debug('libp2p:dialer') -log.error = debug('libp2p:dialer:error') -const { DialRequest } = require('./dial-request') +const DialRequest = require('./dial-request') +const { publicAddressesFirst } = require('libp2p-utils/src/address-sort') const getPeer = require('../get-peer') const { codes } = require('../errors') @@ -18,20 +20,49 @@ const { MAX_PER_PEER_DIALS } = require('../constants') +/** + * @typedef {import('libp2p-interfaces/src/connection').Connection} Connection + * @typedef {import('multiaddr')} Multiaddr + * @typedef {import('peer-id')} PeerId + * @typedef {import('../peer-store')} PeerStore + * @typedef {import('../peer-store/address-book').Address} Address + * @typedef {import('../transport-manager')} TransportManager + */ + +/** + * @typedef {Object} DialerProperties + * @property {PeerStore} peerStore + * @property {TransportManager} transportManager + * + * @typedef {(addr:Multiaddr) => Promise} Resolver + * + * @typedef {Object} DialerOptions + * @property {(addresses: Address[]) => Address[]} [options.addressSorter = publicAddressesFirst] - Sort the known addresses of a peer before trying to dial. + * @property {number} [concurrency = MAX_PARALLEL_DIALS] - Number of max concurrent dials. + * @property {number} [perPeerLimit = MAX_PER_PEER_DIALS] - Number of max concurrent dials per peer. + * @property {number} [timeout = DIAL_TIMEOUT] - How long a dial attempt is allowed to take. + * @property {Record} [resolvers = {}] - multiaddr resolvers to use when dialing + * + * @typedef DialTarget + * @property {string} id + * @property {Multiaddr[]} addrs + * + * @typedef PendingDial + * @property {DialRequest} dialRequest + * @property {TimeoutController} controller + * @property {Promise} promise + * @property {function():void} destroy + */ + class Dialer { /** * @class - * @param {object} options - * @param {TransportManager} options.transportManager - * @param {Peerstore} options.peerStore - * @param {number} [options.concurrency = MAX_PARALLEL_DIALS] - Number of max concurrent dials. - * @param {number} [options.perPeerLimit = MAX_PER_PEER_DIALS] - Number of max concurrent dials per peer. - * @param {number} [options.timeout = DIAL_TIMEOUT] - How long a dial attempt is allowed to take. - * @param {object} [options.resolvers = {}] - multiaddr resolvers to use when dialing + * @param {DialerProperties & DialerOptions} options */ constructor ({ transportManager, peerStore, + addressSorter = publicAddressesFirst, concurrency = MAX_PARALLEL_DIALS, timeout = DIAL_TIMEOUT, perPeerLimit = MAX_PER_PEER_DIALS, @@ -39,6 +70,7 @@ class Dialer { }) { this.transportManager = transportManager this.peerStore = peerStore + this.addressSorter = addressSorter this.concurrency = concurrency this.timeout = timeout this.perPeerLimit = perPeerLimit @@ -98,12 +130,6 @@ class Dialer { } } - /** - * @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. @@ -120,7 +146,7 @@ class Dialer { this.peerStore.addressBook.add(id, multiaddrs) } - let knownAddrs = this.peerStore.addressBook.getMultiaddrsForPeer(id) || [] + let knownAddrs = this.peerStore.addressBook.getMultiaddrsForPeer(id, this.addressSorter) || [] // If received a multiaddr to dial, it should be the first to use // But, if we know other multiaddrs for the peer, we should try them too. @@ -141,14 +167,6 @@ class Dialer { } } - /** - * @typedef PendingDial - * @property {DialRequest} dialRequest - * @property {TimeoutController} controller - * @property {Promise} promise - * @property {function():void} destroy - */ - /** * Creates a PendingDial that wraps the underlying DialRequest * @@ -158,7 +176,7 @@ class Dialer { * @param {AbortSignal} [options.signal] - An AbortController signal * @returns {PendingDial} */ - _createPendingDial (dialTarget, options) { + _createPendingDial (dialTarget, options = {}) { const dialAction = (addr, options) => { if (options.signal.aborted) throw errCode(new Error('already aborted'), codes.ERR_ALREADY_ABORTED) return this.transportManager.dial(addr, options) @@ -207,7 +225,7 @@ class Dialer { * Resolve multiaddr recursively. * * @param {Multiaddr} ma - * @returns {Promise>} + * @returns {Promise} */ async _resolve (ma) { // TODO: recursive logic should live in multiaddr once dns4/dns6 support is in place @@ -224,19 +242,20 @@ class Dialer { return this._resolve(nm) })) - return recursiveMultiaddrs.flat().reduce((array, newM) => { + const addrs = recursiveMultiaddrs.flat() + return addrs.reduce((array, newM) => { if (!array.find(m => m.equals(newM))) { array.push(newM) } return array - }, []) // Unique addresses + }, /** @type {Multiaddr[]} */([])) } /** * Resolve a given multiaddr. If this fails, an empty array will be returned * * @param {Multiaddr} ma - * @returns {Promise>} + * @returns {Promise} */ async _resolveRecord (ma) { try { diff --git a/src/get-peer.js b/src/get-peer.js index bc36b04cc9..807c333384 100644 --- a/src/get-peer.js +++ b/src/get-peer.js @@ -6,12 +6,16 @@ const errCode = require('err-code') const { codes } = require('./errors') +/** + * @typedef {import('multiaddr')} Multiaddr + */ + /** * Converts the given `peer` to a `Peer` object. * If a multiaddr is received, the addressBook is updated. * * @param {PeerId|Multiaddr|string} peer - * @returns {{ id: PeerId, multiaddrs: Array }} + * @returns {{ id: PeerId, multiaddrs: Multiaddr[]|undefined }} */ function getPeer (peer) { if (typeof peer === 'string') { diff --git a/src/identify/consts.js b/src/identify/consts.js index 58ec077faa..1f697e5dc5 100644 --- a/src/identify/consts.js +++ b/src/identify/consts.js @@ -1,5 +1,6 @@ 'use strict' +// @ts-ignore file not listed within the file list of projects const libp2pVersion = require('../../package.json').version module.exports.PROTOCOL_VERSION = 'ipfs/0.1.0' diff --git a/src/identify/index.js b/src/identify/index.js index f42a8b6f94..6a21342993 100644 --- a/src/identify/index.js +++ b/src/identify/index.js @@ -1,13 +1,13 @@ 'use strict' const debug = require('debug') -const log = debug('libp2p:identify') -log.error = debug('libp2p:identify:error') - +const log = Object.assign(debug('libp2p:identify'), { + error: debug('libp2p:identify:err') +}) const errCode = require('err-code') const pb = require('it-protocol-buffers') const lp = require('it-length-prefixed') -const pipe = require('it-pipe') +const { pipe } = require('it-pipe') const { collect, take, consume } = require('streaming-iterables') const uint8ArrayFromString = require('uint8arrays/from-string') @@ -29,72 +29,55 @@ const { const { codes } = require('../errors') -class IdentifyService { - /** - * Takes the `addr` and converts it to a Multiaddr if possible - * - * @param {Uint8Array | string} addr - * @returns {Multiaddr|null} - */ - static getCleanMultiaddr (addr) { - if (addr && addr.length > 0) { - try { - return multiaddr(addr) - } catch (_) { - return null - } - } - return null - } +/** + * @typedef {import('libp2p-interfaces/src/connection').Connection} Connection + * @typedef {import('libp2p-interfaces/src/stream-muxer/types').MuxedStream} MuxedStream + */ +class IdentifyService { /** * @class - * @param {object} options - * @param {Libp2p} options.libp2p - * @param {Map} options.protocols - A reference to the protocols we support + * @param {Object} options + * @param {import('../')} options.libp2p */ - constructor ({ libp2p, protocols }) { - /** - * @property {PeerStore} - */ + constructor ({ libp2p }) { + this._libp2p = libp2p this.peerStore = libp2p.peerStore - - /** - * @property {ConnectionManager} - */ this.connectionManager = libp2p.connectionManager + this.peerId = libp2p.peerId - this.connectionManager.on('peer:connect', (connection) => { - const peerId = connection.remotePeer + this.handleMessage = this.handleMessage.bind(this) - this.identify(connection, peerId).catch(log.error) + // When a new connection happens, trigger identify + this.connectionManager.on('peer:connect', (connection) => { + this.identify(connection).catch(log.error) }) - /** - * @property {PeerId} - */ - this.peerId = libp2p.peerId - - /** - * @property {AddressManager} - */ - this._libp2p = libp2p - - this._protocols = protocols + // When self multiaddrs change, trigger identify-push + this.peerStore.on('change:multiaddrs', ({ peerId }) => { + if (peerId.toString() === this.peerId.toString()) { + this.pushToPeerStore() + } + }) - this.handleMessage = this.handleMessage.bind(this) + // When self protocols change, trigger identify-push + this.peerStore.on('change:protocols', ({ peerId }) => { + if (peerId.toString() === this.peerId.toString()) { + this.pushToPeerStore() + } + }) } /** * Send an Identify Push update to the list of connections * - * @param {Array} connections - * @returns {Promise} + * @param {Connection[]} connections + * @returns {Promise} */ async push (connections) { - const signedPeerRecord = await this._getSelfPeerRecord() + const signedPeerRecord = await this.peerStore.addressBook.getRawEnvelope(this.peerId) const listenAddrs = this._libp2p.multiaddrs.map((ma) => ma.bytes) - const protocols = Array.from(this._protocols.keys()) + const protocols = this.peerStore.protoBook.get(this.peerId) || [] const pushes = connections.map(async connection => { try { @@ -122,12 +105,17 @@ class IdentifyService { /** * Calls `push` for all peers in the `peerStore` that are connected * - * @param {PeerStore} peerStore + * @returns {void} */ - pushToPeerStore (peerStore) { + pushToPeerStore () { + // Do not try to push if libp2p node is not running + if (!this._libp2p.isStarted()) { + return + } + const connections = [] let connection - for (const peer of peerStore.peers.values()) { + for (const peer of this.peerStore.peers.values()) { if (peer.protocols.includes(MULTICODEC_IDENTIFY_PUSH) && (connection = this.connectionManager.get(peer.id))) { connections.push(connection) } @@ -211,11 +199,11 @@ class IdentifyService { /** * A handler to register with Libp2p to process identify messages. * - * @param {object} options - * @param {string} options.protocol - * @param {*} options.stream + * @param {Object} options * @param {Connection} options.connection - * @returns {Promise} + * @param {MuxedStream} options.stream + * @param {string} options.protocol + * @returns {Promise|undefined} */ handleMessage ({ connection, stream, protocol }) { switch (protocol) { @@ -233,9 +221,10 @@ class IdentifyService { * to the requesting peer over the given `connection` * * @private - * @param {object} options - * @param {*} options.stream + * @param {Object} options + * @param {MuxedStream} options.stream * @param {Connection} options.connection + * @returns {Promise} */ async _handleIdentify ({ connection, stream }) { let publicKey = new Uint8Array(0) @@ -243,7 +232,8 @@ class IdentifyService { publicKey = this.peerId.pubKey.bytes } - const signedPeerRecord = await this._getSelfPeerRecord() + const signedPeerRecord = await this.peerStore.addressBook.getRawEnvelope(this.peerId) + const protocols = this.peerStore.protoBook.get(this.peerId) || [] const message = Message.encode({ protocolVersion: PROTOCOL_VERSION, @@ -252,7 +242,7 @@ class IdentifyService { listenAddrs: this._libp2p.multiaddrs.map((ma) => ma.bytes), signedPeerRecord, observedAddr: connection.remoteAddr.bytes, - protocols: Array.from(this._protocols.keys()) + protocols }) try { @@ -272,8 +262,9 @@ class IdentifyService { * * @private * @param {object} options - * @param {*} options.stream + * @param {MuxedStream} options.stream * @param {Connection} options.connection + * @returns {Promise} */ async _handlePush ({ connection, stream }) { let message @@ -315,42 +306,34 @@ class IdentifyService { } /** - * Get self signed peer record raw envelope. + * Takes the `addr` and converts it to a Multiaddr if possible * - * @returns {Uint8Array} + * @param {Uint8Array | string} addr + * @returns {multiaddr|null} */ - async _getSelfPeerRecord () { - const selfSignedPeerRecord = this.peerStore.addressBook.getRawEnvelope(this.peerId) - - // TODO: support invalidation when dynamic multiaddrs are supported - if (selfSignedPeerRecord) { - return selfSignedPeerRecord - } - - try { - const peerRecord = new PeerRecord({ - peerId: this.peerId, - multiaddrs: this._libp2p.multiaddrs - }) - const envelope = await Envelope.seal(peerRecord, this.peerId) - this.peerStore.addressBook.consumePeerRecord(envelope) - - return this.peerStore.addressBook.getRawEnvelope(this.peerId) - } catch (err) { - log.error('failed to get self peer record') + static getCleanMultiaddr (addr) { + if (addr && addr.length > 0) { + try { + return multiaddr(addr) + } catch (_) { + return null + } } return null } } -module.exports.IdentifyService = IdentifyService /** * The protocols the IdentifyService supports * * @property multicodecs */ -module.exports.multicodecs = { +const multicodecs = { IDENTIFY: MULTICODEC_IDENTIFY, IDENTIFY_PUSH: MULTICODEC_IDENTIFY_PUSH } -module.exports.Message = Message + +IdentifyService.multicodecs = multicodecs +IdentifyService.Messsage = Message + +module.exports = IdentifyService diff --git a/src/index.js b/src/index.js index 85547f4702..1bb3b87f89 100644 --- a/src/index.js +++ b/src/index.js @@ -1,23 +1,25 @@ 'use strict' -const { EventEmitter } = require('events') const debug = require('debug') +const log = Object.assign(debug('libp2p'), { + error: debug('libp2p:err') +}) +const { EventEmitter } = require('events') const globalThis = require('ipfs-utils/src/globalthis') -const log = debug('libp2p') -log.error = debug('libp2p:error') const errCode = require('err-code') const PeerId = require('peer-id') -const peerRouting = require('./peer-routing') -const contentRouting = require('./content-routing') +const PeerRouting = require('./peer-routing') +const ContentRouting = require('./content-routing') const getPeer = require('./get-peer') const { validate: validateConfig } = require('./config') const { codes, messages } = require('./errors') const AddressManager = require('./address-manager') const ConnectionManager = require('./connection-manager') -const Circuit = require('./circuit') +const Circuit = require('./circuit/transport') +const Relay = require('./circuit') const Dialer = require('./dialer') const Keychain = require('./keychain') const Metrics = require('./metrics') @@ -28,22 +30,95 @@ const PubsubAdapter = require('./pubsub-adapter') const PersistentPeerStore = require('./peer-store/persistent') const Registrar = require('./registrar') const ping = require('./ping') -const { - IdentifyService, - multicodecs: IDENTIFY_PROTOCOLS -} = require('./identify') +const IdentifyService = require('./identify') +const IDENTIFY_PROTOCOLS = IdentifyService.multicodecs /** + * @typedef {import('multiaddr')} Multiaddr + * @typedef {import('libp2p-interfaces/src/connection').Connection} Connection + * @typedef {import('libp2p-interfaces/src/stream-muxer/types').MuxedStream} MuxedStream + * @typedef {import('libp2p-interfaces/src/transport/types').TransportFactory} TransportFactory + * @typedef {import('libp2p-interfaces/src/stream-muxer/types').MuxerFactory} MuxerFactory + * @typedef {import('libp2p-interfaces/src/crypto/types').Crypto} Crypto + * @typedef {import('libp2p-interfaces/src/pubsub')} Pubsub + */ + +/** + * @typedef {Object} PeerStoreOptions + * @property {boolean} persistence + * + * @typedef {Object} PeerDiscoveryOptions + * @property {boolean} autoDial + * + * @typedef {Object} RelayOptions + * @property {boolean} enabled + * @property {import('./circuit').RelayAdvertiseOptions} advertise + * @property {import('./circuit').HopOptions} hop + * @property {import('./circuit').AutoRelayOptions} autoRelay + * + * @typedef {Object} Libp2pConfig + * @property {Object} [dht] dht module options + * @property {PeerDiscoveryOptions} [peerDiscovery] + * @property {Pubsub} [pubsub] pubsub module options + * @property {RelayOptions} [relay] + * @property {Record} [transport] transport options indexed by transport key + * + * @typedef {Object} Libp2pModules + * @property {TransportFactory[]} transport + * @property {MuxerFactory[]} streamMuxer + * @property {Crypto[]} connEncryption + * + * @typedef {Object} Libp2pOptions + * @property {Libp2pModules} modules libp2p modules to use + * @property {import('./address-manager').AddressManagerOptions} [addresses] + * @property {import('./connection-manager').ConnectionManagerOptions} [connectionManager] + * @property {import('./dialer').DialerOptions} [dialer] + * @property {import('./metrics').MetricsOptions} [metrics] + * @property {Object} [keychain] + * @property {import('./transport-manager').TransportManagerOptions} [transportManager] + * @property {PeerStoreOptions & import('./peer-store/persistent').PersistentPeerStoreOptions} [peerStore] + * @property {Libp2pConfig} [config] + * @property {PeerId} peerId + * + * @typedef {Object} CreateOptions + * @property {PeerId} peerId + * + * @extends {EventEmitter} * @fires Libp2p#error Emitted when an error occurs * @fires Libp2p#peer:discovery Emitted when a peer is discovered */ class Libp2p extends EventEmitter { + /** + * Like `new Libp2p(options)` except it will create a `PeerId` + * instance if one is not provided in options. + * + * @param {Libp2pOptions & CreateOptions} options - Libp2p configuration options + * @returns {Promise} + */ + static async create (options) { + if (options.peerId) { + return new Libp2p(options) + } + + const peerId = await PeerId.create() + + options.peerId = peerId + return new Libp2p(options) + } + + /** + * Libp2p node. + * + * @class + * @param {Libp2pOptions} _options + */ constructor (_options) { super() // validateConfig will ensure the config is correct, // and add default values where appropriate this._options = validateConfig(_options) + /** @type {PeerId} */ this.peerId = this._options.peerId this.datastore = this._options.datastore @@ -135,7 +210,8 @@ class Libp2p extends EventEmitter { concurrency: this._options.dialer.maxParallelDials, perPeerLimit: this._options.dialer.maxDialsPerPeer, timeout: this._options.dialer.dialTimeout, - resolvers: this._options.dialer.resolvers + resolvers: this._options.dialer.resolvers, + addressSorter: this._options.dialer.addressSorter }) this._modules.transport.forEach((Transport) => { @@ -145,7 +221,9 @@ class Libp2p extends EventEmitter { }) if (this._config.relay.enabled) { + // @ts-ignore Circuit prototype this.transportManager.add(Circuit.prototype[Symbol.toStringTag], Circuit) + this.relay = new Relay(this) } // Attach stream multiplexers @@ -156,10 +234,7 @@ class Libp2p extends EventEmitter { }) // Add the identify service since we can multiplex - this.identifyService = new IdentifyService({ - libp2p: this, - protocols: this.upgrader.protocols - }) + this.identifyService = new IdentifyService({ libp2p: this }) this.handle(Object.values(IDENTIFY_PROTOCOLS), this.identifyService.handleMessage) } @@ -188,13 +263,14 @@ class Libp2p extends EventEmitter { if (this._modules.pubsub) { const Pubsub = this._modules.pubsub // using pubsub adapter with *DEPRECATED* handlers functionality + /** @type {Pubsub} */ this.pubsub = PubsubAdapter(Pubsub, this, this._config.pubsub) } // Attach remaining APIs // peer and content routing will automatically get modules from _modules and _dht - this.peerRouting = peerRouting(this) - this.contentRouting = contentRouting(this) + this.peerRouting = new PeerRouting(this) + this.contentRouting = new ContentRouting(this) // Mount default protocols ping.mount(this) @@ -208,13 +284,16 @@ class Libp2p extends EventEmitter { * * @param {string} eventName * @param {...any} args - * @returns {void} + * @returns {boolean} */ emit (eventName, ...args) { + // TODO: do we still need this? + // @ts-ignore _events does not exist in libp2p if (eventName === 'error' && !this._events.error) { - log.error(...args) + log.error(args) + return false } else { - super.emit(eventName, ...args) + return super.emit(eventName, ...args) } } @@ -242,12 +321,17 @@ class Libp2p extends EventEmitter { * Stop the libp2p node by closing its listeners and open connections * * @async - * @returns {void} + * @returns {Promise} */ async stop () { log('libp2p is stopping') try { + this._isStarted = false + + this.relay && this.relay.stop() + this.peerRouting.stop() + for (const service of this._discovery.values()) { service.removeListener('peer', this._onDiscoveryPeer) } @@ -275,7 +359,6 @@ class Libp2p extends EventEmitter { this.emit('error', err) } } - this._isStarted = false log('libp2p has stopped') } @@ -284,9 +367,13 @@ class Libp2p extends EventEmitter { * Imports the private key as 'self', if needed. * * @async - * @returns {void} + * @returns {Promise} */ async loadKeychain () { + if (!this.keychain) { + return + } + try { await this.keychain.findKeyByName('self') } catch (err) { @@ -313,12 +400,12 @@ class Libp2p extends EventEmitter { * peer will be added to the nodes `peerStore` * * @param {PeerId|Multiaddr|string} peer - The peer to dial - * @param {object} options + * @param {object} [options] * @param {AbortSignal} [options.signal] * @returns {Promise} */ dial (peer, options) { - return this.dialProtocol(peer, null, options) + return this.dialProtocol(peer, [], options) } /** @@ -329,7 +416,7 @@ class Libp2p extends EventEmitter { * @async * @param {PeerId|Multiaddr|string} peer - The peer to dial * @param {string[]|string} protocols - * @param {object} options + * @param {object} [options] * @param {AbortSignal} [options.signal] * @returns {Promise} */ @@ -344,7 +431,7 @@ class Libp2p extends EventEmitter { } // If a protocol was provided, create a new stream - if (protocols) { + if (protocols && protocols.length) { return connection.newStream(protocols) } @@ -356,34 +443,24 @@ class Libp2p extends EventEmitter { * by transports to listen with the announce addresses. * Duplicated addresses and noAnnounce addresses are filtered out. * - * @returns {Array} + * @returns {Multiaddr[]} */ get multiaddrs () { - // Filter noAnnounce multiaddrs - const filterMa = this.addressManager.getNoAnnounceAddrs() - - // Create advertising list - return this.transportManager.getAddrs() - .concat(this.addressManager.getAnnounceAddrs()) - .filter((ma, index, array) => { - // Filter out if repeated - if (array.findIndex((otherMa) => otherMa.equals(ma)) !== index) { - return false - } + const announceAddrs = this.addressManager.getAnnounceAddrs() + if (announceAddrs.length) { + return announceAddrs + } - // Filter out if in noAnnounceMultiaddrs - if (filterMa.find((fm) => fm.equals(ma))) { - return false - } + const announceFilter = this._options.addresses.announceFilter || ((multiaddrs) => multiaddrs) - return true - }) + // Create advertising list + return announceFilter(this.transportManager.getAddrs()) } /** * Disconnects all connections to the given `peer` * - * @param {PeerId|multiaddr|string} peer - the peer to close connections to + * @param {PeerId|Multiaddr|string} peer - the peer to close connections to * @returns {Promise} */ async hangUp (peer) { @@ -423,7 +500,7 @@ class Libp2p extends EventEmitter { * Registers the `handler` for each protocol * * @param {string[]|string} protocols - * @param {function({ connection:*, stream:*, protocol:string })} handler + * @param {({ connection: Connection, stream: MuxedStream, protocol: string }) => void} handler */ handle (protocols, handler) { protocols = Array.isArray(protocols) ? protocols : [protocols] @@ -431,10 +508,8 @@ class Libp2p extends EventEmitter { this.upgrader.protocols.set(protocol, handler) }) - // Only push if libp2p is running - if (this.isStarted() && this.identifyService) { - this.identifyService.pushToPeerStore(this.peerStore) - } + // Add new protocols to self protocols in the Protobook + this.peerStore.protoBook.add(this.peerId, protocols) } /** @@ -449,15 +524,14 @@ class Libp2p extends EventEmitter { this.upgrader.protocols.delete(protocol) }) - // Only push if libp2p is running - if (this.isStarted() && this.identifyService) { - this.identifyService.pushToPeerStore(this.peerStore) - } + // Remove protocols from self protocols in the Protobook + this.peerStore.protoBook.remove(this.peerId, protocols) } async _onStarting () { - // Listen on the provided transports - await this.transportManager.listen() + // Listen on the provided transports for the provided addresses + const addrs = this.addressManager.getListenAddrs() + await this.transportManager.listen(addrs) // Start PeerStore await this.peerStore.start() @@ -502,6 +576,11 @@ class Libp2p extends EventEmitter { // Peer discovery await this._setupPeerDiscovery() + + // Relay + this.relay && this.relay.start() + + this.peerRouting.start() } /** @@ -509,7 +588,7 @@ class Libp2p extends EventEmitter { * Known peers may be emitted. * * @private - * @param {{ id: PeerId, multiaddrs: Array, protocols: Array }} peer + * @param {{ id: PeerId, multiaddrs: Multiaddr[], protocols: string[] }} peer */ _onDiscoveryPeer (peer) { if (peer.id.toB58String() === this.peerId.toB58String()) { @@ -587,7 +666,9 @@ class Libp2p extends EventEmitter { // Transport modules with discovery for (const Transport of this.transportManager.getTransports()) { + // @ts-ignore Transport interface does not include discovery if (Transport.discovery) { + // @ts-ignore Transport interface does not include discovery setupService(Transport.discovery) } } @@ -596,22 +677,4 @@ class Libp2p extends EventEmitter { } } -/** - * Like `new Libp2p(options)` except it will create a `PeerId` - * instance if one is not provided in options. - * - * @param {object} options - Libp2p configuration options - * @returns {Libp2p} - */ -Libp2p.create = async function create (options = {}) { - if (options.peerId) { - return new Libp2p(options) - } - - const peerId = await PeerId.create() - - options.peerId = peerId - return new Libp2p(options) -} - module.exports = Libp2p diff --git a/src/insecure/plaintext.js b/src/insecure/plaintext.js index 83e1ba463b..a5eeac2ec3 100644 --- a/src/insecure/plaintext.js +++ b/src/insecure/plaintext.js @@ -1,21 +1,34 @@ 'use strict' +const debug = require('debug') +const log = Object.assign(debug('libp2p:plaintext'), { + error: debug('libp2p:plaintext:err') +}) 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' +/** + * @typedef {import('libp2p-interfaces/src/connection').Connection} Connection + */ + function lpEncodeExchange (exchange) { const pb = Exchange.encode(exchange) + // @ts-ignore lp.encode expects type type 'Buffer | BufferList', not 'Uint8Array' return lp.encode.single(pb) } +/** + * Encrypt connection. + * + * @param {PeerId} localId + * @param {Connection} conn + * @param {PeerId} [remoteId] + */ async function encrypt (localId, conn, remoteId) { const shake = handshake(conn) diff --git a/src/insecure/proto.js b/src/insecure/proto.js index 2c7d7e89a6..9df7149606 100644 --- a/src/insecure/proto.js +++ b/src/insecure/proto.js @@ -2,6 +2,16 @@ const protobuf = require('protons') +/** + * @typedef {Object} Proto + * @property {import('../types').ExchangeProto} Exchange, + * @property {typeof import('../types').KeyType} KeyType + * @property {import('../types').PublicKeyProto} PublicKey + */ + +/** + * @type {Proto} + */ module.exports = protobuf(` message Exchange { optional bytes id = 1; diff --git a/src/keychain/cms.js b/src/keychain/cms.js index 60bfd323f7..3ba99cfb5b 100644 --- a/src/keychain/cms.js +++ b/src/keychain/cms.js @@ -1,3 +1,4 @@ +// @ts-nocheck 'use strict' require('node-forge/lib/pkcs7') @@ -21,7 +22,7 @@ class CMS { /** * Creates a new instance with a keychain * - * @param {Keychain} keychain - the available keys + * @param {import('./index')} keychain - the available keys */ constructor (keychain) { if (!keychain) { @@ -38,7 +39,7 @@ class CMS { * * @param {string} name - The local key name. * @param {Uint8Array} plain - The data to encrypt. - * @returns {undefined} + * @returns {Promise} */ async encrypt (name, plain) { if (!(plain instanceof Uint8Array)) { @@ -68,7 +69,7 @@ class CMS { * exists, an Error is returned with the property 'missingKeys'. It is array of key ids. * * @param {Uint8Array} cmsData - The CMS encrypted data to decrypt. - * @returns {undefined} + * @returns {Promise} */ async decrypt (cmsData) { if (!(cmsData instanceof Uint8Array)) { diff --git a/src/keychain/index.js b/src/keychain/index.js index c823eb3e46..440a2913a5 100644 --- a/src/keychain/index.js +++ b/src/keychain/index.js @@ -1,3 +1,4 @@ +// @ts-nocheck /* eslint max-nested-callbacks: ["error", 5] */ 'use strict' @@ -101,7 +102,8 @@ class Keychain { * Creates a new instance of a key chain. * * @param {DS} store - where the key are. - * @param {object} options - ??? + * @param {object} options + * @class */ constructor (store, options) { if (!store) { diff --git a/src/keychain/util.js b/src/keychain/util.js index 56386fe488..6a332c9ceb 100644 --- a/src/keychain/util.js +++ b/src/keychain/util.js @@ -1,3 +1,4 @@ +// @ts-nocheck 'use strict' require('node-forge/lib/x509') diff --git a/src/metrics/index.js b/src/metrics/index.js index 9d0f436041..8d94861d81 100644 --- a/src/metrics/index.js +++ b/src/metrics/index.js @@ -1,7 +1,8 @@ +// @ts-nocheck 'use strict' const mergeOptions = require('merge-options') -const pipe = require('it-pipe') +const { pipe } = require('it-pipe') const { tap } = require('streaming-iterables') const oldPeerLRU = require('./old-peers') const { METRICS: defaultOptions } = require('../constants') @@ -17,15 +18,26 @@ const directionToEvent = { out: 'dataSent' } +/** + * @typedef {import('peer-id')} PeerId + * @typedef {import('libp2p-interfaces/src/transport/types').MultiaddrConnection} MultiaddrConnection + */ + +/** + * @typedef MetricsProperties + * @property {import('../connection-manager')} connectionManager + * + * @typedef MetricsOptions + * @property {number} [computeThrottleMaxQueueSize = defaultOptions.computeThrottleMaxQueueSize] + * @property {number} [computeThrottleTimeout = defaultOptions.computeThrottleTimeout] + * @property {number[]} [movingAverageIntervals = defaultOptions.movingAverageIntervals] + * @property {number} [maxOldPeersRetention = defaultOptions.maxOldPeersRetention] + */ + class Metrics { /** - * - * @param {object} options - * @param {ConnectionManager} options.connectionManager - * @param {number} options.computeThrottleMaxQueueSize - * @param {number} options.computeThrottleTimeout - * @param {Array} options.movingAverageIntervals - * @param {number} options.maxOldPeersRetention + * @class + * @param {MetricsProperties & MetricsOptions} options */ constructor (options) { this._options = mergeOptions(defaultOptions, options) @@ -76,7 +88,7 @@ class Metrics { /** * Returns a list of `PeerId` strings currently being tracked * - * @returns {Array} + * @returns {string[]} */ get peers () { return Array.from(this._peerStats.keys()) @@ -97,7 +109,7 @@ class Metrics { /** * Returns a list of all protocol strings currently being tracked. * - * @returns {Array} + * @returns {string[]} */ get protocols () { return Array.from(this._protocolStats.keys()) @@ -176,6 +188,7 @@ class Metrics { * * @param {PeerId} placeholder - A peerId string * @param {PeerId} peerId + * @returns {void} */ updatePlaceholder (placeholder, peerId) { if (!this._running) return @@ -205,10 +218,10 @@ class Metrics { * 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 {MultiaddrConnection} options.stream - A duplex iterable stream * @param {PeerId} [options.remotePeer] - 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 + * @returns {MultiaddrConnection} The peerId string or placeholder string */ trackStream ({ stream, remotePeer, protocol }) { const metrics = this diff --git a/src/metrics/old-peers.js b/src/metrics/old-peers.js index 08d317dc09..753bdf5fa1 100644 --- a/src/metrics/old-peers.js +++ b/src/metrics/old-peers.js @@ -6,9 +6,10 @@ const LRU = require('hashlru') * Creates and returns a Least Recently Used Cache * * @param {number} maxSize - * @returns {LRUCache} + * @returns {any} */ module.exports = (maxSize) => { + // @ts-ignore LRU expression is not callable const patched = LRU(maxSize) patched.delete = patched.remove return patched diff --git a/src/metrics/stats.js b/src/metrics/stats.js index 3517766309..e35ab311fc 100644 --- a/src/metrics/stats.js +++ b/src/metrics/stats.js @@ -1,17 +1,19 @@ +// @ts-nocheck 'use strict' -const EventEmitter = require('events') +const { EventEmitter } = require('events') const Big = require('bignumber.js') const MovingAverage = require('moving-average') const retimer = require('retimer') -/** - * A queue based manager for stat processing - * - * @param {Array} initialCounters - * @param {any} options - */ class Stats extends EventEmitter { + /** + * A queue based manager for stat processing + * + * @class + * @param {string[]} initialCounters + * @param {any} options + */ constructor (initialCounters, options) { super() @@ -21,6 +23,7 @@ class Stats extends EventEmitter { this._frequencyLastTime = Date.now() this._frequencyAccumulators = {} + this._movingAverages = {} this._update = this._update.bind(this) @@ -68,7 +71,7 @@ class Stats extends EventEmitter { /** * Returns a clone of the current stats. * - * @returns {Map} + * @returns {Object} */ get snapshot () { return Object.assign({}, this._stats) @@ -77,7 +80,7 @@ class Stats extends EventEmitter { /** * Returns a clone of the internal movingAverages * - * @returns {Array} + * @returns {MovingAverage} */ get movingAverages () { return Object.assign({}, this._movingAverages) @@ -229,7 +232,7 @@ class Stats extends EventEmitter { * will be updated or initialized if they don't already exist. * * @private - * @param {Array} op + * @param {{string, number}[]} op * @throws {InvalidNumber} * @returns {void} */ @@ -238,7 +241,7 @@ class Stats extends EventEmitter { const inc = op[1] if (typeof inc !== 'number') { - throw new Error('invalid increment number:', inc) + throw new Error(`invalid increment number: ${inc}`) } let n diff --git a/src/peer-routing.js b/src/peer-routing.js index 7594f15d77..26fe9625b4 100644 --- a/src/peer-routing.js +++ b/src/peer-routing.js @@ -1,40 +1,128 @@ 'use strict' +const debug = require('debug') +const log = Object.assign(debug('libp2p:peer-routing'), { + error: debug('libp2p:peer-routing:err') +}) const errCode = require('err-code') + +const all = require('it-all') const pAny = require('p-any') +const { + setDelayedInterval, + clearDelayedInterval +} = require('set-delayed-interval') + +/** + * @typedef {import('peer-id')} PeerId + * @typedef {import('multiaddr')} Multiaddr + */ +class PeerRouting { + /** + * @class + * @param {import('./')} libp2p + */ + constructor (libp2p) { + this._peerId = libp2p.peerId + this._peerStore = libp2p.peerStore + this._routers = libp2p._modules.peerRouting || [] + + // If we have the dht, make it first + if (libp2p._dht) { + this._routers.unshift(libp2p._dht) + } + + this._refreshManagerOptions = libp2p._options.peerRouting.refreshManager + + this._findClosestPeersTask = this._findClosestPeersTask.bind(this) + } + + /** + * Start peer routing service. + */ + start () { + if (!this._routers.length || this._timeoutId || !this._refreshManagerOptions.enabled) { + return + } + + this._timeoutId = setDelayedInterval( + this._findClosestPeersTask, this._refreshManagerOptions.interval, this._refreshManagerOptions.bootDelay + ) + } -module.exports = (node) => { - const routers = node._modules.peerRouting || [] + /** + * Recurrent task to find closest peers and add their addresses to the Address Book. + */ + async _findClosestPeersTask () { + try { + for await (const { id, multiaddrs } of this.getClosestPeers(this._peerId.id)) { + this._peerStore.addressBook.add(id, multiaddrs) + } + } catch (err) { + log.error(err) + } + } - // If we have the dht, make it first - if (node._dht) { - routers.unshift(node._dht) + /** + * Stop peer routing service. + */ + stop () { + clearDelayedInterval(this._timeoutId) } - return { - /** - * 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.timeout] - How long the query should run - * @returns {Promise<{ id: PeerId, multiaddrs: Multiaddr[] }>} - */ - findPeer: async (id, options) => { // eslint-disable-line require-await - if (!routers.length) { - throw errCode(new Error('No peer routers available'), 'NO_ROUTERS_AVAILABLE') + /** + * 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.timeout] - How long the query should run + * @returns {Promise<{ id: PeerId, multiaddrs: Multiaddr[] }>} + */ + async findPeer (id, options) { // eslint-disable-line require-await + if (!this._routers.length) { + throw errCode(new Error('No peer routers available'), 'NO_ROUTERS_AVAILABLE') + } + + return pAny(this._routers.map(async (router) => { + const result = await router.findPeer(id, options) + + // 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') } - return pAny(routers.map(async (router) => { - const result = await router.findPeer(id, options) + return result + })) + } + + /** + * Attempt to find the closest peers on the network to the given key. + * + * @param {Uint8Array} key - A CID like key + * @param {Object} [options] + * @param {number} [options.timeout=30e3] - How long the query can take. + * @returns {AsyncIterable<{ id: PeerId, multiaddrs: Multiaddr[] }>} + */ + async * getClosestPeers (key, options = { timeout: 30e3 }) { + if (!this._routers.length) { + throw errCode(new Error('No peer routers available'), 'NO_ROUTERS_AVAILABLE') + } - // If we don't have a result, we need to provide an error to keep trying - if (!result || Object.keys(result).length === 0) { + const result = await pAny( + this._routers.map(async (router) => { + const peers = await all(router.getClosestPeers(key, options)) + + if (!peers || !peers.length) { throw errCode(new Error('not found'), 'NOT_FOUND') } + return peers + }) + ) - return result - })) + for (const peer of result) { + yield peer } } } + +module.exports = PeerRouting diff --git a/src/peer-store/address-book.js b/src/peer-store/address-book.js index c8fa2ec6f5..ebbd260c42 100644 --- a/src/peer-store/address-book.js +++ b/src/peer-store/address-book.js @@ -1,9 +1,10 @@ 'use strict' -const errcode = require('err-code') const debug = require('debug') -const log = debug('libp2p:peer-store:address-book') -log.error = debug('libp2p:peer-store:address-book:error') +const log = Object.assign(debug('libp2p:peer-store:address-book'), { + error: debug('libp2p:peer-store:address-book:err') +}) +const errcode = require('err-code') const multiaddr = require('multiaddr') const PeerId = require('peer-id') @@ -17,35 +18,31 @@ const { const Envelope = require('../record/envelope') /** - * The AddressBook is responsible for keeping the known multiaddrs - * of a peer. + * @typedef {import('multiaddr')} Multiaddr + * @typedef {import('./')} PeerStore */ -class AddressBook extends Book { - /** - * Address object - * - * @typedef {Object} Address - * @property {Multiaddr} multiaddr peer multiaddr. - * @property {boolean} isCertified obtained from a signed peer record. - */ - /** - * CertifiedRecord object - * - * @typedef {Object} CertifiedRecord - * @property {Uint8Array} raw raw envelope. - * @property {number} seqNumber seq counter. - */ +/** + * @typedef {Object} Address + * @property {Multiaddr} multiaddr peer multiaddr. + * @property {boolean} isCertified obtained from a signed peer record. + * + * @typedef {Object} CertifiedRecord + * @property {Uint8Array} raw raw envelope. + * @property {number} seqNumber seq counter. + * + * @typedef {Object} Entry + * @property {Address[]} addresses peer Addresses. + * @property {CertifiedRecord} [record] certified peer record. + */ +/** + * @extends {Book} + */ +class AddressBook extends Book { /** - * Entry object for the addressBook + * The AddressBook is responsible for keeping the known multiaddrs of a peer. * - * @typedef {Object} Entry - * @property {Array
} addresses peer Addresses. - * @property {CertifiedRecord} record certified peer record. - */ - - /** * @class * @param {PeerStore} peerStore */ @@ -59,18 +56,19 @@ class AddressBook extends Book { peerStore, eventName: 'change:multiaddrs', eventProperty: 'multiaddrs', - eventTransformer: (data) => { - if (!data.addresses) { + eventTransformer: (entry) => { + if (!entry || !entry.addresses) { return [] } - return data.addresses.map((address) => address.multiaddr) - } + return entry.addresses.map((address) => address.multiaddr) + }, + getTransformer: (entry) => entry && entry.addresses ? [...entry.addresses] : undefined }) /** * Map known peers to their known Address Entries. * - * @type {Map>} + * @type {Map} */ this.data = new Map() } @@ -105,7 +103,7 @@ class AddressBook extends Book { const peerId = peerRecord.peerId const id = peerId.toB58String() - const entry = this.data.get(id) || {} + const entry = this.data.get(id) || { record: undefined } const storedRecord = entry.record // ensure seq is greater than, or equal to, the last received @@ -151,7 +149,7 @@ class AddressBook extends Book { * Returns undefined if no record exists. * * @param {PeerId} peerId - * @returns {Promise} + * @returns {Promise|undefined} */ getPeerRecord (peerId) { const raw = this.getRawEnvelope(peerId) @@ -171,7 +169,7 @@ class AddressBook extends Book { * * @override * @param {PeerId} peerId - * @param {Array} multiaddrs + * @param {Multiaddr[]} multiaddrs * @returns {AddressBook} */ set (peerId, multiaddrs) { @@ -181,22 +179,22 @@ class AddressBook extends Book { } const addresses = this._toAddresses(multiaddrs) - const id = peerId.toB58String() - const entry = this.data.get(id) || {} - const rec = entry.addresses // Not replace multiaddrs if (!addresses.length) { return this } + const id = peerId.toB58String() + const entry = this.data.get(id) + // Already knows the peer - if (rec && rec.length === addresses.length) { - const intersection = rec.filter((addr) => addresses.some((newAddr) => addr.multiaddr.equals(newAddr.multiaddr))) + if (entry && entry.addresses && entry.addresses.length === addresses.length) { + const intersection = entry.addresses.filter((addr) => addresses.some((newAddr) => addr.multiaddr.equals(newAddr.multiaddr))) // Are new addresses equal to the old ones? // If yes, no changes needed! - if (intersection.length === rec.length) { + if (intersection.length === entry.addresses.length) { log(`the addresses provided to store are equal to the already stored for ${id}`) return this } @@ -204,12 +202,12 @@ class AddressBook extends Book { this._setData(peerId, { addresses, - record: entry.record + record: entry && entry.record }) log(`stored provided multiaddrs for ${id}`) // Notify the existance of a new peer - if (!rec) { + if (!entry) { this._ps.emit('peer', peerId) } @@ -221,7 +219,7 @@ class AddressBook extends Book { * If the peer is not known, it is set with the given addresses. * * @param {PeerId} peerId - * @param {Array} multiaddrs + * @param {Multiaddr[]} multiaddrs * @returns {AddressBook} */ add (peerId, multiaddrs) { @@ -233,62 +231,46 @@ class AddressBook extends Book { const addresses = this._toAddresses(multiaddrs) const id = peerId.toB58String() - const entry = this.data.get(id) || {} - const rec = entry.addresses || [] + const entry = this.data.get(id) - // Add recorded uniquely to the new array (Union) - rec.forEach((addr) => { - if (!addresses.find(r => r.multiaddr.equals(addr.multiaddr))) { - addresses.push(addr) - } - }) + if (entry && entry.addresses) { + // Add recorded uniquely to the new array (Union) + entry.addresses.forEach((addr) => { + if (!addresses.find(r => r.multiaddr.equals(addr.multiaddr))) { + addresses.push(addr) + } + }) - // If the recorded length is equal to the new after the unique union - // The content is the same, no need to update. - if (rec && rec.length === addresses.length) { - log(`the addresses provided to store are already stored for ${id}`) - return this + // If the recorded length is equal to the new after the unique union + // The content is the same, no need to update. + if (entry.addresses.length === addresses.length) { + log(`the addresses provided to store are already stored for ${id}`) + return this + } } this._setData(peerId, { addresses, - record: entry.record + record: entry && entry.record }) log(`added provided multiaddrs for ${id}`) // Notify the existance of a new peer - if (!entry.addresses) { + if (!(entry && entry.addresses)) { this._ps.emit('peer', peerId) } return this } - /** - * Get the known data of a provided peer. - * - * @override - * @param {PeerId} peerId - * @returns {Array} - */ - get (peerId) { - if (!PeerId.isPeerId(peerId)) { - throw errcode(new Error('peerId must be an instance of peer-id'), ERR_INVALID_PARAMETERS) - } - - const entry = this.data.get(peerId.toB58String()) - - return entry && entry.addresses ? [...entry.addresses] : undefined - } - /** * Transforms received multiaddrs into Address. * * @private - * @param {Array} multiaddrs + * @param {Multiaddr[]} multiaddrs * @param {boolean} [isCertified] - * @returns {Array
} + * @returns {Address[]} */ _toAddresses (multiaddrs, isCertified = false) { if (!multiaddrs) { @@ -319,20 +301,22 @@ class AddressBook extends Book { * Returns `undefined` if there are no known multiaddrs for the given peer. * * @param {PeerId} peerId - * @returns {Array|undefined} + * @param {(addresses: Address[]) => Address[]} [addressSorter] + * @returns {Multiaddr[]|undefined} */ - getMultiaddrsForPeer (peerId) { + getMultiaddrsForPeer (peerId, addressSorter = (ms) => ms) { if (!PeerId.isPeerId(peerId)) { throw errcode(new Error('peerId must be an instance of peer-id'), ERR_INVALID_PARAMETERS) } const entry = this.data.get(peerId.toB58String()) - if (!entry || !entry.addresses) { return undefined } - return entry.addresses.map((address) => { + return addressSorter( + entry.addresses || [] + ).map((address) => { const multiaddr = address.multiaddr const idString = multiaddr.getPeerId() diff --git a/src/peer-store/book.js b/src/peer-store/book.js index f0a830f97a..14218e7611 100644 --- a/src/peer-store/book.js +++ b/src/peer-store/book.js @@ -10,27 +10,36 @@ const { const passthrough = data => data /** - * The Book is the skeleton for the PeerStore books. + * @typedef {import('./')} PeerStore */ + +/** + * @template Data, GetData, EventData + */ + class Book { /** + * The Book is the skeleton for the PeerStore books. + * * @class * @param {Object} properties * @param {PeerStore} properties.peerStore - PeerStore instance. * @param {string} properties.eventName - Name of the event to emit by the PeerStore. * @param {string} properties.eventProperty - Name of the property to emit by the PeerStore. - * @param {Function} [properties.eventTransformer] - Transformer function of the provided data for being emitted. + * @param {(data: Data | undefined) => EventData | undefined} [properties.eventTransformer] - Transformer function of the provided data for being emitted. + * @param {(data: Data | undefined) => GetData | undefined} [properties.getTransformer] - Transformer function of the provided data for being returned on get. */ - constructor ({ peerStore, eventName, eventProperty, eventTransformer = passthrough }) { + constructor ({ peerStore, eventName, eventProperty, eventTransformer = passthrough, getTransformer = passthrough }) { this._ps = peerStore this.eventName = eventName this.eventProperty = eventProperty this.eventTransformer = eventTransformer + this.getTransformer = getTransformer /** * Map known peers to their data. * - * @type {Map} + * @type {Map} */ this.data = new Map() } @@ -39,7 +48,7 @@ class Book { * Set known data of a provided peer. * * @param {PeerId} peerId - * @param {Array|Data} data + * @param {unknown} data */ set (peerId, data) { throw errcode(new Error('set must be implemented by the subclass'), 'ERR_NOT_IMPLEMENTED') @@ -48,9 +57,9 @@ class Book { /** * Set data into the datastructure, persistence and emit it using the provided transformers. * - * @private + * @protected * @param {PeerId} peerId - peerId of the data to store - * @param {*} data - data to store. + * @param {Data} data - data to store. * @param {Object} [options] - storing options. * @param {boolean} [options.emit = true] - emit the provided data. * @returns {void} @@ -68,9 +77,9 @@ class Book { /** * Emit data. * - * @private + * @protected * @param {PeerId} peerId - * @param {*} data + * @param {Data | undefined} [data] */ _emit (peerId, data) { this._ps.emit(this.eventName, { @@ -84,7 +93,7 @@ class Book { * Returns `undefined` if there is no available data for the given peer. * * @param {PeerId} peerId - * @returns {Array|undefined} + * @returns {GetData | undefined} */ get (peerId) { if (!PeerId.isPeerId(peerId)) { @@ -93,7 +102,7 @@ class Book { const rec = this.data.get(peerId.toB58String()) - return rec ? [...rec] : undefined + return this.getTransformer(rec) } /** @@ -111,7 +120,7 @@ class Book { return false } - this._emit(peerId, []) + this._emit(peerId, undefined) return true } diff --git a/src/peer-store/index.js b/src/peer-store/index.js index 69a1f15a57..b3df1bbb94 100644 --- a/src/peer-store/index.js +++ b/src/peer-store/index.js @@ -1,9 +1,6 @@ 'use strict' const errcode = require('err-code') -const debug = require('debug') -const log = debug('libp2p:peer-store') -log.error = debug('libp2p:peer-store:error') const { EventEmitter } = require('events') const PeerId = require('peer-id') @@ -14,11 +11,15 @@ const MetadataBook = require('./metadata-book') const ProtoBook = require('./proto-book') const { - ERR_INVALID_PARAMETERS + codes: { ERR_INVALID_PARAMETERS } } = require('../errors') /** - * Responsible for managing known peers, as well as their addresses, protocols and metadata. + * @typedef {import('./address-book').Address} Address + */ + +/** + * @extends {EventEmitter} * * @fires PeerStore#peer Emitted when a new peer is added. * @fires PeerStore#change:protocols Emitted when a known peer supports a different set of protocols. @@ -32,12 +33,14 @@ class PeerStore extends EventEmitter { * * @typedef {Object} Peer * @property {PeerId} id peer's peer-id instance. - * @property {Array
} addresses peer's addresses containing its multiaddrs and metadata. - * @property {Array} protocols peer's supported protocols. - * @property {Map} metadata peer's metadata map. + * @property {Address[]} addresses peer's addresses containing its multiaddrs and metadata. + * @property {string[]} protocols peer's supported protocols. + * @property {Map|undefined} metadata peer's metadata map. */ /** + * Responsible for managing known peers, as well as their addresses, protocols and metadata. + * * @param {object} options * @param {PeerId} options.peerId * @class @@ -121,7 +124,7 @@ class PeerStore extends EventEmitter { * Get the stored information of a given peer. * * @param {PeerId} peerId - * @returns {Peer} + * @returns {Peer|undefined} */ get (peerId) { if (!PeerId.isPeerId(peerId)) { diff --git a/src/peer-store/key-book.js b/src/peer-store/key-book.js index 607d12795f..ba92b87afb 100644 --- a/src/peer-store/key-book.js +++ b/src/peer-store/key-book.js @@ -1,9 +1,10 @@ 'use strict' -const errcode = require('err-code') const debug = require('debug') -const log = debug('libp2p:peer-store:key-book') -log.error = debug('libp2p:peer-store:key-book:error') +const log = Object.assign(debug('libp2p:peer-store:key-book'), { + error: debug('libp2p:peer-store:key-book:err') +}) +const errcode = require('err-code') const PeerId = require('peer-id') @@ -14,10 +15,17 @@ const { } = require('../errors') /** - * The KeyBook is responsible for keeping the known public keys of a peer. + * @typedef {import('./')} PeerStore + * @typedef {import('libp2p-crypto').PublicKey} PublicKey + */ + +/** + * @extends {Book} */ class KeyBook extends Book { /** + * The KeyBook is responsible for keeping the known public keys of a peer. + * * @class * @param {PeerStore} peerStore */ @@ -26,7 +34,8 @@ class KeyBook extends Book { peerStore, eventName: 'change:pubkey', eventProperty: 'pubkey', - eventTransformer: (data) => data.pubKey + eventTransformer: (data) => data && data.pubKey, + getTransformer: (data) => data && data.pubKey }) /** @@ -42,7 +51,7 @@ class KeyBook extends Book { * * @override * @param {PeerId} peerId - * @param {RsaPublicKey|Ed25519PublicKey|Secp256k1PublicKey} publicKey + * @param {PublicKey} publicKey * @returns {KeyBook} */ set (peerId, publicKey) { @@ -66,23 +75,6 @@ class KeyBook extends Book { return this } - - /** - * Get Public key of the given PeerId, if stored. - * - * @override - * @param {PeerId} peerId - * @returns {RsaPublicKey|Ed25519PublicKey|Secp256k1PublicKey} - */ - get (peerId) { - if (!PeerId.isPeerId(peerId)) { - throw errcode(new Error('peerId must be an instance of peer-id'), ERR_INVALID_PARAMETERS) - } - - const rec = this.data.get(peerId.toB58String()) - - return rec ? rec.pubKey : undefined - } } module.exports = KeyBook diff --git a/src/peer-store/metadata-book.js b/src/peer-store/metadata-book.js index 490ef02b09..4008ed5434 100644 --- a/src/peer-store/metadata-book.js +++ b/src/peer-store/metadata-book.js @@ -1,27 +1,40 @@ 'use strict' -const errcode = require('err-code') const debug = require('debug') -const log = debug('libp2p:peer-store:proto-book') -log.error = debug('libp2p:peer-store:proto-book:error') +const log = Object.assign(debug('libp2p:peer-store:proto-book'), { + error: debug('libp2p:peer-store:proto-book:err') +}) +const errcode = require('err-code') const uint8ArrayEquals = require('uint8arrays/equals') const PeerId = require('peer-id') const Book = require('./book') - const { codes: { ERR_INVALID_PARAMETERS } } = require('../errors') +const eventName = 'change:metadata' +const eventProperty = 'metadata' + /** - * The MetadataBook is responsible for keeping the known supported - * protocols of a peer. + * @typedef {import('./')} PeerStore + */ + +/** + * @typedef {Map} Metadata + */ + +/** + * @extends {Book} * * @fires MetadataBook#change:metadata */ class MetadataBook extends Book { /** + * The MetadataBook is responsible for keeping the known supported + * protocols of a peer. + * * @class * @param {PeerStore} peerStore */ @@ -32,14 +45,14 @@ class MetadataBook extends Book { */ super({ peerStore, - eventName: 'change:metadata', - eventProperty: 'metadata' + eventName, + eventProperty }) /** * Map known peers to their known protocols. * - * @type {Map>} + * @type {Map} */ this.data = new Map() } @@ -51,8 +64,9 @@ class MetadataBook extends Book { * @param {PeerId} peerId * @param {string} key - metadata key * @param {Uint8Array} value - metadata value - * @returns {ProtoBook} + * @returns {MetadataBook} */ + // @ts-ignore override with more then the parameters expected in Book set (peerId, key, value) { if (!PeerId.isPeerId(peerId)) { log.error('peerId must be an instance of peer-id to store data') @@ -91,26 +105,12 @@ class MetadataBook extends Book { emit && this._emit(peerId, key) } - /** - * Get the known data of a provided peer. - * - * @param {PeerId} peerId - * @returns {Map} - */ - get (peerId) { - if (!PeerId.isPeerId(peerId)) { - throw errcode(new Error('peerId must be an instance of peer-id'), ERR_INVALID_PARAMETERS) - } - - return this.data.get(peerId.toB58String()) - } - /** * Get specific metadata value, if it exists * * @param {PeerId} peerId * @param {string} key - * @returns {Uint8Array} + * @returns {Uint8Array | undefined} */ getValue (peerId, key) { if (!PeerId.isPeerId(peerId)) { @@ -159,7 +159,10 @@ class MetadataBook extends Book { return false } - this._emit(peerId, key) + this._ps.emit(eventName, { + peerId, + [eventProperty]: key + }) return true } diff --git a/src/peer-store/persistent/index.js b/src/peer-store/persistent/index.js index 70a417f4f7..bbab49657e 100644 --- a/src/peer-store/persistent/index.js +++ b/src/peer-store/persistent/index.js @@ -1,9 +1,9 @@ 'use strict' const debug = require('debug') -const log = debug('libp2p:persistent-peer-store') -log.error = debug('libp2p:persistent-peer-store:error') - +const log = Object.assign(debug('libp2p:persistent-peer-store'), { + error: debug('libp2p:persistent-peer-store:err') +}) const { Key } = require('interface-datastore') const multiaddr = require('multiaddr') const PeerId = require('peer-id') @@ -21,16 +21,22 @@ const { const Addresses = require('./pb/address-book.proto') const Protocols = require('./pb/proto-book.proto') +/** + * @typedef {Object} PersistentPeerStoreProperties + * @property {PeerId} peerId + * @property {any} datastore + * + * @typedef {Object} PersistentPeerStoreOptions + * @property {number} [threshold = 5] - Number of dirty peers allowed before commit data. + */ + /** * Responsible for managing the persistence of data in the PeerStore. */ class PersistentPeerStore extends PeerStore { /** * @class - * @param {Object} properties - * @param {PeerId} properties.peerId - * @param {Datastore} properties.datastore - Datastore to persist data. - * @param {number} [properties.threshold = 5] - Number of dirty peers allowed before commit data. + * @param {PersistentPeerStoreProperties & PersistentPeerStoreOptions} properties */ constructor ({ peerId, datastore, threshold = 5 }) { super({ peerId }) @@ -340,6 +346,7 @@ class PersistentPeerStore extends PeerStore { case 'addrs': decoded = Addresses.decode(value) + // @ts-ignore protected function this.addressBook._setData( peerId, { @@ -357,6 +364,7 @@ class PersistentPeerStore extends PeerStore { case 'keys': decoded = await PeerId.createFromPubKey(value) + // @ts-ignore protected function this.keyBook._setData( decoded, decoded, @@ -372,6 +380,7 @@ class PersistentPeerStore extends PeerStore { case 'protos': decoded = Protocols.decode(value) + // @ts-ignore protected function this.protoBook._setData( peerId, new Set(decoded.protocols), diff --git a/src/peer-store/proto-book.js b/src/peer-store/proto-book.js index 073b7e47e5..f02ba94ef4 100644 --- a/src/peer-store/proto-book.js +++ b/src/peer-store/proto-book.js @@ -1,10 +1,10 @@ 'use strict' -const errcode = require('err-code') const debug = require('debug') -const log = debug('libp2p:peer-store:proto-book') -log.error = debug('libp2p:peer-store:proto-book:error') - +const log = Object.assign(debug('libp2p:peer-store:proto-book'), { + error: debug('libp2p:peer-store:proto-book:err') +}) +const errcode = require('err-code') const PeerId = require('peer-id') const Book = require('./book') @@ -14,13 +14,32 @@ const { } = require('../errors') /** - * The ProtoBook is responsible for keeping the known supported - * protocols of a peer. + * @typedef {import('./')} PeerStore + */ + +// @extends {Book} + +/** + * @param {Set | undefined} set + * @returns {string[] | undefined} + */ +const transformSetToArray = (set) => { + if (!set) { + return undefined + } + return Array.from(set) +} + +/** + * @extends {Book, string[], string[]>} * * @fires ProtoBook#change:protocols */ class ProtoBook extends Book { /** + * The ProtoBook is responsible for keeping the known supported + * protocols of a peer. + * * @class * @param {PeerStore} peerStore */ @@ -33,7 +52,8 @@ class ProtoBook extends Book { peerStore, eventName: 'change:protocols', eventProperty: 'protocols', - eventTransformer: (data) => Array.from(data) + eventTransformer: (data) => transformSetToArray(data) || [], + getTransformer: (data) => transformSetToArray(data) }) /** @@ -50,7 +70,7 @@ class ProtoBook extends Book { * * @override * @param {PeerId} peerId - * @param {Array} protocols + * @param {string[]} protocols * @returns {ProtoBook} */ set (peerId, protocols) { @@ -88,7 +108,7 @@ class ProtoBook extends Book { * If the peer was not known before, it will be added. * * @param {PeerId} peerId - * @param {Array} protocols + * @param {string[]} protocols * @returns {ProtoBook} */ add (peerId, protocols) { @@ -112,13 +132,50 @@ class ProtoBook extends Book { return this } - protocols = [...newSet] - this._setData(peerId, newSet) log(`added provided protocols for ${id}`) return this } + + /** + * Removes known protocols of a provided peer. + * If the protocols did not exist before, nothing will be done. + * + * @param {PeerId} peerId + * @param {string[]} protocols + * @returns {ProtoBook} + */ + remove (peerId, protocols) { + if (!PeerId.isPeerId(peerId)) { + log.error('peerId must be an instance of peer-id to store data') + throw errcode(new Error('peerId must be an instance of peer-id'), ERR_INVALID_PARAMETERS) + } + + if (!protocols) { + log.error('protocols must be provided to store data') + throw errcode(new Error('protocols must be provided'), ERR_INVALID_PARAMETERS) + } + + const id = peerId.toB58String() + const recSet = this.data.get(id) + + if (recSet) { + const newSet = new Set([ + ...recSet + ].filter((p) => !protocols.includes(p))) + + // Any protocol removed? + if (recSet.size === newSet.size) { + return this + } + + this._setData(peerId, newSet) + log(`removed provided protocols for ${id}`) + } + + return this + } } module.exports = ProtoBook diff --git a/src/ping/index.js b/src/ping/index.js index e882f81f4c..eb8d7b96e9 100644 --- a/src/ping/index.js +++ b/src/ping/index.js @@ -1,30 +1,39 @@ 'use strict' const debug = require('debug') -const log = debug('libp2p-ping') -log.error = debug('libp2p-ping:error') +const log = Object.assign(debug('libp2p:ping'), { + error: debug('libp2p:ping:err') +}) const errCode = require('err-code') const crypto = require('libp2p-crypto') -const pipe = require('it-pipe') +const { pipe } = require('it-pipe') const { toBuffer } = require('it-buffer') const { collect, take } = require('streaming-iterables') +const equals = require('uint8arrays/equals') const { PROTOCOL, PING_LENGTH } = require('./constants') +/** + * @typedef {import('../')} Libp2p + * @typedef {import('multiaddr')} Multiaddr + * @typedef {import('peer-id')} PeerId + */ + /** * Ping a given peer and wait for its response, getting the operation latency. * * @param {Libp2p} node - * @param {PeerId|multiaddr} peer + * @param {PeerId|Multiaddr} peer * @returns {Promise} */ async function ping (node, peer) { + // @ts-ignore multiaddr might not have toB58String log('dialing %s to %s', PROTOCOL, peer.toB58String ? peer.toB58String() : peer) const { stream } = await node.dialProtocol(peer, PROTOCOL) - const start = new Date() + const start = Date.now() const data = crypto.randomBytes(PING_LENGTH) const [result] = await pipe( @@ -36,7 +45,7 @@ async function ping (node, peer) { ) const end = Date.now() - if (!data.equals(result)) { + if (!equals(data, result)) { throw errCode(new Error('Received wrong ping ack'), 'ERR_WRONG_PING_ACK') } diff --git a/src/pnet/crypto.js b/src/pnet/crypto.js index ae824bfcfb..9cfcbc8e6f 100644 --- a/src/pnet/crypto.js +++ b/src/pnet/crypto.js @@ -1,16 +1,17 @@ 'use strict' const debug = require('debug') +const log = Object.assign(debug('libp2p:pnet'), { + trace: debug('libp2p:pnet:trace'), + error: debug('libp2p:pnet:err') +}) + const Errors = require('./errors') const xsalsa20 = require('xsalsa20') const KEY_LENGTH = require('./key-generator').KEY_LENGTH const uint8ArrayFromString = require('uint8arrays/from-string') const uint8ArrayToString = require('uint8arrays/to-string') -const log = debug('libp2p:pnet') -log.trace = debug('libp2p:pnet:trace') -log.error = debug('libp2p:pnet:err') - /** * Creates a stream iterable to encrypt messages in a private network * diff --git a/src/pnet/index.js b/src/pnet/index.js index e5f82331a2..194a5005ec 100644 --- a/src/pnet/index.js +++ b/src/pnet/index.js @@ -1,12 +1,16 @@ 'use strict' -const pipe = require('it-pipe') +const debug = require('debug') +const log = Object.assign(debug('libp2p:pnet'), { + error: debug('libp2p:pnet:err') +}) +const { pipe } = require('it-pipe') const errcode = require('err-code') const duplexPair = require('it-pair/duplex') const crypto = require('libp2p-crypto') const Errors = require('./errors') const { - ERR_INVALID_PARAMETERS + codes: { ERR_INVALID_PARAMETERS } } = require('../errors') const { createBoxStream, @@ -15,16 +19,16 @@ const { } = require('./crypto') const handshake = require('it-handshake') const { NONCE_LENGTH } = require('./key-generator') -const debug = require('debug') -const log = debug('libp2p:pnet') -log.error = debug('libp2p:pnet:err') /** - * Takes a Private Shared Key (psk) and provides a `protect` method - * for wrapping existing connections in a private encryption stream + * @typedef {import('libp2p-interfaces/src/transport/types').MultiaddrConnection} MultiaddrConnection */ + class Protector { /** + * Takes a Private Shared Key (psk) and provides a `protect` method + * for wrapping existing connections in a private encryption stream. + * * @param {Uint8Array} keyBuffer - The private shared key buffer * @class */ @@ -39,8 +43,8 @@ class Protector { * between its two peers from the PSK the Protector instance was * created with. * - * @param {Connection} connection - The connection to protect - * @returns {*} A protected duplex iterable + * @param {MultiaddrConnection} connection - The connection to protect + * @returns {Promise} A protected duplex iterable */ async protect (connection) { if (!connection) { diff --git a/src/pnet/key-generator.js b/src/pnet/key-generator.js index b3676dcccf..8a7a1ef5a2 100644 --- a/src/pnet/key-generator.js +++ b/src/pnet/key-generator.js @@ -22,6 +22,8 @@ module.exports = generate module.exports.NONCE_LENGTH = 24 module.exports.KEY_LENGTH = KEY_LENGTH +// @ts-ignore This condition will always return 'false' since the types 'Module | undefined' if (require.main === module) { + // @ts-ignore generate(process.stdout) } diff --git a/src/pubsub-adapter.js b/src/pubsub-adapter.js index 1f42cc7d15..7d7af8df2f 100644 --- a/src/pubsub-adapter.js +++ b/src/pubsub-adapter.js @@ -1,42 +1,54 @@ 'use strict' +/** + * @typedef {import('libp2p-interfaces/src/pubsub').InMessage} InMessage + * @typedef {import('libp2p-interfaces/src/pubsub')} PubsubRouter + */ + // Pubsub adapter to keep API with handlers while not removed. -module.exports = (PubsubRouter, libp2p, options) => { - class Pubsub extends PubsubRouter { - /** - * Subscribes to a given topic. - * - * @override - * @param {string} topic - * @param {function(msg: InMessage)} [handler] - * @returns {void} - */ - subscribe (topic, handler) { - // Bind provided handler - handler && this.on(topic, handler) - super.subscribe(topic) +function pubsubAdapter (PubsubRouter, libp2p, options) { + const pubsub = new PubsubRouter(libp2p, options) + pubsub._subscribeAdapter = pubsub.subscribe + pubsub._unsubscribeAdapter = pubsub.unsubscribe + + /** + * Subscribes to a given topic. + * + * @override + * @param {string} topic + * @param {(msg: InMessage) => void} [handler] + * @returns {void} + */ + function subscribe (topic, handler) { + // Bind provided handler + handler && pubsub.on(topic, handler) + pubsub._subscribeAdapter(topic) + } + + /** + * Unsubscribe from the given topic. + * + * @override + * @param {string} topic + * @param {(msg: InMessage) => void} [handler] + * @returns {void} + */ + function unsubscribe (topic, handler) { + if (!handler) { + pubsub.removeAllListeners(topic) + } else { + pubsub.removeListener(topic, handler) } - /** - * Unsubscribe from the given topic. - * - * @override - * @param {string} topic - * @param {function(msg: InMessage)} [handler] - * @returns {void} - */ - unsubscribe (topic, handler) { - if (!handler) { - this.removeAllListeners(topic) - } else { - this.removeListener(topic, handler) - } - - if (this.listenerCount(topic) === 0) { - super.unsubscribe(topic) - } + if (pubsub.listenerCount(topic) === 0) { + pubsub._unsubscribeAdapter(topic) } } - return new Pubsub(libp2p, options) + pubsub.subscribe = subscribe + pubsub.unsubscribe = unsubscribe + + return pubsub } + +module.exports = pubsubAdapter diff --git a/src/record/envelope/envelope.proto.js b/src/record/envelope/envelope.proto.js index ca0074961a..c8907debda 100644 --- a/src/record/envelope/envelope.proto.js +++ b/src/record/envelope/envelope.proto.js @@ -2,7 +2,8 @@ const protons = require('protons') -const message = ` +/** @type {{Envelope: import('../../types').MessageProto}} */ +module.exports = protons(` message Envelope { // public_key is the public key of the keypair the enclosed payload was // signed with. @@ -20,6 +21,4 @@ message Envelope { // additional security. bytes signature = 5; } -` - -module.exports = protons(message).Envelope +`) diff --git a/src/record/envelope/index.js b/src/record/envelope/index.js index 6a73914f2a..46f9c3ccf6 100644 --- a/src/record/envelope/index.js +++ b/src/record/envelope/index.js @@ -1,8 +1,5 @@ 'use strict' -const debug = require('debug') -const log = debug('libp2p:envelope') -log.error = debug('libp2p:envelope:error') const errCode = require('err-code') const uint8arraysConcat = require('uint8arrays/concat') const uint8arraysFromString = require('uint8arrays/from-string') @@ -15,11 +12,14 @@ const { codes } = require('../../errors') const Protobuf = require('./envelope.proto') /** - * The Envelope is responsible for keeping an arbitrary signed record - * by a libp2p peer. + * @typedef {import('libp2p-interfaces/src/record/types').Record} Record */ + class Envelope { /** + * The Envelope is responsible for keeping an arbitrary signed record + * by a libp2p peer. + * * @class * @param {object} params * @param {PeerId} params.peerId @@ -49,7 +49,7 @@ class Envelope { const publicKey = cryptoKeys.marshalPublicKey(this.peerId.pubKey) - this._marshal = Protobuf.encode({ + this._marshal = Protobuf.Envelope.encode({ public_key: publicKey, payload_type: this.payloadType, payload: this.payload, @@ -102,14 +102,14 @@ const formatSignaturePayload = (domain, payloadType, payload) => { // - The length of the payload field in bytes // - The value of the payload field - domain = uint8arraysFromString(domain) - const domainLength = varint.encode(domain.byteLength) + const domainUint8Array = uint8arraysFromString(domain) + const domainLength = varint.encode(domainUint8Array.byteLength) const payloadTypeLength = varint.encode(payloadType.length) const payloadLength = varint.encode(payload.length) return uint8arraysConcat([ new Uint8Array(domainLength), - domain, + domainUint8Array, new Uint8Array(payloadTypeLength), payloadType, new Uint8Array(payloadLength), @@ -124,7 +124,7 @@ const formatSignaturePayload = (domain, payloadType, payload) => { * @returns {Promise} */ Envelope.createFromProtobuf = async (data) => { - const envelopeData = Protobuf.decode(data) + const envelopeData = Protobuf.Envelope.decode(data) const peerId = await PeerId.createFromPubKey(envelopeData.public_key) return new Envelope({ @@ -142,7 +142,7 @@ Envelope.createFromProtobuf = async (data) => { * @async * @param {Record} record * @param {PeerId} peerId - * @returns {Envelope} + * @returns {Promise} */ Envelope.seal = async (record, peerId) => { const domain = record.domain @@ -166,7 +166,7 @@ Envelope.seal = async (record, peerId) => { * * @param {Uint8Array} data * @param {string} domain - * @returns {Envelope} + * @returns {Promise} */ Envelope.openAndCertify = async (data, domain) => { const envelope = await Envelope.createFromProtobuf(data) diff --git a/src/record/peer-record/index.js b/src/record/peer-record/index.js index 51c43a7970..a0421f6ec8 100644 --- a/src/record/peer-record/index.js +++ b/src/record/peer-record/index.js @@ -2,7 +2,6 @@ const multiaddr = require('multiaddr') const PeerId = require('peer-id') -const Record = require('libp2p-interfaces/src/record') const arrayEquals = require('libp2p-utils/src/array-equals') const Protobuf = require('./peer-record.proto') @@ -12,19 +11,27 @@ const { } = require('./consts') /** - * The PeerRecord is used for distributing peer routing records across the network. - * It contains the peer's reachable listen addresses. + * @typedef {import('multiaddr')} Multiaddr + * @typedef {import('libp2p-interfaces/src/record/types').Record} Record */ -class PeerRecord extends Record { + +/** + * @implements {Record} + */ +class PeerRecord { /** + * The PeerRecord is used for distributing peer routing records across the network. + * It contains the peer's reachable listen addresses. + * * @class - * @param {object} params + * @param {Object} params * @param {PeerId} params.peerId - * @param {Array} params.multiaddrs - addresses of the associated peer. + * @param {Multiaddr[]} params.multiaddrs - addresses of the associated peer. * @param {number} [params.seqNumber] - monotonically-increasing sequence counter that's used to order PeerRecords in time. */ constructor ({ peerId, multiaddrs = [], seqNumber = Date.now() }) { - super(ENVELOPE_DOMAIN_PEER_RECORD, ENVELOPE_PAYLOAD_TYPE_PEER_RECORD) + this.domain = ENVELOPE_DOMAIN_PEER_RECORD + this.codec = ENVELOPE_PAYLOAD_TYPE_PEER_RECORD this.peerId = peerId this.multiaddrs = multiaddrs @@ -44,7 +51,7 @@ class PeerRecord extends Record { return this._marshal } - this._marshal = Protobuf.encode({ + this._marshal = Protobuf.PeerRecord.encode({ peer_id: this.peerId.toBytes(), seq: this.seqNumber, addresses: this.multiaddrs.map((m) => ({ @@ -58,10 +65,14 @@ class PeerRecord extends Record { /** * Returns true if `this` record equals the `other`. * - * @param {Record} other + * @param {unknown} other * @returns {boolean} */ equals (other) { + if (!(other instanceof PeerRecord)) { + return false + } + // Validate PeerId if (!this.peerId.equals(other.peerId)) { return false @@ -89,7 +100,7 @@ class PeerRecord extends Record { */ PeerRecord.createFromProtobuf = (buf) => { // Decode - const peerRecord = Protobuf.decode(buf) + const peerRecord = Protobuf.PeerRecord.decode(buf) const peerId = PeerId.createFromBytes(peerRecord.peer_id) const multiaddrs = (peerRecord.addresses || []).map((a) => multiaddr(a.multiaddr)) diff --git a/src/record/peer-record/peer-record.proto.js b/src/record/peer-record/peer-record.proto.js index 9da916ca87..0ebb3b90d0 100644 --- a/src/record/peer-record/peer-record.proto.js +++ b/src/record/peer-record/peer-record.proto.js @@ -7,7 +7,8 @@ const protons = require('protons') // is expected to expand to include other information in the future. // PeerRecords are designed to be serialized to bytes and placed inside of // SignedEnvelopes before sharing with other peers. -const message = ` +/** @type {{PeerRecord: import('../../types').MessageProto}} */ +module.exports = protons(` message PeerRecord { // AddressInfo is a wrapper around a binary multiaddr. It is defined as a // separate message to allow us to add per-address metadata in the future. @@ -24,6 +25,4 @@ message PeerRecord { // addresses is a list of public listen addresses for the peer. repeated AddressInfo addresses = 3; } -` - -module.exports = protons(message).PeerRecord +`) diff --git a/src/record/utils.js b/src/record/utils.js new file mode 100644 index 0000000000..0a92ade177 --- /dev/null +++ b/src/record/utils.js @@ -0,0 +1,25 @@ +'use strict' + +const Envelope = require('./envelope') +const PeerRecord = require('./peer-record') + +/** + * @typedef {import('../')} Libp2p + */ + +/** + * Create (or update if existing) self peer record and store it in the AddressBook. + * + * @param {Libp2p} libp2p + * @returns {Promise} + */ +async function updateSelfPeerRecord (libp2p) { + const peerRecord = new PeerRecord({ + peerId: libp2p.peerId, + multiaddrs: libp2p.multiaddrs + }) + const envelope = await Envelope.seal(peerRecord, libp2p.peerId) + libp2p.peerStore.addressBook.consumePeerRecord(envelope) +} + +module.exports.updateSelfPeerRecord = updateSelfPeerRecord diff --git a/src/registrar.js b/src/registrar.js index 5130a02fcb..1a977cebbe 100644 --- a/src/registrar.js +++ b/src/registrar.js @@ -1,15 +1,23 @@ 'use strict' const debug = require('debug') +const log = Object.assign(debug('libp2p:peer-store'), { + error: debug('libp2p:peer-store:err') +}) const errcode = require('err-code') -const log = debug('libp2p:peer-store') -log.error = debug('libp2p:peer-store:error') const { - ERR_INVALID_PARAMETERS + codes: { ERR_INVALID_PARAMETERS } } = require('./errors') const Topology = require('libp2p-interfaces/src/topology') +/** + * @typedef {import('peer-id')} PeerId + * @typedef {import('./peer-store')} PeerStore + * @typedef {import('./connection-manager')} ConnectionManager + * @typedef {import('libp2p-interfaces/src/connection').Connection} Connection + */ + /** * Responsible for notifying registered protocols of events in the network. */ @@ -17,7 +25,7 @@ class Registrar { /** * @param {Object} props * @param {PeerStore} props.peerStore - * @param {connectionManager} props.connectionManager + * @param {ConnectionManager} props.connectionManager * @class */ constructor ({ peerStore, connectionManager }) { @@ -51,7 +59,7 @@ class Registrar { * Get a connection with a peer. * * @param {PeerId} peerId - * @returns {Connection} + * @returns {Connection | null} */ getConnection (peerId) { return this.connectionManager.get(peerId) @@ -65,11 +73,12 @@ class Registrar { */ register (topology) { if (!Topology.isTopology(topology)) { + log.error('topology must be an instance of interfaces/topology') throw errcode(new Error('topology must be an instance of interfaces/topology'), ERR_INVALID_PARAMETERS) } // Create topology - const id = (parseInt(Math.random() * 1e9)).toString(36) + Date.now() + const id = (Math.random() * 1e9).toString(36) + Date.now() this.topologies.set(id, topology) diff --git a/src/transport-manager.js b/src/transport-manager.js index e18841bf02..6d1326833f 100644 --- a/src/transport-manager.js +++ b/src/transport-manager.js @@ -1,23 +1,39 @@ 'use strict' +const debug = require('debug') +const log = Object.assign(debug('libp2p:transports'), { + error: debug('libp2p:transports:err') +}) + 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') + +const { updateSelfPeerRecord } = require('./record/utils') + +/** + * @typedef {import('multiaddr')} Multiaddr + * @typedef {import('libp2p-interfaces/src/connection').Connection} Connection + * @typedef {import('libp2p-interfaces/src/transport/types').TransportFactory} TransportFactory + * @typedef {import('libp2p-interfaces/src/transport/types').Transport} Transport + * + * @typedef {Object} TransportManagerProperties + * @property {import('./')} libp2p + * @property {import('./upgrader')} upgrader + * + * @typedef {Object} TransportManagerOptions + * @property {number} [faultTolerance = FAULT_TOLERANCE.FATAL_ALL] - Address listen error tolerance. + */ class TransportManager { /** * @class - * @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 {boolean} [options.faultTolerance = FAULT_TOLERANCE.FATAL_ALL] - Address listen error tolerance. + * @param {TransportManagerProperties & TransportManagerOptions} options */ constructor ({ libp2p, upgrader, faultTolerance = FAULT_TOLERANCE.FATAL_ALL }) { this.libp2p = libp2p this.upgrader = upgrader + /** @type {Map} */ this._transports = new Map() this._listeners = new Map() this.faultTolerance = faultTolerance @@ -27,7 +43,7 @@ class TransportManager { * Adds a `Transport` to the manager * * @param {string} key - * @param {Transport} Transport + * @param {TransportFactory} Transport * @param {*} transportOptions - Additional options to pass to the transport * @returns {void} */ @@ -63,6 +79,8 @@ class TransportManager { log('closing listeners for %s', key) while (listeners.length) { const listener = listeners.pop() + listener.removeAllListeners('listening') + listener.removeAllListeners('close') tasks.push(listener.close()) } } @@ -113,7 +131,7 @@ class TransportManager { /** * Returns all the transports instances. * - * @returns {Iterator} + * @returns {IterableIterator} */ getTransports () { return this._transports.values() @@ -137,11 +155,10 @@ class TransportManager { * Starts listeners for each listen Multiaddr. * * @async + * @param {Multiaddr[]} addrs - addresses to attempt to listen on */ - async listen () { - const addrs = this.libp2p.addressManager.getListenAddrs() - - if (addrs.length === 0) { + async listen (addrs) { + if (!addrs || addrs.length === 0) { log('no addresses were provided for listening, this node is dial only') return } @@ -154,9 +171,13 @@ class TransportManager { // 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) + const listener = transport.createListener({}) this._listeners.get(key).push(listener) + // Track listen/close events + listener.on('listening', () => updateSelfPeerRecord(this.libp2p)) + listener.on('close', () => updateSelfPeerRecord(this.libp2p)) + // We need to attempt to listen on everything tasks.push(listener.listen(addr)) } @@ -201,6 +222,8 @@ class TransportManager { if (this._listeners.has(key)) { // Close any running listeners for (const listener of this._listeners.get(key)) { + listener.removeAllListeners('listening') + listener.removeAllListeners('close') await listener.close() } } diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000000..a60d526028 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,103 @@ + +// Insecure Message types +export enum KeyType { + RSA = 0, + Ed25519 = 1, + Secp256k1 = 2, + ECDSA = 3 +} + +// Protobufs +export interface MessageProto { + encode(value: T): Uint8Array + decode(bytes: Uint8Array): T +} + +export type SUCCESS = 100; +export type HOP_SRC_ADDR_TOO_LONG = 220; +export type HOP_DST_ADDR_TOO_LONG = 221; +export type HOP_SRC_MULTIADDR_INVALID = 250; +export type HOP_DST_MULTIADDR_INVALID = 251; +export type HOP_NO_CONN_TO_DST = 260; +export type HOP_CANT_DIAL_DST = 261; +export type HOP_CANT_OPEN_DST_STREAM = 262; +export type HOP_CANT_SPEAK_RELAY = 270; +export type HOP_CANT_RELAY_TO_SELF = 280; +export type STOP_SRC_ADDR_TOO_LONG = 320; +export type STOP_DST_ADDR_TOO_LONG = 321; +export type STOP_SRC_MULTIADDR_INVALID = 350; +export type STOP_DST_MULTIADDR_INVALID = 351; +export type STOP_RELAY_REFUSED = 390; +export type MALFORMED_MESSAGE = 400; + +export type CircuitStatus = SUCCESS | HOP_SRC_ADDR_TOO_LONG | HOP_DST_ADDR_TOO_LONG + | HOP_SRC_MULTIADDR_INVALID | HOP_DST_MULTIADDR_INVALID | HOP_NO_CONN_TO_DST + | HOP_CANT_DIAL_DST | HOP_CANT_OPEN_DST_STREAM | HOP_CANT_SPEAK_RELAY | HOP_CANT_RELAY_TO_SELF + | STOP_SRC_ADDR_TOO_LONG | STOP_DST_ADDR_TOO_LONG | STOP_SRC_MULTIADDR_INVALID + | STOP_DST_MULTIADDR_INVALID | STOP_RELAY_REFUSED | MALFORMED_MESSAGE + +export type HOP = 1; +export type STOP = 2; +export type STATUS = 3; +export type CAN_HOP = 4; + +export type CircuitType = HOP | STOP | STATUS | CAN_HOP + +export type CircuitPeer = { + id: Uint8Array + addrs: Uint8Array[] +} + +export type CircuitRequest = { + type: CircuitType + code?: CircuitStatus + dstPeer: CircuitPeer + srcPeer: CircuitPeer +} + +export type CircuitMessage = { + type?: CircuitType + dstPeer?: CircuitPeer + srcPeer?: CircuitPeer + code?: CircuitStatus +} + +export interface CircuitMessageProto extends MessageProto { + Status: { + SUCCESS: SUCCESS, + HOP_SRC_ADDR_TOO_LONG: HOP_SRC_ADDR_TOO_LONG, + HOP_DST_ADDR_TOO_LONG: HOP_DST_ADDR_TOO_LONG, + HOP_SRC_MULTIADDR_INVALID: HOP_SRC_MULTIADDR_INVALID, + HOP_DST_MULTIADDR_INVALID: HOP_DST_MULTIADDR_INVALID, + HOP_NO_CONN_TO_DST: HOP_NO_CONN_TO_DST, + HOP_CANT_DIAL_DST: HOP_CANT_DIAL_DST, + HOP_CANT_OPEN_DST_STREAM: HOP_CANT_OPEN_DST_STREAM, + HOP_CANT_SPEAK_RELAY: HOP_CANT_SPEAK_RELAY, + HOP_CANT_RELAY_TO_SELF: HOP_CANT_RELAY_TO_SELF, + STOP_SRC_ADDR_TOO_LONG: STOP_SRC_ADDR_TOO_LONG, + STOP_DST_ADDR_TOO_LONG: STOP_DST_ADDR_TOO_LONG, + STOP_SRC_MULTIADDR_INVALID: STOP_SRC_MULTIADDR_INVALID, + STOP_DST_MULTIADDR_INVALID: STOP_DST_MULTIADDR_INVALID, + STOP_RELAY_REFUSED: STOP_RELAY_REFUSED, + MALFORMED_MESSAGE: MALFORMED_MESSAGE + }, + Type: { + HOP: HOP, + STOP: STOP, + STATUS: STATUS, + CAN_HOP: CAN_HOP + } +} + +export type Exchange = { + id: Uint8Array + pubkey: PublicKey +} +export type ExchangeProto = MessageProto + +export type PublicKey = { + Type: KeyType, + Data: Uint8Array +} + +export type PublicKeyProto = MessageProto diff --git a/src/upgrader.js b/src/upgrader.js index 92997eb15f..e087ddee6e 100644 --- a/src/upgrader.js +++ b/src/upgrader.js @@ -1,29 +1,30 @@ 'use strict' const debug = require('debug') -const log = debug('libp2p:upgrader') -log.error = debug('libp2p:upgrader:error') +const log = Object.assign(debug('libp2p:upgrader'), { + error: debug('libp2p:upgrader:err') +}) +const errCode = require('err-code') const Multistream = require('multistream-select') -const { Connection } = require('libp2p-interfaces/src/connection') -const ConnectionStatus = require('libp2p-interfaces/src/connection/status') +const Connection = require('libp2p-interfaces/src/connection/connection') const PeerId = require('peer-id') -const pipe = require('it-pipe') -const errCode = require('err-code') +const { pipe } = require('it-pipe') const mutableProxy = require('mutable-proxy') const { codes } = require('./errors') /** - * @typedef MultiaddrConnection - * @property {Function} sink - * @property {AsyncIterator} source - * @property {*} conn - * @property {Multiaddr} remoteAddr + * @typedef {import('libp2p-interfaces/src/transport/types').MultiaddrConnection} MultiaddrConnection + * @typedef {import('libp2p-interfaces/src/stream-muxer/types').MuxerFactory} MuxerFactory + * @typedef {import('libp2p-interfaces/src/stream-muxer/types').Muxer} Muxer + * @typedef {import('libp2p-interfaces/src/stream-muxer/types').MuxedStream} MuxedStream + * @typedef {import('libp2p-interfaces/src/crypto/types').Crypto} Crypto + * @typedef {import('multiaddr')} Multiaddr */ /** * @typedef CryptoResult - * @property {*} conn A duplex iterable + * @property {MultiaddrConnection} conn A duplex iterable * @property {PeerId} remotePeer * @property {string} protocol */ @@ -32,24 +33,24 @@ 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 - * @param {function(Connection)} options.onConnectionEnd + * @param {import('./metrics')} [options.metrics] + * @param {Map} [options.cryptos] + * @param {Map} [options.muxers] + * @param {(Connection) => void} options.onConnection - Called when a connection is upgraded + * @param {(Connection) => void} options.onConnectionEnd */ constructor ({ localPeer, metrics, - cryptos, - muxers, + cryptos = new Map(), + muxers = new Map(), onConnectionEnd = () => {}, onConnection = () => {} }) { this.localPeer = localPeer this.metrics = metrics - this.cryptos = cryptos || new Map() - this.muxers = muxers || new Map() + this.cryptos = cryptos + this.muxers = muxers this.protector = null this.protocols = new Map() this.onConnection = onConnection @@ -74,7 +75,7 @@ class Upgrader { if (this.metrics) { ({ setTarget: setPeer, proxy: proxyPeer } = mutableProxy()) - const idString = (parseInt(Math.random() * 1e9)).toString(36) + Date.now() + const idString = (Math.random() * 1e9).toString(36) + Date.now() setPeer({ toB58String: () => idString }) maConn = this.metrics.trackStream({ stream: maConn, remotePeer: proxyPeer }) } @@ -132,12 +133,7 @@ class Upgrader { * @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) - } + const remotePeerId = PeerId.createFromB58String(maConn.remoteAddr.getPeerId()) let encryptedConn let remotePeer @@ -149,7 +145,7 @@ class Upgrader { if (this.metrics) { ({ setTarget: setPeer, proxy: proxyPeer } = mutableProxy()) - const idString = (parseInt(Math.random() * 1e9)).toString(36) + Date.now() + const idString = (Math.random() * 1e9).toString(36) + Date.now() setPeer({ toB58String: () => idString }) maConn = this.metrics.trackStream({ stream: maConn, remotePeer: proxyPeer }) } @@ -207,8 +203,8 @@ class Upgrader { * @param {string} options.cryptoProtocol - The crypto protocol that was negotiated * @param {string} options.direction - One of ['inbound', 'outbound'] * @param {MultiaddrConnection} options.maConn - The transport layer connection - * @param {*} options.upgradedConn - A duplex connection returned from multiplexer and/or crypto selection - * @param {Muxer} options.Muxer - The muxer to be used for muxing + * @param {MuxedStream | MultiaddrConnection} options.upgradedConn - A duplex connection returned from multiplexer and/or crypto selection + * @param {MuxerFactory} [options.Muxer] - The muxer to be used for muxing * @param {PeerId} options.remotePeer - The peer the connection is with * @returns {Connection} */ @@ -222,8 +218,8 @@ class Upgrader { }) { let muxer let newStream - // eslint-disable-next-line prefer-const - let connection + /** @type {Connection} */ + let connection // eslint-disable-line prefer-const if (Muxer) { // Create the muxer @@ -272,7 +268,7 @@ class Upgrader { // Wait for close to finish before notifying of the closure (async () => { try { - if (connection.stat.status === ConnectionStatus.OPEN) { + if (connection.stat.status === 'open') { await connection.close() } } catch (err) { @@ -300,6 +296,7 @@ class Upgrader { remotePeer: remotePeer, stat: { direction, + // @ts-ignore timeline: maConn.timeline, multiplexer: Muxer && Muxer.multicodec, encryption: cryptoProtocol @@ -326,7 +323,7 @@ class Upgrader { * @private * @param {object} options * @param {Connection} options.connection - The connection the stream belongs to - * @param {Stream} options.stream + * @param {MuxedStream} options.stream * @param {string} options.protocol */ _onStream ({ connection, stream, protocol }) { @@ -342,7 +339,7 @@ class Upgrader { * @param {PeerId} localPeer - The initiators PeerId * @param {*} connection * @param {Map} cryptos - * @returns {CryptoResult} An encrypted connection, remote peer `PeerId` and the protocol of the `Crypto` used + * @returns {Promise} An encrypted connection, remote peer `PeerId` and the protocol of the `Crypto` used */ async _encryptInbound (localPeer, connection, cryptos) { const mss = new Multistream.Listener(connection) @@ -354,6 +351,10 @@ class Upgrader { const crypto = cryptos.get(protocol) log('encrypting inbound connection...') + if (!crypto) { + throw new Error(`no crypto module found for ${protocol}`) + } + return { ...await crypto.secureInbound(localPeer, stream), protocol @@ -373,7 +374,7 @@ class Upgrader { * @param {*} connection * @param {PeerId} remotePeerId * @param {Map} cryptos - * @returns {CryptoResult} An encrypted connection, remote peer `PeerId` and the protocol of the `Crypto` used + * @returns {Promise} 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) @@ -385,6 +386,10 @@ class Upgrader { const crypto = cryptos.get(protocol) log('encrypting outbound connection to %j', remotePeerId) + if (!crypto) { + throw new Error(`no crypto module found for ${protocol}`) + } + return { ...await crypto.secureOutbound(localPeer, stream, remotePeerId), protocol @@ -400,9 +405,9 @@ class Upgrader { * * @private * @async - * @param {*} connection - A basic duplex connection to multiplex - * @param {Map} muxers - The muxers to attempt multiplexing with - * @returns {*} A muxed connection + * @param {MultiaddrConnection} connection - A basic duplex connection to multiplex + * @param {Map} muxers - The muxers to attempt multiplexing with + * @returns {Promise<{ stream: MuxedStream, Muxer?: MuxerFactory}>} A muxed connection */ async _multiplexOutbound (connection, muxers) { const dialer = new Multistream.Dialer(connection) @@ -424,9 +429,9 @@ class Upgrader { * * @private * @async - * @param {*} connection - A basic duplex connection to multiplex - * @param {Map} muxers - The muxers to attempt multiplexing with - * @returns {*} A muxed connection + * @param {MultiaddrConnection} connection - A basic duplex connection to multiplex + * @param {Map} muxers - The muxers to attempt multiplexing with + * @returns {Promise<{ stream: MuxedStream, Muxer?: MuxerFactory}>} A muxed connection */ async _multiplexInbound (connection, muxers) { const listener = new Multistream.Listener(connection) diff --git a/test/addresses/address-manager.spec.js b/test/addresses/address-manager.spec.js index 3e0d0efdc3..4d58387a23 100644 --- a/test/addresses/address-manager.spec.js +++ b/test/addresses/address-manager.spec.js @@ -1,11 +1,7 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) -chai.use(require('chai-as-promised')) -const { expect } = chai - +const { expect } = require('aegir/utils/chai') const multiaddr = require('multiaddr') const AddressManager = require('../../src/address-manager') @@ -20,7 +16,6 @@ describe('Address Manager', () => { expect(am.listen.size).to.equal(0) expect(am.announce.size).to.equal(0) - expect(am.noAnnounce.size).to.equal(0) }) it('should return listen multiaddrs on get', () => { @@ -30,7 +25,6 @@ describe('Address Manager', () => { expect(am.listen.size).to.equal(listenAddresses.length) expect(am.announce.size).to.equal(0) - expect(am.noAnnounce.size).to.equal(0) const listenMultiaddrs = am.getListenAddrs() expect(listenMultiaddrs.length).to.equal(2) @@ -46,28 +40,11 @@ describe('Address Manager', () => { expect(am.listen.size).to.equal(listenAddresses.length) expect(am.announce.size).to.equal(announceAddreses.length) - expect(am.noAnnounce.size).to.equal(0) const announceMultiaddrs = am.getAnnounceAddrs() expect(announceMultiaddrs.length).to.equal(1) expect(announceMultiaddrs[0].equals(multiaddr(announceAddreses[0]))).to.equal(true) }) - - it('should return noAnnounce multiaddrs on get', () => { - const am = new AddressManager({ - listen: listenAddresses, - noAnnounce: listenAddresses - }) - - expect(am.listen.size).to.equal(listenAddresses.length) - expect(am.announce.size).to.equal(0) - expect(am.noAnnounce.size).to.equal(listenAddresses.length) - - const noAnnounceMultiaddrs = am.getNoAnnounceAddrs() - expect(noAnnounceMultiaddrs.length).to.equal(2) - expect(noAnnounceMultiaddrs[0].equals(multiaddr(listenAddresses[0]))).to.equal(true) - expect(noAnnounceMultiaddrs[1].equals(multiaddr(listenAddresses[1]))).to.equal(true) - }) }) describe('libp2p.addressManager', () => { @@ -80,14 +57,12 @@ describe('libp2p.addressManager', () => { config: { addresses: { listen: listenAddresses, - announce: announceAddreses, - noAnnounce: listenAddresses + announce: announceAddreses } } }) expect(libp2p.addressManager.listen.size).to.equal(listenAddresses.length) expect(libp2p.addressManager.announce.size).to.equal(announceAddreses.length) - expect(libp2p.addressManager.noAnnounce.size).to.equal(listenAddresses.length) }) }) diff --git a/test/addresses/addresses.node.js b/test/addresses/addresses.node.js index 8993fe1c54..2d6bceade5 100644 --- a/test/addresses/addresses.node.js +++ b/test/addresses/addresses.node.js @@ -1,17 +1,17 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) -chai.use(require('chai-as-promised')) -const { expect } = chai +const { expect } = require('aegir/utils/chai') const sinon = require('sinon') +const multiaddr = require('multiaddr') +const isLoopback = require('libp2p-utils/src/multiaddr/is-loopback') + const { AddressesOptions } = require('./utils') const peerUtils = require('../utils/creators/peer') const listenAddresses = ['/ip4/127.0.0.1/tcp/0', '/ip4/127.0.0.1/tcp/8000/ws'] -const announceAddreses = ['/dns4/peer.io'] +const announceAddreses = ['/dns4/peer.io/tcp/433/p2p/12D3KooWNvSZnPi3RrhrTwEY4LuuBeB6K6facKUCJcyWG1aoDd2p'] describe('libp2p.multiaddrs', () => { let libp2p @@ -45,82 +45,106 @@ describe('libp2p.multiaddrs', () => { expect(listenAddrs.has(listenAddresses[1])).to.equal(true) }) - it('should advertise all addresses if noAnnounce addresses are not provided, but with correct ports', async () => { + it('should announce transport listen addresses if announce addresses are not provided', async () => { [libp2p] = await peerUtils.createPeer({ + started: false, config: { ...AddressesOptions, addresses: { - listen: listenAddresses, - announce: announceAddreses + listen: listenAddresses } } }) - const tmListen = libp2p.transportManager.getAddrs().map((ma) => ma.toString()) + await libp2p.start() - const spyAnnounce = sinon.spy(libp2p.addressManager, 'getAnnounceAddrs') - const spyNoAnnounce = sinon.spy(libp2p.addressManager, 'getNoAnnounceAddrs') - const spyListen = sinon.spy(libp2p.addressManager, 'getListenAddrs') - const spyTranspMgr = sinon.spy(libp2p.transportManager, 'getAddrs') + const tmListen = libp2p.transportManager.getAddrs().map((ma) => ma.toString()) + // Announce 2 listen (transport) const advertiseMultiaddrs = libp2p.multiaddrs.map((ma) => ma.toString()) - - expect(spyAnnounce).to.have.property('callCount', 1) - expect(spyNoAnnounce).to.have.property('callCount', 1) - expect(spyListen).to.have.property('callCount', 0) // Listen addr should not be used - expect(spyTranspMgr).to.have.property('callCount', 1) - - // Announce 2 listen (transport) + 1 announce - expect(advertiseMultiaddrs.length).to.equal(3) + expect(advertiseMultiaddrs.length).to.equal(2) tmListen.forEach((m) => { expect(advertiseMultiaddrs).to.include(m) }) - announceAddreses.forEach((m) => { - expect(advertiseMultiaddrs).to.include(m) - }) expect(advertiseMultiaddrs).to.not.include(listenAddresses[0]) // Random Port switch }) - it('should remove replicated addresses', async () => { + it('should only announce the given announce addresses when provided', async () => { [libp2p] = await peerUtils.createPeer({ + started: false, config: { ...AddressesOptions, addresses: { listen: listenAddresses, - announce: [listenAddresses[1]] + announce: announceAddreses } } }) + await libp2p.start() + + const tmListen = libp2p.transportManager.getAddrs().map((ma) => ma.toString()) + + // Announce 1 announce addr const advertiseMultiaddrs = libp2p.multiaddrs.map((ma) => ma.toString()) + expect(advertiseMultiaddrs.length).to.equal(announceAddreses.length) + advertiseMultiaddrs.forEach((m) => { + expect(tmListen).to.not.include(m) + expect(announceAddreses).to.include(m) + }) + }) - // Announce 2 listen (transport), ignoring duplicated in announce - expect(advertiseMultiaddrs.length).to.equal(2) + it('can filter out loopback addresses by the announce filter', async () => { + [libp2p] = await peerUtils.createPeer({ + started: false, + config: { + ...AddressesOptions, + addresses: { + listen: listenAddresses, + announceFilter: (multiaddrs) => multiaddrs.filter(m => !isLoopback(m)) + } + } + }) + + await libp2p.start() + + expect(libp2p.multiaddrs.length).to.equal(0) + + // Stub transportManager addresses to add a public address + const stubMa = multiaddr('/ip4/120.220.10.1/tcp/1000') + sinon.stub(libp2p.transportManager, 'getAddrs').returns([ + ...listenAddresses.map((a) => multiaddr(a)), + stubMa + ]) + + const multiaddrs = libp2p.multiaddrs + expect(multiaddrs.length).to.equal(1) + expect(multiaddrs[0].equals(stubMa)).to.eql(true) }) - it('should not advertise noAnnounce addresses', async () => { - const noAnnounce = [listenAddresses[1]] - ;[libp2p] = await peerUtils.createPeer({ + it('can filter out loopback addresses to announced by the announce filter', async () => { + [libp2p] = await peerUtils.createPeer({ + started: false, config: { ...AddressesOptions, addresses: { listen: listenAddresses, announce: announceAddreses, - noAnnounce + announceFilter: (multiaddrs) => multiaddrs.filter(m => !isLoopback(m)) } } }) - const advertiseMultiaddrs = libp2p.multiaddrs.map((ma) => ma.toString()) + const listenAddrs = libp2p.addressManager.listen + expect(listenAddrs.size).to.equal(listenAddresses.length) + expect(listenAddrs.has(listenAddresses[0])).to.equal(true) + expect(listenAddrs.has(listenAddresses[1])).to.equal(true) - // Announce 1 listen (transport) not in the noAnnounce and the announce - expect(advertiseMultiaddrs.length).to.equal(2) + await libp2p.start() - announceAddreses.forEach((m) => { - expect(advertiseMultiaddrs).to.include(m) - }) - noAnnounce.forEach((m) => { - expect(advertiseMultiaddrs).to.not.include(m) - }) + const multiaddrs = libp2p.multiaddrs + expect(multiaddrs.length).to.equal(announceAddreses.length) + expect(multiaddrs.includes(listenAddresses[0])).to.equal(false) + expect(multiaddrs.includes(listenAddresses[1])).to.equal(false) }) }) diff --git a/test/connection-manager/index.node.js b/test/connection-manager/index.node.js index f7820e88f0..8aa55f5830 100644 --- a/test/connection-manager/index.node.js +++ b/test/connection-manager/index.node.js @@ -1,10 +1,7 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) -chai.use(require('chai-as-promised')) -const { expect } = chai +const { expect } = require('aegir/utils/chai') const sinon = require('sinon') const delay = require('delay') diff --git a/test/connection-manager/index.spec.js b/test/connection-manager/index.spec.js index caf6becb8a..77e0934c64 100644 --- a/test/connection-manager/index.spec.js +++ b/test/connection-manager/index.spec.js @@ -1,10 +1,7 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) -chai.use(require('chai-as-promised')) -const { expect } = chai +const { expect } = require('aegir/utils/chai') const sinon = require('sinon') const peerUtils = require('../utils/creators/peer') diff --git a/test/content-routing/content-routing.node.js b/test/content-routing/content-routing.node.js index 17850e9924..1bef142d22 100644 --- a/test/content-routing/content-routing.node.js +++ b/test/content-routing/content-routing.node.js @@ -1,10 +1,7 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) -chai.use(require('chai-as-promised')) -const { expect } = chai +const { expect } = require('aegir/utils/chai') const nock = require('nock') const sinon = require('sinon') diff --git a/test/content-routing/dht/configuration.node.js b/test/content-routing/dht/configuration.node.js index e4a0bda929..9213d6e22d 100644 --- a/test/content-routing/dht/configuration.node.js +++ b/test/content-routing/dht/configuration.node.js @@ -1,10 +1,7 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) -const { expect } = chai - +const { expect } = require('aegir/utils/chai') const mergeOptions = require('merge-options') const { create } = require('../../../src') diff --git a/test/core/encryption.spec.js b/test/core/encryption.spec.js index 1173828568..bac84be800 100644 --- a/test/core/encryption.spec.js +++ b/test/core/encryption.spec.js @@ -1,10 +1,7 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) -chai.use(require('chai-as-promised')) -const { expect } = chai +const { expect } = require('aegir/utils/chai') const Transport = require('libp2p-websockets') const { NOISE: Crypto } = require('libp2p-noise') diff --git a/test/core/listening.node.js b/test/core/listening.node.js index 46976733c5..fe202b1ab1 100644 --- a/test/core/listening.node.js +++ b/test/core/listening.node.js @@ -1,9 +1,7 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) -const { expect } = chai +const { expect } = require('aegir/utils/chai') const Transport = require('libp2p-tcp') const { NOISE: Crypto } = require('libp2p-noise') diff --git a/test/core/ping.node.js b/test/core/ping.node.js index 2438758f69..510e5e3cb7 100644 --- a/test/core/ping.node.js +++ b/test/core/ping.node.js @@ -1,9 +1,7 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) -const { expect } = chai +const { expect } = require('aegir/utils/chai') const pTimes = require('p-times') const pipe = require('it-pipe') diff --git a/test/dialing/dial-request.spec.js b/test/dialing/dial-request.spec.js index f88db77275..fd56620a00 100644 --- a/test/dialing/dial-request.spec.js +++ b/test/dialing/dial-request.spec.js @@ -1,10 +1,7 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) -chai.use(require('chai-as-promised')) -const { expect } = chai +const { expect } = require('aegir/utils/chai') const sinon = require('sinon') const { AbortError } = require('libp2p-interfaces/src/transport/errors') @@ -13,7 +10,7 @@ const AggregateError = require('aggregate-error') const pDefer = require('p-defer') const delay = require('delay') -const { DialRequest } = require('../../src/dialer/dial-request') +const DialRequest = require('../../src/dialer/dial-request') const createMockConnection = require('../utils/mockConnection') const error = new Error('dial failes') diff --git a/test/dialing/direct.node.js b/test/dialing/direct.node.js index 6b89fee4af..0d9d1dd718 100644 --- a/test/dialing/direct.node.js +++ b/test/dialing/direct.node.js @@ -42,21 +42,28 @@ describe('Dialing (direct, TCP)', () => { let peerStore let remoteAddr - before(async () => { - const [remotePeerId] = await Promise.all([ - PeerId.createFromJSON(Peers[0]) + beforeEach(async () => { + const [localPeerId, remotePeerId] = await Promise.all([ + PeerId.createFromJSON(Peers[0]), + PeerId.createFromJSON(Peers[1]) ]) + + peerStore = new PeerStore({ peerId: remotePeerId }) remoteTM = new TransportManager({ libp2p: { - addressManager: new AddressManager({ listen: [listenAddr] }) + addressManager: new AddressManager({ listen: [listenAddr] }), + peerId: remotePeerId, + peerStore }, upgrader: mockUpgrader }) remoteTM.add(Transport.prototype[Symbol.toStringTag], Transport) - peerStore = new PeerStore({ peerId: remotePeerId }) localTM = new TransportManager({ - libp2p: {}, + libp2p: { + peerId: localPeerId, + peerStore: new PeerStore({ peerId: localPeerId }) + }, upgrader: mockUpgrader }) localTM.add(Transport.prototype[Symbol.toStringTag], Transport) @@ -66,7 +73,7 @@ describe('Dialing (direct, TCP)', () => { remoteAddr = remoteTM.getAddrs()[0].encapsulate(`/p2p/${remotePeerId.toB58String()}`) }) - after(() => remoteTM.close()) + afterEach(() => remoteTM.close()) afterEach(() => { sinon.restore() @@ -112,7 +119,7 @@ describe('Dialing (direct, TCP)', () => { peerStore }) - peerStore.addressBook.set(peerId, [remoteAddr]) + peerStore.addressBook.set(peerId, remoteTM.getAddrs()) const connection = await dialer.connectToPeer(peerId) expect(connection).to.exist() diff --git a/test/dialing/direct.spec.js b/test/dialing/direct.spec.js index 540f528b65..525558f507 100644 --- a/test/dialing/direct.spec.js +++ b/test/dialing/direct.spec.js @@ -1,15 +1,13 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) -chai.use(require('chai-as-promised')) -const { expect } = chai +const { expect } = require('aegir/utils/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 filters = require('libp2p-websockets/src/filters') const Muxer = require('libp2p-mplex') const { NOISE: Crypto } = require('libp2p-noise') const multiaddr = require('multiaddr') @@ -19,6 +17,7 @@ const { AbortError } = require('libp2p-interfaces/src/transport/errors') const { codes: ErrorCodes } = require('../../src/errors') const Constants = require('../../src/constants') const Dialer = require('../../src/dialer') +const addressSort = require('libp2p-utils/src/address-sort') const PeerStore = require('../../src/peer-store') const TransportManager = require('../../src/transport-manager') const Libp2p = require('../../src') @@ -43,10 +42,11 @@ describe('Dialing (direct, WebSockets)', () => { upgrader: mockUpgrader, onConnection: () => {} }) - localTM.add(Transport.prototype[Symbol.toStringTag], Transport) + localTM.add(Transport.prototype[Symbol.toStringTag], Transport, { filter: filters.all }) }) afterEach(() => { + peerStore.delete(peerId) sinon.restore() }) @@ -179,6 +179,37 @@ describe('Dialing (direct, WebSockets)', () => { .and.to.have.property('code', ErrorCodes.ERR_TIMEOUT) }) + it('should sort addresses on dial', async () => { + const peerMultiaddrs = [ + multiaddr('/ip4/127.0.0.1/tcp/15001/ws'), + multiaddr('/ip4/20.0.0.1/tcp/15001/ws'), + multiaddr('/ip4/30.0.0.1/tcp/15001/ws') + ] + + sinon.spy(addressSort, 'publicAddressesFirst') + sinon.stub(localTM, 'dial').callsFake(createMockConnection) + + const dialer = new Dialer({ + transportManager: localTM, + addressSorter: addressSort.publicAddressesFirst, + concurrency: 3, + peerStore + }) + + // Inject data in the AddressBook + peerStore.addressBook.add(peerId, peerMultiaddrs) + + // Perform 3 multiaddr dials + await dialer.connectToPeer(peerId) + + expect(addressSort.publicAddressesFirst.callCount).to.eql(1) + + const sortedAddresses = addressSort.publicAddressesFirst(peerMultiaddrs.map((m) => ({ multiaddr: m }))) + expect(localTM.dial.getCall(0).args[0].equals(sortedAddresses[0].multiaddr)) + expect(localTM.dial.getCall(1).args[0].equals(sortedAddresses[1].multiaddr)) + expect(localTM.dial.getCall(2).args[0].equals(sortedAddresses[2].multiaddr)) + }) + it('should dial to the max concurrency', async () => { const dialer = new Dialer({ transportManager: localTM, @@ -262,6 +293,7 @@ describe('Dialing (direct, WebSockets)', () => { }) describe('libp2p.dialer', () => { + const transportKey = Transport.prototype[Symbol.toStringTag] let libp2p afterEach(async () => { @@ -277,6 +309,13 @@ describe('Dialing (direct, WebSockets)', () => { transport: [Transport], streamMuxer: [Muxer], connEncryption: [Crypto] + }, + config: { + transport: { + [transportKey]: { + filter: filters.all + } + } } }) @@ -300,6 +339,13 @@ describe('Dialing (direct, WebSockets)', () => { maxParallelDials: 10, maxDialsPerPeer: 1, dialTimeout: 1e3 // 30 second dial timeout per peer + }, + config: { + transport: { + [transportKey]: { + filter: filters.all + } + } } } libp2p = await Libp2p.create(config) @@ -317,6 +363,13 @@ describe('Dialing (direct, WebSockets)', () => { transport: [Transport], streamMuxer: [Muxer], connEncryption: [Crypto] + }, + config: { + transport: { + [transportKey]: { + filter: filters.all + } + } } }) @@ -340,6 +393,13 @@ describe('Dialing (direct, WebSockets)', () => { transport: [Transport], streamMuxer: [Muxer], connEncryption: [Crypto] + }, + config: { + transport: { + [transportKey]: { + filter: filters.all + } + } } }) @@ -349,7 +409,6 @@ describe('Dialing (direct, WebSockets)', () => { const connection = await libp2p.dial(remoteAddr) expect(connection).to.exist() - sinon.spy(libp2p.peerStore.addressBook, 'consumePeerRecord') sinon.spy(libp2p.peerStore.protoBook, 'set') // Wait for onConnection to be called @@ -358,8 +417,6 @@ describe('Dialing (direct, WebSockets)', () => { expect(libp2p.identifyService.identify.callCount).to.equal(1) await libp2p.identifyService.identify.firstCall.returnValue - // Self + New peer - expect(libp2p.peerStore.addressBook.consumePeerRecord.callCount).to.equal(2) expect(libp2p.peerStore.protoBook.set.callCount).to.equal(1) }) @@ -370,6 +427,13 @@ describe('Dialing (direct, WebSockets)', () => { transport: [Transport], streamMuxer: [Muxer], connEncryption: [Crypto] + }, + config: { + transport: { + [transportKey]: { + filter: filters.all + } + } } }) @@ -387,6 +451,13 @@ describe('Dialing (direct, WebSockets)', () => { transport: [Transport], streamMuxer: [Muxer], connEncryption: [Crypto] + }, + config: { + transport: { + [transportKey]: { + filter: filters.all + } + } } }) @@ -400,6 +471,13 @@ describe('Dialing (direct, WebSockets)', () => { transport: [Transport], streamMuxer: [Muxer], connEncryption: [Crypto] + }, + config: { + transport: { + [transportKey]: { + filter: filters.all + } + } } }) diff --git a/test/dialing/resolver.spec.js b/test/dialing/resolver.spec.js index ba81a5c8b2..094f657a6d 100644 --- a/test/dialing/resolver.spec.js +++ b/test/dialing/resolver.spec.js @@ -37,11 +37,12 @@ describe('Dialing (resolvable addresses)', () => { [libp2p, remoteLibp2p] = await peerUtils.createPeer({ number: 2, config: { - modules: baseOptions.modules, + ...baseOptions, addresses: { listen: [multiaddr(`${relayAddr}/p2p-circuit`)] }, config: { + ...baseOptions.config, peerDiscovery: { autoDial: false } diff --git a/test/identify/index.spec.js b/test/identify/index.spec.js index 1ccbf67122..cccd071871 100644 --- a/test/identify/index.spec.js +++ b/test/identify/index.spec.js @@ -1,14 +1,10 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) -chai.use(require('chai-as-promised')) -const { expect } = chai +const { expect } = require('aegir/utils/chai') const sinon = require('sinon') const { EventEmitter } = require('events') -const delay = require('delay') const PeerId = require('peer-id') const duplexPair = require('it-pair/duplex') const multiaddr = require('multiaddr') @@ -16,12 +12,14 @@ const pWaitFor = require('p-wait-for') const unit8ArrayToString = require('uint8arrays/to-string') const { codes: Errors } = require('../../src/errors') -const { IdentifyService, multicodecs } = require('../../src/identify') +const IdentifyService = require('../../src/identify') +const multicodecs = IdentifyService.multicodecs const Peers = require('../fixtures/peers') const Libp2p = require('../../src') const Envelope = require('../../src/record/envelope') const PeerStore = require('../../src/peer-store') const baseOptions = require('../utils/base-options.browser') +const { updateSelfPeerRecord } = require('../../src/record/utils') const pkg = require('../../package.json') const { MULTIADDRS_WEBSOCKETS } = require('../fixtures/browser') @@ -29,18 +27,21 @@ const remoteAddr = MULTIADDRS_WEBSOCKETS[0] const listenMaddrs = [multiaddr('/ip4/127.0.0.1/tcp/15002/ws')] describe('Identify', () => { - let localPeer - let remotePeer - const protocols = new Map([ - [multicodecs.IDENTIFY, () => {}], - [multicodecs.IDENTIFY_PUSH, () => {}] - ]) + let localPeer, localPeerStore + let remotePeer, remotePeerStore + const protocols = [multicodecs.IDENTIFY, multicodecs.IDENTIFY_PUSH] before(async () => { [localPeer, remotePeer] = (await Promise.all([ PeerId.createFromJSON(Peers[0]), PeerId.createFromJSON(Peers[1]) ])) + + localPeerStore = new PeerStore({ peerId: localPeer }) + localPeerStore.protoBook.set(localPeer, protocols) + + remotePeerStore = new PeerStore({ peerId: remotePeer }) + remotePeerStore.protoBook.set(remotePeer, protocols) }) afterEach(() => { @@ -52,20 +53,19 @@ describe('Identify', () => { libp2p: { peerId: localPeer, connectionManager: new EventEmitter(), - peerStore: new PeerStore({ peerId: localPeer }), - multiaddrs: listenMaddrs - }, - protocols + peerStore: localPeerStore, + multiaddrs: listenMaddrs, + isStarted: () => true + } }) - const remoteIdentify = new IdentifyService({ libp2p: { peerId: remotePeer, connectionManager: new EventEmitter(), - peerStore: new PeerStore({ peerId: remotePeer }), - multiaddrs: listenMaddrs - }, - protocols + peerStore: remotePeerStore, + multiaddrs: listenMaddrs, + isStarted: () => true + } }) const observedAddr = multiaddr('/ip4/127.0.0.1/tcp/1234') @@ -78,6 +78,9 @@ describe('Identify', () => { sinon.spy(localIdentify.peerStore.addressBook, 'consumePeerRecord') sinon.spy(localIdentify.peerStore.protoBook, 'set') + // Transport Manager creates signed peer record + await updateSelfPeerRecord(remoteIdentify._libp2p) + // Run identify await Promise.all([ localIdentify.identify(localConnectionMock), @@ -105,20 +108,20 @@ describe('Identify', () => { libp2p: { peerId: localPeer, connectionManager: new EventEmitter(), - peerStore: new PeerStore({ peerId: localPeer }), - multiaddrs: listenMaddrs - }, - protocols + peerStore: localPeerStore, + multiaddrs: listenMaddrs, + isStarted: () => true + } }) const remoteIdentify = new IdentifyService({ libp2p: { peerId: remotePeer, connectionManager: new EventEmitter(), - peerStore: new PeerStore({ peerId: remotePeer }), - multiaddrs: listenMaddrs - }, - protocols + peerStore: remotePeerStore, + multiaddrs: listenMaddrs, + isStarted: () => true + } }) const observedAddr = multiaddr('/ip4/127.0.0.1/tcp/1234') @@ -164,19 +167,17 @@ describe('Identify', () => { libp2p: { peerId: localPeer, connectionManager: new EventEmitter(), - peerStore: new PeerStore({ peerId: localPeer }), + peerStore: localPeerStore, multiaddrs: [] - }, - protocols + } }) const remoteIdentify = new IdentifyService({ libp2p: { peerId: remotePeer, connectionManager: new EventEmitter(), - peerStore: new PeerStore({ peerId: remotePeer }), + peerStore: remotePeerStore, multiaddrs: [] - }, - protocols + } }) const observedAddr = multiaddr('/ip4/127.0.0.1/tcp/1234') @@ -203,33 +204,38 @@ describe('Identify', () => { describe('push', () => { it('should be able to push identify updates to another peer', async () => { + const storedProtocols = [multicodecs.IDENTIFY, multicodecs.IDENTIFY_PUSH, '/echo/1.0.0'] const connectionManager = new EventEmitter() connectionManager.getConnection = () => { } + const localPeerStore = new PeerStore({ peerId: localPeer }) + localPeerStore.protoBook.set(localPeer, storedProtocols) + const localIdentify = new IdentifyService({ libp2p: { peerId: localPeer, connectionManager: new EventEmitter(), - peerStore: new PeerStore({ peerId: localPeer }), - multiaddrs: listenMaddrs - }, - protocols: new Map([ - [multicodecs.IDENTIFY], - [multicodecs.IDENTIFY_PUSH], - ['/echo/1.0.0'] - ]) + peerStore: localPeerStore, + multiaddrs: listenMaddrs, + isStarted: () => true + } }) + + const remotePeerStore = new PeerStore({ peerId: remotePeer }) + remotePeerStore.protoBook.set(remotePeer, storedProtocols) + const remoteIdentify = new IdentifyService({ libp2p: { peerId: remotePeer, connectionManager, - peerStore: new PeerStore({ peerId: remotePeer }), - multiaddrs: [] + peerStore: remotePeerStore, + multiaddrs: [], + isStarted: () => true } }) // Setup peer protocols and multiaddrs - const localProtocols = new Set([multicodecs.IDENTIFY, multicodecs.IDENTIFY_PUSH, '/echo/1.0.0']) + const localProtocols = new Set(storedProtocols) const localConnectionMock = { newStream: () => { } } const remoteConnectionMock = { remotePeer: localPeer } @@ -239,6 +245,10 @@ describe('Identify', () => { sinon.spy(remoteIdentify.peerStore.addressBook, 'consumePeerRecord') sinon.spy(remoteIdentify.peerStore.protoBook, 'set') + // Transport Manager creates signed peer record + await updateSelfPeerRecord(localIdentify._libp2p) + await updateSelfPeerRecord(remoteIdentify._libp2p) + // Run identify await Promise.all([ localIdentify.push([localConnectionMock]), @@ -249,7 +259,7 @@ describe('Identify', () => { }) ]) - expect(remoteIdentify.peerStore.addressBook.consumePeerRecord.callCount).to.equal(1) + expect(remoteIdentify.peerStore.addressBook.consumePeerRecord.callCount).to.equal(2) expect(remoteIdentify.peerStore.protoBook.set.callCount).to.equal(1) const addresses = localIdentify.peerStore.addressBook.get(localPeer) @@ -264,33 +274,38 @@ describe('Identify', () => { // LEGACY it('should be able to push identify updates to another peer with no certified peer records support', async () => { + const storedProtocols = [multicodecs.IDENTIFY, multicodecs.IDENTIFY_PUSH, '/echo/1.0.0'] const connectionManager = new EventEmitter() connectionManager.getConnection = () => { } + const localPeerStore = new PeerStore({ peerId: localPeer }) + localPeerStore.protoBook.set(localPeer, storedProtocols) + const localIdentify = new IdentifyService({ libp2p: { peerId: localPeer, connectionManager: new EventEmitter(), - peerStore: new PeerStore({ peerId: localPeer }), - multiaddrs: listenMaddrs - }, - protocols: new Map([ - [multicodecs.IDENTIFY], - [multicodecs.IDENTIFY_PUSH], - ['/echo/1.0.0'] - ]) + peerStore: localPeerStore, + multiaddrs: listenMaddrs, + isStarted: () => true + } }) + + const remotePeerStore = new PeerStore({ peerId: remotePeer }) + remotePeerStore.protoBook.set(remotePeer, storedProtocols) + const remoteIdentify = new IdentifyService({ libp2p: { peerId: remotePeer, connectionManager, peerStore: new PeerStore({ peerId: remotePeer }), - multiaddrs: [] + multiaddrs: [], + isStarted: () => true } }) // Setup peer protocols and multiaddrs - const localProtocols = new Set([multicodecs.IDENTIFY, multicodecs.IDENTIFY_PUSH, '/echo/1.0.0']) + const localProtocols = new Set(storedProtocols) const localConnectionMock = { newStream: () => {} } const remoteConnectionMock = { remotePeer: localPeer } @@ -359,8 +374,8 @@ describe('Identify', () => { expect(connection).to.exist() // Wait for peer store to be updated - // Dialer._createDialTarget (add), Identify (consume), Create self (consume) - await pWaitFor(() => peerStoreSpyConsumeRecord.callCount === 2 && peerStoreSpyAdd.callCount === 1) + // Dialer._createDialTarget (add), Identify (consume) + await pWaitFor(() => peerStoreSpyConsumeRecord.callCount === 1 && peerStoreSpyAdd.callCount === 1) expect(libp2p.identifyService.identify.callCount).to.equal(1) // The connection should have no open streams @@ -381,8 +396,6 @@ describe('Identify', () => { const connection = await libp2p.dialer.connectToPeer(remoteAddr) expect(connection).to.exist() - // Wait for nextTick to trigger the identify call - await delay(1) // Wait for identify to finish await libp2p.identifyService.identify.firstCall.returnValue @@ -404,5 +417,39 @@ describe('Identify', () => { // Verify the streams close await pWaitFor(() => connection.streams.length === 0) }) + + it('should push multiaddr updates to an already connected peer', async () => { + libp2p = new Libp2p({ + ...baseOptions, + peerId + }) + + await libp2p.start() + + sinon.spy(libp2p.identifyService, 'identify') + sinon.spy(libp2p.identifyService, 'push') + + const connection = await libp2p.dialer.connectToPeer(remoteAddr) + expect(connection).to.exist() + + // Wait for identify to finish + await libp2p.identifyService.identify.firstCall.returnValue + sinon.stub(libp2p, 'isStarted').returns(true) + + libp2p.peerStore.addressBook.add(libp2p.peerId, [multiaddr('/ip4/180.0.0.1/tcp/15001/ws')]) + + // Verify the remote peer is notified of change + expect(libp2p.identifyService.push.callCount).to.equal(1) + 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()) + const results = await call.returnValue + expect(results.length).to.equal(1) + } + + // Verify the streams close + await pWaitFor(() => connection.streams.length === 0) + }) }) }) diff --git a/test/insecure/plaintext.spec.js b/test/insecure/plaintext.spec.js index b0c0e9cb61..41f10c5a5b 100644 --- a/test/insecure/plaintext.spec.js +++ b/test/insecure/plaintext.spec.js @@ -1,9 +1,7 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) -const { expect } = chai +const { expect } = require('aegir/utils/chai') const sinon = require('sinon') const PeerId = require('peer-id') diff --git a/test/keychain/cms-interop.spec.js b/test/keychain/cms-interop.spec.js index 1c39eda5bd..546d163c79 100644 --- a/test/keychain/cms-interop.spec.js +++ b/test/keychain/cms-interop.spec.js @@ -2,10 +2,7 @@ /* eslint-env mocha */ 'use strict' -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) +const { chai, expect } = require('aegir/utils/chai') chai.use(require('chai-string')) const uint8ArrayFromString = require('uint8arrays/from-string') const uint8ArrayToString = require('uint8arrays/to-string') diff --git a/test/keychain/keychain.spec.js b/test/keychain/keychain.spec.js index 8124c72c2f..1b74c91eb2 100644 --- a/test/keychain/keychain.spec.js +++ b/test/keychain/keychain.spec.js @@ -73,9 +73,9 @@ describe('keychain', () => { it('can find a key without a password', async () => { const keychain = new Keychain(datastore2) const keychainWithPassword = new Keychain(datastore2, { passPhrase: `hello-${Date.now()}-${Date.now()}` }) - const id = `key-${Math.random()}` + const name = `key-${Math.random()}` - await keychainWithPassword.createKey(id, 'ed25519') + const { id } = await keychainWithPassword.createKey(name, 'ed25519') await expect(keychain.findKeyById(id)).to.eventually.be.ok() }) diff --git a/test/keychain/peerid.spec.js b/test/keychain/peerid.spec.js index 430d47a775..fd393d6a5c 100644 --- a/test/keychain/peerid.spec.js +++ b/test/keychain/peerid.spec.js @@ -1,10 +1,7 @@ /* eslint-env mocha */ 'use strict' -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) +const { expect } = require('aegir/utils/chai') const PeerId = require('peer-id') const multihash = require('multihashes') const crypto = require('libp2p-crypto') diff --git a/test/metrics/index.node.js b/test/metrics/index.node.js index 4387bf16e7..cdd43e7e88 100644 --- a/test/metrics/index.node.js +++ b/test/metrics/index.node.js @@ -1,10 +1,7 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) -chai.use(require('chai-as-promised')) -const { expect } = chai +const { expect } = require('aegir/utils/chai') const sinon = require('sinon') const { randomBytes } = require('libp2p-crypto') diff --git a/test/metrics/index.spec.js b/test/metrics/index.spec.js index da0c10d555..5f401e8ac6 100644 --- a/test/metrics/index.spec.js +++ b/test/metrics/index.spec.js @@ -1,10 +1,7 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) -chai.use(require('chai-as-promised')) -const { expect } = chai +const { expect } = require('aegir/utils/chai') const sinon = require('sinon') const { EventEmitter } = require('events') diff --git a/test/peer-discovery/index.node.js b/test/peer-discovery/index.node.js index bbea8a0a08..63976cb83c 100644 --- a/test/peer-discovery/index.node.js +++ b/test/peer-discovery/index.node.js @@ -1,9 +1,7 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) -const { expect } = chai +const { expect } = require('aegir/utils/chai') const sinon = require('sinon') const defer = require('p-defer') const mergeOptions = require('merge-options') diff --git a/test/peer-discovery/index.spec.js b/test/peer-discovery/index.spec.js index fcada30ca9..2fd037a0ef 100644 --- a/test/peer-discovery/index.spec.js +++ b/test/peer-discovery/index.spec.js @@ -1,9 +1,7 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) -const { expect } = chai +const { expect } = require('aegir/utils/chai') const sinon = require('sinon') const defer = require('p-defer') diff --git a/test/peer-routing/peer-routing.node.js b/test/peer-routing/peer-routing.node.js index 105ef6148f..74cc6393e6 100644 --- a/test/peer-routing/peer-routing.node.js +++ b/test/peer-routing/peer-routing.node.js @@ -1,17 +1,20 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) -const { expect } = chai +const { expect } = require('aegir/utils/chai') const nock = require('nock') const sinon = require('sinon') +const intoStream = require('into-stream') +const delay = require('delay') const pDefer = require('p-defer') +const pWaitFor = require('p-wait-for') const mergeOptions = require('merge-options') const ipfsHttpClient = require('ipfs-http-client') const DelegatedPeerRouter = require('libp2p-delegated-peer-routing') +const multiaddr = require('multiaddr') +const PeerId = require('peer-id') const peerUtils = require('../utils/creators/peer') const { baseOptions, routingOptions } = require('./utils') @@ -31,6 +34,16 @@ describe('peer-routing', () => { .to.eventually.be.rejected() .and.to.have.property('code', 'NO_ROUTERS_AVAILABLE') }) + + it('.getClosestPeers should return an error', async () => { + try { + for await (const _ of node.peerRouting.getClosestPeers('a cid')) { } // eslint-disable-line + throw new Error('.getClosestPeers should return an error') + } catch (err) { + expect(err).to.exist() + expect(err.code).to.equal('NO_ROUTERS_AVAILABLE') + } + }) }) describe('via dht router', () => { @@ -66,6 +79,19 @@ describe('peer-routing', () => { nodes[0].peerRouting.findPeer() return deferred.promise }) + + it('should use the nodes dht to get the closest peers', async () => { + const deferred = pDefer() + + sinon.stub(nodes[0]._dht, 'getClosestPeers').callsFake(function * () { + deferred.resolve() + yield + }) + + await nodes[0].peerRouting.getClosestPeers().next() + + return deferred.promise + }) }) describe('via delegate router', () => { @@ -112,6 +138,19 @@ describe('peer-routing', () => { return deferred.promise }) + it('should use the delegate router to get the closest peers', async () => { + const deferred = pDefer() + + sinon.stub(delegate, 'getClosestPeers').callsFake(function * () { + deferred.resolve() + yield + }) + + await node.peerRouting.getClosestPeers().next() + + return deferred.promise + }) + it('should be able to find a peer', async () => { const peerKey = 'QmTp9VkYvnHyrqKQuFPiuZkiX9gPcqj6x5LJ1rmWuSySnL' const mockApi = nock('http://0.0.0.0:60197') @@ -156,6 +195,60 @@ describe('peer-routing', () => { expect(mockApi.isDone()).to.equal(true) }) + + it('should be able to get the closest peers', async () => { + const peerId = await PeerId.create({ keyType: 'ed25519' }) + + const closest1 = '12D3KooWLewYMMdGWAtuX852n4rgCWkK7EBn4CWbwwBzhsVoKxk3' + const closest2 = '12D3KooWDtoQbpKhtnWddfj72QmpFvvLDTsBLTFkjvgQm6cde2AK' + + const mockApi = nock('http://0.0.0.0:60197') + .post('/api/v0/dht/query') + .query(true) + .reply(200, + () => intoStream([ + `{"extra":"","id":"${closest1}","responses":[{"ID":"${closest1}","Addrs":["/ip4/127.0.0.1/tcp/63930","/ip4/127.0.0.1/tcp/63930"]}],"type":1}\n`, + `{"extra":"","id":"${closest2}","responses":[{"ID":"${closest2}","Addrs":["/ip4/127.0.0.1/tcp/63506","/ip4/127.0.0.1/tcp/63506"]}],"type":1}\n`, + `{"Extra":"","ID":"${closest2}","Responses":[],"Type":2}\n`, + `{"Extra":"","ID":"${closest1}","Responses":[],"Type":2}\n` + ]), + [ + 'Content-Type', 'application/json', + 'X-Chunked-Output', '1' + ]) + + const closestPeers = [] + for await (const peer of node.peerRouting.getClosestPeers(peerId.id, { timeout: 1000 })) { + closestPeers.push(peer) + } + + expect(closestPeers).to.have.length(2) + expect(closestPeers[0].id.toB58String()).to.equal(closest2) + expect(closestPeers[0].multiaddrs).to.have.lengthOf(2) + expect(closestPeers[1].id.toB58String()).to.equal(closest1) + expect(closestPeers[1].multiaddrs).to.have.lengthOf(2) + expect(mockApi.isDone()).to.equal(true) + }) + + it('should handle errors when getting the closest peers', async () => { + const peerId = await PeerId.create({ keyType: 'ed25519' }) + + const mockApi = nock('http://0.0.0.0:60197') + .post('/api/v0/dht/query') + .query(true) + .reply(502, 'Bad Gateway', [ + 'X-Chunked-Output', '1' + ]) + + try { + for await (const _ of node.peerRouting.getClosestPeers(peerId.id)) { } // eslint-disable-line + throw new Error('should handle errors when getting the closest peers') + } catch (err) { + expect(err).to.exist() + } + + expect(mockApi.isDone()).to.equal(true) + }) }) describe('via dht and delegate routers', () => { @@ -210,5 +303,148 @@ describe('peer-routing', () => { const peer = await node.peerRouting.findPeer('a peer id') expect(peer).to.eql(results) }) + + it('should only use the dht if it gets the closest peers', async () => { + const results = [true] + + sinon.stub(node._dht, 'getClosestPeers').callsFake(function * () { + yield results[0] + }) + + sinon.stub(delegate, 'getClosestPeers').callsFake(function * () { // eslint-disable-line require-yield + throw new Error('the delegate should not have been called') + }) + + const closest = [] + for await (const peer of node.peerRouting.getClosestPeers('a cid')) { + closest.push(peer) + } + + expect(closest).to.have.length.above(0) + expect(closest).to.eql(results) + }) + + it('should use the delegate if the dht fails to get the closest peer', async () => { + const results = [true] + + sinon.stub(node._dht, 'getClosestPeers').callsFake(function * () { }) + + sinon.stub(delegate, 'getClosestPeers').callsFake(function * () { + yield results[0] + }) + + const closest = [] + for await (const peer of node.peerRouting.getClosestPeers('a cid')) { + closest.push(peer) + } + + expect(closest).to.have.length.above(0) + expect(closest).to.eql(results) + }) + }) + + describe('peer routing refresh manager service', () => { + let node + let peerIds + + before(async () => { + peerIds = await peerUtils.createPeerId({ number: 2 }) + }) + + afterEach(() => { + sinon.restore() + + return node && node.stop() + }) + + it('should be enabled and start by default', async () => { + const results = [ + { id: peerIds[0], multiaddrs: [multiaddr('/ip4/30.0.0.1/tcp/2000')] }, + { id: peerIds[1], multiaddrs: [multiaddr('/ip4/32.0.0.1/tcp/2000')] } + ] + + ;[node] = await peerUtils.createPeer({ + config: mergeOptions(routingOptions, { + peerRouting: { + refreshManager: { + bootDelay: 100 + } + } + }), + started: false + }) + + sinon.spy(node.peerStore.addressBook, 'add') + sinon.stub(node._dht, 'getClosestPeers').callsFake(function * () { + yield results[0] + yield results[1] + }) + + await node.start() + + await pWaitFor(() => node._dht.getClosestPeers.callCount === 1) + await pWaitFor(() => node.peerStore.addressBook.add.callCount === results.length) + + const call0 = node.peerStore.addressBook.add.getCall(0) + expect(call0.args[0].equals(results[0].id)) + call0.args[1].forEach((m, index) => { + expect(m.equals(results[0].multiaddrs[index])) + }) + + const call1 = node.peerStore.addressBook.add.getCall(1) + expect(call1.args[0].equals(results[1].id)) + call0.args[1].forEach((m, index) => { + expect(m.equals(results[1].multiaddrs[index])) + }) + }) + + it('should support being disabled', async () => { + [node] = await peerUtils.createPeer({ + config: mergeOptions(routingOptions, { + peerRouting: { + refreshManager: { + bootDelay: 100, + enabled: false + } + } + }), + started: false + }) + + sinon.stub(node._dht, 'getClosestPeers').callsFake(function * () { + yield + throw new Error('should not be called') + }) + + await node.start() + await delay(100) + + expect(node._dht.getClosestPeers.callCount === 0) + }) + + it('should start and run recurrently on interval', async () => { + [node] = await peerUtils.createPeer({ + config: mergeOptions(routingOptions, { + peerRouting: { + refreshManager: { + interval: 500, + bootDelay: 200 + } + } + }), + started: false + }) + + sinon.stub(node._dht, 'getClosestPeers').callsFake(function * () { + yield { id: peerIds[0], multiaddrs: [multiaddr('/ip4/30.0.0.1/tcp/2000')] } + }) + + await node.start() + + await delay(300) + expect(node._dht.getClosestPeers.callCount).to.eql(1) + await delay(500) + expect(node._dht.getClosestPeers.callCount).to.eql(2) + }) }) }) diff --git a/test/peer-store/address-book.spec.js b/test/peer-store/address-book.spec.js index a5578e918f..0adae21d61 100644 --- a/test/peer-store/address-book.spec.js +++ b/test/peer-store/address-book.spec.js @@ -2,13 +2,11 @@ /* eslint-env mocha */ /* eslint max-nested-callbacks: ["error", 6] */ -const chai = require('chai') -chai.use(require('dirty-chai')) -const { expect } = chai - +const { expect } = require('aegir/utils/chai') const { Buffer } = require('buffer') const multiaddr = require('multiaddr') const arrayEquals = require('libp2p-utils/src/array-equals') +const addressSort = require('libp2p-utils/src/address-sort') const PeerId = require('peer-id') const pDefer = require('p-defer') @@ -22,7 +20,7 @@ const { } = require('../../src/errors') const addr1 = multiaddr('/ip4/127.0.0.1/tcp/8000') -const addr2 = multiaddr('/ip4/127.0.0.1/tcp/8001') +const addr2 = multiaddr('/ip4/20.0.0.1/tcp/8001') const addr3 = multiaddr('/ip4/127.0.0.1/tcp/8002') describe('addressBook', () => { @@ -343,6 +341,18 @@ describe('addressBook', () => { expect(m.getPeerId()).to.equal(peerId.toB58String()) }) }) + + it('can sort multiaddrs providing a sorter', () => { + const supportedMultiaddrs = [addr1, addr2] + ab.set(peerId, supportedMultiaddrs) + + const multiaddrs = ab.getMultiaddrsForPeer(peerId, addressSort.publicAddressesFirst) + const sortedAddresses = addressSort.publicAddressesFirst(supportedMultiaddrs.map((m) => ({ multiaddr: m }))) + + multiaddrs.forEach((m, index) => { + expect(m.equals(sortedAddresses[index].multiaddr)) + }) + }) }) describe('addressBook.delete', () => { diff --git a/test/peer-store/key-book.spec.js b/test/peer-store/key-book.spec.js index 4f51acb05e..af41a334e1 100644 --- a/test/peer-store/key-book.spec.js +++ b/test/peer-store/key-book.spec.js @@ -1,10 +1,8 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) +const { chai, expect } = require('aegir/utils/chai') chai.use(require('chai-bytes')) -const { expect } = chai const sinon = require('sinon') const PeerStore = require('../../src/peer-store') diff --git a/test/peer-store/metadata-book.spec.js b/test/peer-store/metadata-book.spec.js index 8d3e815bde..155b220599 100644 --- a/test/peer-store/metadata-book.spec.js +++ b/test/peer-store/metadata-book.spec.js @@ -1,10 +1,8 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) +const { chai, expect } = require('aegir/utils/chai') chai.use(require('chai-bytes')) -const { expect } = chai const uint8ArrayFromString = require('uint8arrays/from-string') const pDefer = require('p-defer') diff --git a/test/peer-store/peer-store.node.js b/test/peer-store/peer-store.node.js index f89c166bc6..8c322a2cce 100644 --- a/test/peer-store/peer-store.node.js +++ b/test/peer-store/peer-store.node.js @@ -1,10 +1,8 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) +const { chai, expect } = require('aegir/utils/chai') chai.use(require('chai-bytes')) -const { expect } = chai const sinon = require('sinon') const baseOptions = require('../utils/base-options') diff --git a/test/peer-store/peer-store.spec.js b/test/peer-store/peer-store.spec.js index c9d1880069..39c2c7187d 100644 --- a/test/peer-store/peer-store.spec.js +++ b/test/peer-store/peer-store.spec.js @@ -1,9 +1,7 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) -const { expect } = chai +const { expect } = require('aegir/utils/chai') const PeerStore = require('../../src/peer-store') const multiaddr = require('multiaddr') diff --git a/test/peer-store/persisted-peer-store.spec.js b/test/peer-store/persisted-peer-store.spec.js index 3c58e21dcc..e1d7047629 100644 --- a/test/peer-store/persisted-peer-store.spec.js +++ b/test/peer-store/persisted-peer-store.spec.js @@ -1,9 +1,7 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) -const { expect } = chai +const { expect } = require('aegir/utils/chai') const sinon = require('sinon') const Envelope = require('../../src/record/envelope') diff --git a/test/peer-store/proto-book.spec.js b/test/peer-store/proto-book.spec.js index 15b5199757..db05955f69 100644 --- a/test/peer-store/proto-book.spec.js +++ b/test/peer-store/proto-book.spec.js @@ -1,11 +1,11 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) -const { expect } = chai +const { expect } = require('aegir/utils/chai') +const sinon = require('sinon') const pDefer = require('p-defer') +const pWaitFor = require('p-wait-for') const PeerStore = require('../../src/peer-store') @@ -224,6 +224,96 @@ describe('protoBook', () => { }) }) + describe('protoBook.remove', () => { + let peerStore, pb + + beforeEach(() => { + peerStore = new PeerStore({ peerId }) + pb = peerStore.protoBook + }) + + afterEach(() => { + peerStore.removeAllListeners() + }) + + it('throws invalid parameters error if invalid PeerId is provided', () => { + expect(() => { + pb.remove('invalid peerId') + }).to.throw(ERR_INVALID_PARAMETERS) + }) + + it('throws invalid parameters error if no protocols provided', () => { + expect(() => { + pb.remove(peerId) + }).to.throw(ERR_INVALID_PARAMETERS) + }) + + it('removes the given protocol and emits change event', async () => { + const spy = sinon.spy() + + const supportedProtocols = ['protocol1', 'protocol2'] + const removedProtocols = ['protocol1'] + const finalProtocols = supportedProtocols.filter(p => !removedProtocols.includes(p)) + + peerStore.on('change:protocols', spy) + + // Replace + pb.set(peerId, supportedProtocols) + let protocols = pb.get(peerId) + expect(protocols).to.have.deep.members(supportedProtocols) + + // Remove + pb.remove(peerId, removedProtocols) + protocols = pb.get(peerId) + expect(protocols).to.have.deep.members(finalProtocols) + + await pWaitFor(() => spy.callCount === 2) + + const [firstCallArgs] = spy.firstCall.args + const [secondCallArgs] = spy.secondCall.args + expect(arraysAreEqual(firstCallArgs.protocols, supportedProtocols)) + expect(arraysAreEqual(secondCallArgs.protocols, finalProtocols)) + }) + + it('emits on remove if the content changes', () => { + const spy = sinon.spy() + + const supportedProtocols = ['protocol1', 'protocol2'] + const removedProtocols = ['protocol2'] + const finalProtocols = supportedProtocols.filter(p => !removedProtocols.includes(p)) + + peerStore.on('change:protocols', spy) + + // set + pb.set(peerId, supportedProtocols) + + // remove (content already existing) + pb.remove(peerId, removedProtocols) + const protocols = pb.get(peerId) + expect(protocols).to.have.deep.members(finalProtocols) + + return pWaitFor(() => spy.callCount === 2) + }) + + it('does not emit on remove if the content does not change', () => { + const spy = sinon.spy() + + const supportedProtocols = ['protocol1', 'protocol2'] + const removedProtocols = ['protocol3'] + + peerStore.on('change:protocols', spy) + + // set + pb.set(peerId, supportedProtocols) + + // remove + pb.remove(peerId, removedProtocols) + + // Only one event + expect(spy.callCount).to.eql(1) + }) + }) + describe('protoBook.get', () => { let peerStore, pb diff --git a/test/pnet/index.spec.js b/test/pnet/index.spec.js index 9fa2b9f2ae..98327146d8 100644 --- a/test/pnet/index.spec.js +++ b/test/pnet/index.spec.js @@ -1,10 +1,7 @@ /* eslint-env mocha */ 'use strict' -const chai = require('chai') -const dirtyChai = require('dirty-chai') -chai.use(dirtyChai) -const expect = chai.expect +const { expect } = require('aegir/utils/chai') const duplexPair = require('it-pair/duplex') const pipe = require('it-pipe') const { collect } = require('streaming-iterables') diff --git a/test/pubsub/configuration.node.js b/test/pubsub/configuration.node.js index ed8cd90c44..12b0b584a6 100644 --- a/test/pubsub/configuration.node.js +++ b/test/pubsub/configuration.node.js @@ -1,10 +1,7 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) -const { expect } = chai - +const { expect } = require('aegir/utils/chai') const mergeOptions = require('merge-options') const multiaddr = require('multiaddr') diff --git a/test/pubsub/implementations.node.js b/test/pubsub/implementations.node.js index a17d7744ed..3243d94b94 100644 --- a/test/pubsub/implementations.node.js +++ b/test/pubsub/implementations.node.js @@ -1,10 +1,7 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) -const { expect } = chai - +const { expect } = require('aegir/utils/chai') const pWaitFor = require('p-wait-for') const pDefer = require('p-defer') const mergeOptions = require('merge-options') diff --git a/test/pubsub/operation.node.js b/test/pubsub/operation.node.js index 08e2afbc02..2ebe39dae4 100644 --- a/test/pubsub/operation.node.js +++ b/test/pubsub/operation.node.js @@ -1,9 +1,7 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) -const { expect } = chai +const { expect } = require('aegir/utils/chai') const sinon = require('sinon') const pWaitFor = require('p-wait-for') diff --git a/test/record/envelope.spec.js b/test/record/envelope.spec.js index 81cbfb1296..d95a925ecb 100644 --- a/test/record/envelope.spec.js +++ b/test/record/envelope.spec.js @@ -1,16 +1,11 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) +const { chai, expect } = require('aegir/utils/chai') chai.use(require('chai-bytes')) -chai.use(require('chai-as-promised')) -const { expect } = chai - const uint8arrayFromString = require('uint8arrays/from-string') const uint8arrayEquals = require('uint8arrays/equals') const Envelope = require('../../src/record/envelope') -const Record = require('libp2p-interfaces/src/record') const { codes: ErrorCodes } = require('../../src/errors') const peerUtils = require('../utils/creators/peer') @@ -18,9 +13,10 @@ const peerUtils = require('../utils/creators/peer') const domain = 'libp2p-testing' const codec = uint8arrayFromString('/libp2p/testdata') -class TestRecord extends Record { +class TestRecord { constructor (data) { - super(domain, codec) + this.domain = domain + this.codec = codec this.data = data } diff --git a/test/record/peer-record.spec.js b/test/record/peer-record.spec.js index 638e145835..e8b8ed64cd 100644 --- a/test/record/peer-record.spec.js +++ b/test/record/peer-record.spec.js @@ -1,9 +1,7 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) -const { expect } = chai +const { expect } = require('aegir/utils/chai') const tests = require('libp2p-interfaces/src/record/tests') const multiaddr = require('multiaddr') diff --git a/test/registrar/registrar.spec.js b/test/registrar/registrar.spec.js index 6befd0599b..8f90b9fcf4 100644 --- a/test/registrar/registrar.spec.js +++ b/test/registrar/registrar.spec.js @@ -1,9 +1,7 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) -const { expect } = chai +const { expect } = require('aegir/utils/chai') const pDefer = require('p-defer') const { EventEmitter } = require('events') diff --git a/test/relay/auto-relay.node.js b/test/relay/auto-relay.node.js new file mode 100644 index 0000000000..2411f48a57 --- /dev/null +++ b/test/relay/auto-relay.node.js @@ -0,0 +1,582 @@ +'use strict' +/* eslint-env mocha */ + +const { expect } = require('aegir/utils/chai') +const delay = require('delay') +const pWaitFor = require('p-wait-for') +const sinon = require('sinon') +const nock = require('nock') + +const ipfsHttpClient = require('ipfs-http-client') +const DelegatedContentRouter = require('libp2p-delegated-content-routing') +const multiaddr = require('multiaddr') +const Libp2p = require('../../src') +const { relay: relayMulticodec } = require('../../src/circuit/multicodec') + +const { createPeerId } = require('../utils/creators/peer') +const baseOptions = require('../utils/base-options') + +const listenAddr = '/ip4/0.0.0.0/tcp/0' + +describe('auto-relay', () => { + describe('basics', () => { + let libp2p + let relayLibp2p + let autoRelay + + beforeEach(async () => { + const peerIds = await createPeerId({ number: 2 }) + // Create 2 nodes, and turn HOP on for the relay + ;[libp2p, relayLibp2p] = peerIds.map((peerId, index) => { + const opts = { + ...baseOptions, + config: { + ...baseOptions.config, + relay: { + hop: { + enabled: index !== 0 + }, + autoRelay: { + enabled: true, + maxListeners: 1 + } + } + } + } + + return new Libp2p({ + ...opts, + addresses: { + listen: [listenAddr] + }, + connectionManager: { + autoDial: false + }, + peerDiscovery: { + autoDial: false + }, + peerId + }) + }) + + autoRelay = libp2p.relay._autoRelay + + expect(autoRelay.maxListeners).to.eql(1) + }) + + beforeEach(() => { + // Start each node + return Promise.all([libp2p, relayLibp2p].map(libp2p => libp2p.start())) + }) + + afterEach(() => { + // Stop each node + return Promise.all([libp2p, relayLibp2p].map(libp2p => libp2p.stop())) + }) + + it('should ask if node supports hop on protocol change (relay protocol) and add to listen multiaddrs', async () => { + // Spy if a connected peer is being added as listen relay + sinon.spy(autoRelay, '_addListenRelay') + + const originalMultiaddrsLength = relayLibp2p.multiaddrs.length + + // Discover relay + libp2p.peerStore.addressBook.add(relayLibp2p.peerId, relayLibp2p.multiaddrs) + await libp2p.dial(relayLibp2p.peerId) + + // Wait for peer added as listen relay + await pWaitFor(() => autoRelay._addListenRelay.callCount === 1) + expect(autoRelay._listenRelays.size).to.equal(1) + + // Wait for listen multiaddr update + await pWaitFor(() => libp2p.multiaddrs.length === originalMultiaddrsLength + 1) + expect(libp2p.multiaddrs[originalMultiaddrsLength].getPeerId()).to.eql(relayLibp2p.peerId.toB58String()) + + // Peer has relay multicodec + const knownProtocols = libp2p.peerStore.protoBook.get(relayLibp2p.peerId) + expect(knownProtocols).to.include(relayMulticodec) + }) + }) + + describe('flows with 1 listener max', () => { + let libp2p + let relayLibp2p1 + let relayLibp2p2 + let relayLibp2p3 + let autoRelay1 + + beforeEach(async () => { + const peerIds = await createPeerId({ number: 4 }) + // Create 4 nodes, and turn HOP on for the relay + ;[libp2p, relayLibp2p1, relayLibp2p2, relayLibp2p3] = peerIds.map((peerId, index) => { + let opts = baseOptions + + if (index !== 0) { + opts = { + ...baseOptions, + config: { + ...baseOptions.config, + relay: { + hop: { + enabled: true + }, + autoRelay: { + enabled: true, + maxListeners: 1 + } + } + } + } + } + + return new Libp2p({ + ...opts, + addresses: { + listen: [listenAddr] + }, + connectionManager: { + autoDial: false + }, + peerDiscovery: { + autoDial: false + }, + peerId + }) + }) + + autoRelay1 = relayLibp2p1.relay._autoRelay + + expect(autoRelay1.maxListeners).to.eql(1) + }) + + beforeEach(() => { + // Start each node + return Promise.all([libp2p, relayLibp2p1, relayLibp2p2, relayLibp2p3].map(libp2p => libp2p.start())) + }) + + afterEach(() => { + // Stop each node + return Promise.all([libp2p, relayLibp2p1, relayLibp2p2, relayLibp2p3].map(libp2p => libp2p.stop())) + }) + + it('should ask if node supports hop on protocol change (relay protocol) and add to listen multiaddrs', async () => { + // Spy if a connected peer is being added as listen relay + sinon.spy(autoRelay1, '_addListenRelay') + + // Discover relay + relayLibp2p1.peerStore.addressBook.add(relayLibp2p2.peerId, relayLibp2p2.multiaddrs) + + const originalMultiaddrs1Length = relayLibp2p1.multiaddrs.length + const originalMultiaddrs2Length = relayLibp2p2.multiaddrs.length + + await relayLibp2p1.dial(relayLibp2p2.peerId) + + // Wait for peer added as listen relay + await pWaitFor(() => autoRelay1._addListenRelay.callCount === 1) + expect(autoRelay1._listenRelays.size).to.equal(1) + + // Wait for listen multiaddr update + await Promise.all([ + pWaitFor(() => relayLibp2p1.multiaddrs.length === originalMultiaddrs1Length + 1), + pWaitFor(() => relayLibp2p2.multiaddrs.length === originalMultiaddrs2Length + 1) + ]) + expect(relayLibp2p1.multiaddrs[originalMultiaddrs1Length].getPeerId()).to.eql(relayLibp2p2.peerId.toB58String()) + + // Peer has relay multicodec + const knownProtocols = relayLibp2p1.peerStore.protoBook.get(relayLibp2p2.peerId) + expect(knownProtocols).to.include(relayMulticodec) + }) + + it('should be able to dial a peer from its relayed address previously added', async () => { + const originalMultiaddrs1Length = relayLibp2p1.multiaddrs.length + const originalMultiaddrs2Length = relayLibp2p2.multiaddrs.length + + // Discover relay + relayLibp2p1.peerStore.addressBook.add(relayLibp2p2.peerId, relayLibp2p2.multiaddrs) + + await relayLibp2p1.dial(relayLibp2p2.peerId) + + // Wait for listen multiaddr update + await Promise.all([ + pWaitFor(() => relayLibp2p1.multiaddrs.length === originalMultiaddrs1Length + 1), + pWaitFor(() => relayLibp2p2.multiaddrs.length === originalMultiaddrs2Length + 1) + ]) + expect(relayLibp2p1.multiaddrs[originalMultiaddrs1Length].getPeerId()).to.eql(relayLibp2p2.peerId.toB58String()) + + // Dial from the other through a relay + const relayedMultiaddr2 = multiaddr(`${relayLibp2p1.multiaddrs[0]}/p2p/${relayLibp2p1.peerId.toB58String()}/p2p-circuit`) + libp2p.peerStore.addressBook.add(relayLibp2p2.peerId, [relayedMultiaddr2]) + + await libp2p.dial(relayLibp2p2.peerId) + }) + + it('should only add maxListeners relayed addresses', async () => { + const originalMultiaddrs1Length = relayLibp2p1.multiaddrs.length + const originalMultiaddrs2Length = relayLibp2p2.multiaddrs.length + + // Spy if a connected peer is being added as listen relay + sinon.spy(autoRelay1, '_addListenRelay') + sinon.spy(autoRelay1._listenRelays, 'add') + + // Discover one relay and connect + relayLibp2p1.peerStore.addressBook.add(relayLibp2p2.peerId, relayLibp2p2.multiaddrs) + await relayLibp2p1.dial(relayLibp2p2.peerId) + + expect(relayLibp2p1.connectionManager.size).to.eql(1) + + // Wait for peer added as listen relay + await pWaitFor(() => autoRelay1._addListenRelay.callCount === 1 && autoRelay1._listenRelays.add.callCount === 1) + expect(autoRelay1._listenRelays.size).to.equal(1) + + // Wait for listen multiaddr update + await Promise.all([ + pWaitFor(() => relayLibp2p1.multiaddrs.length === originalMultiaddrs1Length + 1), + pWaitFor(() => relayLibp2p2.multiaddrs.length === originalMultiaddrs2Length + 1) + ]) + expect(relayLibp2p1.multiaddrs[originalMultiaddrs1Length].getPeerId()).to.eql(relayLibp2p2.peerId.toB58String()) + + // Relay2 has relay multicodec + const knownProtocols2 = relayLibp2p1.peerStore.protoBook.get(relayLibp2p2.peerId) + expect(knownProtocols2).to.include(relayMulticodec) + + // Discover an extra relay and connect + relayLibp2p1.peerStore.addressBook.add(relayLibp2p3.peerId, relayLibp2p3.multiaddrs) + await relayLibp2p1.dial(relayLibp2p3.peerId) + + // Wait to guarantee the dialed peer is not added as a listen relay + await delay(300) + + expect(autoRelay1._addListenRelay.callCount).to.equal(2) + expect(autoRelay1._listenRelays.add.callCount).to.equal(1) + expect(autoRelay1._listenRelays.size).to.equal(1) + expect(relayLibp2p1.connectionManager.size).to.eql(2) + + // Relay2 has relay multicodec + const knownProtocols3 = relayLibp2p1.peerStore.protoBook.get(relayLibp2p3.peerId) + expect(knownProtocols3).to.include(relayMulticodec) + }) + + it('should not listen on a relayed address if peer disconnects', async () => { + const originalMultiaddrs1Length = relayLibp2p1.multiaddrs.length + + // Spy if identify push is fired on adding/removing listen addr + sinon.spy(relayLibp2p1.identifyService, 'pushToPeerStore') + + // Discover one relay and connect + relayLibp2p1.peerStore.addressBook.add(relayLibp2p2.peerId, relayLibp2p2.multiaddrs) + await relayLibp2p1.dial(relayLibp2p2.peerId) + + // Wait for listenning on the relay + await pWaitFor(() => relayLibp2p1.multiaddrs.length === originalMultiaddrs1Length + 1) + expect(autoRelay1._listenRelays.size).to.equal(1) + expect(relayLibp2p1.multiaddrs[originalMultiaddrs1Length].getPeerId()).to.eql(relayLibp2p2.peerId.toB58String()) + + // Identify push for adding listen relay multiaddr + expect(relayLibp2p1.identifyService.pushToPeerStore.callCount).to.equal(1) + + // Disconnect from peer used for relay + await relayLibp2p1.hangUp(relayLibp2p2.peerId) + + // Wait for removed listening on the relay + await pWaitFor(() => relayLibp2p1.multiaddrs.length === originalMultiaddrs1Length) + expect(autoRelay1._listenRelays.size).to.equal(0) + + // Identify push for removing listen relay multiaddr + expect(relayLibp2p1.identifyService.pushToPeerStore.callCount).to.equal(2) + }) + + it('should try to listen on other connected peers relayed address if one used relay disconnects', async () => { + const originalMultiaddrs1Length = relayLibp2p1.multiaddrs.length + + // Spy if a connected peer is being added as listen relay + sinon.spy(autoRelay1, '_addListenRelay') + sinon.spy(relayLibp2p1.transportManager, 'listen') + + // Discover one relay and connect + relayLibp2p1.peerStore.addressBook.add(relayLibp2p2.peerId, relayLibp2p2.multiaddrs) + await relayLibp2p1.dial(relayLibp2p2.peerId) + + // Discover an extra relay and connect + relayLibp2p1.peerStore.addressBook.add(relayLibp2p3.peerId, relayLibp2p3.multiaddrs) + await relayLibp2p1.dial(relayLibp2p3.peerId) + + // Wait for both peer to be attempted to added as listen relay + await pWaitFor(() => autoRelay1._addListenRelay.callCount === 1) + expect(autoRelay1._listenRelays.size).to.equal(1) + expect(relayLibp2p1.connectionManager.size).to.equal(2) + + // Wait for listen multiaddr update + await pWaitFor(() => relayLibp2p1.multiaddrs.length === originalMultiaddrs1Length + 1) + expect(relayLibp2p1.multiaddrs[originalMultiaddrs1Length].getPeerId()).to.eql(relayLibp2p2.peerId.toB58String()) + + // Only one will be used for listeninng + expect(relayLibp2p1.transportManager.listen.callCount).to.equal(1) + + // Spy if relay from listen map was removed + sinon.spy(autoRelay1._listenRelays, 'delete') + + // Disconnect from peer used for relay + await relayLibp2p1.hangUp(relayLibp2p2.peerId) + expect(autoRelay1._listenRelays.delete.callCount).to.equal(1) + expect(autoRelay1._addListenRelay.callCount).to.equal(1) + + // Wait for other peer connected to be added as listen addr + await pWaitFor(() => relayLibp2p1.transportManager.listen.callCount === 2) + expect(autoRelay1._listenRelays.size).to.equal(1) + expect(relayLibp2p1.connectionManager.size).to.eql(1) + + // Wait for listen multiaddr update + await pWaitFor(() => relayLibp2p1.multiaddrs.length === originalMultiaddrs1Length + 1) + expect(relayLibp2p1.multiaddrs[originalMultiaddrs1Length].getPeerId()).to.eql(relayLibp2p3.peerId.toB58String()) + }) + + it('should try to listen on stored peers relayed address if one used relay disconnects and there are not enough connected', async () => { + // Spy if a connected peer is being added as listen relay + sinon.spy(autoRelay1, '_addListenRelay') + sinon.spy(relayLibp2p1.transportManager, 'listen') + + // Discover one relay and connect + relayLibp2p1.peerStore.addressBook.add(relayLibp2p2.peerId, relayLibp2p2.multiaddrs) + await relayLibp2p1.dial(relayLibp2p2.peerId) + + // Discover an extra relay and connect to gather its Hop support + relayLibp2p1.peerStore.addressBook.add(relayLibp2p3.peerId, relayLibp2p3.multiaddrs) + await relayLibp2p1.dial(relayLibp2p3.peerId) + + // Wait for both peer to be attempted to added as listen relay + await pWaitFor(() => autoRelay1._addListenRelay.callCount === 2) + expect(autoRelay1._listenRelays.size).to.equal(1) + expect(relayLibp2p1.connectionManager.size).to.equal(2) + + // Only one will be used for listeninng + expect(relayLibp2p1.transportManager.listen.callCount).to.equal(1) + + // Disconnect not used listen relay + await relayLibp2p1.hangUp(relayLibp2p3.peerId) + + expect(autoRelay1._listenRelays.size).to.equal(1) + expect(relayLibp2p1.connectionManager.size).to.equal(1) + + // Spy on dial + sinon.spy(relayLibp2p1, 'dial') + + // Remove peer used as relay from peerStore and disconnect it + relayLibp2p1.peerStore.delete(relayLibp2p2.peerId) + await relayLibp2p1.hangUp(relayLibp2p2.peerId) + expect(autoRelay1._listenRelays.size).to.equal(0) + expect(relayLibp2p1.connectionManager.size).to.equal(0) + + // Wait for other peer connected to be added as listen addr + await pWaitFor(() => relayLibp2p1.transportManager.listen.callCount === 2) + expect(autoRelay1._listenRelays.size).to.equal(1) + expect(relayLibp2p1.connectionManager.size).to.eql(1) + }) + }) + + describe('flows with 2 max listeners', () => { + let relayLibp2p1 + let relayLibp2p2 + let relayLibp2p3 + let autoRelay1 + let autoRelay2 + + beforeEach(async () => { + const peerIds = await createPeerId({ number: 3 }) + // Create 3 nodes, and turn HOP on for the relay + ;[relayLibp2p1, relayLibp2p2, relayLibp2p3] = peerIds.map((peerId) => { + return new Libp2p({ + ...baseOptions, + config: { + ...baseOptions.config, + relay: { + ...baseOptions.config.relay, + hop: { + enabled: true + }, + autoRelay: { + enabled: true, + maxListeners: 2 + } + } + }, + addresses: { + listen: [listenAddr] + }, + connectionManager: { + autoDial: false + }, + peerDiscovery: { + autoDial: false + }, + peerId + }) + }) + + autoRelay1 = relayLibp2p1.relay._autoRelay + autoRelay2 = relayLibp2p2.relay._autoRelay + }) + + beforeEach(() => { + // Start each node + return Promise.all([relayLibp2p1, relayLibp2p2, relayLibp2p3].map(libp2p => libp2p.start())) + }) + + afterEach(() => { + // Stop each node + return Promise.all([relayLibp2p1, relayLibp2p2, relayLibp2p3].map(libp2p => libp2p.stop())) + }) + + it('should not add listener to a already relayed connection', async () => { + // Spy if a connected peer is being added as listen relay + sinon.spy(autoRelay1, '_addListenRelay') + sinon.spy(autoRelay2, '_addListenRelay') + + // Relay 1 discovers Relay 3 and connect + relayLibp2p1.peerStore.addressBook.add(relayLibp2p3.peerId, relayLibp2p3.multiaddrs) + await relayLibp2p1.dial(relayLibp2p3.peerId) + + // Wait for peer added as listen relay + await pWaitFor(() => autoRelay1._addListenRelay.callCount === 1) + expect(autoRelay1._listenRelays.size).to.equal(1) + + // Relay 2 discovers Relay 3 and connect + relayLibp2p2.peerStore.addressBook.add(relayLibp2p3.peerId, relayLibp2p3.multiaddrs) + await relayLibp2p2.dial(relayLibp2p3.peerId) + + // Wait for peer added as listen relay + await pWaitFor(() => autoRelay2._addListenRelay.callCount === 1) + expect(autoRelay2._listenRelays.size).to.equal(1) + + // Relay 1 discovers Relay 2 relayed multiaddr via Relay 3 + const ma2RelayedBy3 = relayLibp2p2.multiaddrs[relayLibp2p2.multiaddrs.length - 1] + relayLibp2p1.peerStore.addressBook.add(relayLibp2p2.peerId, [ma2RelayedBy3]) + await relayLibp2p1.dial(relayLibp2p2.peerId) + + // Peer not added as listen relay + expect(autoRelay1._addListenRelay.callCount).to.equal(1) + expect(autoRelay1._listenRelays.size).to.equal(1) + }) + }) + + describe('discovery', () => { + let local + let remote + let relayLibp2p + + beforeEach(async () => { + const peerIds = await createPeerId({ number: 3 }) + + // Create 2 nodes, and turn HOP on for the relay + ;[local, remote, relayLibp2p] = peerIds.map((peerId, index) => { + const delegate = new DelegatedContentRouter(peerId, ipfsHttpClient({ + host: '0.0.0.0', + protocol: 'http', + port: 60197 + }), [ + multiaddr('/ip4/0.0.0.0/tcp/60197') + ]) + + const opts = { + ...baseOptions, + config: { + ...baseOptions.config, + relay: { + advertise: { + bootDelay: 1000, + ttl: 1000, + enabled: true + }, + hop: { + enabled: index === 2 + }, + autoRelay: { + enabled: true, + maxListeners: 1 + } + } + } + } + + return new Libp2p({ + ...opts, + modules: { + ...opts.modules, + contentRouting: [delegate] + }, + addresses: { + listen: [listenAddr] + }, + connectionManager: { + autoDial: false + }, + peerDiscovery: { + autoDial: false + }, + peerId + }) + }) + + sinon.spy(relayLibp2p.contentRouting, 'provide') + }) + + beforeEach(async () => { + nock('http://0.0.0.0:60197') + // mock the refs call + .post('/api/v0/refs') + .query(true) + .reply(200, null, [ + 'Content-Type', 'application/json', + 'X-Chunked-Output', '1' + ]) + + // Start each node + await Promise.all([local, remote, relayLibp2p].map(libp2p => libp2p.start())) + + // Should provide on start + await pWaitFor(() => relayLibp2p.contentRouting.provide.callCount === 1) + + const provider = relayLibp2p.peerId.toB58String() + const multiaddrs = relayLibp2p.multiaddrs.map((m) => m.toString()) + + // Mock findProviders + nock('http://0.0.0.0:60197') + .post('/api/v0/dht/findprovs') + .query(true) + .reply(200, `{"Extra":"","ID":"${provider}","Responses":[{"Addrs":${JSON.stringify(multiaddrs)},"ID":"${provider}"}],"Type":4}\n`, [ + 'Content-Type', 'application/json', + 'X-Chunked-Output', '1' + ]) + }) + + afterEach(() => { + // Stop each node + return Promise.all([local, remote, relayLibp2p].map(libp2p => libp2p.stop())) + }) + + it('should find providers for relay and add it as listen relay', async () => { + const originalMultiaddrsLength = local.multiaddrs.length + + // Spy add listen relay + sinon.spy(local.relay._autoRelay, '_addListenRelay') + // Spy Find Providers + sinon.spy(local.contentRouting, 'findProviders') + + // Try to listen on Available hop relays + await local.relay._autoRelay._listenOnAvailableHopRelays() + + // Should try to find relay service providers + await pWaitFor(() => local.contentRouting.findProviders.callCount === 1) + // Wait for peer added as listen relay + await pWaitFor(() => local.relay._autoRelay._addListenRelay.callCount === 1) + expect(local.relay._autoRelay._listenRelays.size).to.equal(1) + await pWaitFor(() => local.multiaddrs.length === originalMultiaddrsLength + 1) + + const relayedAddr = local.multiaddrs[local.multiaddrs.length - 1] + remote.peerStore.addressBook.set(local.peerId, [relayedAddr]) + + // Dial from remote through the relayed address + const conn = await remote.dial(local.peerId) + expect(conn).to.exist() + }) + }) +}) diff --git a/test/dialing/relay.node.js b/test/relay/relay.node.js similarity index 96% rename from test/dialing/relay.node.js rename to test/relay/relay.node.js index a591940801..163bd83105 100644 --- a/test/dialing/relay.node.js +++ b/test/relay/relay.node.js @@ -1,10 +1,7 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) -chai.use(require('chai-as-promised')) -const { expect } = chai +const { expect } = require('aegir/utils/chai') const sinon = require('sinon') const multiaddr = require('multiaddr') @@ -72,7 +69,7 @@ describe('Dialing (via relay, TCP)', () => { const tcpAddrs = dstLibp2p.transportManager.getAddrs() sinon.stub(dstLibp2p.addressManager, 'listen').value([multiaddr(`/p2p-circuit${relayAddr}/p2p/${relayIdString}`)]) - await dstLibp2p.transportManager.listen() + await dstLibp2p.transportManager.listen(dstLibp2p.addressManager.getListenAddrs()) expect(dstLibp2p.transportManager.getAddrs()).to.have.deep.members([...tcpAddrs, dialAddr.decapsulate('p2p')]) const connection = await srcLibp2p.dial(dialAddr) @@ -157,7 +154,7 @@ describe('Dialing (via relay, TCP)', () => { const tcpAddrs = dstLibp2p.transportManager.getAddrs() sinon.stub(dstLibp2p.addressManager, 'getListenAddrs').returns([multiaddr(`${relayAddr}/p2p-circuit`)]) - await dstLibp2p.transportManager.listen() + await dstLibp2p.transportManager.listen(dstLibp2p.addressManager.getListenAddrs()) expect(dstLibp2p.transportManager.getAddrs()).to.have.deep.members([...tcpAddrs, dialAddr.decapsulate('p2p')]) // Tamper with the our multiaddrs for the circuit message diff --git a/test/transports/transport-manager.node.js b/test/transports/transport-manager.node.js index 1036230acb..0f8cde5a8b 100644 --- a/test/transports/transport-manager.node.js +++ b/test/transports/transport-manager.node.js @@ -1,15 +1,17 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) -const { expect } = chai +const { expect } = require('aegir/utils/chai') const AddressManager = require('../../src/address-manager') const TransportManager = require('../../src/transport-manager') +const PeerStore = require('../../src/peer-store') +const PeerRecord = require('../../src/record/peer-record') const Transport = require('libp2p-tcp') +const PeerId = require('peer-id') const multiaddr = require('multiaddr') const mockUpgrader = require('../utils/mockUpgrader') +const Peers = require('../fixtures/peers') const addrs = [ multiaddr('/ip4/127.0.0.1/tcp/0'), multiaddr('/ip4/127.0.0.1/tcp/0') @@ -17,11 +19,19 @@ const addrs = [ describe('Transport Manager (TCP)', () => { let tm + let localPeer - before(() => { + before(async () => { + localPeer = await PeerId.createFromJSON(Peers[0]) + }) + + beforeEach(() => { tm = new TransportManager({ libp2p: { - addressManager: new AddressManager({ listen: addrs }) + peerId: localPeer, + multiaddrs: addrs, + addressManager: new AddressManager({ listen: addrs }), + peerStore: new PeerStore({ peerId: localPeer }) }, upgrader: mockUpgrader, onConnection: () => {} @@ -41,18 +51,38 @@ describe('Transport Manager (TCP)', () => { it('should be able to listen', async () => { tm.add(Transport.prototype[Symbol.toStringTag], Transport) - await tm.listen() + await tm.listen(addrs) 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.get(Transport.prototype[Symbol.toStringTag])).to.have.length(0) }) + it('should create self signed peer record on listen', async () => { + let signedPeerRecord = await tm.libp2p.peerStore.addressBook.getPeerRecord(localPeer) + expect(signedPeerRecord).to.not.exist() + + tm.add(Transport.prototype[Symbol.toStringTag], Transport) + await tm.listen(addrs) + + // Should created Self Peer record on new listen address + signedPeerRecord = await tm.libp2p.peerStore.addressBook.getPeerRecord(localPeer) + expect(signedPeerRecord).to.exist() + + const record = PeerRecord.createFromProtobuf(signedPeerRecord.payload) + expect(record).to.exist() + expect(record.multiaddrs.length).to.equal(addrs.length) + addrs.forEach((a, i) => { + expect(record.multiaddrs[i].equals(a)).to.be.true() + }) + }) + it('should be able to dial', async () => { tm.add(Transport.prototype[Symbol.toStringTag], Transport) - await tm.listen() + await tm.listen(addrs) const addr = tm.getAddrs().shift() const connection = await tm.dial(addr) expect(connection).to.exist() diff --git a/test/transports/transport-manager.spec.js b/test/transports/transport-manager.spec.js index b32b280725..28cc2c9b15 100644 --- a/test/transports/transport-manager.spec.js +++ b/test/transports/transport-manager.spec.js @@ -1,14 +1,12 @@ 'use strict' /* eslint-env mocha */ -const chai = require('chai') -chai.use(require('dirty-chai')) -chai.use(require('chai-as-promised')) -const { expect } = chai +const { expect } = require('aegir/utils/chai') const sinon = require('sinon') const multiaddr = require('multiaddr') const Transport = require('libp2p-websockets') +const filters = require('libp2p-websockets/src/filters') const { NOISE: Crypto } = require('libp2p-noise') const AddressManager = require('../../src/address-manager') const TransportManager = require('../../src/transport-manager') @@ -42,7 +40,7 @@ describe('Transport Manager (WebSockets)', () => { }) it('should be able to add and remove a transport', async () => { - tm.add(Transport.prototype[Symbol.toStringTag], Transport) + tm.add(Transport.prototype[Symbol.toStringTag], Transport, { filter: filters.all }) expect(tm._transports.size).to.equal(1) await tm.remove(Transport.prototype[Symbol.toStringTag]) }) @@ -69,7 +67,7 @@ describe('Transport Manager (WebSockets)', () => { }) it('should be able to dial', async () => { - tm.add(Transport.prototype[Symbol.toStringTag], Transport) + tm.add(Transport.prototype[Symbol.toStringTag], Transport, { filter: filters.all }) const addr = MULTIADDRS_WEBSOCKETS[0] const connection = await tm.dial(addr) expect(connection).to.exist() @@ -77,7 +75,7 @@ describe('Transport Manager (WebSockets)', () => { }) it('should fail to dial an unsupported address', async () => { - tm.add(Transport.prototype[Symbol.toStringTag], Transport) + tm.add(Transport.prototype[Symbol.toStringTag], Transport, { filter: filters.all }) const addr = multiaddr('/ip4/127.0.0.1/tcp/0') await expect(tm.dial(addr)) .to.eventually.be.rejected() @@ -85,9 +83,9 @@ describe('Transport Manager (WebSockets)', () => { }) it('should fail to listen with no valid address', async () => { - tm.add(Transport.prototype[Symbol.toStringTag], Transport) + tm.add(Transport.prototype[Symbol.toStringTag], Transport, { filter: filters.all }) - await expect(tm.listen()) + await expect(tm.listen([listenAddr])) .to.eventually.be.rejected() .and.to.have.property('code', ErrorCodes.ERR_NO_VALID_ADDRESSES) }) diff --git a/test/upgrading/upgrader.spec.js b/test/upgrading/upgrader.spec.js index 7282dcd1c2..96df354952 100644 --- a/test/upgrading/upgrader.spec.js +++ b/test/upgrading/upgrader.spec.js @@ -56,28 +56,6 @@ describe('Upgrader', () => { 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([[Crypto.protocol, Crypto]]) - 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 }) diff --git a/test/utils/base-options.browser.js b/test/utils/base-options.browser.js index c9c570ed8b..d033a4c945 100644 --- a/test/utils/base-options.browser.js +++ b/test/utils/base-options.browser.js @@ -1,9 +1,12 @@ 'use strict' const Transport = require('libp2p-websockets') +const filters = require('libp2p-websockets/src/filters') const Muxer = require('libp2p-mplex') const { NOISE: Crypto } = require('libp2p-noise') +const transportKey = Transport.prototype[Symbol.toStringTag] + module.exports = { modules: { transport: [Transport], @@ -16,6 +19,11 @@ module.exports = { hop: { enabled: false } + }, + transport: { + [transportKey]: { + filter: filters.all + } } } } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..5b9a618c43 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "./node_modules/aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src" + ] +} \ No newline at end of file