Skip to content

Commit ae595d8

Browse files
authored
fix: return closest known peers, even if they are not closer (#3182)
Aligns with Kad-DHT spec PR, when responding to a `FIND_NODE` query, return the `k` known closest peers, excluding the server and the requester, even if they are not closer than the requester.
1 parent 748f962 commit ae595d8

File tree

13 files changed

+118
-125
lines changed

13 files changed

+118
-125
lines changed

packages/kad-dht/src/peer-routing/index.ts

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ import { InvalidPublicKeyError, NotFoundError } from '@libp2p/interface'
33
import { peerIdFromPublicKey, peerIdFromMultihash } from '@libp2p/peer-id'
44
import { Libp2pRecord } from '@libp2p/record'
55
import * as Digest from 'multiformats/hashes/digest'
6-
import { xor as uint8ArrayXor } from 'uint8arrays/xor'
7-
import { xorCompare as uint8ArrayXorCompare } from 'uint8arrays/xor-compare'
86
import { QueryError, InvalidRecordError } from '../errors.js'
97
import { MessageType } from '../message/dht.js'
108
import { PeerDistanceList } from '../peer-distance-list.js'
@@ -14,13 +12,14 @@ import {
1412
valueEvent
1513
} from '../query/events.js'
1614
import { verifyRecord } from '../record/validators.js'
17-
import { convertBuffer, convertPeerId, keyForPublicKey } from '../utils.js'
15+
import { convertBuffer, keyForPublicKey } from '../utils.js'
1816
import type { DHTRecord, FinalPeerEvent, QueryEvent, Validators } from '../index.js'
1917
import type { Message } from '../message/dht.js'
2018
import type { Network, SendMessageOptions } from '../network.js'
2119
import type { QueryManager, QueryOptions } from '../query/manager.js'
2220
import type { QueryFunc } from '../query/types.js'
2321
import type { RoutingTable } from '../routing-table/index.js'
22+
import type { GetClosestPeersOptions } from '../routing-table/k-bucket.ts'
2423
import type { ComponentLogger, Logger, Metrics, PeerId, PeerInfo, PeerStore, RoutingOptions } from '@libp2p/interface'
2524
import type { ConnectionManager } from '@libp2p/interface-internal'
2625
import type { AbortOptions } from 'it-pushable'
@@ -246,7 +245,7 @@ export class PeerRouting {
246245
const self = this
247246

248247
const getCloserPeersQuery: QueryFunc = async function * ({ peer, path, peerKadId, signal }) {
249-
self.log('getClosestPeers asking %p', peer)
248+
self.log('getClosestPeers asking %p', peer.id)
250249
const request: Partial<Message> = {
251250
type: MessageType.FIND_NODE,
252251
key
@@ -336,10 +335,9 @@ export class PeerRouting {
336335
}
337336

338337
/**
339-
* Get the peers in our routing table that are closer than the passed PeerId
340-
* to the passed key
338+
* Get the peers in our routing table that are closest to the passed key
341339
*/
342-
async getCloserPeersOffline (key: Uint8Array, closerThan: PeerId, options?: AbortOptions): Promise<PeerInfo[]> {
340+
async getClosestPeersOffline (key: Uint8Array, options?: GetClosestPeersOptions): Promise<PeerInfo[]> {
343341
const output: PeerInfo[] = []
344342

345343
// try getting the peer directly
@@ -356,19 +354,9 @@ export class PeerRouting {
356354
} catch {}
357355

358356
const keyKadId = await convertBuffer(key, options)
359-
const ids = this.routingTable.closestPeers(keyKadId)
360-
const closerThanKadId = await convertPeerId(closerThan, options)
361-
const requesterXor = uint8ArrayXor(closerThanKadId, keyKadId)
357+
const ids = this.routingTable.closestPeers(keyKadId, options)
362358

363359
for (const peerId of ids) {
364-
const peerKadId = await convertPeerId(peerId, options)
365-
const peerXor = uint8ArrayXor(peerKadId, keyKadId)
366-
367-
// only include if peer is closer than requester
368-
if (uint8ArrayXorCompare(peerXor, requesterXor) !== -1) {
369-
continue
370-
}
371-
372360
try {
373361
output.push(await this.components.peerStore.getInfo(peerId, options))
374362
} catch (err: any) {
@@ -379,9 +367,9 @@ export class PeerRouting {
379367
}
380368

381369
if (output.length > 0) {
382-
this.log('getCloserPeersOffline found %d peer(s) closer to %b than %p', output.length, key, closerThan)
370+
this.log('getClosestPeersOffline returning the %d closest peer(s) %b we know', output.length, key)
383371
} else {
384-
this.log('getCloserPeersOffline could not find peer closer to %b than %p with %d peers in the routing table', key, closerThan, this.routingTable.size)
372+
this.log('getClosestPeersOffline could not any peers close to %b with %d peers in the routing table', key, this.routingTable.size)
385373
}
386374

387375
return output

packages/kad-dht/src/query/manager.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,9 @@ export class QueryManager implements Startable {
168168
const id = await convertBuffer(key, {
169169
signal
170170
})
171-
const peers = this.routingTable.closestPeers(id, this.routingTable.kBucketSize)
171+
const peers = this.routingTable.closestPeers(id, {
172+
count: this.routingTable.kBucketSize
173+
})
172174

173175
// split peers into d buckets evenly(ish)
174176
const peersToQuery = peers.sort(() => {

packages/kad-dht/src/routing-table/index.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { TypedEventEmitter, setMaxListeners } from 'main-event'
77
import * as utils from '../utils.js'
88
import { ClosestPeers } from './closest-peers.js'
99
import { KBucket, isLeafBucket } from './k-bucket.js'
10-
import type { Bucket, LeafBucket, Peer } from './k-bucket.js'
10+
import type { Bucket, GetClosestPeersOptions, LeafBucket, Peer } from './k-bucket.js'
1111
import type { Network } from '../network.js'
1212
import type { AbortOptions, ComponentLogger, CounterGroup, Logger, Metric, Metrics, PeerId, PeerStore, Startable, Stream } from '@libp2p/interface'
1313
import type { Ping } from '@libp2p/ping'
@@ -444,7 +444,9 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
444444
* Retrieve the closest peers to the given kadId
445445
*/
446446
closestPeer (kadId: Uint8Array): PeerId | undefined {
447-
const res = this.closestPeers(kadId, 1)
447+
const res = this.closestPeers(kadId, {
448+
count: 1
449+
})
448450

449451
if (res.length > 0) {
450452
return res[0]
@@ -456,12 +458,12 @@ export class RoutingTable extends TypedEventEmitter<RoutingTableEvents> implemen
456458
/**
457459
* Retrieve the `count`-closest peers to the given kadId
458460
*/
459-
closestPeers (kadId: Uint8Array, count = this.kBucketSize): PeerId[] {
461+
closestPeers (kadId: Uint8Array, options?: GetClosestPeersOptions): PeerId[] {
460462
if (this.kb == null) {
461463
return []
462464
}
463465

464-
return [...this.kb.closest(kadId, count)]
466+
return [...this.kb.closest(kadId, options)]
465467
}
466468

467469
/**

packages/kad-dht/src/routing-table/k-bucket.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,11 @@ export function isLeafBucket (obj: any): obj is LeafBucket {
130130
return Array.isArray(obj?.peers)
131131
}
132132

133+
export interface GetClosestPeersOptions extends AbortOptions {
134+
count?: number
135+
exclude?: PeerId[]
136+
}
137+
133138
/**
134139
* Implementation of a Kademlia DHT routing table as a prefix binary trie with
135140
* configurable prefix length, bucket split threshold and size.
@@ -318,10 +323,14 @@ export class KBucket {
318323
* @param {Uint8Array} id - Contact node id
319324
* @returns {Generator<Peer, void, undefined>} Array Maximum of n closest contacts to the node id
320325
*/
321-
* closest (id: Uint8Array, n: number = this.kBucketSize): Generator<PeerId, void, undefined> {
322-
const list = new PeerDistanceList(id, n)
326+
* closest (id: Uint8Array, options?: GetClosestPeersOptions): Generator<PeerId, void, undefined> {
327+
const list = new PeerDistanceList(id, options?.count ?? this.kBucketSize)
323328

324329
for (const peer of this.toIterable()) {
330+
if (options?.exclude?.some(p => p.equals(peer.peerId)) === true) {
331+
continue
332+
}
333+
325334
list.addWithKadId({ id: peer.peerId, multiaddrs: [] }, peer.kadId)
326335
}
327336

packages/kad-dht/src/rpc/handlers/find-node.ts

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -42,38 +42,52 @@ export class FindNodeHandler implements DHTMessageHandler {
4242
* Process `FindNode` DHT messages
4343
*/
4444
async handle (peerId: PeerId, msg: Message): Promise<Message> {
45-
this.log('incoming request from %p for peers closer to %b', peerId, msg.key)
45+
this.log('incoming request from %p for peers close to %b', peerId, msg.key)
46+
try {
47+
if (msg.key == null) {
48+
throw new InvalidMessageError('Invalid FIND_NODE message received - key was missing')
49+
}
4650

47-
if (msg.key == null) {
48-
throw new InvalidMessageError('Invalid FIND_NODE message received - key was missing')
49-
}
50-
51-
const closer: PeerInfo[] = await this.peerRouting.getCloserPeersOffline(msg.key, peerId)
51+
const closer: PeerInfo[] = await this.peerRouting.getClosestPeersOffline(msg.key, {
52+
exclude: [
53+
// never tell a peer about itself
54+
peerId,
5255

53-
if (uint8ArrayEquals(this.peerId.toMultihash().bytes, msg.key)) {
54-
closer.push({
55-
id: this.peerId,
56-
multiaddrs: this.addressManager.getAddresses().map(ma => ma.decapsulateCode(protocols('p2p').code))
56+
// do not include the server in the results
57+
this.peerId
58+
]
5759
})
58-
}
5960

60-
const response: Message = {
61-
type: MessageType.FIND_NODE,
62-
clusterLevel: msg.clusterLevel,
63-
closer: closer
64-
.map(this.peerInfoMapper)
65-
.filter(({ multiaddrs }) => multiaddrs.length)
66-
.map(peerInfo => ({
67-
id: peerInfo.id.toMultihash().bytes,
68-
multiaddrs: peerInfo.multiaddrs.map(ma => ma.bytes)
69-
})),
70-
providers: []
71-
}
61+
if (uint8ArrayEquals(this.peerId.toMultihash().bytes, msg.key)) {
62+
closer.push({
63+
id: this.peerId,
64+
multiaddrs: this.addressManager.getAddresses().map(ma => ma.decapsulateCode(protocols('p2p').code))
65+
})
66+
}
7267

73-
if (response.closer.length === 0) {
74-
this.log('could not find any peers closer to %b than %p', msg.key, peerId)
75-
}
68+
const response: Message = {
69+
type: MessageType.FIND_NODE,
70+
clusterLevel: msg.clusterLevel,
71+
closer: closer
72+
.map(this.peerInfoMapper)
73+
.filter(({ multiaddrs }) => multiaddrs.length)
74+
.map(peerInfo => ({
75+
id: peerInfo.id.toMultihash().bytes,
76+
multiaddrs: peerInfo.multiaddrs.map(ma => ma.bytes)
77+
})),
78+
providers: []
79+
}
80+
81+
if (response.closer.length === 0) {
82+
this.log('could not find any peers closer to %b for %p', msg.key, peerId)
83+
} else {
84+
this.log('found %d peers close to %b for %p', response.closer.length, msg.key, peerId)
85+
}
7686

77-
return response
87+
return response
88+
} catch (err: any) {
89+
this.log('error during finding peers closer to %b for %p - %e', msg.key, peerId, err)
90+
throw err
91+
}
7892
}
7993
}

packages/kad-dht/src/rpc/handlers/get-providers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export class GetProvidersHandler implements DHTMessageHandler {
6767

6868
return info
6969
})),
70-
this.peerRouting.getCloserPeersOffline(msg.key, peerId)
70+
this.peerRouting.getClosestPeersOffline(msg.key)
7171
])
7272

7373
const response: Message = {

packages/kad-dht/src/rpc/handlers/get-value.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export class GetValueHandler implements DHTMessageHandler {
8484

8585
const [record, closer] = await Promise.all([
8686
this._checkLocalDatastore(key),
87-
this.peerRouting.getCloserPeersOffline(key, peerId)
87+
this.peerRouting.getClosestPeersOffline(key)
8888
])
8989

9090
if (record != null) {

packages/kad-dht/test/peer-routing.spec.ts

Lines changed: 4 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,7 @@ describe('peer-routing', () => {
6565
it('should only return DHT servers', async () => {
6666
const key = Uint8Array.from([0, 1, 2, 3, 4])
6767
const [
68-
serverPeerId,
69-
requester
68+
serverPeerId
7069
] = await getSortedPeers(key)
7170

7271
const serverPeer: Peer = stubInterface<Peer>({
@@ -86,7 +85,7 @@ describe('peer-routing', () => {
8685
multiaddrs: serverPeer.addresses.map(({ multiaddr }) => multiaddr)
8786
})
8887

89-
const closer = await peerRouting.getCloserPeersOffline(key, requester.peerId)
88+
const closer = await peerRouting.getClosestPeersOffline(key)
9089

9190
expect(closer).to.have.lengthOf(1)
9291
expect(closer[0].id).to.equal(serverPeer.id)
@@ -96,8 +95,7 @@ describe('peer-routing', () => {
9695
const clientPeerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519'))
9796
const key = clientPeerId.toMultihash().bytes
9897
const [
99-
serverPeerId,
100-
requester
98+
serverPeerId
10199
] = await getSortedPeers(key)
102100

103101
const clientPeer: Peer = stubInterface<Peer>({
@@ -130,58 +128,12 @@ describe('peer-routing', () => {
130128
multiaddrs: clientPeer.addresses.map(({ multiaddr }) => multiaddr)
131129
})
132130

133-
const closer = await peerRouting.getCloserPeersOffline(key, requester.peerId)
131+
const closer = await peerRouting.getClosestPeersOffline(key)
134132

135133
expect(closer).to.have.lengthOf(2)
136134
expect(closer[0].id).to.equal(clientPeer.id)
137135
expect(closer[1].id).to.equal(serverPeer.id)
138136
})
139-
140-
// this is not in the spec
141-
it.skip('should only include peers closer than the requesting peer', async () => {
142-
const key = Uint8Array.from([0, 1, 2, 3, 4])
143-
const [
144-
closerPeerId,
145-
requester,
146-
furtherPeerId
147-
] = await getSortedPeers(key)
148-
149-
const closerPeer: Peer = stubInterface<Peer>({
150-
id: closerPeerId.peerId,
151-
addresses: [{
152-
isCertified: true,
153-
multiaddr: multiaddr('/ip4/127.0.0.1/tcp/4001')
154-
}]
155-
})
156-
const furtherPeer: Peer = stubInterface<Peer>({
157-
id: furtherPeerId.peerId,
158-
addresses: [{
159-
isCertified: true,
160-
multiaddr: multiaddr('/ip4/127.0.0.1/tcp/4002')
161-
}]
162-
})
163-
164-
init.routingTable.closestPeers.returns([
165-
closerPeer.id,
166-
furtherPeer.id
167-
])
168-
169-
components.peerStore.get.withArgs(closerPeer.id).resolves(closerPeer)
170-
components.peerStore.getInfo.withArgs(closerPeer.id).resolves({
171-
id: closerPeer.id,
172-
multiaddrs: closerPeer.addresses.map(({ multiaddr }) => multiaddr)
173-
})
174-
components.peerStore.get.withArgs(furtherPeer.id).resolves(furtherPeer)
175-
components.peerStore.getInfo.withArgs(furtherPeer.id).resolves({
176-
id: furtherPeer.id,
177-
multiaddrs: furtherPeer.addresses.map(({ multiaddr }) => multiaddr)
178-
})
179-
180-
const closer = await peerRouting.getCloserPeersOffline(key, requester.peerId)
181-
182-
expect(closer).to.have.lengthOf(1)
183-
expect(closer[0].id).to.equal(closerPeer.id)
184-
})
185137
})
186138
})
187139

packages/kad-dht/test/routing-table.spec.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,9 @@ describe('Routing Table', () => {
210210
const id = ids[random(ids.length - 1)]
211211
const key = await kadUtils.convertPeerId(id.peerId)
212212

213-
expect(table.closestPeers(key, 5).length)
213+
expect(table.closestPeers(key, {
214+
count: 5
215+
}).length)
214216
.to.be.above(0)
215217
})
216218
)
@@ -291,7 +293,9 @@ describe('Routing Table', () => {
291293
}))
292294

293295
const key = await kadUtils.convertPeerId(peers[2].peerId)
294-
expect(table.closestPeers(key, 10)).to.have.length(10)
296+
expect(table.closestPeers(key, {
297+
count: 10
298+
})).to.have.length(10)
295299
await expect(table.find(peers[5].peerId)).to.eventually.be.ok()
296300
expect(table.size).to.equal(10)
297301

@@ -329,7 +333,9 @@ describe('Routing Table', () => {
329333
await Promise.all(peers.map(async (peer) => { await table.add(peer.peerId) }))
330334

331335
const key = await kadUtils.convertPeerId(peers[2].peerId)
332-
expect(table.closestPeers(key, 15)).to.have.length(15)
336+
expect(table.closestPeers(key, {
337+
count: 15
338+
})).to.have.length(15)
333339
})
334340

335341
it('favours old peers that respond to pings', async () => {

0 commit comments

Comments
 (0)