@libp2p/kad-dht 13.1.2-32c176fd5 → 13.1.2-661d6586a

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. package/dist/index.min.js +2 -2
  2. package/dist/src/index.d.ts +26 -2
  3. package/dist/src/index.d.ts.map +1 -1
  4. package/dist/src/index.js.map +1 -1
  5. package/dist/src/kad-dht.d.ts.map +1 -1
  6. package/dist/src/kad-dht.js +25 -13
  7. package/dist/src/kad-dht.js.map +1 -1
  8. package/dist/src/network.d.ts +1 -1
  9. package/dist/src/network.d.ts.map +1 -1
  10. package/dist/src/network.js +1 -3
  11. package/dist/src/network.js.map +1 -1
  12. package/dist/src/query-self.d.ts.map +1 -1
  13. package/dist/src/query-self.js +13 -6
  14. package/dist/src/query-self.js.map +1 -1
  15. package/dist/src/routing-table/index.d.ts +53 -30
  16. package/dist/src/routing-table/index.d.ts.map +1 -1
  17. package/dist/src/routing-table/index.js +279 -167
  18. package/dist/src/routing-table/index.js.map +1 -1
  19. package/dist/src/routing-table/k-bucket.d.ts +67 -21
  20. package/dist/src/routing-table/k-bucket.d.ts.map +1 -1
  21. package/dist/src/routing-table/k-bucket.js +128 -60
  22. package/dist/src/routing-table/k-bucket.js.map +1 -1
  23. package/dist/src/routing-table/refresh.d.ts.map +1 -1
  24. package/dist/src/routing-table/refresh.js +4 -1
  25. package/dist/src/routing-table/refresh.js.map +1 -1
  26. package/dist/src/rpc/index.d.ts.map +1 -1
  27. package/dist/src/rpc/index.js +3 -7
  28. package/dist/src/rpc/index.js.map +1 -1
  29. package/package.json +11 -12
  30. package/src/index.ts +30 -2
  31. package/src/kad-dht.ts +29 -15
  32. package/src/network.ts +1 -4
  33. package/src/query-self.ts +14 -6
  34. package/src/routing-table/index.ts +318 -199
  35. package/src/routing-table/k-bucket.ts +203 -74
  36. package/src/routing-table/refresh.ts +5 -1
  37. package/src/rpc/index.ts +4 -7
@@ -1,45 +1,49 @@
1
- import { TypedEventEmitter } from '@libp2p/interface'
1
+ import { PeerMap } from '@libp2p/peer-collections'
2
2
  import map from 'it-map'
3
+ import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
3
4
  import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
4
5
  import { xor as uint8ArrayXor } from 'uint8arrays/xor'
5
6
  import { PeerDistanceList } from '../peer-list/peer-distance-list.js'
6
- import { KBUCKET_SIZE } from './index.js'
7
+ import { convertPeerId } from '../utils.js'
8
+ import { KBUCKET_SIZE, LAST_PING_THRESHOLD, PING_OLD_CONTACT_COUNT, PREFIX_LENGTH } from './index.js'
7
9
  import type { PeerId } from '@libp2p/interface'
10
+ import type { AbortOptions } from 'it-protobuf-stream'
8
11
 
9
- function arrayEquals (array1: Uint8Array, array2: Uint8Array): boolean {
10
- if (array1 === array2) {
11
- return true
12
- }
13
- if (array1.length !== array2.length) {
14
- return false
15
- }
16
- for (let i = 0, length = array1.length; i < length; ++i) {
17
- if (array1[i] !== array2[i]) {
18
- return false
19
- }
20
- }
21
- return true
12
+ export interface PingFunction {
13
+ /**
14
+ * Return either none or at least one contact that does not respond to a ping
15
+ * message
16
+ */
17
+ (oldContacts: Peer[], options?: AbortOptions): AsyncGenerator<Peer>
22
18
  }
23
19
 
24
- function ensureInt8 (name: string, val?: Uint8Array): void {
25
- if (!(val instanceof Uint8Array)) {
26
- throw new TypeError(name + ' is not a Uint8Array')
27
- }
20
+ /**
21
+ * Before a peer can be added to the table, verify that it is online and working
22
+ * correctly
23
+ */
24
+ export interface VerifyFunction {
25
+ (contact: Peer, options?: AbortOptions): Promise<boolean>
26
+ }
28
27
 
29
- if (val.byteLength !== 32) {
30
- throw new TypeError(name + ' had incorrect length')
31
- }
28
+ export interface OnAddCallback {
29
+ /**
30
+ * Invoked when a new peer is added to the routing tables
31
+ */
32
+ (peer: Peer, bucket: LeafBucket): Promise<void>
32
33
  }
33
34
 
34
- export interface PingEventDetails {
35
- oldContacts: Peer[]
36
- newContact: Peer
35
+ export interface OnRemoveCallback {
36
+ /**
37
+ * Invoked when a peer is evicted from the routing tables
38
+ */
39
+ (peer: Peer, bucket: LeafBucket): Promise<void>
37
40
  }
38
41
 
39
- export interface KBucketEvents {
40
- 'ping': CustomEvent<PingEventDetails>
41
- 'added': CustomEvent<Peer>
42
- 'removed': CustomEvent<Peer>
42
+ export interface OnMoveCallback {
43
+ /**
44
+ * Invoked when a peer is moved between buckets in the routing tables
45
+ */
46
+ (peer: Peer, oldBucket: LeafBucket, newBucket: LeafBucket): Promise<void>
43
47
  }
44
48
 
45
49
  export interface KBucketOptions {
@@ -47,14 +51,16 @@ export interface KBucketOptions {
47
51
  * The current peer. All subsequently added peers must have a KadID that is
48
52
  * the same length as this peer.
49
53
  */
50
- localPeer: Peer
54
+ // localPeer: Peer
51
55
 
52
56
  /**
53
57
  * How many bits of the key to use when forming the bucket trie. The larger
54
58
  * this value, the deeper the tree will grow and the slower the lookups will
55
59
  * be but the peers returned will be more specific to the key.
60
+ *
61
+ * @default 32
56
62
  */
57
- prefixLength: number
63
+ prefixLength?: number
58
64
 
59
65
  /**
60
66
  * The number of nodes that a max-depth k-bucket can contain before being
@@ -74,20 +80,37 @@ export interface KBucketOptions {
74
80
 
75
81
  /**
76
82
  * The number of nodes to ping when a bucket that should not be split becomes
77
- * full. KBucket will emit a `ping` event that contains `numberOfNodesToPing`
78
- * nodes that have not been contacted the longest.
83
+ * full. KBucket will emit a `ping` event that contains
84
+ * `numberOfOldContactsToPing` nodes that have not been contacted the longest.
85
+ *
86
+ * @default 3
79
87
  */
80
- numberOfNodesToPing?: number
88
+ numberOfOldContactsToPing?: number
89
+
90
+ /**
91
+ * Do not re-ping a peer during this time window in ms
92
+ *
93
+ * @default 600000
94
+ */
95
+ lastPingThreshold?: number
96
+
97
+ ping: PingFunction
98
+ verify: VerifyFunction
99
+ onAdd?: OnAddCallback
100
+ onRemove?: OnRemoveCallback
101
+ onMove?: OnMoveCallback
81
102
  }
82
103
 
83
104
  export interface Peer {
84
105
  kadId: Uint8Array
85
106
  peerId: PeerId
107
+ lastPing: number
86
108
  }
87
109
 
88
110
  export interface LeafBucket {
89
111
  prefix: string
90
112
  depth: number
113
+ containsSelf?: boolean
91
114
  peers: Peer[]
92
115
  }
93
116
 
@@ -108,24 +131,33 @@ export function isLeafBucket (obj: any): obj is LeafBucket {
108
131
  * Implementation of a Kademlia DHT routing table as a prefix binary trie with
109
132
  * configurable prefix length, bucket split threshold and size.
110
133
  */
111
- export class KBucket extends TypedEventEmitter<KBucketEvents> {
134
+ export class KBucket {
112
135
  public root: Bucket
113
- public localPeer: Peer
136
+ public localPeer?: Peer
114
137
  private readonly prefixLength: number
115
138
  private readonly splitThreshold: number
116
139
  private readonly kBucketSize: number
117
140
  private readonly numberOfNodesToPing: number
141
+ private readonly lastPingThreshold: number
142
+ public ping: PingFunction
143
+ public verify: VerifyFunction
144
+ private readonly onAdd?: OnAddCallback
145
+ private readonly onRemove?: OnRemoveCallback
146
+ private readonly onMove?: OnMoveCallback
147
+ private readonly addingPeerMap: PeerMap<Promise<void>>
118
148
 
119
149
  constructor (options: KBucketOptions) {
120
- super()
121
-
122
- this.localPeer = options.localPeer
123
- this.prefixLength = options.prefixLength
150
+ this.prefixLength = options.prefixLength ?? PREFIX_LENGTH
124
151
  this.kBucketSize = options.kBucketSize ?? KBUCKET_SIZE
125
152
  this.splitThreshold = options.splitThreshold ?? this.kBucketSize
126
- this.numberOfNodesToPing = options.numberOfNodesToPing ?? 3
127
-
128
- ensureInt8('options.localPeer.kadId', options.localPeer.kadId)
153
+ this.numberOfNodesToPing = options.numberOfOldContactsToPing ?? PING_OLD_CONTACT_COUNT
154
+ this.lastPingThreshold = options.lastPingThreshold ?? LAST_PING_THRESHOLD
155
+ this.ping = options.ping
156
+ this.verify = options.verify
157
+ this.onAdd = options.onAdd
158
+ this.onRemove = options.onRemove
159
+ this.onMove = options.onMove
160
+ this.addingPeerMap = new PeerMap()
129
161
 
130
162
  this.root = {
131
163
  prefix: '',
@@ -134,14 +166,43 @@ export class KBucket extends TypedEventEmitter<KBucketEvents> {
134
166
  }
135
167
  }
136
168
 
169
+ async addSelfPeer (peerId: PeerId): Promise<void> {
170
+ this.localPeer = {
171
+ peerId,
172
+ kadId: await convertPeerId(peerId),
173
+ lastPing: Date.now()
174
+ }
175
+
176
+ const bucket = this._determineBucket(this.localPeer.kadId)
177
+ bucket.containsSelf = true
178
+ }
179
+
137
180
  /**
138
- * Adds a contact to the k-bucket.
139
- *
140
- * @param {Peer} peer - the contact object to add
181
+ * Adds a contact to the trie
141
182
  */
142
- add (peer: Peer): void {
143
- ensureInt8('peer.kadId', peer?.kadId)
183
+ async add (peerId: PeerId, options?: AbortOptions): Promise<void> {
184
+ const peer = {
185
+ peerId,
186
+ kadId: await convertPeerId(peerId),
187
+ lastPing: 0
188
+ }
189
+
190
+ const existingPromise = this.addingPeerMap.get(peerId)
191
+
192
+ if (existingPromise != null) {
193
+ return existingPromise
194
+ }
144
195
 
196
+ try {
197
+ const p = this._add(peer, options)
198
+ this.addingPeerMap.set(peerId, p)
199
+ await p
200
+ } finally {
201
+ this.addingPeerMap.delete(peerId)
202
+ }
203
+ }
204
+
205
+ private async _add (peer: Peer, options?: AbortOptions): Promise<void> {
145
206
  const bucket = this._determineBucket(peer.kadId)
146
207
 
147
208
  // check if the contact already exists
@@ -152,18 +213,32 @@ export class KBucket extends TypedEventEmitter<KBucketEvents> {
152
213
  // are there too many peers in the bucket and can we make the trie deeper?
153
214
  if (bucket.peers.length === this.splitThreshold && bucket.depth < this.prefixLength) {
154
215
  // split the bucket
155
- this._split(bucket)
216
+ await this._split(bucket)
156
217
 
157
218
  // try again
158
- this.add(peer)
219
+ await this._add(peer, options)
159
220
 
160
221
  return
161
222
  }
162
223
 
163
224
  // is there space in the bucket?
164
225
  if (bucket.peers.length < this.kBucketSize) {
165
- bucket.peers.push(peer)
166
- this.safeDispatchEvent('added', { detail: peer })
226
+ // we've ping this peer previously, just add them to the bucket
227
+ if (!needsPing(peer, this.lastPingThreshold)) {
228
+ bucket.peers.push(peer)
229
+ await this.onAdd?.(peer, bucket)
230
+ return
231
+ }
232
+
233
+ const result = await this.verify(peer, options)
234
+
235
+ // only add if peer is online and functioning correctly
236
+ if (result) {
237
+ peer.lastPing = Date.now()
238
+
239
+ // try again - buckets may have changed during ping
240
+ await this._add(peer, options)
241
+ }
167
242
 
168
243
  return
169
244
  }
@@ -171,17 +246,51 @@ export class KBucket extends TypedEventEmitter<KBucketEvents> {
171
246
  // we are at the bottom of the trie and the bucket is full so we can't add
172
247
  // any more peers.
173
248
  //
174
- // instead ping the first this.numberOfNodesToPing in order to determine
249
+ // instead ping the first `this.numberOfNodesToPing` in order to determine
175
250
  // if they are still online.
176
251
  //
177
252
  // only add the new peer if one of the pinged nodes does not respond, this
178
253
  // prevents DoS flooding with new invalid contacts.
179
- this.safeDispatchEvent('ping', {
180
- detail: {
181
- oldContacts: bucket.peers.slice(0, this.numberOfNodesToPing),
182
- newContact: peer
183
- }
184
- })
254
+ const toPing = bucket.peers
255
+ .filter(peer => {
256
+ if (peer.peerId.equals(this.localPeer?.peerId)) {
257
+ return false
258
+ }
259
+
260
+ if (peer.lastPing > (Date.now() - this.lastPingThreshold)) {
261
+ return false
262
+ }
263
+
264
+ return true
265
+ })
266
+ .sort((a, b) => {
267
+ // sort oldest ping -> newest
268
+ if (a.lastPing < b.lastPing) {
269
+ return -1
270
+ }
271
+
272
+ if (a.lastPing > b.lastPing) {
273
+ return 1
274
+ }
275
+
276
+ return 0
277
+ })
278
+ .slice(0, this.numberOfNodesToPing)
279
+
280
+ let evicted = false
281
+
282
+ for await (const toEvict of this.ping(toPing, options)) {
283
+ evicted = true
284
+ await this.remove(toEvict.kadId)
285
+ }
286
+
287
+ // did not evict any peers, cannot add new contact
288
+ if (!evicted) {
289
+ return
290
+ }
291
+
292
+ // try again - buckets may have changed during ping
293
+ await this._add(peer, options)
185
294
  }
186
295
 
187
296
  /**
@@ -235,7 +344,7 @@ export class KBucket extends TypedEventEmitter<KBucketEvents> {
235
344
  * which branch of the tree to traverse and repeat.
236
345
  *
237
346
  * @param {Uint8Array} kadId - The ID of the contact to fetch.
238
- * @returns {object | undefined} The contact if available, otherwise null
347
+ * @returns {Peer | undefined} The contact if available, otherwise null
239
348
  */
240
349
  get (kadId: Uint8Array): Peer | undefined {
241
350
  const bucket = this._determineBucket(kadId)
@@ -249,15 +358,14 @@ export class KBucket extends TypedEventEmitter<KBucketEvents> {
249
358
  *
250
359
  * @param {Uint8Array} kadId - The ID of the contact to remove
251
360
  */
252
- remove (kadId: Uint8Array): void {
361
+ async remove (kadId: Uint8Array): Promise<void> {
253
362
  const bucket = this._determineBucket(kadId)
254
363
  const index = this._indexOf(bucket, kadId)
255
364
 
256
365
  if (index > -1) {
257
366
  const peer = bucket.peers.splice(index, 1)[0]
258
- this.safeDispatchEvent('removed', {
259
- detail: peer
260
- })
367
+
368
+ await this.onRemove?.(peer, bucket)
261
369
  }
262
370
  }
263
371
 
@@ -331,7 +439,7 @@ export class KBucket extends TypedEventEmitter<KBucketEvents> {
331
439
  * @returns {number} Integer Index of contact with provided id if it exists, -1 otherwise.
332
440
  */
333
441
  private _indexOf (bucket: LeafBucket, kadId: Uint8Array): number {
334
- return bucket.peers.findIndex(peer => arrayEquals(peer.kadId, kadId))
442
+ return bucket.peers.findIndex(peer => uint8ArrayEquals(peer.kadId, kadId))
335
443
  }
336
444
 
337
445
  /**
@@ -339,8 +447,8 @@ export class KBucket extends TypedEventEmitter<KBucketEvents> {
339
447
  *
340
448
  * @param {any} bucket - bucket for splitting
341
449
  */
342
- private _split (bucket: LeafBucket): void {
343
- const depth = bucket.depth + 1
450
+ private async _split (bucket: LeafBucket): Promise<void> {
451
+ const depth = bucket.prefix === '' ? bucket.depth : bucket.depth + 1
344
452
 
345
453
  // create child buckets
346
454
  const left: LeafBucket = {
@@ -354,23 +462,44 @@ export class KBucket extends TypedEventEmitter<KBucketEvents> {
354
462
  peers: []
355
463
  }
356
464
 
465
+ if (bucket.containsSelf === true && this.localPeer != null) {
466
+ delete bucket.containsSelf
467
+ const selfNodeBitString = uint8ArrayToString(this.localPeer.kadId, 'base2')
468
+
469
+ if (selfNodeBitString[depth] === '0') {
470
+ left.containsSelf = true
471
+ } else {
472
+ right.containsSelf = true
473
+ }
474
+ }
475
+
357
476
  // redistribute peers
358
477
  for (const peer of bucket.peers) {
359
478
  const bitString = uint8ArrayToString(peer.kadId, 'base2')
360
479
 
361
480
  if (bitString[depth] === '0') {
362
481
  left.peers.push(peer)
482
+ await this.onMove?.(peer, bucket, left)
363
483
  } else {
364
484
  right.peers.push(peer)
485
+ await this.onMove?.(peer, bucket, right)
365
486
  }
366
487
  }
367
488
 
368
- // convert leaf bucket to internal bucket
369
- // @ts-expect-error peers is not a property of LeafBucket
370
- delete bucket.peers
371
- // @ts-expect-error left is not a property of LeafBucket
372
- bucket.left = left
373
- // @ts-expect-error right is not a property of LeafBucket
374
- bucket.right = right
489
+ // convert old leaf bucket to internal bucket
490
+ convertToInternalBucket(bucket, left, right)
375
491
  }
376
492
  }
493
+
494
+ function convertToInternalBucket (bucket: any, left: any, right: any): bucket is InternalBucket {
495
+ delete bucket.peers
496
+ delete bucket.containsSelf
497
+ bucket.left = left
498
+ bucket.right = right
499
+
500
+ return true
501
+ }
502
+
503
+ function needsPing (peer: Peer, threshold: number): boolean {
504
+ return peer.lastPing < (Date.now() - threshold)
505
+ }
@@ -169,6 +169,10 @@ export class RoutingTableRefresh {
169
169
  throw new Error('Routing table not started')
170
170
  }
171
171
 
172
+ if (this.routingTable.kb.localPeer == null) {
173
+ throw new Error('Local peer not set')
174
+ }
175
+
172
176
  const randomData = randomBytes(2)
173
177
  const randomUint16 = (randomData[1] << 8) + randomData[0]
174
178
 
@@ -245,7 +249,7 @@ export class RoutingTableRefresh {
245
249
  * Yields the common prefix length of every peer in the table
246
250
  */
247
251
  * _prefixLengths (): Generator<number> {
248
- if (this.routingTable.kb == null) {
252
+ if (this.routingTable.kb?.localPeer == null) {
249
253
  return
250
254
  }
251
255
 
package/src/rpc/index.ts CHANGED
@@ -63,12 +63,6 @@ export class RPC {
63
63
  * Process incoming DHT messages
64
64
  */
65
65
  async handleMessage (peerId: PeerId, msg: Message): Promise<Message | undefined> {
66
- try {
67
- await this.routingTable.add(peerId)
68
- } catch (err: any) {
69
- this.log.error('Failed to update the kbucket store', err)
70
- }
71
-
72
66
  // get handler & execute it
73
67
  const handler = this.handlers[msg.type]
74
68
 
@@ -94,6 +88,8 @@ export class RPC {
94
88
  * Handle incoming streams on the dht protocol
95
89
  */
96
90
  onIncomingStream (data: IncomingStreamData): void {
91
+ let message = 'unknown'
92
+
97
93
  Promise.resolve().then(async () => {
98
94
  const { stream, connection } = data
99
95
  const peerId = connection.remotePeer
@@ -113,6 +109,7 @@ export class RPC {
113
109
  for await (const msg of source) {
114
110
  // handle the message
115
111
  const desMessage = Message.decode(msg)
112
+ message = desMessage.type
116
113
  self.log('incoming %s from %p', desMessage.type, peerId)
117
114
  const res = await self.handleMessage(peerId, desMessage)
118
115
 
@@ -127,7 +124,7 @@ export class RPC {
127
124
  )
128
125
  })
129
126
  .catch(err => {
130
- this.log.error(err)
127
+ this.log.error('error handling %s RPC message from %p - %e', message, data.connection.remotePeer, err)
131
128
  })
132
129
  }
133
130
  }