Compare commits

...

18 Commits

Author SHA1 Message Date
98f6fd7157 chore: add links from getting started and readme 2020-11-09 15:17:02 +01:00
62acb72ae4 docs: discoverability and connectivity 2020-11-09 15:17:02 +01:00
8456d0e051 chore: store self protocols in protobook (#760) 2020-11-09 14:11:48 +01:00
558bcf9541 chore: improve logging for auto relay active listen 2020-11-09 14:11:48 +01:00
3bd1768b04 chore: sort relay addresses to listen for public first 2020-11-09 14:11:48 +01:00
722cacd6d2 chore: lint issues fixed 2020-11-09 14:11:48 +01:00
2746b4b025 chore: apply suggestions from code review
Co-authored-by: Jacob Heun <jacobheun@gmail.com>
2020-11-09 14:11:48 +01:00
29e30c2199 chore: add configuration docs for auto relay and hop service 2020-11-09 14:11:48 +01:00
3d2181f6fa chore: address review 2020-11-09 14:11:48 +01:00
e6b0134299 feat: auto relay network query for new relays 2020-11-09 14:11:48 +01:00
2530b834a1 chore: lint issue fixed 0.30 2020-11-09 14:11:48 +01:00
05e6472cce chore: address review 2020-11-09 14:11:48 +01:00
abba305bd6 chore: add identify test for multiaddr change 2020-11-09 14:11:48 +01:00
87d20ac46d chore: create signed peer record on new listen addresses in transport manager 2020-11-09 14:11:48 +01:00
ee8ee5b49b chore: use listening events to create self peer record on updates 2020-11-09 14:11:48 +01:00
971655ff27 chore: _isStarted is false when stop starts 2020-11-09 14:11:48 +01:00
8d75093dcb chore: auto relay multiaddr update push 2020-11-09 14:11:48 +01:00
25488853ef feat: auto relay (#723)
* feat: auto relay

* fix: leverage protoBook events to ask relay peers if they support hop

* chore: refactor disconnect

* chore: do not listen on a relayed conn

* chore: tweaks

* chore: improve _listenOnAvailableHopRelays logic

* chore: default value of 1 to maxListeners on auto-relay
2020-11-09 14:11:48 +01:00
28 changed files with 1782 additions and 334 deletions

View File

@ -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).

View File

@ -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.

View File

@ -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
}
}
}

View 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 its 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`)

View File

@ -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

View File

@ -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
View 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

View File

@ -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
View 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
}

View File

@ -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

View File

@ -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
View 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
View 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)
}

View File

@ -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: {}

View File

@ -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

View File

@ -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()
}
/**

View File

@ -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)) {

View File

@ -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
View 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

View File

@ -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()
}
}

View File

@ -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()

View File

@ -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)
})

View File

@ -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)
})
})
})

View File

@ -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

View 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()
})
})
})

View File

@ -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

View File

@ -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()

View File

@ -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)
})