Skip to content

Commit e9d225c

Browse files
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>
1 parent 2aac3b0 commit e9d225c

23 files changed

+2012
-473
lines changed

doc/API.md

+382-27
Large diffs are not rendered by default.

src/dialer/index.js

+23-13
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ const errCode = require('err-code')
55
const TimeoutController = require('timeout-abort-controller')
66
const anySignal = require('any-signal')
77
const PeerId = require('peer-id')
8-
const PeerInfo = require('peer-info')
98
const debug = require('debug')
109
const log = debug('libp2p:dialer')
1110
log.error = debug('libp2p:dialer:error')
@@ -62,13 +61,13 @@ class Dialer {
6261
* The dial to the first address that is successfully able to upgrade a connection
6362
* will be used.
6463
*
65-
* @param {PeerInfo|Multiaddr} peer The peer to dial
64+
* @param {PeerId|Multiaddr} peerId The peer to dial
6665
* @param {object} [options]
6766
* @param {AbortSignal} [options.signal] An AbortController signal
6867
* @returns {Promise<Connection>}
6968
*/
70-
async connectToPeer (peer, options = {}) {
71-
const dialTarget = this._createDialTarget(peer)
69+
async connectToPeer (peerId, options = {}) {
70+
const dialTarget = this._createDialTarget(peerId)
7271
if (dialTarget.addrs.length === 0) {
7372
throw errCode(new Error('The dial request has no addresses'), codes.ERR_NO_VALID_ADDRESSES)
7473
}
@@ -100,7 +99,7 @@ class Dialer {
10099
* Creates a DialTarget. The DialTarget is used to create and track
101100
* the DialRequest to a given peer.
102101
* @private
103-
* @param {PeerInfo|Multiaddr} peer A PeerId or Multiaddr
102+
* @param {PeerId|Multiaddr} peer A PeerId or Multiaddr
104103
* @returns {DialTarget}
105104
*/
106105
_createDialTarget (peer) {
@@ -111,7 +110,10 @@ class Dialer {
111110
addrs: [dialable]
112111
}
113112
}
114-
const addrs = this.peerStore.multiaddrsForPeer(dialable)
113+
114+
dialable.multiaddrs && this.peerStore.addressBook.add(dialable.id, Array.from(dialable.multiaddrs))
115+
const addrs = this.peerStore.addressBook.getMultiaddrsForPeer(dialable.id)
116+
115117
return {
116118
id: dialable.id.toB58String(),
117119
addrs
@@ -179,21 +181,27 @@ class Dialer {
179181
this.tokens.push(token)
180182
}
181183

184+
/**
185+
* PeerInfo object
186+
* @typedef {Object} peerInfo
187+
* @property {Multiaddr} multiaddr peer multiaddr.
188+
* @property {PeerId} id peer id.
189+
*/
190+
182191
/**
183192
* Converts the given `peer` into a `PeerInfo` or `Multiaddr`.
184193
* @static
185-
* @param {PeerInfo|PeerId|Multiaddr|string} peer
186-
* @returns {PeerInfo|Multiaddr}
194+
* @param {PeerId|Multiaddr|string} peer
195+
* @returns {peerInfo|Multiaddr}
187196
*/
188197
static getDialable (peer) {
189-
if (PeerInfo.isPeerInfo(peer)) return peer
190198
if (typeof peer === 'string') {
191199
peer = multiaddr(peer)
192200
}
193201

194-
let addr
202+
let addrs
195203
if (multiaddr.isMultiaddr(peer)) {
196-
addr = peer
204+
addrs = new Set([peer]) // TODO: after peer-info removal, a Set should not be needed
197205
try {
198206
peer = PeerId.createFromCID(peer.getPeerId())
199207
} catch (err) {
@@ -202,10 +210,12 @@ class Dialer {
202210
}
203211

204212
if (PeerId.isPeerId(peer)) {
205-
peer = new PeerInfo(peer)
213+
peer = {
214+
id: peer,
215+
multiaddrs: addrs
216+
}
206217
}
207218

208-
addr && peer.multiaddrs.add(addr)
209219
return peer
210220
}
211221
}

src/get-peer-info.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,12 @@ function getPeerInfo (peer, peerStore) {
3838

3939
addr && peer.multiaddrs.add(addr)
4040

41-
return peerStore ? peerStore.put(peer) : peer
41+
if (peerStore) {
42+
peerStore.addressBook.add(peer.id, peer.multiaddrs.toArray())
43+
peerStore.protoBook.add(peer.id, Array.from(peer.protocols))
44+
}
45+
46+
return peer
4247
}
4348

4449
/**

src/identify/index.js

+8-47
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ const lp = require('it-length-prefixed')
77
const pipe = require('it-pipe')
88
const { collect, take, consume } = require('streaming-iterables')
99

10-
const PeerInfo = require('peer-info')
1110
const PeerId = require('peer-id')
1211
const multiaddr = require('multiaddr')
1312
const { toBuffer } = require('it-buffer')
@@ -28,39 +27,6 @@ const errCode = require('err-code')
2827
const { codes } = require('../errors')
2928

3029
class IdentifyService {
31-
/**
32-
* Replaces the multiaddrs on the given `peerInfo`,
33-
* with the provided `multiaddrs`
34-
* @param {PeerInfo} peerInfo
35-
* @param {Array<Multiaddr>|Array<Buffer>} multiaddrs
36-
*/
37-
static updatePeerAddresses (peerInfo, multiaddrs) {
38-
if (multiaddrs && multiaddrs.length > 0) {
39-
peerInfo.multiaddrs.clear()
40-
multiaddrs.forEach(ma => {
41-
try {
42-
peerInfo.multiaddrs.add(ma)
43-
} catch (err) {
44-
log.error('could not add multiaddr', err)
45-
}
46-
})
47-
}
48-
}
49-
50-
/**
51-
* Replaces the protocols on the given `peerInfo`,
52-
* with the provided `protocols`
53-
* @static
54-
* @param {PeerInfo} peerInfo
55-
* @param {Array<string>} protocols
56-
*/
57-
static updatePeerProtocols (peerInfo, protocols) {
58-
if (protocols && protocols.length > 0) {
59-
peerInfo.protocols.clear()
60-
protocols.forEach(proto => peerInfo.protocols.add(proto))
61-
}
62-
}
63-
6430
/**
6531
* Takes the `addr` and converts it to a Multiaddr if possible
6632
* @param {Buffer|String} addr
@@ -182,19 +148,18 @@ class IdentifyService {
182148
} = message
183149

184150
const id = await PeerId.createFromPubKey(publicKey)
185-
const peerInfo = new PeerInfo(id)
151+
186152
if (connection.remotePeer.toB58String() !== id.toB58String()) {
187153
throw errCode(new Error('identified peer does not match the expected peer'), codes.ERR_INVALID_PEER)
188154
}
189155

190156
// Get the observedAddr if there is one
191157
observedAddr = IdentifyService.getCleanMultiaddr(observedAddr)
192158

193-
// Copy the listenAddrs and protocols
194-
IdentifyService.updatePeerAddresses(peerInfo, listenAddrs)
195-
IdentifyService.updatePeerProtocols(peerInfo, protocols)
159+
// Update peers data in PeerStore
160+
this.registrar.peerStore.addressBook.set(id, listenAddrs.map((addr) => multiaddr(addr)))
161+
this.registrar.peerStore.protoBook.set(id, protocols)
196162

197-
this.registrar.peerStore.replace(peerInfo)
198163
// TODO: Track our observed address so that we can score it
199164
log('received observed address of %s', observedAddr)
200165
}
@@ -274,20 +239,16 @@ class IdentifyService {
274239
return log.error('received invalid message', err)
275240
}
276241

277-
// Update the listen addresses
278-
const peerInfo = new PeerInfo(connection.remotePeer)
279-
242+
// Update peers data in PeerStore
243+
const id = connection.remotePeer
280244
try {
281-
IdentifyService.updatePeerAddresses(peerInfo, message.listenAddrs)
245+
this.registrar.peerStore.addressBook.set(id, message.listenAddrs.map((addr) => multiaddr(addr)))
282246
} catch (err) {
283247
return log.error('received invalid listen addrs', err)
284248
}
285249

286250
// Update the protocols
287-
IdentifyService.updatePeerProtocols(peerInfo, message.protocols)
288-
289-
// Update the peer in the PeerStore
290-
this.registrar.peerStore.replace(peerInfo)
251+
this.registrar.peerStore.protoBook.set(id, message.protocols)
291252
}
292253
}
293254

src/index.js

+11-4
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ class Libp2p extends EventEmitter {
6060
localPeer: this.peerInfo.id,
6161
metrics: this.metrics,
6262
onConnection: (connection) => {
63-
const peerInfo = this.peerStore.put(new PeerInfo(connection.remotePeer), { silent: true })
63+
const peerInfo = new PeerInfo(connection.remotePeer)
6464
this.registrar.onConnect(peerInfo, connection)
6565
this.connectionManager.onConnect(connection)
6666
this.emit('peer:connect', peerInfo)
@@ -292,7 +292,11 @@ class Libp2p extends EventEmitter {
292292
const dialable = Dialer.getDialable(peer)
293293
let connection
294294
if (PeerInfo.isPeerInfo(dialable)) {
295-
this.peerStore.put(dialable, { silent: true })
295+
// TODO Inconsistency from: getDialable adds a set, while regular peerInfo uses a Multiaddr set
296+
// This should be handled on `peer-info` removal
297+
const multiaddrs = dialable.multiaddrs.toArray ? dialable.multiaddrs.toArray() : Array.from(dialable.multiaddrs)
298+
this.peerStore.addressBook.add(dialable.id, multiaddrs)
299+
296300
connection = this.registrar.getConnection(dialable)
297301
}
298302

@@ -338,7 +342,7 @@ class Libp2p extends EventEmitter {
338342
async ping (peer) {
339343
const peerInfo = await getPeerInfo(peer, this.peerStore)
340344

341-
return ping(this, peerInfo)
345+
return ping(this, peerInfo.id)
342346
}
343347

344348
/**
@@ -440,7 +444,10 @@ class Libp2p extends EventEmitter {
440444
log.error(new Error(codes.ERR_DISCOVERED_SELF))
441445
return
442446
}
443-
this.peerStore.put(peerInfo)
447+
448+
// TODO: once we deprecate peer-info, we should only set if we have data
449+
this.peerStore.addressBook.add(peerInfo.id, peerInfo.multiaddrs.toArray())
450+
this.peerStore.protoBook.set(peerInfo.id, Array.from(peerInfo.protocols))
444451
}
445452

446453
/**

src/peer-store/README.md

+88-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,89 @@
1-
# Peerstore
1+
# PeerStore
22

3-
WIP
3+
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`.
4+
5+
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.
6+
7+
## Data gathering
8+
9+
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.
10+
11+
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).
12+
13+
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.
14+
15+
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.
16+
17+
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.
18+
19+
(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.)
20+
21+
While it is currently not supported in js-libp2p, future iterations may also support the [IdentifyDelta protocol](https://github.com/libp2p/specs/pull/176).
22+
23+
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.
24+
25+
## Data Consumption
26+
27+
When the PeerStore data is updated, this information might be important for different parties.
28+
29+
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`.
30+
31+
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.
32+
33+
## PeerStore implementation
34+
35+
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.
36+
37+
### Components
38+
39+
#### Address Book
40+
41+
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.
42+
43+
`Map<string, multiaddrInfo>`
44+
45+
A `peerId.toString()` identifier mapping to a `multiaddrInfo` object, which should have the following structure:
46+
47+
```js
48+
{
49+
multiaddr: <Multiaddr>
50+
}
51+
```
52+
53+
#### Key Book
54+
55+
The `keyBook` tracks the keys of the peers.
56+
57+
**Not Yet Implemented**
58+
59+
#### Protocol Book
60+
61+
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.
62+
63+
`Map<string, Set<string>>`
64+
65+
A `peerId.toString()` identifier mapping to a `Set` of protocol identifier strings.
66+
67+
#### Metadata Book
68+
69+
**Not Yet Implemented**
70+
71+
### API
72+
73+
For the complete API documentation, you should check the [API.md](../../doc/API.md).
74+
75+
Access to its underlying books:
76+
77+
- `peerStore.protoBook.*`
78+
- `peerStore.addressBook.*`
79+
80+
### Events
81+
82+
- `peer` - emitted when a new peer is added.
83+
- `change:multiaadrs` - emitted when a known peer has a different set of multiaddrs.
84+
- `change:protocols` - emitted when a known peer supports a different set of protocols.
85+
86+
## Future Considerations
87+
88+
- If multiaddr TTLs are added, the PeerStore may schedule jobs to delete all addresses that exceed the TTL to prevent AddressBook bloating
89+
- Further API methods will probably need to be added in the context of multiaddr validity and confidence.

0 commit comments

Comments
 (0)