@libp2p/kad-dht 0.28.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +4 -0
- package/README.md +105 -0
- package/dist/src/constants.d.ts +20 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +34 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/content-fetching/index.d.ts +55 -0
- package/dist/src/content-fetching/index.d.ts.map +1 -0
- package/dist/src/content-fetching/index.js +190 -0
- package/dist/src/content-fetching/index.js.map +1 -0
- package/dist/src/content-routing/index.d.ts +42 -0
- package/dist/src/content-routing/index.d.ts.map +1 -0
- package/dist/src/content-routing/index.js +129 -0
- package/dist/src/content-routing/index.js.map +1 -0
- package/dist/src/dual-kad-dht.d.ts +65 -0
- package/dist/src/dual-kad-dht.d.ts.map +1 -0
- package/dist/src/dual-kad-dht.js +191 -0
- package/dist/src/dual-kad-dht.js.map +1 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +15 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/kad-dht.d.ts +131 -0
- package/dist/src/kad-dht.d.ts.map +1 -0
- package/dist/src/kad-dht.js +268 -0
- package/dist/src/kad-dht.js.map +1 -0
- package/dist/src/message/dht.d.ts +297 -0
- package/dist/src/message/dht.js +921 -0
- package/dist/src/message/index.d.ts +32 -0
- package/dist/src/message/index.d.ts.map +1 -0
- package/dist/src/message/index.js +81 -0
- package/dist/src/message/index.js.map +1 -0
- package/dist/src/network.d.ts +60 -0
- package/dist/src/network.d.ts.map +1 -0
- package/dist/src/network.js +124 -0
- package/dist/src/network.js.map +1 -0
- package/dist/src/peer-list/index.d.ts +29 -0
- package/dist/src/peer-list/index.d.ts.map +1 -0
- package/dist/src/peer-list/index.js +44 -0
- package/dist/src/peer-list/index.js.map +1 -0
- package/dist/src/peer-list/peer-distance-list.d.ts +34 -0
- package/dist/src/peer-list/peer-distance-list.d.ts.map +1 -0
- package/dist/src/peer-list/peer-distance-list.js +64 -0
- package/dist/src/peer-list/peer-distance-list.js.map +1 -0
- package/dist/src/peer-routing/index.d.ts +71 -0
- package/dist/src/peer-routing/index.d.ts.map +1 -0
- package/dist/src/peer-routing/index.js +256 -0
- package/dist/src/peer-routing/index.js.map +1 -0
- package/dist/src/providers.d.ts +64 -0
- package/dist/src/providers.d.ts.map +1 -0
- package/dist/src/providers.js +208 -0
- package/dist/src/providers.js.map +1 -0
- package/dist/src/query/events.d.ts +46 -0
- package/dist/src/query/events.d.ts.map +1 -0
- package/dist/src/query/events.js +73 -0
- package/dist/src/query/events.js.map +1 -0
- package/dist/src/query/manager.d.ts +40 -0
- package/dist/src/query/manager.d.ts.map +1 -0
- package/dist/src/query/manager.js +140 -0
- package/dist/src/query/manager.js.map +1 -0
- package/dist/src/query/query-path.d.ts +58 -0
- package/dist/src/query/query-path.d.ts.map +1 -0
- package/dist/src/query/query-path.js +171 -0
- package/dist/src/query/query-path.js.map +1 -0
- package/dist/src/query/types.d.ts +16 -0
- package/dist/src/query/types.d.ts.map +1 -0
- package/dist/src/query/types.js +2 -0
- package/dist/src/query/types.js.map +1 -0
- package/dist/src/query-self.d.ts +31 -0
- package/dist/src/query-self.d.ts.map +1 -0
- package/dist/src/query-self.js +73 -0
- package/dist/src/query-self.js.map +1 -0
- package/dist/src/routing-table/generated-prefix-list-browser.d.ts +3 -0
- package/dist/src/routing-table/generated-prefix-list-browser.d.ts.map +1 -0
- package/dist/src/routing-table/generated-prefix-list-browser.js +1027 -0
- package/dist/src/routing-table/generated-prefix-list-browser.js.map +1 -0
- package/dist/src/routing-table/generated-prefix-list.d.ts +3 -0
- package/dist/src/routing-table/generated-prefix-list.d.ts.map +1 -0
- package/dist/src/routing-table/generated-prefix-list.js +4099 -0
- package/dist/src/routing-table/generated-prefix-list.js.map +1 -0
- package/dist/src/routing-table/index.d.ts +91 -0
- package/dist/src/routing-table/index.d.ts.map +1 -0
- package/dist/src/routing-table/index.js +183 -0
- package/dist/src/routing-table/index.js.map +1 -0
- package/dist/src/routing-table/refresh.d.ts +50 -0
- package/dist/src/routing-table/refresh.d.ts.map +1 -0
- package/dist/src/routing-table/refresh.js +204 -0
- package/dist/src/routing-table/refresh.js.map +1 -0
- package/dist/src/routing-table/types.d.ts +24 -0
- package/dist/src/routing-table/types.d.ts.map +1 -0
- package/dist/src/rpc/handlers/add-provider.d.ts +13 -0
- package/dist/src/rpc/handlers/add-provider.d.ts.map +1 -0
- package/dist/src/rpc/handlers/add-provider.js +42 -0
- package/dist/src/rpc/handlers/add-provider.js.map +1 -0
- package/dist/src/rpc/handlers/find-node.d.ts +18 -0
- package/dist/src/rpc/handlers/find-node.d.ts.map +1 -0
- package/dist/src/rpc/handlers/find-node.js +32 -0
- package/dist/src/rpc/handlers/find-node.js.map +1 -0
- package/dist/src/rpc/handlers/get-providers.d.ts +24 -0
- package/dist/src/rpc/handlers/get-providers.d.ts.map +1 -0
- package/dist/src/rpc/handlers/get-providers.js +60 -0
- package/dist/src/rpc/handlers/get-providers.js.map +1 -0
- package/dist/src/rpc/handlers/get-value.d.ts +27 -0
- package/dist/src/rpc/handlers/get-value.d.ts.map +1 -0
- package/dist/src/rpc/handlers/get-value.js +94 -0
- package/dist/src/rpc/handlers/get-value.js.map +1 -0
- package/dist/src/rpc/handlers/index.d.ts +13 -0
- package/dist/src/rpc/handlers/index.d.ts.map +1 -0
- package/dist/src/rpc/handlers/ping.d.ts +7 -0
- package/dist/src/rpc/handlers/ping.d.ts.map +1 -0
- package/dist/src/rpc/handlers/ping.js +9 -0
- package/dist/src/rpc/handlers/ping.js.map +1 -0
- package/dist/src/rpc/handlers/put-value.d.ts +18 -0
- package/dist/src/rpc/handlers/put-value.d.ts.map +1 -0
- package/dist/src/rpc/handlers/put-value.js +35 -0
- package/dist/src/rpc/handlers/put-value.js.map +1 -0
- package/dist/src/rpc/index.d.ts +38 -0
- package/dist/src/rpc/index.d.ts.map +1 -0
- package/dist/src/rpc/index.js +75 -0
- package/dist/src/rpc/index.js.map +1 -0
- package/dist/src/rpc/types.d.ts +6 -0
- package/dist/src/rpc/types.d.ts.map +1 -0
- package/dist/src/topology-listener.d.ts +33 -0
- package/dist/src/topology-listener.d.ts.map +1 -0
- package/dist/src/topology-listener.js +50 -0
- package/dist/src/topology-listener.js.map +1 -0
- package/dist/src/types.d.ts +143 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/utils.d.ts +33 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/utils.js +89 -0
- package/dist/src/utils.js.map +1 -0
- package/package.json +200 -0
- package/src/constants.ts +50 -0
- package/src/content-fetching/index.ts +276 -0
- package/src/content-routing/index.ts +202 -0
- package/src/dual-kad-dht.ts +257 -0
- package/src/index.ts +21 -0
- package/src/kad-dht.ts +396 -0
- package/src/message/dht.d.ts +297 -0
- package/src/message/dht.js +921 -0
- package/src/message/dht.proto +75 -0
- package/src/message/index.ts +111 -0
- package/src/network.ts +185 -0
- package/src/peer-list/index.ts +54 -0
- package/src/peer-list/peer-distance-list.ts +93 -0
- package/src/peer-routing/index.ts +332 -0
- package/src/providers.ts +278 -0
- package/src/query/events.ts +126 -0
- package/src/query/manager.ts +188 -0
- package/src/query/query-path.ts +263 -0
- package/src/query/types.ts +22 -0
- package/src/query-self.ts +106 -0
- package/src/routing-table/generated-prefix-list-browser.ts +1026 -0
- package/src/routing-table/generated-prefix-list.ts +4098 -0
- package/src/routing-table/index.ts +265 -0
- package/src/routing-table/refresh.ts +263 -0
- package/src/rpc/handlers/add-provider.ts +63 -0
- package/src/rpc/handlers/find-node.ts +57 -0
- package/src/rpc/handlers/get-providers.ts +95 -0
- package/src/rpc/handlers/get-value.ts +130 -0
- package/src/rpc/handlers/ping.ts +13 -0
- package/src/rpc/handlers/put-value.ts +58 -0
- package/src/rpc/index.ts +118 -0
- package/src/topology-listener.ts +78 -0
- package/src/utils.ts +108 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import errcode from 'err-code'
|
|
2
|
+
import { verifyRecord } from '@libp2p/record/validators'
|
|
3
|
+
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
|
|
4
|
+
import { Message, MESSAGE_TYPE } from '../message/index.js'
|
|
5
|
+
import * as utils from '../utils.js'
|
|
6
|
+
import {
|
|
7
|
+
queryErrorEvent,
|
|
8
|
+
finalPeerEvent,
|
|
9
|
+
valueEvent
|
|
10
|
+
} from '../query/events.js'
|
|
11
|
+
import { PeerDistanceList } from '../peer-list/peer-distance-list.js'
|
|
12
|
+
import { Libp2pRecord } from '@libp2p/record'
|
|
13
|
+
import { base58btc } from 'multiformats/bases/base58'
|
|
14
|
+
import { logger } from '@libp2p/logger'
|
|
15
|
+
import { keys } from '@libp2p/crypto'
|
|
16
|
+
import { peerIdFromKeys } from '@libp2p/peer-id'
|
|
17
|
+
import type { DHTRecord, QueryOptions, Validators } from '@libp2p/interfaces/dht'
|
|
18
|
+
import type { RoutingTable } from '../routing-table/index.js'
|
|
19
|
+
import type { PeerStore } from '@libp2p/interfaces/peer-store'
|
|
20
|
+
import type { QueryManager } from '../query/manager.js'
|
|
21
|
+
import type { Network } from '../network.js'
|
|
22
|
+
import type { Logger } from '@libp2p/logger'
|
|
23
|
+
import type { AbortOptions } from '@libp2p/interfaces'
|
|
24
|
+
import type { QueryFunc } from '../query/types.js'
|
|
25
|
+
import type { PeerData } from '@libp2p/interfaces/peer-data'
|
|
26
|
+
import type { PeerId } from '@libp2p/interfaces/peer-id'
|
|
27
|
+
|
|
28
|
+
export interface PeerRoutingOptions {
|
|
29
|
+
peerId: PeerId
|
|
30
|
+
routingTable: RoutingTable
|
|
31
|
+
peerStore: PeerStore
|
|
32
|
+
network: Network
|
|
33
|
+
validators: Validators
|
|
34
|
+
queryManager: QueryManager
|
|
35
|
+
lan: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class PeerRouting {
|
|
39
|
+
private readonly log: Logger
|
|
40
|
+
private readonly peerId: PeerId
|
|
41
|
+
private readonly routingTable: RoutingTable
|
|
42
|
+
private readonly peerStore: PeerStore
|
|
43
|
+
private readonly network: Network
|
|
44
|
+
private readonly validators: Validators
|
|
45
|
+
private readonly queryManager: QueryManager
|
|
46
|
+
|
|
47
|
+
constructor (options: PeerRoutingOptions) {
|
|
48
|
+
const { peerId, routingTable, peerStore, network, validators, queryManager, lan } = options
|
|
49
|
+
|
|
50
|
+
this.peerId = peerId
|
|
51
|
+
this.routingTable = routingTable
|
|
52
|
+
this.peerStore = peerStore
|
|
53
|
+
this.network = network
|
|
54
|
+
this.validators = validators
|
|
55
|
+
this.queryManager = queryManager
|
|
56
|
+
this.log = logger(`libp2p:kad-dht:${lan ? 'lan' : 'wan'}:peer-routing`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Look if we are connected to a peer with the given id.
|
|
61
|
+
* Returns its id and addresses, if found, otherwise `undefined`.
|
|
62
|
+
*/
|
|
63
|
+
async findPeerLocal (peer: PeerId) {
|
|
64
|
+
let peerData
|
|
65
|
+
const p = await this.routingTable.find(peer)
|
|
66
|
+
|
|
67
|
+
if (p != null) {
|
|
68
|
+
this.log('findPeerLocal found %p in routing table', peer)
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
peerData = await this.peerStore.get(p)
|
|
72
|
+
} catch (err: any) {
|
|
73
|
+
if (err.code !== 'ERR_NOT_FOUND') {
|
|
74
|
+
throw err
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (peerData == null) {
|
|
80
|
+
try {
|
|
81
|
+
peerData = await this.peerStore.get(peer)
|
|
82
|
+
} catch (err: any) {
|
|
83
|
+
if (err.code !== 'ERR_NOT_FOUND') {
|
|
84
|
+
throw err
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (peerData != null) {
|
|
90
|
+
this.log('findPeerLocal found %p in peer store', peer)
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
id: peerData.id,
|
|
94
|
+
multiaddrs: peerData.addresses.map((address) => address.multiaddr),
|
|
95
|
+
protocols: []
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get a value via rpc call for the given parameters
|
|
102
|
+
*/
|
|
103
|
+
async * _getValueSingle (peer: PeerId, key: Uint8Array, options: AbortOptions = {}) { // eslint-disable-line require-await
|
|
104
|
+
const msg = new Message(MESSAGE_TYPE.GET_VALUE, key, 0)
|
|
105
|
+
yield * this.network.sendRequest(peer, msg, options)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get the public key directly from a node
|
|
110
|
+
*/
|
|
111
|
+
async * getPublicKeyFromNode (peer: PeerId, options: AbortOptions = {}) {
|
|
112
|
+
const pkKey = utils.keyForPublicKey(peer)
|
|
113
|
+
|
|
114
|
+
for await (const event of this._getValueSingle(peer, pkKey, options)) {
|
|
115
|
+
yield event
|
|
116
|
+
|
|
117
|
+
if (event.name === 'PEER_RESPONSE' && event.record != null) {
|
|
118
|
+
const recPeer = await peerIdFromKeys(keys.marshalPublicKey({ bytes: event.record.value }))
|
|
119
|
+
|
|
120
|
+
// compare hashes of the pub key
|
|
121
|
+
if (!recPeer.equals(peer)) {
|
|
122
|
+
throw errcode(new Error('public key does not match id'), 'ERR_PUBLIC_KEY_DOES_NOT_MATCH_ID')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (recPeer.publicKey == null) {
|
|
126
|
+
throw errcode(new Error('public key missing'), 'ERR_PUBLIC_KEY_MISSING')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
yield valueEvent({ from: peer, value: recPeer.publicKey })
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
throw errcode(new Error(`Node not responding with its public key: ${peer.toString(base58btc)}`), 'ERR_INVALID_RECORD')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Search for a peer with the given ID
|
|
138
|
+
*/
|
|
139
|
+
async * findPeer (id: PeerId, options: QueryOptions = {}) {
|
|
140
|
+
this.log('findPeer %p', id)
|
|
141
|
+
|
|
142
|
+
// Try to find locally
|
|
143
|
+
const pi = await this.findPeerLocal(id)
|
|
144
|
+
|
|
145
|
+
// already got it
|
|
146
|
+
if (pi != null) {
|
|
147
|
+
this.log('found local')
|
|
148
|
+
yield finalPeerEvent({
|
|
149
|
+
from: this.peerId,
|
|
150
|
+
peer: pi
|
|
151
|
+
})
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const key = await utils.convertPeerId(id)
|
|
156
|
+
const peers = this.routingTable.closestPeers(key)
|
|
157
|
+
|
|
158
|
+
// sanity check
|
|
159
|
+
const match = peers.find((p) => p.equals(id))
|
|
160
|
+
|
|
161
|
+
if (match != null) {
|
|
162
|
+
try {
|
|
163
|
+
const peer = await this.peerStore.get(id)
|
|
164
|
+
|
|
165
|
+
this.log('found in peerStore')
|
|
166
|
+
yield finalPeerEvent({
|
|
167
|
+
from: this.peerId,
|
|
168
|
+
peer: {
|
|
169
|
+
id: peer.id,
|
|
170
|
+
multiaddrs: peer.addresses.map((address) => address.multiaddr),
|
|
171
|
+
protocols: []
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
return
|
|
176
|
+
} catch (err: any) {
|
|
177
|
+
if (err.code !== 'ERR_NOT_FOUND') {
|
|
178
|
+
throw err
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const self = this // eslint-disable-line @typescript-eslint/no-this-alias
|
|
184
|
+
|
|
185
|
+
const findPeerQuery: QueryFunc = async function * ({ peer, signal }) {
|
|
186
|
+
const request = new Message(MESSAGE_TYPE.FIND_NODE, id.toBytes(), 0)
|
|
187
|
+
|
|
188
|
+
for await (const event of self.network.sendRequest(peer, request, { signal })) {
|
|
189
|
+
yield event
|
|
190
|
+
|
|
191
|
+
if (event.name === 'PEER_RESPONSE') {
|
|
192
|
+
const match = event.closer.find((p) => p.id.equals(id))
|
|
193
|
+
|
|
194
|
+
// found the peer
|
|
195
|
+
if (match != null) {
|
|
196
|
+
yield finalPeerEvent({ from: event.from, peer: match })
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let foundPeer = false
|
|
203
|
+
|
|
204
|
+
for await (const event of this.queryManager.run(id.toBytes(), peers, findPeerQuery, options)) {
|
|
205
|
+
if (event.name === 'FINAL_PEER') {
|
|
206
|
+
foundPeer = true
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
yield event
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!foundPeer) {
|
|
213
|
+
yield queryErrorEvent({ from: this.peerId, error: errcode(new Error('Not found'), 'ERR_NOT_FOUND') })
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Kademlia 'node lookup' operation on a key, which could be a the
|
|
219
|
+
* bytes from a multihash or a peer ID
|
|
220
|
+
*/
|
|
221
|
+
async * getClosestPeers (key: Uint8Array, options: QueryOptions = {}) {
|
|
222
|
+
this.log('getClosestPeers to %b', key)
|
|
223
|
+
const id = await utils.convertBuffer(key)
|
|
224
|
+
const tablePeers = this.routingTable.closestPeers(id)
|
|
225
|
+
const self = this // eslint-disable-line @typescript-eslint/no-this-alias
|
|
226
|
+
|
|
227
|
+
const peers = new PeerDistanceList(id, this.routingTable.kBucketSize)
|
|
228
|
+
await Promise.all(tablePeers.map(async peer => await peers.add(peer)))
|
|
229
|
+
|
|
230
|
+
const getCloserPeersQuery: QueryFunc = async function * ({ peer, signal }) {
|
|
231
|
+
self.log('closerPeersSingle %s from %p', uint8ArrayToString(key, 'base32'), peer)
|
|
232
|
+
const request = new Message(MESSAGE_TYPE.FIND_NODE, key, 0)
|
|
233
|
+
|
|
234
|
+
yield * self.network.sendRequest(peer, request, { signal })
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
for await (const event of this.queryManager.run(key, tablePeers, getCloserPeersQuery, options)) {
|
|
238
|
+
yield event
|
|
239
|
+
|
|
240
|
+
if (event.name === 'PEER_RESPONSE') {
|
|
241
|
+
await Promise.all(event.closer.map(async peerData => await peers.add(peerData.id)))
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
this.log('found %d peers close to %b', peers.length, key)
|
|
246
|
+
|
|
247
|
+
for (const peer of peers.peers) {
|
|
248
|
+
yield finalPeerEvent({
|
|
249
|
+
from: this.peerId,
|
|
250
|
+
peer: {
|
|
251
|
+
id: peer,
|
|
252
|
+
multiaddrs: (await (this.peerStore.addressBook.get(peer)) ?? []).map(addr => addr.multiaddr),
|
|
253
|
+
protocols: []
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Query a particular peer for the value for the given key.
|
|
261
|
+
* It will either return the value or a list of closer peers.
|
|
262
|
+
*
|
|
263
|
+
* Note: The peerStore is updated with new addresses found for the given peer.
|
|
264
|
+
*/
|
|
265
|
+
async * getValueOrPeers (peer: PeerId, key: Uint8Array, options: AbortOptions = {}) {
|
|
266
|
+
for await (const event of this._getValueSingle(peer, key, options)) {
|
|
267
|
+
if (event.name === 'PEER_RESPONSE') {
|
|
268
|
+
if (event.record != null) {
|
|
269
|
+
// We have a record
|
|
270
|
+
try {
|
|
271
|
+
await this._verifyRecordOnline(event.record)
|
|
272
|
+
} catch (err: any) {
|
|
273
|
+
const errMsg = 'invalid record received, discarded'
|
|
274
|
+
this.log(errMsg)
|
|
275
|
+
|
|
276
|
+
yield queryErrorEvent({ from: event.from, error: errcode(new Error(errMsg), 'ERR_INVALID_RECORD') })
|
|
277
|
+
continue
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
yield event
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Verify a record, fetching missing public keys from the network.
|
|
288
|
+
* Throws an error if the record is invalid.
|
|
289
|
+
*/
|
|
290
|
+
async _verifyRecordOnline (record: DHTRecord) {
|
|
291
|
+
await verifyRecord(this.validators, new Libp2pRecord(record.key, record.value, record.timeReceived))
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Get the nearest peers to the given query, but if closer
|
|
296
|
+
* than self
|
|
297
|
+
*/
|
|
298
|
+
async getCloserPeersOffline (key: Uint8Array, closerThan: PeerId) {
|
|
299
|
+
const id = await utils.convertBuffer(key)
|
|
300
|
+
const ids = this.routingTable.closestPeers(id)
|
|
301
|
+
const output: PeerData[] = []
|
|
302
|
+
|
|
303
|
+
for (const peerId of ids) {
|
|
304
|
+
if (peerId.equals(closerThan)) {
|
|
305
|
+
continue
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const addresses = await this.peerStore.addressBook.get(peerId)
|
|
310
|
+
const protocols = await this.peerStore.protoBook.get(peerId)
|
|
311
|
+
|
|
312
|
+
output.push({
|
|
313
|
+
id: peerId,
|
|
314
|
+
multiaddrs: addresses.map((address) => address.multiaddr),
|
|
315
|
+
protocols
|
|
316
|
+
})
|
|
317
|
+
} catch (err: any) {
|
|
318
|
+
if (err.code !== 'ERR_NOT_FOUND') {
|
|
319
|
+
throw err
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (output.length > 0) {
|
|
325
|
+
this.log('getCloserPeersOffline found %d peer(s) closer to %b than %p', output.length, key, closerThan)
|
|
326
|
+
} else {
|
|
327
|
+
this.log('getCloserPeersOffline could not find peer closer to %b than %p', key, closerThan)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return output
|
|
331
|
+
}
|
|
332
|
+
}
|
package/src/providers.ts
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import cache from 'hashlru'
|
|
2
|
+
import varint from 'varint'
|
|
3
|
+
import { Key } from 'interface-datastore/key'
|
|
4
|
+
import Queue from 'p-queue'
|
|
5
|
+
import {
|
|
6
|
+
PROVIDERS_CLEANUP_INTERVAL,
|
|
7
|
+
PROVIDERS_VALIDITY,
|
|
8
|
+
PROVIDERS_LRU_CACHE_SIZE,
|
|
9
|
+
PROVIDER_KEY_PREFIX
|
|
10
|
+
} from './constants.js'
|
|
11
|
+
import { logger } from '@libp2p/logger'
|
|
12
|
+
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
|
|
13
|
+
import { peerIdFromString } from '@libp2p/peer-id'
|
|
14
|
+
import type { Datastore } from 'interface-datastore'
|
|
15
|
+
import type { Startable } from '@libp2p/interfaces'
|
|
16
|
+
import type { CID } from 'multiformats'
|
|
17
|
+
import type { PeerId } from '@libp2p/interfaces/peer-id'
|
|
18
|
+
|
|
19
|
+
const log = logger('libp2p:kad-dht:providers')
|
|
20
|
+
|
|
21
|
+
export interface ProvidersOptions {
|
|
22
|
+
datastore: Datastore
|
|
23
|
+
cacheSize?: number
|
|
24
|
+
/**
|
|
25
|
+
* How often invalid records are cleaned. (in seconds)
|
|
26
|
+
*/
|
|
27
|
+
cleanupInterval?: number
|
|
28
|
+
/**
|
|
29
|
+
* How long is a provider valid for. (in seconds)
|
|
30
|
+
*/
|
|
31
|
+
provideValidity?: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* This class manages known providers.
|
|
36
|
+
* A provider is a peer that we know to have the content for a given CID.
|
|
37
|
+
*
|
|
38
|
+
* Every `cleanupInterval` providers are checked if they
|
|
39
|
+
* are still valid, i.e. younger than the `provideValidity`.
|
|
40
|
+
* If they are not, they are deleted.
|
|
41
|
+
*
|
|
42
|
+
* To ensure the list survives restarts of the daemon,
|
|
43
|
+
* providers are stored in the datastore, but to ensure
|
|
44
|
+
* access is fast there is an LRU cache in front of that.
|
|
45
|
+
*/
|
|
46
|
+
export class Providers implements Startable {
|
|
47
|
+
private readonly datastore: Datastore
|
|
48
|
+
private readonly cache: ReturnType<typeof cache>
|
|
49
|
+
private readonly cleanupInterval: number
|
|
50
|
+
private readonly provideValidity: number
|
|
51
|
+
private readonly syncQueue: Queue
|
|
52
|
+
private started: boolean
|
|
53
|
+
private cleaner?: NodeJS.Timer
|
|
54
|
+
|
|
55
|
+
constructor (options: ProvidersOptions) {
|
|
56
|
+
const { datastore, cacheSize, cleanupInterval, provideValidity } = options
|
|
57
|
+
|
|
58
|
+
this.datastore = datastore
|
|
59
|
+
this.cleanupInterval = cleanupInterval ?? PROVIDERS_CLEANUP_INTERVAL
|
|
60
|
+
this.provideValidity = provideValidity ?? PROVIDERS_VALIDITY
|
|
61
|
+
this.cache = cache(cacheSize ?? PROVIDERS_LRU_CACHE_SIZE)
|
|
62
|
+
this.syncQueue = new Queue({ concurrency: 1 })
|
|
63
|
+
this.started = false
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
isStarted () {
|
|
67
|
+
return this.started
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Start the provider cleanup service
|
|
72
|
+
*/
|
|
73
|
+
async start () {
|
|
74
|
+
if (this.started) {
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.started = true
|
|
79
|
+
|
|
80
|
+
this.cleaner = setInterval(
|
|
81
|
+
() => {
|
|
82
|
+
this._cleanup().catch(err => {
|
|
83
|
+
log.error(err)
|
|
84
|
+
})
|
|
85
|
+
},
|
|
86
|
+
this.cleanupInterval
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Release any resources.
|
|
92
|
+
*/
|
|
93
|
+
async stop () {
|
|
94
|
+
this.started = false
|
|
95
|
+
|
|
96
|
+
if (this.cleaner != null) {
|
|
97
|
+
clearInterval(this.cleaner)
|
|
98
|
+
this.cleaner = undefined
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check all providers if they are still valid, and if not delete them
|
|
104
|
+
*/
|
|
105
|
+
async _cleanup () {
|
|
106
|
+
return await this.syncQueue.add(async () => {
|
|
107
|
+
const start = Date.now()
|
|
108
|
+
|
|
109
|
+
let count = 0
|
|
110
|
+
let deleteCount = 0
|
|
111
|
+
const deleted = new Map<string, Set<string>>()
|
|
112
|
+
const batch = this.datastore.batch()
|
|
113
|
+
|
|
114
|
+
// Get all provider entries from the datastore
|
|
115
|
+
const query = this.datastore.query({ prefix: PROVIDER_KEY_PREFIX })
|
|
116
|
+
|
|
117
|
+
for await (const entry of query) {
|
|
118
|
+
try {
|
|
119
|
+
// Add a delete to the batch for each expired entry
|
|
120
|
+
const { cid, peerId } = parseProviderKey(entry.key)
|
|
121
|
+
const time = readTime(entry.value).getTime()
|
|
122
|
+
const now = Date.now()
|
|
123
|
+
const delta = now - time
|
|
124
|
+
const expired = delta > this.provideValidity
|
|
125
|
+
|
|
126
|
+
log('comparing: %d - %d = %d > %d %s', now, time, delta, this.provideValidity, expired ? '(expired)' : '')
|
|
127
|
+
|
|
128
|
+
if (expired) {
|
|
129
|
+
deleteCount++
|
|
130
|
+
batch.delete(entry.key)
|
|
131
|
+
const peers = deleted.get(cid) ?? new Set<string>()
|
|
132
|
+
peers.add(peerId)
|
|
133
|
+
deleted.set(cid, peers)
|
|
134
|
+
}
|
|
135
|
+
count++
|
|
136
|
+
} catch (err: any) {
|
|
137
|
+
log.error(err.message)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Commit the deletes to the datastore
|
|
142
|
+
if (deleted.size > 0) {
|
|
143
|
+
log('deleting %d / %d entries', deleteCount, count)
|
|
144
|
+
await batch.commit()
|
|
145
|
+
} else {
|
|
146
|
+
log('nothing to delete')
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Clear expired entries from the cache
|
|
150
|
+
for (const [cid, peers] of deleted) {
|
|
151
|
+
const key = makeProviderKey(cid)
|
|
152
|
+
const provs = this.cache.get(key)
|
|
153
|
+
|
|
154
|
+
if (provs != null) {
|
|
155
|
+
for (const peerId of peers) {
|
|
156
|
+
provs.delete(peerId)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (provs.size === 0) {
|
|
160
|
+
this.cache.remove(key)
|
|
161
|
+
} else {
|
|
162
|
+
this.cache.set(key, provs)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
log('Cleanup successful (%dms)', Date.now() - start)
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get the currently known provider peer ids for a given CID
|
|
173
|
+
*/
|
|
174
|
+
async _getProvidersMap (cid: CID) {
|
|
175
|
+
const cacheKey = makeProviderKey(cid)
|
|
176
|
+
let provs: Map<string, Date> = this.cache.get(cacheKey)
|
|
177
|
+
|
|
178
|
+
if (provs == null) {
|
|
179
|
+
provs = await loadProviders(this.datastore, cid)
|
|
180
|
+
this.cache.set(cacheKey, provs)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return provs
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Add a new provider for the given CID
|
|
188
|
+
*/
|
|
189
|
+
async addProvider (cid: CID, provider: PeerId) {
|
|
190
|
+
return await this.syncQueue.add(async () => {
|
|
191
|
+
log('%p provides %s', provider, cid)
|
|
192
|
+
const provs = await this._getProvidersMap(cid)
|
|
193
|
+
|
|
194
|
+
log('loaded %s provs', provs.size)
|
|
195
|
+
const now = new Date()
|
|
196
|
+
provs.set(provider.toString(), now)
|
|
197
|
+
|
|
198
|
+
const dsKey = makeProviderKey(cid)
|
|
199
|
+
this.cache.set(dsKey, provs)
|
|
200
|
+
|
|
201
|
+
await writeProviderEntry(this.datastore, cid, provider, now)
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get a list of providers for the given CID
|
|
207
|
+
*/
|
|
208
|
+
async getProviders (cid: CID): Promise<PeerId[]> {
|
|
209
|
+
return await this.syncQueue.add(async () => {
|
|
210
|
+
log('get providers for %s', cid)
|
|
211
|
+
const provs = await this._getProvidersMap(cid)
|
|
212
|
+
|
|
213
|
+
return [...provs.keys()].map(peerIdStr => {
|
|
214
|
+
return peerIdFromString(peerIdStr)
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Encode the given key its matching datastore key
|
|
222
|
+
*/
|
|
223
|
+
function makeProviderKey (cid: CID | string) {
|
|
224
|
+
const cidStr = typeof cid === 'string' ? cid : uint8ArrayToString(cid.multihash.bytes, 'base32')
|
|
225
|
+
|
|
226
|
+
return `${PROVIDER_KEY_PREFIX}/${cidStr}`
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Write a provider into the given store
|
|
231
|
+
*/
|
|
232
|
+
async function writeProviderEntry (store: Datastore, cid: CID, peer: PeerId, time: Date) { // eslint-disable-line require-await
|
|
233
|
+
const dsKey = [
|
|
234
|
+
makeProviderKey(cid),
|
|
235
|
+
'/',
|
|
236
|
+
peer.toString()
|
|
237
|
+
].join('')
|
|
238
|
+
|
|
239
|
+
const key = new Key(dsKey)
|
|
240
|
+
const buffer = Uint8Array.from(varint.encode(time.getTime()))
|
|
241
|
+
|
|
242
|
+
return await store.put(key, buffer)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Parse the CID and provider peer id from the key
|
|
247
|
+
*/
|
|
248
|
+
function parseProviderKey (key: Key) {
|
|
249
|
+
const parts = key.toString().split('/')
|
|
250
|
+
|
|
251
|
+
if (parts.length !== 5) {
|
|
252
|
+
throw new Error(`incorrectly formatted provider entry key in datastore: ${key.toString()}`)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
cid: parts[3],
|
|
257
|
+
peerId: parts[4]
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Load providers for the given CID from the store
|
|
263
|
+
*/
|
|
264
|
+
async function loadProviders (store: Datastore, cid: CID) {
|
|
265
|
+
const providers = new Map<string, Date>()
|
|
266
|
+
const query = store.query({ prefix: makeProviderKey(cid) })
|
|
267
|
+
|
|
268
|
+
for await (const entry of query) {
|
|
269
|
+
const { peerId } = parseProviderKey(entry.key)
|
|
270
|
+
providers.set(peerId, readTime(entry.value))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return providers
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function readTime (buf: Uint8Array) {
|
|
277
|
+
return new Date(varint.decode(buf))
|
|
278
|
+
}
|