feat: address and proto books (#590)

* feat: address and proto books

* chore: apply suggestions from code review

Co-Authored-By: Jacob Heun <jacobheun@gmail.com>

* chore: minor fixes and initial tests added

* chore: integrate new peer-store with code using adapters for other modules

* chore: do not use peerstore.put on get-peer-info

* chore: apply suggestions from code review

Co-Authored-By: Jacob Heun <jacobheun@gmail.com>

* chore: add new peer store tests

* chore: apply suggestions from code review

Co-Authored-By: Jacob Heun <jacobheun@gmail.com>

Co-authored-by: Jacob Heun <jacobheun@gmail.com>
This commit is contained in:
Vasco Santos
2020-04-09 16:07:18 +02:00
committed by Jacob Heun
parent 2aac3b0f69
commit e9d225c9dc
23 changed files with 2019 additions and 480 deletions

View File

@@ -5,7 +5,6 @@ const errCode = require('err-code')
const TimeoutController = require('timeout-abort-controller')
const anySignal = require('any-signal')
const PeerId = require('peer-id')
const PeerInfo = require('peer-info')
const debug = require('debug')
const log = debug('libp2p:dialer')
log.error = debug('libp2p:dialer:error')
@@ -62,13 +61,13 @@ class Dialer {
* The dial to the first address that is successfully able to upgrade a connection
* will be used.
*
* @param {PeerInfo|Multiaddr} peer The peer to dial
* @param {PeerId|Multiaddr} peerId The peer to dial
* @param {object} [options]
* @param {AbortSignal} [options.signal] An AbortController signal
* @returns {Promise<Connection>}
*/
async connectToPeer (peer, options = {}) {
const dialTarget = this._createDialTarget(peer)
async connectToPeer (peerId, options = {}) {
const dialTarget = this._createDialTarget(peerId)
if (dialTarget.addrs.length === 0) {
throw errCode(new Error('The dial request has no addresses'), codes.ERR_NO_VALID_ADDRESSES)
}
@@ -100,7 +99,7 @@ class Dialer {
* Creates a DialTarget. The DialTarget is used to create and track
* the DialRequest to a given peer.
* @private
* @param {PeerInfo|Multiaddr} peer A PeerId or Multiaddr
* @param {PeerId|Multiaddr} peer A PeerId or Multiaddr
* @returns {DialTarget}
*/
_createDialTarget (peer) {
@@ -111,7 +110,10 @@ class Dialer {
addrs: [dialable]
}
}
const addrs = this.peerStore.multiaddrsForPeer(dialable)
dialable.multiaddrs && this.peerStore.addressBook.add(dialable.id, Array.from(dialable.multiaddrs))
const addrs = this.peerStore.addressBook.getMultiaddrsForPeer(dialable.id)
return {
id: dialable.id.toB58String(),
addrs
@@ -179,21 +181,27 @@ class Dialer {
this.tokens.push(token)
}
/**
* PeerInfo object
* @typedef {Object} peerInfo
* @property {Multiaddr} multiaddr peer multiaddr.
* @property {PeerId} id peer id.
*/
/**
* Converts the given `peer` into a `PeerInfo` or `Multiaddr`.
* @static
* @param {PeerInfo|PeerId|Multiaddr|string} peer
* @returns {PeerInfo|Multiaddr}
* @param {PeerId|Multiaddr|string} peer
* @returns {peerInfo|Multiaddr}
*/
static getDialable (peer) {
if (PeerInfo.isPeerInfo(peer)) return peer
if (typeof peer === 'string') {
peer = multiaddr(peer)
}
let addr
let addrs
if (multiaddr.isMultiaddr(peer)) {
addr = peer
addrs = new Set([peer]) // TODO: after peer-info removal, a Set should not be needed
try {
peer = PeerId.createFromCID(peer.getPeerId())
} catch (err) {
@@ -202,10 +210,12 @@ class Dialer {
}
if (PeerId.isPeerId(peer)) {
peer = new PeerInfo(peer)
peer = {
id: peer,
multiaddrs: addrs
}
}
addr && peer.multiaddrs.add(addr)
return peer
}
}

View File

@@ -38,7 +38,12 @@ function getPeerInfo (peer, peerStore) {
addr && peer.multiaddrs.add(addr)
return peerStore ? peerStore.put(peer) : peer
if (peerStore) {
peerStore.addressBook.add(peer.id, peer.multiaddrs.toArray())
peerStore.protoBook.add(peer.id, Array.from(peer.protocols))
}
return peer
}
/**

View File

@@ -7,7 +7,6 @@ const lp = require('it-length-prefixed')
const pipe = require('it-pipe')
const { collect, take, consume } = require('streaming-iterables')
const PeerInfo = require('peer-info')
const PeerId = require('peer-id')
const multiaddr = require('multiaddr')
const { toBuffer } = require('it-buffer')
@@ -28,39 +27,6 @@ const errCode = require('err-code')
const { codes } = require('../errors')
class IdentifyService {
/**
* Replaces the multiaddrs on the given `peerInfo`,
* with the provided `multiaddrs`
* @param {PeerInfo} peerInfo
* @param {Array<Multiaddr>|Array<Buffer>} multiaddrs
*/
static updatePeerAddresses (peerInfo, multiaddrs) {
if (multiaddrs && multiaddrs.length > 0) {
peerInfo.multiaddrs.clear()
multiaddrs.forEach(ma => {
try {
peerInfo.multiaddrs.add(ma)
} catch (err) {
log.error('could not add multiaddr', err)
}
})
}
}
/**
* Replaces the protocols on the given `peerInfo`,
* with the provided `protocols`
* @static
* @param {PeerInfo} peerInfo
* @param {Array<string>} protocols
*/
static updatePeerProtocols (peerInfo, protocols) {
if (protocols && protocols.length > 0) {
peerInfo.protocols.clear()
protocols.forEach(proto => peerInfo.protocols.add(proto))
}
}
/**
* Takes the `addr` and converts it to a Multiaddr if possible
* @param {Buffer|String} addr
@@ -182,7 +148,7 @@ class IdentifyService {
} = message
const id = await PeerId.createFromPubKey(publicKey)
const peerInfo = new PeerInfo(id)
if (connection.remotePeer.toB58String() !== id.toB58String()) {
throw errCode(new Error('identified peer does not match the expected peer'), codes.ERR_INVALID_PEER)
}
@@ -190,11 +156,10 @@ class IdentifyService {
// Get the observedAddr if there is one
observedAddr = IdentifyService.getCleanMultiaddr(observedAddr)
// Copy the listenAddrs and protocols
IdentifyService.updatePeerAddresses(peerInfo, listenAddrs)
IdentifyService.updatePeerProtocols(peerInfo, protocols)
// Update peers data in PeerStore
this.registrar.peerStore.addressBook.set(id, listenAddrs.map((addr) => multiaddr(addr)))
this.registrar.peerStore.protoBook.set(id, protocols)
this.registrar.peerStore.replace(peerInfo)
// TODO: Track our observed address so that we can score it
log('received observed address of %s', observedAddr)
}
@@ -274,20 +239,16 @@ class IdentifyService {
return log.error('received invalid message', err)
}
// Update the listen addresses
const peerInfo = new PeerInfo(connection.remotePeer)
// Update peers data in PeerStore
const id = connection.remotePeer
try {
IdentifyService.updatePeerAddresses(peerInfo, message.listenAddrs)
this.registrar.peerStore.addressBook.set(id, message.listenAddrs.map((addr) => multiaddr(addr)))
} catch (err) {
return log.error('received invalid listen addrs', err)
}
// Update the protocols
IdentifyService.updatePeerProtocols(peerInfo, message.protocols)
// Update the peer in the PeerStore
this.registrar.peerStore.replace(peerInfo)
this.registrar.peerStore.protoBook.set(id, message.protocols)
}
}

View File

@@ -60,7 +60,7 @@ class Libp2p extends EventEmitter {
localPeer: this.peerInfo.id,
metrics: this.metrics,
onConnection: (connection) => {
const peerInfo = this.peerStore.put(new PeerInfo(connection.remotePeer), { silent: true })
const peerInfo = new PeerInfo(connection.remotePeer)
this.registrar.onConnect(peerInfo, connection)
this.connectionManager.onConnect(connection)
this.emit('peer:connect', peerInfo)
@@ -292,7 +292,11 @@ class Libp2p extends EventEmitter {
const dialable = Dialer.getDialable(peer)
let connection
if (PeerInfo.isPeerInfo(dialable)) {
this.peerStore.put(dialable, { silent: true })
// TODO Inconsistency from: getDialable adds a set, while regular peerInfo uses a Multiaddr set
// This should be handled on `peer-info` removal
const multiaddrs = dialable.multiaddrs.toArray ? dialable.multiaddrs.toArray() : Array.from(dialable.multiaddrs)
this.peerStore.addressBook.add(dialable.id, multiaddrs)
connection = this.registrar.getConnection(dialable)
}
@@ -338,7 +342,7 @@ class Libp2p extends EventEmitter {
async ping (peer) {
const peerInfo = await getPeerInfo(peer, this.peerStore)
return ping(this, peerInfo)
return ping(this, peerInfo.id)
}
/**
@@ -440,7 +444,10 @@ class Libp2p extends EventEmitter {
log.error(new Error(codes.ERR_DISCOVERED_SELF))
return
}
this.peerStore.put(peerInfo)
// TODO: once we deprecate peer-info, we should only set if we have data
this.peerStore.addressBook.add(peerInfo.id, peerInfo.multiaddrs.toArray())
this.peerStore.protoBook.set(peerInfo.id, Array.from(peerInfo.protocols))
}
/**

View File

@@ -1,3 +1,89 @@
# Peerstore
# PeerStore
WIP
Libp2p's PeerStore is responsible for keeping an updated register with the relevant information of the known peers. It should be the single source of truth for all peer data, where a subsystem can learn about peers' data and where someone can listen for updates. The PeerStore comprises four main components: `addressBook`, `keyBook`, `protocolBook` and `metadataBook`.
The PeerStore manages the high level operations on its inner books. Moreover, the PeerStore should be responsible for notifying interested parties of relevant events, through its Event Emitter.
## Data gathering
Several libp2p subsystems will perform operations, which will gather relevant information about peers. Some operations might not have this as an end goal, but can also gather important data.
In a libp2p node's life, it will discover peers through its discovery protocols. In a typical discovery protocol, addresses of the peer are discovered along with its peer id. Once this happens, the PeerStore should collect this information for future (or immediate) usage by other subsystems. When the information is stored, the PeerStore should inform interested parties of the peer discovered (`peer` event).
Taking into account a different scenario, a peer might perform/receive a dial request to/from a unkwown peer. In such a scenario, the PeerStore must store the peer's multiaddr once a connection is established.
After a connection is established with a peer, the Identify protocol will run automatically. A stream is created and peers exchange their information (Multiaddrs, running protocols and their public key). Once this information is obtained, it should be added to the PeerStore. In this specific case, as we are speaking to the source of truth, we should ensure the PeerStore is prioritizing these records. If the recorded `multiaddrs` or `protocols` have changed, interested parties must be informed via the `change:multiaddrs` or `change:protocols` events respectively.
In the background, the Identify Service is also waiting for protocol change notifications of peers via the IdentifyPush protocol. Peers may leverage the `identify-push` message to communicate protocol changes to all connected peers, so that their PeerStore can be updated with the updated protocols. As the `identify-push` also sends complete and updated information, the data in the PeerStore can be replaced.
(To consider: Should we not replace until we get to multiaddr confidence? we might loose true information as we will talk with older nodes on the network.)
While it is currently not supported in js-libp2p, future iterations may also support the [IdentifyDelta protocol](https://github.com/libp2p/specs/pull/176).
It is also possible to gather relevant information for peers from other protocols / subsystems. For instance, in `DHT` operations, nodes can exchange peer data as part of the `DHT` operation. In this case, we can learn additional information about a peer we already know. In this scenario the PeerStore should not replace the existing data it has, just add it.
## Data Consumption
When the PeerStore data is updated, this information might be important for different parties.
Every time a peer needs to dial another peer, it is essential that it knows the multiaddrs used by the peer, in order to perform a successful dial to it. The same is true for pinging a peer. While the `AddressBook` is going to keep its data updated, it will also emit `change:multiaddrs` events so that subsystems/users interested in knowing these changes can be notified instead of polling the `AddressBook`.
Everytime a peer starts/stops supporting a protocol, libp2p subsystems or users might need to act accordingly. `js-libp2p` registrar orchestrates known peers, established connections and protocol topologies. This way, once a protocol is supported for a peer, the topology of that protocol should be informed that a new peer may be used and the subsystem can decide if it should open a new stream with that peer or not. For these situations, the `ProtoBook` will emit `change:protocols` events whenever supported protocols of a peer change.
## PeerStore implementation
The PeerStore wraps four main components: `addressBook`, `keyBook`, `protocolBook` and `metadataBook`. Moreover, it provides a high level API for those components, as well as data events.
### Components
#### Address Book
The `addressBook` keeps the known multiaddrs of a peer. The multiaddrs of each peer may change over time and the Address Book must account for this.
`Map<string, multiaddrInfo>`
A `peerId.toString()` identifier mapping to a `multiaddrInfo` object, which should have the following structure:
```js
{
multiaddr: <Multiaddr>
}
```
#### Key Book
The `keyBook` tracks the keys of the peers.
**Not Yet Implemented**
#### Protocol Book
The `protoBook` holds the identifiers of the protocols supported by each peer. The protocols supported by each peer are dynamic and will change over time.
`Map<string, Set<string>>`
A `peerId.toString()` identifier mapping to a `Set` of protocol identifier strings.
#### Metadata Book
**Not Yet Implemented**
### API
For the complete API documentation, you should check the [API.md](../../doc/API.md).
Access to its underlying books:
- `peerStore.protoBook.*`
- `peerStore.addressBook.*`
### Events
- `peer` - emitted when a new peer is added.
- `change:multiaadrs` - emitted when a known peer has a different set of multiaddrs.
- `change:protocols` - emitted when a known peer supports a different set of protocols.
## Future Considerations
- If multiaddr TTLs are added, the PeerStore may schedule jobs to delete all addresses that exceed the TTL to prevent AddressBook bloating
- Further API methods will probably need to be added in the context of multiaddr validity and confidence.

View File

@@ -0,0 +1,214 @@
'use strict'
const errcode = require('err-code')
const debug = require('debug')
const log = debug('libp2p:peer-store:address-book')
log.error = debug('libp2p:peer-store:address-book:error')
const multiaddr = require('multiaddr')
const PeerId = require('peer-id')
const PeerInfo = require('peer-info')
const Book = require('./book')
const {
ERR_INVALID_PARAMETERS
} = require('../errors')
/**
* The AddressBook is responsible for keeping the known multiaddrs
* of a peer.
*/
class AddressBook extends Book {
/**
* MultiaddrInfo object
* @typedef {Object} MultiaddrInfo
* @property {Multiaddr} multiaddr peer multiaddr.
*/
/**
* @constructor
* @param {EventEmitter} peerStore
*/
constructor (peerStore) {
/**
* PeerStore Event emitter, used by the AddressBook to emit:
* "peer" - emitted when a peer is discovered by the node.
* "change:multiaddrs" - emitted when the known multiaddrs of a peer change.
*/
super(peerStore, 'change:multiaddrs', 'multiaddrs')
/**
* Map known peers to their known multiaddrs.
* @type {Map<string, Array<MultiaddrInfo>>}
*/
this.data = new Map()
}
/**
* Set known addresses of a provided peer.
* @override
* @param {PeerId} peerId
* @param {Array<Multiaddr>} addresses
* @returns {AddressBook}
*/
set (peerId, addresses) {
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)
}
const multiaddrInfos = this._toMultiaddrInfos(addresses)
const id = peerId.toB58String()
const rec = this.data.get(id)
// Not replace multiaddrs
if (!multiaddrInfos.length) {
return this
}
// Already knows the peer
if (rec && rec.length === multiaddrInfos.length) {
const intersection = rec.filter((mi) => multiaddrInfos.some((newMi) => mi.multiaddr.equals(newMi.multiaddr)))
// Are new addresses equal to the old ones?
// If yes, no changes needed!
if (intersection.length === rec.length) {
log(`the addresses provided to store are equal to the already stored for ${id}`)
return this
}
}
this.data.set(id, multiaddrInfos)
log(`stored provided multiaddrs for ${id}`)
// TODO: Remove peerInfo and its usage on peer-info deprecate
const peerInfo = new PeerInfo(peerId)
multiaddrInfos.forEach((mi) => peerInfo.multiaddrs.add(mi.multiaddr))
// Notify the existance of a new peer
if (!rec) {
// this._ps.emit('peer', peerId)
this._ps.emit('peer', peerInfo)
}
this._ps.emit('change:multiaddrs', {
peerId,
peerInfo,
multiaddrs: multiaddrInfos.map((mi) => mi.multiaddr)
})
return this
}
/**
* Add known addresses of a provided peer.
* If the peer is not known, it is set with the given addresses.
* @override
* @param {PeerId} peerId
* @param {Array<Multiaddr>} addresses
* @returns {AddressBook}
*/
add (peerId, addresses) {
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)
}
const multiaddrInfos = this._toMultiaddrInfos(addresses)
const id = peerId.toB58String()
const rec = this.data.get(id)
// Add recorded uniquely to the new array (Union)
rec && rec.forEach((mi) => {
if (!multiaddrInfos.find(r => r.multiaddr.equals(mi.multiaddr))) {
multiaddrInfos.push(mi)
}
})
// If the recorded length is equal to the new after the unique union
// The content is the same, no need to update.
if (rec && rec.length === multiaddrInfos.length) {
log(`the addresses provided to store are already stored for ${id}`)
return this
}
this.data.set(id, multiaddrInfos)
log(`added provided multiaddrs for ${id}`)
// TODO: Remove peerInfo and its usage on peer-info deprecate
const peerInfo = new PeerInfo(peerId)
multiaddrInfos.forEach((mi) => peerInfo.multiaddrs.add(mi.multiaddr))
this._ps.emit('change:multiaddrs', {
peerId,
peerInfo,
multiaddrs: multiaddrInfos.map((mi) => mi.multiaddr)
})
// Notify the existance of a new peer
if (!rec) {
// this._ps.emit('peer', peerId)
this._ps.emit('peer', peerInfo)
}
return this
}
/**
* Transforms received multiaddrs into MultiaddrInfo.
* @param {Array<Multiaddr>} addresses
* @returns {Array<MultiaddrInfo>}
*/
_toMultiaddrInfos (addresses) {
if (!addresses) {
log.error('addresses must be provided to store data')
throw errcode(new Error('addresses must be provided'), ERR_INVALID_PARAMETERS)
}
// create MultiaddrInfo for each address
const multiaddrInfos = []
addresses.forEach((addr) => {
if (!multiaddr.isMultiaddr(addr)) {
log.error(`multiaddr ${addr} must be an instance of multiaddr`)
throw errcode(new Error(`multiaddr ${addr} must be an instance of multiaddr`), ERR_INVALID_PARAMETERS)
}
multiaddrInfos.push({
multiaddr: addr
})
})
return multiaddrInfos
}
/**
* Get the known multiaddrs for a given peer. All returned multiaddrs
* will include the encapsulated `PeerId` of the peer.
* @param {PeerId} peerId
* @returns {Array<Multiaddr>}
*/
getMultiaddrsForPeer (peerId) {
if (!PeerId.isPeerId(peerId)) {
throw errcode(new Error('peerId must be an instance of peer-id'), ERR_INVALID_PARAMETERS)
}
const record = this.data.get(peerId.toB58String())
if (!record) {
return undefined
}
return record.map((multiaddrInfo) => {
const addr = multiaddrInfo.multiaddr
const idString = addr.getPeerId()
if (idString && idString === peerId.toB58String()) return addr
return addr.encapsulate(`/p2p/${peerId.toB58String()}`)
})
}
}
module.exports = AddressBook

87
src/peer-store/book.js Normal file
View File

@@ -0,0 +1,87 @@
'use strict'
const errcode = require('err-code')
const PeerId = require('peer-id')
const PeerInfo = require('peer-info')
const {
ERR_INVALID_PARAMETERS
} = require('../errors')
/**
* The Book is the skeleton for the PeerStore books.
*/
class Book {
constructor (peerStore, eventName, eventProperty) {
this._ps = peerStore
this.eventName = eventName
this.eventProperty = eventProperty
/**
* Map known peers to their data.
* @type {Map<string, Array<Data>}
*/
this.data = new Map()
}
/**
* Set known data of a provided peer.
* @param {PeerId} peerId
* @param {Array<Data>|Data} data
*/
set (peerId, data) {
throw errcode(new Error('set must be implemented by the subclass'), 'ERR_NOT_IMPLEMENTED')
}
/**
* Add known data of a provided peer.
* @param {PeerId} peerId
* @param {Array<Data>|Data} data
*/
add (peerId, data) {
throw errcode(new Error('set must be implemented by the subclass'), 'ERR_NOT_IMPLEMENTED')
}
/**
* Get the known data of a provided peer.
* @param {PeerId} peerId
* @returns {Array<Data>}
*/
get (peerId) {
if (!PeerId.isPeerId(peerId)) {
throw errcode(new Error('peerId must be an instance of peer-id'), ERR_INVALID_PARAMETERS)
}
const rec = this.data.get(peerId.toB58String())
return rec ? [...rec] : undefined
}
/**
* Deletes the provided peer from the book.
* @param {PeerId} peerId
* @returns {boolean}
*/
delete (peerId) {
if (!PeerId.isPeerId(peerId)) {
throw errcode(new Error('peerId must be an instance of peer-id'), ERR_INVALID_PARAMETERS)
}
if (!this.data.delete(peerId.toB58String())) {
return false
}
// TODO: Remove peerInfo and its usage on peer-info deprecate
const peerInfo = new PeerInfo(peerId)
this._ps.emit(this.eventName, {
peerId,
peerInfo,
[this.eventProperty]: []
})
return true
}
}
module.exports = Book

View File

@@ -9,244 +9,212 @@ const { EventEmitter } = require('events')
const PeerId = require('peer-id')
const PeerInfo = require('peer-info')
const AddressBook = require('./address-book')
const ProtoBook = require('./proto-book')
const {
ERR_INVALID_PARAMETERS
} = require('../errors')
/**
* Responsible for managing known peers, as well as their addresses and metadata
* @fires PeerStore#peer Emitted when a peer is connected to this node
* @fires PeerStore#change:protocols
* @fires PeerStore#change:multiaddrs
* Responsible for managing known peers, as well as their addresses, protocols and metadata.
* @fires PeerStore#peer Emitted when a new peer is added.
* @fires PeerStore#change:protocols Emitted when a known peer supports a different set of protocols.
* @fires PeerStore#change:multiaddrs Emitted when a known peer has a different set of multiaddrs.
*/
class PeerStore extends EventEmitter {
/**
* PeerInfo object
* @typedef {Object} peerInfo
* @property {Array<multiaddrInfo>} multiaddrsInfos peer's information of the multiaddrs.
* @property {Array<string>} protocols peer's supported protocols.
*/
constructor () {
super()
/**
* Map of peers
*
* @type {Map<string, PeerInfo>}
* AddressBook containing a map of peerIdStr to multiaddrsInfo
*/
this.peers = new Map()
this.addressBook = new AddressBook(this)
// TODO: Track ourselves. We should split `peerInfo` up into its pieces so we get better
// control and observability. This will be the initial step for removing PeerInfo
// https://github.com/libp2p/go-libp2p-core/blob/master/peerstore/peerstore.go
// this.addressBook = new Map()
// this.protoBook = new Map()
/**
* ProtoBook containing a map of peerIdStr to supported protocols.
*/
this.protoBook = new ProtoBook(this)
}
// TODO: Temporary adapter for modules using PeerStore
// This should be removed under a breaking change
/**
* Stores the peerInfo of a new peer.
* If already exist, its info is updated. If `silent` is set to
* true, no 'peer' event will be emitted. This can be useful if you
* are already in the process of dialing the peer. The peer is technically
* known, but may not have been added to the PeerStore yet.
* Stores the peerInfo of a new peer on each book.
* @param {PeerInfo} peerInfo
* @param {object} [options]
* @param {boolean} [options.silent] (Default=false)
* @param {boolean} [options.replace = true]
* @return {PeerInfo}
*/
put (peerInfo, options = { silent: false }) {
if (!PeerInfo.isPeerInfo(peerInfo)) {
throw errcode(new Error('peerInfo must be an instance of peer-info'), ERR_INVALID_PARAMETERS)
put (peerInfo, options) {
const multiaddrs = peerInfo.multiaddrs.toArray()
const protocols = Array.from(peerInfo.protocols || new Set())
this.addressBook.set(peerInfo.id, multiaddrs, options)
this.protoBook.set(peerInfo.id, protocols, options)
const peer = this.find(peerInfo.id)
const pInfo = new PeerInfo(peerInfo.id)
if (!peer) {
return pInfo
}
let peer
// Already know the peer?
if (this.has(peerInfo.id)) {
peer = this.update(peerInfo)
} else {
peer = this.add(peerInfo)
peer.protocols.forEach((p) => pInfo.protocols.add(p))
peer.multiaddrInfos.forEach((mi) => pInfo.multiaddrs.add(mi.multiaddr))
// Emit the peer if silent = false
!options.silent && this.emit('peer', peerInfo)
}
return peer
return pInfo
}
// TODO: Temporary adapter for modules using PeerStore
// This should be removed under a breaking change
/**
* Add a new peer to the store.
* @param {PeerInfo} peerInfo
* @return {PeerInfo}
*/
add (peerInfo) {
if (!PeerInfo.isPeerInfo(peerInfo)) {
throw errcode(new Error('peerInfo must be an instance of peer-info'), ERR_INVALID_PARAMETERS)
}
// Create new instance and add values to it
const newPeerInfo = new PeerInfo(peerInfo.id)
peerInfo.multiaddrs.forEach((ma) => newPeerInfo.multiaddrs.add(ma))
peerInfo.protocols.forEach((p) => newPeerInfo.protocols.add(p))
const connectedMa = peerInfo.isConnected()
connectedMa && newPeerInfo.connect(connectedMa)
const peerProxy = new Proxy(newPeerInfo, {
set: (obj, prop, value) => {
if (prop === 'multiaddrs') {
this.emit('change:multiaddrs', {
peerInfo: obj,
multiaddrs: value.toArray()
})
} else if (prop === 'protocols') {
this.emit('change:protocols', {
peerInfo: obj,
protocols: Array.from(value)
})
}
return Reflect.set(...arguments)
}
})
this.peers.set(peerInfo.id.toB58String(), peerProxy)
return peerProxy
}
/**
* Updates an already known peer.
* @param {PeerInfo} peerInfo
* @return {PeerInfo}
*/
update (peerInfo) {
if (!PeerInfo.isPeerInfo(peerInfo)) {
throw errcode(new Error('peerInfo must be an instance of peer-info'), ERR_INVALID_PARAMETERS)
}
const id = peerInfo.id.toB58String()
const recorded = this.peers.get(id)
// pass active connection state
const ma = peerInfo.isConnected()
if (ma) {
recorded.connect(ma)
}
// Verify new multiaddrs
// TODO: better track added and removed multiaddrs
const multiaddrsIntersection = [
...recorded.multiaddrs.toArray()
].filter((m) => peerInfo.multiaddrs.has(m))
if (multiaddrsIntersection.length !== peerInfo.multiaddrs.size ||
multiaddrsIntersection.length !== recorded.multiaddrs.size) {
for (const ma of peerInfo.multiaddrs.toArray()) {
recorded.multiaddrs.add(ma)
}
this.emit('change:multiaddrs', {
peerInfo: recorded,
multiaddrs: recorded.multiaddrs.toArray()
})
}
// Update protocols
// TODO: better track added and removed protocols
const protocolsIntersection = new Set(
[...recorded.protocols].filter((p) => peerInfo.protocols.has(p))
)
if (protocolsIntersection.size !== peerInfo.protocols.size ||
protocolsIntersection.size !== recorded.protocols.size) {
for (const protocol of peerInfo.protocols) {
recorded.protocols.add(protocol)
}
this.emit('change:protocols', {
peerInfo: recorded,
protocols: Array.from(recorded.protocols)
})
}
// Add the public key if missing
if (!recorded.id.pubKey && peerInfo.id.pubKey) {
recorded.id.pubKey = peerInfo.id.pubKey
}
return recorded
}
/**
* Get the info to the given id.
* @param {PeerId|string} peerId b58str id
* Get the info of the given id.
* @param {peerId} peerId
* @returns {PeerInfo}
*/
get (peerId) {
// TODO: deprecate this and just accept `PeerId` instances
if (PeerId.isPeerId(peerId)) {
peerId = peerId.toB58String()
}
const peer = this.find(peerId)
return this.peers.get(peerId)
const pInfo = new PeerInfo(peerId)
peer.protocols.forEach((p) => pInfo.protocols.add(p))
peer.multiaddrInfos.forEach((mi) => pInfo.multiaddrs.add(mi.multiaddr))
return pInfo
}
// TODO: Temporary adapter for modules using PeerStore
// This should be removed under a breaking change
/**
* Has the info to the given id.
* @param {PeerId|string} peerId b58str id
* @param {PeerId} peerId
* @returns {boolean}
*/
has (peerId) {
// TODO: deprecate this and just accept `PeerId` instances
if (PeerId.isPeerId(peerId)) {
peerId = peerId.toB58String()
}
return this.peers.has(peerId)
return Boolean(this.find(peerId))
}
// TODO: Temporary adapter for modules using PeerStore
// This should be removed under a breaking change
/**
* Removes the Peer with the matching `peerId` from the PeerStore
* @param {PeerId|string} peerId b58str id
* Removes the peer provided.
* @param {PeerId} peerId
* @returns {boolean} true if found and removed
*/
remove (peerId) {
// TODO: deprecate this and just accept `PeerId` instances
if (PeerId.isPeerId(peerId)) {
peerId = peerId.toB58String()
}
return this.peers.delete(peerId)
return this.delete(peerId)
}
// TODO: Temporary adapter for modules using PeerStore
// This should be removed under a breaking change
/**
* Completely replaces the existing peers metadata with the given `peerInfo`
* @param {PeerInfo} peerInfo
* @returns {void}
*/
replace (peerInfo) {
if (!PeerInfo.isPeerInfo(peerInfo)) {
throw errcode(new Error('peerInfo must be an instance of peer-info'), ERR_INVALID_PARAMETERS)
}
this.remove(peerInfo.id.toB58String())
this.add(peerInfo)
// This should be cleaned up in PeerStore v2
this.emit('change:multiaddrs', {
peerInfo,
multiaddrs: peerInfo.multiaddrs.toArray()
})
this.emit('change:protocols', {
peerInfo,
protocols: Array.from(peerInfo.protocols)
})
this.put(peerInfo)
}
// TODO: Temporary adapter for modules using PeerStore
// This should be removed under a breaking change
/**
* Returns the known multiaddrs for a given `PeerInfo`. All returned multiaddrs
* will include the encapsulated `PeerId` of the peer.
* @param {PeerInfo} peer
* @param {PeerInfo} peerInfo
* @returns {Array<Multiaddr>}
*/
multiaddrsForPeer (peer) {
return this.put(peer, true).multiaddrs.toArray().map(addr => {
const idString = addr.getPeerId()
if (idString && idString === peer.id.toB58String()) return addr
return addr.encapsulate(`/p2p/${peer.id.toB58String()}`)
})
multiaddrsForPeer (peerInfo) {
return this.addressBook.getMultiaddrsForPeer(peerInfo.id)
}
/**
* Get all the stored information of every peer.
* @returns {Map<string, peerInfo>}
*/
get peers () {
const peerInfos = new Map()
// AddressBook
for (const [idStr, multiaddrInfos] of this.addressBook.data.entries()) {
// TODO: Remove peerInfo and its usage on peer-info deprecate
const peerInfo = new PeerInfo(PeerId.createFromCID(idStr))
multiaddrInfos.forEach((mi) => peerInfo.multiaddrs.add((mi.multiaddr)))
const protocols = this.protoBook.data.get(idStr) || []
protocols.forEach((p) => peerInfo.protocols.add(p))
peerInfos.set(idStr, peerInfo)
// TODO
// peerInfos.set(idStr, {
// id: PeerId.createFromCID(idStr),
// multiaddrInfos,
// protocols: this.protoBook.data.get(idStr) || []
// })
}
// ProtoBook
for (const [idStr, protocols] of this.protoBook.data.entries()) {
// TODO: Remove peerInfo and its usage on peer-info deprecate
const peerInfo = peerInfos.get(idStr)
if (!peerInfo) {
const peerInfo = new PeerInfo(PeerId.createFromCID(idStr))
protocols.forEach((p) => peerInfo.protocols.add(p))
peerInfos.set(idStr, peerInfo)
// peerInfos.set(idStr, {
// id: PeerId.createFromCID(idStr),
// multiaddrInfos: [],
// protocols: protocols
// })
}
}
return peerInfos
}
/**
* Delete the information of the given peer in every book.
* @param {PeerId} peerId
* @returns {boolean} true if found and removed
*/
delete (peerId) {
const addressesDeleted = this.addressBook.delete(peerId)
const protocolsDeleted = this.protoBook.delete(peerId)
return addressesDeleted || protocolsDeleted
}
/**
* Find the stored information of a given peer.
* @param {PeerId} peerId
* @returns {peerInfo}
*/
find (peerId) {
if (!PeerId.isPeerId(peerId)) {
throw errcode(new Error('peerId must be an instance of peer-id'), ERR_INVALID_PARAMETERS)
}
const multiaddrInfos = this.addressBook.get(peerId)
const protocols = this.protoBook.get(peerId)
if (!multiaddrInfos && !protocols) {
return undefined
}
return {
multiaddrInfos: multiaddrInfos || [],
protocols: protocols || []
}
}
}

View File

@@ -0,0 +1,137 @@
'use strict'
const errcode = require('err-code')
const debug = require('debug')
const log = debug('libp2p:peer-store:proto-book')
log.error = debug('libp2p:peer-store:proto-book:error')
const PeerId = require('peer-id')
const PeerInfo = require('peer-info')
const Book = require('./book')
const {
ERR_INVALID_PARAMETERS
} = require('../errors')
/**
* The ProtoBook is responsible for keeping the known supported
* protocols of a peer.
* @fires ProtoBook#change:protocols
*/
class ProtoBook extends Book {
/**
* @constructor
* @param {EventEmitter} peerStore
*/
constructor (peerStore) {
/**
* PeerStore Event emitter, used by the ProtoBook to emit:
* "change:protocols" - emitted when the known protocols of a peer change.
*/
super(peerStore, 'change:protocols', 'protocols')
/**
* Map known peers to their known protocols.
* @type {Map<string, Set<string>>}
*/
this.data = new Map()
}
/**
* Set known protocols of a provided peer.
* If the peer was not known before, it will be added.
* @override
* @param {PeerId} peerId
* @param {Array<string>} protocols
* @returns {ProtoBook}
*/
set (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)
const newSet = new Set(protocols)
const isSetEqual = (a, b) => a.size === b.size && [...a].every(value => b.has(value))
// Already knows the peer and the recorded protocols are the same?
// If yes, no changes needed!
if (recSet && isSetEqual(recSet, newSet)) {
log(`the protocols provided to store are equal to the already stored for ${id}`)
return this
}
this.data.set(id, newSet)
log(`stored provided protocols for ${id}`)
// TODO: Remove peerInfo and its usage on peer-info deprecate
const peerInfo = new PeerInfo(peerId)
protocols.forEach((p) => peerInfo.protocols.add(p))
this._ps.emit('change:protocols', {
peerId,
peerInfo,
protocols
})
return this
}
/**
* Adds known protocols of a provided peer.
* If the peer was not known before, it will be added.
* @override
* @param {PeerId} peerId
* @param {Array<string>} protocols
* @returns {ProtoBook}
*/
add (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) || new Set()
const newSet = new Set([...recSet, ...protocols]) // Set Union
// Any new protocol added?
if (recSet.size === newSet.size) {
log(`the protocols provided to store are already stored for ${id}`)
return this
}
protocols = [...newSet]
this.data.set(id, newSet)
log(`added provided protocols for ${id}`)
// TODO: Remove peerInfo and its usage on peer-info deprecate
const peerInfo = new PeerInfo(peerId)
protocols.forEach((p) => peerInfo.protocols.add(p))
this._ps.emit('change:protocols', {
peerId,
peerInfo,
protocols
})
return this
}
}
module.exports = ProtoBook

View File

@@ -15,11 +15,11 @@ const { PROTOCOL, PING_LENGTH } = require('./constants')
/**
* Ping a given peer and wait for its response, getting the operation latency.
* @param {Libp2p} node
* @param {PeerInfo} peer
* @param {PeerId} peer
* @returns {Promise<Number>}
*/
async function ping (node, peer) {
log('dialing %s to %s', PROTOCOL, peer.id.toB58String())
log('dialing %s to %s', PROTOCOL, peer.toB58String())
const { stream } = await node.dialProtocol(peer, PROTOCOL)

View File

@@ -10,7 +10,6 @@ const {
} = require('./errors')
const Topology = require('libp2p-interfaces/src/topology')
const { Connection } = require('libp2p-interfaces/src/connection')
const PeerInfo = require('peer-info')
/**
* Responsible for notifying registered protocols of events in the network.
@@ -22,6 +21,7 @@ class Registrar {
* @constructor
*/
constructor ({ peerStore }) {
// Used on topology to listen for protocol changes
this.peerStore = peerStore
/**
@@ -74,9 +74,11 @@ class Registrar {
* @returns {void}
*/
onConnect (peerInfo, conn) {
if (!PeerInfo.isPeerInfo(peerInfo)) {
throw errcode(new Error('peerInfo must be an instance of peer-info'), ERR_INVALID_PARAMETERS)
}
// TODO: This is not a `peer-info` instance anymore, but an object with the data.
// This can be modified to `peer-id` though, once `peer-info` is deprecated.
// if (!PeerInfo.isPeerInfo(peerInfo)) {
// throw errcode(new Error('peerInfo must be an instance of peer-info'), ERR_INVALID_PARAMETERS)
// }
if (!Connection.isConnection(conn)) {
throw errcode(new Error('conn must be an instance of interface-connection'), ERR_INVALID_PARAMETERS)
@@ -101,9 +103,11 @@ class Registrar {
* @returns {void}
*/
onDisconnect (peerInfo, connection, error) {
if (!PeerInfo.isPeerInfo(peerInfo)) {
throw errcode(new Error('peerInfo must be an instance of peer-info'), ERR_INVALID_PARAMETERS)
}
// TODO: This is not a `peer-info` instance anymore, but an object with the data.
// This can be modified to `peer-id` though, once `peer-info` is deprecated.
// if (!PeerInfo.isPeerInfo(peerInfo)) {
// throw errcode(new Error('peerInfo must be an instance of peer-info'), ERR_INVALID_PARAMETERS)
// }
const id = peerInfo.id.toB58String()
let storedConn = this.connections.get(id)
@@ -126,9 +130,11 @@ class Registrar {
* @returns {Connection}
*/
getConnection (peerInfo) {
if (!PeerInfo.isPeerInfo(peerInfo)) {
throw errcode(new Error('peerInfo must be an instance of peer-info'), ERR_INVALID_PARAMETERS)
}
// TODO: This is not a `peer-info` instance anymore, but an object with the data.
// This can be modified to `peer-id` though, once `peer-info` is deprecated.
// if (!PeerInfo.isPeerInfo(peerInfo)) {
// throw errcode(new Error('peerInfo must be an instance of peer-info'), ERR_INVALID_PARAMETERS)
// }
const connections = this.connections.get(peerInfo.id.toB58String())
// Return the first, open connection

View File

@@ -317,7 +317,7 @@ class Upgrader {
* Attempts to encrypt the incoming `connection` with the provided `cryptos`.
* @private
* @async
* @param {PeerId} localPeer The initiators PeerInfo
* @param {PeerId} localPeer The initiators PeerId
* @param {*} connection
* @param {Map<string, Crypto>} cryptos
* @returns {CryptoResult} An encrypted connection, remote peer `PeerId` and the protocol of the `Crypto` used
@@ -346,7 +346,7 @@ class Upgrader {
* The first `Crypto` module to succeed will be used
* @private
* @async
* @param {PeerId} localPeer The initiators PeerInfo
* @param {PeerId} localPeer The initiators PeerId
* @param {*} connection
* @param {PeerId} remotePeerId
* @param {Map<string, Crypto>} cryptos