mirror of
https://github.com/fluencelabs/js-libp2p
synced 2025-07-08 05:11:33 +00:00
Compare commits
18 Commits
chore/use-
...
docs/disco
Author | SHA1 | Date | |
---|---|---|---|
98f6fd7157 | |||
62acb72ae4 | |||
8456d0e051 | |||
558bcf9541 | |||
3bd1768b04 | |||
722cacd6d2 | |||
2746b4b025 | |||
29e30c2199 | |||
3d2181f6fa | |||
e6b0134299 | |||
2530b834a1 | |||
05e6472cce | |||
abba305bd6 | |||
87d20ac46d | |||
ee8ee5b49b | |||
971655ff27 | |||
8d75093dcb | |||
25488853ef |
@ -35,7 +35,7 @@ We've come a long way, but this project is still in Alpha, lots of development i
|
||||
The documentation in the master branch may contain changes from a pre-release.
|
||||
If you are looking for the documentation of the latest release, you can view the latest release on [**npm**](https://www.npmjs.com/package/libp2p), or select the tag in github that matches the version you are looking for.
|
||||
|
||||
**Want to get started?** Check our [GETTING_STARTED.md](./doc/GETTING_STARTED.md) guide and [examples folder](/examples).
|
||||
**Want to get started?** Check our [GETTING_STARTED.md](./doc/GETTING_STARTED.md) guide, [Discoverability and Connectivity Readme](./DISCOVERABILITY_AND_CONNECTIVITY.md) and [examples folder](/examples).
|
||||
|
||||
**Want to update libp2p in your project?** Check our [migrations folder](./doc/migrations).
|
||||
|
||||
|
77
doc/API.md
77
doc/API.md
@ -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)
|
||||
@ -843,32 +844,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<string>` | 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 +1066,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<string>` | 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 +1147,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<string>` | 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.
|
||||
|
@ -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)
|
||||
@ -419,6 +420,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 SECIO = require('libp2p-secio')
|
||||
|
||||
const node = await Libp2p.create({
|
||||
modules: {
|
||||
transport: [TCP],
|
||||
streamMuxer: [MPLEX],
|
||||
connEncryption: [SECIO]
|
||||
},
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
99
doc/DISCOVERABILITY_AND_CONNECTIVITY.md
Normal file
99
doc/DISCOVERABILITY_AND_CONNECTIVITY.md
Normal file
@ -0,0 +1,99 @@
|
||||
# Discoverability and Connectivity
|
||||
|
||||
While different p2p applications have different needs and requirements, they might also run in different environments and have different hardware capabilities. These characteristics will influence how other peers can be discovered, as well as how connections are established and kept open.
|
||||
|
||||
This document contains a set of guidelines to setup libp2p for the most common use cases, in the context of the typical environments where you can run a `js-libp2p` node.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
[Background](#background)
|
||||
- [Discovery](#discovery)
|
||||
- [Connectivity](#connectivity)
|
||||
[Browser](#browser)
|
||||
- [Discovery](#discovery)
|
||||
- [Connectivity](#connectivity)
|
||||
- [Routing](#routing)
|
||||
- [Overview](#overview)
|
||||
[Node](#node)
|
||||
|
||||
## Background
|
||||
|
||||
### Discovery
|
||||
|
||||
Libp2p offers a variety of options to discover other peers on the network. They range from specifying well known peer addresses, to issue queries in a local network or exchanging peer addresses with other previously discovered peers.
|
||||
|
||||
To enable peer addresses exchange, peers need to specify their own announce addresses. Accordingly, announce addresses should be reachable from other peers to be valuable.
|
||||
|
||||
### Connectivity
|
||||
|
||||
A libp2p node cannot keep a unlimited number of connections over time due to hardware and network constraints. As a consequence, a node must keep the most important connections open at any moment. While certain connections will probably be important over longer periods of time, others might only be important for a smaller interval. Accordingly, libp2p needs to keep track of its open connections over time and verify if there are better connections to establish while keeping an healthy number open.
|
||||
|
||||
Well known peers are important for bootstrapping and getting to know other peers in the network. However, they will become less important over time since their main purpose is usually to bootstrap the network and not to provide other services. Moreover, as they will be reached by most of the network, they should be disconnected when they do not provide any more clear value to keep the network healthy.
|
||||
|
||||
Libp2p is able to automatically identify the importance of some connections over time, but the application layer should also flag important connections manually to improve the node's sensing of the network. For instance, libp2p will protect connections that are used in their listening addresses, in order to be reachable by other nodes, as well as connections with relevant peers for core protocols like gossipsub.
|
||||
|
||||
## Browser
|
||||
|
||||
Regarding enabling p2p applications, browsers currently have limitations that have impact on how libp2p should be setup.
|
||||
|
||||
### Discovery
|
||||
|
||||
Taking into account that a web browser does not offer any mDNS-like local discovery method to find peers on the same network and/or on the same web origin, a browser node will need to know other peers' addresses beforehand, so that it can bootstrap its network. These initial nodes should be used as a way to get to know other peers in the network and establish connections with them. Moreover, some of these peers can also advertise the browser peer to other nodes in the network, so that they can connect to it.
|
||||
|
||||
### Connectivity
|
||||
|
||||
Browser nodes do not have the ability to "listen" for incoming connections, nor a permanent address that can be dialed later for quick resume. However, Libp2p provides a set of possibilities to overcome these limitations. These solutions usually rely on other nodes to listen for connections on its behalf, as well as to advertise its information to other peers.
|
||||
|
||||
A browser node should start by establishing a connection with a known machine. As a result of this connection, the browser node will likely be interested to have its addresses announced to other peers in the network. Given that a browser cannot be dialed, the announced addresses of the node will be addresses that rely on this previously connection as the entry point of a dial request. For example, a circuit relay address from a connected peer. Shortly, browser nodes should have auto-relay enabled, so that they can bind to relay nodes that support HOP and become diable via them.
|
||||
|
||||
### Routing
|
||||
|
||||
DHTs are an essential building block of a p2p system to provide a lookup mechanism similar to a key-value hash table.
|
||||
|
||||
As browsers cannot handle large pools of open connections at the same time, as well as establish direct connections to each others, browser nodes cannot participate efficiently in DHTs. Once again, the best way to circumvent this limitation is to rely on more capable nodes in the network to handle DHT queries on their behalf. Browser nodes can rely on delegate nodes or use the DHT in client mode.
|
||||
|
||||
### Overview
|
||||
|
||||
The base connections to have a fully functional libp2p browser node are:
|
||||
- nodes that can listen for incoming connections
|
||||
- Relay nodes, Webrtc-star servers, ...
|
||||
- nodes that can enable peer discovery and service discovery
|
||||
- Webrtc-star servers, Rendezvous servers, DHT server Nodes, ...
|
||||
- closest nodes
|
||||
- nodes that can enable efficient routing
|
||||
- DHT server nodes
|
||||
- nodes from the pubsub topics mesh
|
||||
- application protocol peers (as needed via `MulticodecTopology`)
|
||||
|
||||
While the first three points are important in any context, the last three points depend on the application use case and if the mentioned subsystems are needed.
|
||||
|
||||
TODO: Clearly define what libp2p handles and how it is handled
|
||||
|
||||
- Libp2p will protect connections used in their listening addresses like connections to a `webrtc-star` server or connections to a node acting as a relay through the `AutoRelay`, as well as nodes used for peer and service discovery
|
||||
- Libp2p pubsub routers will protect the most important peer connections
|
||||
- How to control and avoid excess
|
||||
- Libp2p will protect connections to n DHT servers
|
||||
- Libp2p will protect the n (configurable) closest peers on the network and refresh them over time, if needed
|
||||
- Application protocol peers should be protected
|
||||
- TODO: define how
|
||||
|
||||
## Node
|
||||
|
||||
In a Node.js context, there are less limitations that need to be considered regarding discoverability and connectivity compared to browser nodes.
|
||||
|
||||
The most common issue is when Libp2p nodes are behind NATs. While NAT is usually transparent for outgoing connections, listening for incoming connections might require some configuration. While it’s usually possible to manually configure routers, not everyone that wants to run a peer-to-peer application or other network service will have the ability to do so. Moreover, libp2p applications should run everywhere, not just in data centers or on machines with stable public IP addresses.
|
||||
|
||||
The best approach at the moment to circumvent this limitation is to rely on relay to communicate indirectly via an intermediary peer.
|
||||
|
||||
### Overview
|
||||
|
||||
The base connections to have a fully functional libp2p browser node are:
|
||||
- nodes that can listen for incoming connections when behind a NAT
|
||||
- Relay nodes, ...
|
||||
- nodes that can enable service discovery
|
||||
- Rendezvous servers, DHT server Nodes, ...
|
||||
- closest nodes
|
||||
- nodes that can enable efficient routing
|
||||
- DHT server nodes
|
||||
- nodes from the pubsub topics mesh
|
||||
- application protocol peers (as needed via `MulticodecTopology`)
|
@ -248,7 +248,9 @@ If you want to know more about libp2p peer discovery, you should read the follow
|
||||
|
||||
## What is next
|
||||
|
||||
There are a lot of other concepts within `libp2p`, that are not covered in this guide. For additional configuration options we recommend checking out the [Configuration Readme](./CONFIGURATION.md) and the [examples folder](../examples). If you have any problems getting started, or if anything isn't clear, please let us know by submitting an issue!
|
||||
There are a lot of other concepts within `libp2p`, that are not covered in this guide. For additional configuration options we recommend checking out the [Configuration Readme](./CONFIGURATION.md) and the [examples folder](../examples).
|
||||
For guidelines on how to enable discoverability and connectivity for your node environment and use case, you can check the [Discoverability and Connectivity Readme](./DISCOVERABILITY_AND_CONNECTIVITY.md).
|
||||
If you have any problems getting started, or if anything isn't clear, please let us know by submitting an issue!
|
||||
|
||||
|
||||
[transport]: https://github.com/libp2p/js-interfaces/tree/master/src/transport
|
||||
|
@ -45,6 +45,7 @@
|
||||
"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",
|
||||
@ -60,12 +61,13 @@
|
||||
"it-protocol-buffers": "^0.2.0",
|
||||
"libp2p-crypto": "^0.18.0",
|
||||
"libp2p-interfaces": "^0.5.1",
|
||||
"libp2p-utils": "^0.2.0",
|
||||
"libp2p-utils": "^0.2.1",
|
||||
"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",
|
||||
@ -89,7 +91,6 @@
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"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",
|
||||
|
292
src/circuit/auto-relay.js
Normal file
292
src/circuit/auto-relay.js
Normal file
@ -0,0 +1,292 @@
|
||||
'use strict'
|
||||
|
||||
const debug = require('debug')
|
||||
const log = debug('libp2p:auto-relay')
|
||||
log.error = debug('libp2p:auto-relay:error')
|
||||
|
||||
const isPrivate = require('libp2p-utils/src/multiaddr/is-private')
|
||||
|
||||
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')
|
||||
|
||||
class AutoRelay {
|
||||
/**
|
||||
* Creates an instance of AutoRelay.
|
||||
*
|
||||
* @class
|
||||
* @param {object} props
|
||||
* @param {Libp2p} props.libp2p
|
||||
* @param {number} [props.maxListeners = 1] - maximum number of relays to listen.
|
||||
*/
|
||||
constructor ({ libp2p, maxListeners = 1 }) {
|
||||
this._libp2p = libp2p
|
||||
this._peerId = libp2p.peerId
|
||||
this._peerStore = libp2p.peerStore
|
||||
this._connectionManager = libp2p.connectionManager
|
||||
this._transportManager = libp2p.transportManager
|
||||
|
||||
this.maxListeners = maxListeners
|
||||
|
||||
/**
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
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 {Array<string>} props.protocols
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
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)
|
||||
|
||||
// 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<void>}
|
||||
*/
|
||||
async _addListenRelay (connection, id) {
|
||||
// Check if already listening on enough relays
|
||||
if (this._listenRelays.size >= this.maxListeners) {
|
||||
return
|
||||
}
|
||||
|
||||
// Create relay listen addr
|
||||
let listenAddr, remoteMultiaddr, remoteAddrs
|
||||
|
||||
try {
|
||||
// Get peer known addresses and sort them per public addresses first
|
||||
remoteAddrs = this._peerStore.addressBook.get(connection.remotePeer)
|
||||
// TODO: This sort should be customizable in the config (dialer addr sort)
|
||||
remoteAddrs.sort(multiaddrsCompareFunction)
|
||||
|
||||
remoteMultiaddr = remoteAddrs.find(a => a.isCertified).multiaddr // Get first announced address certified
|
||||
// TODO: HOP Relays should avoid advertising private addresses!
|
||||
} catch (_) {
|
||||
log.error(`${id} does not have announced certified multiaddrs`)
|
||||
|
||||
// Attempt first if existing
|
||||
if (!remoteAddrs || !remoteAddrs.length) {
|
||||
return
|
||||
}
|
||||
|
||||
remoteMultiaddr = remoteAddrs[0].multiaddr
|
||||
}
|
||||
|
||||
if (!remoteMultiaddr.protoNames().includes('p2p')) {
|
||||
listenAddr = `${remoteMultiaddr.toString()}/p2p/${connection.remotePeer.toB58String()}/p2p-circuit`
|
||||
} else {
|
||||
listenAddr = `${remoteMultiaddr.toString()}/p2p-circuit`
|
||||
}
|
||||
|
||||
// Attempt to listen on relay
|
||||
this._listenRelays.add(id)
|
||||
|
||||
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 {Array<string>} [peersToIgnore]
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare function for array.sort().
|
||||
* This sort aims to move the private adresses to the end of the array.
|
||||
*
|
||||
* @param {Address} a
|
||||
* @param {Address} b
|
||||
* @returns {number}
|
||||
*/
|
||||
function multiaddrsCompareFunction (a, b) {
|
||||
const isAPrivate = isPrivate(a.multiaddr)
|
||||
const isBPrivate = isPrivate(b.multiaddr)
|
||||
|
||||
if (isAPrivate && !isBPrivate) {
|
||||
return 1
|
||||
} else if (!isAPrivate && isBPrivate) {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
module.exports = AutoRelay
|
@ -116,6 +116,34 @@ module.exports.hop = async function hop ({
|
||||
throw errCode(new Error(`HOP request failed with code ${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<boolean>}
|
||||
*/
|
||||
module.exports.canHop = 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.code !== CircuitPB.Status.SUCCESS) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an unencoded CAN_HOP response based on the Circuits configuration
|
||||
*
|
||||
|
12
src/circuit/constants.js
Normal file
12
src/circuit/constants.js
Normal file
@ -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
|
||||
}
|
@ -1,187 +1,91 @@
|
||||
'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 = debug('libp2p:relay')
|
||||
log.error = debug('libp2p:relay:error')
|
||||
|
||||
const { relay: multicodec } = require('./multicodec')
|
||||
const createListener = require('./listener')
|
||||
const { handleCanHop, handleHop, hop } = require('./circuit/hop')
|
||||
const { handleStop } = require('./circuit/stop')
|
||||
const StreamHandler = require('./circuit/stream-handler')
|
||||
const AutoRelay = require('./auto-relay')
|
||||
const { namespaceToCid } = require('./utils')
|
||||
const {
|
||||
ADVERTISE_BOOT_DELAY,
|
||||
ADVERTISE_TTL,
|
||||
RELAY_RENDEZVOUS_NS
|
||||
} = require('./constants')
|
||||
|
||||
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)
|
||||
/**
|
||||
* Start Relay service.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
start () {
|
||||
// Advertise service if HOP enabled
|
||||
const canHop = this._options.hop.enabled
|
||||
|
||||
if (canHop && this._options.advertise.enabled) {
|
||||
this._timeout = setTimeout(() => {
|
||||
this._advertiseService()
|
||||
}, this._options.advertise.bootDelay)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dial a peer over a relay
|
||||
* Stop 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
|
||||
}
|
||||
stop () {
|
||||
clearTimeout(this._timeout)
|
||||
}
|
||||
|
||||
/**
|
||||
* Advertise hop relay service in the network.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async _advertiseService () {
|
||||
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)
|
||||
const cid = await namespaceToCid(RELAY_RENDEZVOUS_NS)
|
||||
await this._libp2p.contentRouting.provide(cid)
|
||||
} catch (err) {
|
||||
log.error('Circuit relay dial failed', err)
|
||||
disconnectOnFailure && await relayConnection.close()
|
||||
throw 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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a listener
|
||||
*
|
||||
* @param {any} options
|
||||
* @param {Function} handler
|
||||
* @returns {listener}
|
||||
*/
|
||||
createListener (options, handler) {
|
||||
if (typeof options === 'function') {
|
||||
handler = options
|
||||
options = {}
|
||||
return
|
||||
}
|
||||
|
||||
// Called on successful HOP and STOP requests
|
||||
this.handler = handler
|
||||
|
||||
return createListener(this, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter check for all Multiaddrs that this transport can dial on
|
||||
*
|
||||
* @param {Array<Multiaddr>} multiaddrs
|
||||
* @returns {Array<Multiaddr>}
|
||||
*/
|
||||
filter (multiaddrs) {
|
||||
multiaddrs = Array.isArray(multiaddrs) ? multiaddrs : [multiaddrs]
|
||||
|
||||
return multiaddrs.filter((ma) => {
|
||||
return mafmt.Circuit.matches(ma)
|
||||
})
|
||||
// Restart timeout
|
||||
this._timeout = setTimeout(() => {
|
||||
this._advertiseService()
|
||||
}, this._options.advertise.ttl)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Circuit}
|
||||
*/
|
||||
module.exports = withIs(Circuit, { className: 'Circuit', symbolName: '@libp2p/js-libp2p-circuit/circuit' })
|
||||
module.exports = Relay
|
||||
|
@ -8,13 +8,23 @@ const log = debug('libp2p:circuit:listener')
|
||||
log.err = debug('libp2p:circuit:error:listener')
|
||||
|
||||
/**
|
||||
* @param {*} circuit
|
||||
* @param {Libp2p} libp2p
|
||||
* @returns {Listener} a transport listener
|
||||
*/
|
||||
module.exports = (circuit) => {
|
||||
module.exports = (libp2p) => {
|
||||
const listener = new EventEmitter()
|
||||
const listeningAddrs = new Map()
|
||||
|
||||
// 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')
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Add swarm handler and listen for incoming connections
|
||||
*
|
||||
@ -24,7 +34,7 @@ module.exports = (circuit) => {
|
||||
listener.listen = async (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)
|
||||
|
194
src/circuit/transport.js
Normal file
194
src/circuit/transport.js
Normal file
@ -0,0 +1,194 @@
|
||||
'use strict'
|
||||
|
||||
const debug = require('debug')
|
||||
const log = debug('libp2p:circuit')
|
||||
log.error = debug('libp2p:circuit:error')
|
||||
|
||||
const mafmt = require('mafmt')
|
||||
const multiaddr = require('multiaddr')
|
||||
const PeerId = require('peer-id')
|
||||
const withIs = require('class-is')
|
||||
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')
|
||||
|
||||
class Circuit {
|
||||
/**
|
||||
* Creates an instance of the Circuit Transport.
|
||||
*
|
||||
* @class
|
||||
* @param {object} options
|
||||
* @param {Libp2p} options.libp2p
|
||||
* @param {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))
|
||||
}
|
||||
|
||||
async _onProtocol ({ connection, stream }) {
|
||||
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,
|
||||
circuit
|
||||
})
|
||||
break
|
||||
}
|
||||
default: {
|
||||
log('Request of type %s not supported', request.type)
|
||||
}
|
||||
}
|
||||
|
||||
if (virtualConnection) {
|
||||
const remoteAddr = multiaddr(request.dstPeer.addrs[0])
|
||||
const localAddr = multiaddr(request.srcPeer.addrs[0])
|
||||
const maConn = toConnection({
|
||||
stream: virtualConnection,
|
||||
remoteAddr,
|
||||
localAddr
|
||||
})
|
||||
const type = CircuitPB.Type === CircuitPB.Type.HOP ? 'relay' : 'inbound'
|
||||
log('new %s connection %s', type, maConn.remoteAddr)
|
||||
|
||||
const conn = await this._upgrader.upgradeInbound(maConn)
|
||||
log('%s connection %s upgraded', type, maConn.remoteAddr)
|
||||
this.handler && this.handler(conn)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dial a peer over a relay
|
||||
*
|
||||
* @param {multiaddr} ma - the multiaddr of the peer to dial
|
||||
* @param {Object} options - dial options
|
||||
* @param {AbortSignal} [options.signal] - An optional abort signal
|
||||
* @returns {Connection} - the connection
|
||||
*/
|
||||
async dial (ma, options) {
|
||||
// Check the multiaddr to see if it contains a relay and a destination peer
|
||||
const addrs = ma.toString().split('/p2p-circuit')
|
||||
const relayAddr = multiaddr(addrs[0])
|
||||
const destinationAddr = multiaddr(addrs[addrs.length - 1])
|
||||
const relayPeer = PeerId.createFromCID(relayAddr.getPeerId())
|
||||
const destinationPeer = PeerId.createFromCID(destinationAddr.getPeerId())
|
||||
|
||||
let disconnectOnFailure = false
|
||||
let relayConnection = this._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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a listener
|
||||
*
|
||||
* @param {any} options
|
||||
* @param {Function} handler
|
||||
* @returns {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, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter check for all Multiaddrs that this transport can dial on
|
||||
*
|
||||
* @param {Array<Multiaddr>} multiaddrs
|
||||
* @returns {Array<Multiaddr>}
|
||||
*/
|
||||
filter (multiaddrs) {
|
||||
multiaddrs = Array.isArray(multiaddrs) ? multiaddrs : [multiaddrs]
|
||||
|
||||
return multiaddrs.filter((ma) => {
|
||||
return mafmt.Circuit.matches(ma)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Circuit}
|
||||
*/
|
||||
module.exports = withIs(Circuit, { className: 'Circuit', symbolName: '@libp2p/js-libp2p-circuit/circuit' })
|
17
src/circuit/utils.js
Normal file
17
src/circuit/utils.js
Normal file
@ -0,0 +1,17 @@
|
||||
'use strict'
|
||||
|
||||
const CID = require('cids')
|
||||
const multihashing = require('multihashing-async')
|
||||
|
||||
/**
|
||||
* Convert a namespace string into a cid.
|
||||
*
|
||||
* @param {string} namespace
|
||||
* @returns {Promise<CID>}
|
||||
*/
|
||||
module.exports.namespaceToCid = async (namespace) => {
|
||||
const bytes = new TextEncoder('utf8').encode(namespace)
|
||||
const hash = await multihashing(bytes, 'sha2-256')
|
||||
|
||||
return new CID(hash)
|
||||
}
|
@ -4,6 +4,7 @@ const mergeOptions = require('merge-options')
|
||||
const { dnsaddrResolver } = require('multiaddr/src/resolvers')
|
||||
|
||||
const Constants = require('./constants')
|
||||
const RelayConstants = require('./circuit/constants')
|
||||
|
||||
const { FaultTolerance } = require('./transport-manager')
|
||||
|
||||
@ -56,9 +57,18 @@ const DefaultConfig = {
|
||||
},
|
||||
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: {}
|
||||
|
@ -51,9 +51,8 @@ class IdentifyService {
|
||||
* @class
|
||||
* @param {object} options
|
||||
* @param {Libp2p} options.libp2p
|
||||
* @param {Map<string, handler>} options.protocols - A reference to the protocols we support
|
||||
*/
|
||||
constructor ({ libp2p, protocols }) {
|
||||
constructor ({ libp2p }) {
|
||||
/**
|
||||
* @property {PeerStore}
|
||||
*/
|
||||
@ -64,12 +63,6 @@ class IdentifyService {
|
||||
*/
|
||||
this.connectionManager = libp2p.connectionManager
|
||||
|
||||
this.connectionManager.on('peer:connect', (connection) => {
|
||||
const peerId = connection.remotePeer
|
||||
|
||||
this.identify(connection, peerId).catch(log.error)
|
||||
})
|
||||
|
||||
/**
|
||||
* @property {PeerId}
|
||||
*/
|
||||
@ -80,9 +73,28 @@ class IdentifyService {
|
||||
*/
|
||||
this._libp2p = libp2p
|
||||
|
||||
this._protocols = protocols
|
||||
|
||||
this.handleMessage = this.handleMessage.bind(this)
|
||||
|
||||
// When a new connection happens, trigger identify
|
||||
this.connectionManager.on('peer:connect', (connection) => {
|
||||
const peerId = connection.remotePeer
|
||||
|
||||
this.identify(connection, peerId).catch(log.error)
|
||||
})
|
||||
|
||||
// When self multiaddrs change, trigger identify-push
|
||||
this.peerStore.on('change:multiaddrs', ({ peerId }) => {
|
||||
if (peerId.toString() === this.peerId.toString()) {
|
||||
this.pushToPeerStore()
|
||||
}
|
||||
})
|
||||
|
||||
// When self protocols change, trigger identify-push
|
||||
this.peerStore.on('change:protocols', ({ peerId }) => {
|
||||
if (peerId.toString() === this.peerId.toString()) {
|
||||
this.pushToPeerStore()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@ -92,9 +104,9 @@ class IdentifyService {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
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 +134,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)
|
||||
}
|
||||
@ -243,7 +260,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 +270,7 @@ class IdentifyService {
|
||||
listenAddrs: this._libp2p.multiaddrs.map((ma) => ma.bytes),
|
||||
signedPeerRecord,
|
||||
observedAddr: connection.remoteAddr.bytes,
|
||||
protocols: Array.from(this._protocols.keys())
|
||||
protocols
|
||||
})
|
||||
|
||||
try {
|
||||
@ -313,34 +331,6 @@ class IdentifyService {
|
||||
// Update the protocols
|
||||
this.peerStore.protoBook.set(id, message.protocols)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get self signed peer record raw envelope.
|
||||
*
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
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')
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.IdentifyService = IdentifyService
|
||||
|
35
src/index.js
35
src/index.js
@ -17,7 +17,8 @@ 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')
|
||||
@ -146,6 +147,7 @@ class Libp2p extends EventEmitter {
|
||||
|
||||
if (this._config.relay.enabled) {
|
||||
this.transportManager.add(Circuit.prototype[Symbol.toStringTag], Circuit)
|
||||
this.relay = new Relay(this)
|
||||
}
|
||||
|
||||
// Attach stream multiplexers
|
||||
@ -156,10 +158,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)
|
||||
}
|
||||
|
||||
@ -248,6 +247,11 @@ class Libp2p extends EventEmitter {
|
||||
log('libp2p is stopping')
|
||||
|
||||
try {
|
||||
this._isStarted = false
|
||||
|
||||
// Relay
|
||||
this.relay && this.relay.stop()
|
||||
|
||||
for (const service of this._discovery.values()) {
|
||||
service.removeListener('peer', this._onDiscoveryPeer)
|
||||
}
|
||||
@ -275,7 +279,6 @@ class Libp2p extends EventEmitter {
|
||||
this.emit('error', err)
|
||||
}
|
||||
}
|
||||
this._isStarted = false
|
||||
log('libp2p has stopped')
|
||||
}
|
||||
|
||||
@ -431,10 +434,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 +450,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 +502,9 @@ class Libp2p extends EventEmitter {
|
||||
|
||||
// Peer discovery
|
||||
await this._setupPeerDiscovery()
|
||||
|
||||
// Relay
|
||||
this.relay && this.relay.start()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -270,7 +270,7 @@ class AddressBook extends Book {
|
||||
*
|
||||
* @override
|
||||
* @param {PeerId} peerId
|
||||
* @returns {Array<data>}
|
||||
* @returns {Array<Address>|undefined}
|
||||
*/
|
||||
get (peerId) {
|
||||
if (!PeerId.isPeerId(peerId)) {
|
||||
|
@ -112,13 +112,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 {Array<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
|
||||
|
21
src/record/utils.js
Normal file
21
src/record/utils.js
Normal file
@ -0,0 +1,21 @@
|
||||
'use strict'
|
||||
|
||||
const Envelope = require('./envelope')
|
||||
const PeerRecord = require('./peer-record')
|
||||
|
||||
/**
|
||||
* Create (or update if existing) self peer record and store it in the AddressBook.
|
||||
*
|
||||
* @param {libp2p} libp2p
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
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
|
@ -7,6 +7,8 @@ const debug = require('debug')
|
||||
const log = debug('libp2p:transports')
|
||||
log.error = debug('libp2p:transports:error')
|
||||
|
||||
const { updateSelfPeerRecord } = require('./record/utils')
|
||||
|
||||
class TransportManager {
|
||||
/**
|
||||
* @class
|
||||
@ -63,6 +65,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())
|
||||
}
|
||||
}
|
||||
@ -137,11 +141,10 @@ class TransportManager {
|
||||
* Starts listeners for each listen Multiaddr.
|
||||
*
|
||||
* @async
|
||||
* @param {Array<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
|
||||
}
|
||||
@ -157,6 +160,10 @@ class TransportManager {
|
||||
const listener = transport.createListener({}, this.onConnection)
|
||||
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 +208,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()
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -349,7 +349,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 +357,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)
|
||||
})
|
||||
|
||||
|
@ -8,7 +8,6 @@ const { expect } = 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')
|
||||
@ -22,6 +21,7 @@ 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 +29,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 +55,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 +80,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 +110,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 +169,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 +206,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 +247,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 +261,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 +276,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 +376,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 +398,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 +419,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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -5,7 +5,9 @@ const chai = require('chai')
|
||||
chai.use(require('dirty-chai'))
|
||||
const { expect } = chai
|
||||
|
||||
const sinon = require('sinon')
|
||||
const pDefer = require('p-defer')
|
||||
const pWaitFor = require('p-wait-for')
|
||||
|
||||
const PeerStore = require('../../src/peer-store')
|
||||
|
||||
@ -224,6 +226,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
|
||||
|
||||
|
585
test/relay/auto-relay.node.js
Normal file
585
test/relay/auto-relay.node.js
Normal file
@ -0,0 +1,585 @@
|
||||
'use strict'
|
||||
/* eslint-env mocha */
|
||||
|
||||
const chai = require('chai')
|
||||
chai.use(require('dirty-chai'))
|
||||
const { expect } = 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()
|
||||
})
|
||||
})
|
||||
})
|
@ -72,7 +72,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 +157,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
|
@ -7,9 +7,13 @@ const { expect } = 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 +21,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 +53,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()
|
||||
|
@ -87,7 +87,7 @@ describe('Transport Manager (WebSockets)', () => {
|
||||
it('should fail to listen with no valid address', async () => {
|
||||
tm.add(Transport.prototype[Symbol.toStringTag], Transport)
|
||||
|
||||
await expect(tm.listen())
|
||||
await expect(tm.listen([listenAddr]))
|
||||
.to.eventually.be.rejected()
|
||||
.and.to.have.property('code', ErrorCodes.ERR_NO_VALID_ADDRESSES)
|
||||
})
|
||||
|
Reference in New Issue
Block a user