Skip to content

Commit f4db68e

Browse files
committed
fix: ensure refresh of nearby buckets
1 parent 8d48215 commit f4db68e

3 files changed

Lines changed: 102 additions & 6 deletions

File tree

libp2p/protocols/kademlia.nim

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,10 @@ proc refreshTable*(
3636
if not (forceRefresh or bucket.isStale()):
3737
continue
3838

39-
# Buckets too near to target are covered by the findNode on selfId above.
40-
let randomKey = randomKeyInBucket(rtable, i, kad.rng).valueOr:
39+
let target = rtable.refreshTarget(i, kad.rng).valueOr:
4140
trace "No refresh target for bucket", bucket = i
4241
continue
43-
discard await kad.findNode(randomKey, rtable)
42+
discard await kad.findNode(target, rtable)
4443

4544
proc bootstrap*(
4645
kad: KadDHT, forceRefresh = false

libp2p/protocols/kademlia/routing_table.nim

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# SPDX-License-Identifier: Apache-2.0 OR MIT
22
# Copyright (c) Status Research & Development GmbH
33

4-
import algorithm, sequtils
4+
import algorithm, sequtils, math
55
import chronos, chronicles, results
66
import ./types
77
import ./kademlia_metrics
@@ -57,12 +57,14 @@ func bucketIndexFor(rtable: RoutingTable, selfHash: Key, key: Key): int =
5757
func bucketIndex*(rtable: RoutingTable, key: Key): int =
5858
rtable.bucketIndexFor(rtable.selfHash(), key)
5959

60-
func minLeadingZeros(rtable: RoutingTable, bucketIndex: int): int =
60+
func minLeadingZeros*(rtable: RoutingTable, bucketIndex: int): int =
6161
## Fewest leading zeros in the XOR distance that lands a key in `bucketIndex`.
62+
## Inverts `bucketIndexFor`, which floors; the inverse must round up, or it
63+
## names an lz that falls into the bucket below when maxBuckets ∤ 256.
6264
if rtable.config.maxBuckets <= 1:
6365
return 0
6466

65-
bucketIndex * 256 div rtable.config.maxBuckets
67+
ceilDiv(bucketIndex * 256, rtable.config.maxBuckets)
6668

6769
proc peerIndexInBucket(bucket: Bucket, nodeId: Key): Opt[int] =
6870
for i, p in bucket.peers:
@@ -243,3 +245,15 @@ proc randomKey*(bucket: Bucket, rng: Rng): Opt[Key] =
243245
proc(e: NodeEntry): Key =
244246
e.nodeId
245247
)
248+
249+
proc refreshTarget*(rtable: RoutingTable, bucketIndex: int, rng: Rng): Opt[Key] =
250+
## Key to run a findNode against in order to refresh `bucketIndex`.
251+
let random = randomKeyInBucket(rtable, bucketIndex, rng)
252+
if random.isSome():
253+
return random
254+
255+
# Buckets near self need a shared prefix too long to draw at random. A peer
256+
# the bucket already holds has that prefix by construction.
257+
if bucketIndex notin 0 ..< rtable.buckets.len:
258+
return Opt.none(Key)
259+
rtable.buckets[bucketIndex].randomKey(rng)

tests/libp2p/kademlia/test_routing_table.nim

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,19 @@ proc testKey*(x: byte): Key =
1212
buf[31] = x
1313
return @buf
1414

15+
proc randomTestKey(): Key =
16+
var buf = newSeqUninit[byte](IdLength)
17+
rng().generate(buf)
18+
buf
19+
20+
proc keyWithLeadingZeros(n: int): Key =
21+
## Key whose XOR distance to an all-zero selfId under `noOpHasher` has
22+
## exactly `n` leading zero bits.
23+
doAssert n < IdLength * 8, "an all-zero key has no first set bit"
24+
var buf: array[IdLength, byte]
25+
buf[n div 8] = 0x80'u8 shr (n mod 8)
26+
return @buf
27+
1528
suite "KadDHT Routing Table":
1629
const TargetBucket = 6
1730

@@ -162,6 +175,38 @@ suite "KadDHT Routing Table":
162175
bucket.peers = @[NodeEntry(nodeId: testKey(1), lastSeen: Moment.now())]
163176
check isStale(bucket) == false
164177

178+
test "minLeadingZeros names the first leading-zero count that lands in a bucket":
179+
let selfId = testKey(0)
180+
for maxBuckets in [256, 16, 100, 3]:
181+
var rt = RoutingTable.new(
182+
selfId,
183+
RoutingTableConfig.new(hasher = Opt.some(noOpHasher), maxBuckets = maxBuckets),
184+
)
185+
186+
for i in 0 ..< maxBuckets:
187+
let lz = rt.minLeadingZeros(i)
188+
check rt.bucketIndex(keyWithLeadingZeros(lz)) == i
189+
# A key one bit short of the boundary belongs to a lower bucket.
190+
if i > 0:
191+
check rt.bucketIndex(keyWithLeadingZeros(lz - 1)) < i
192+
193+
test "minLeadingZeros rounds up when maxBuckets does not divide 256":
194+
let selfId = testKey(0)
195+
196+
proc table(maxBuckets: int): RoutingTable =
197+
RoutingTable.new(
198+
selfId,
199+
RoutingTableConfig.new(hasher = Opt.some(noOpHasher), maxBuckets = maxBuckets),
200+
)
201+
202+
check:
203+
table(256).minLeadingZeros(12) == 12
204+
# A 16-way split gives each bucket 16 bits of prefix.
205+
table(16).minLeadingZeros(1) == 16
206+
table(16).minLeadingZeros(12) == 192
207+
# 1 * 256 / 100 is 2.56, and lz 2 still lands in bucket 0.
208+
table(100).minLeadingZeros(1) == 3
209+
165210
test "randomKeyInBucket returns id at correct distance":
166211
let selfId = testKey(0)
167212
var rt =
@@ -199,6 +244,44 @@ suite "KadDHT Routing Table":
199244
# Bucket 1 of a 16-way split needs 16 shared prefix bits — beyond the cap.
200245
check randomKeyInBucket(rt, 1, rng()).isNone()
201246

247+
test "refreshTarget falls back to a peer of a bucket too near to search for":
248+
# Service-discovery tables split the id space into few, very wide buckets,
249+
# so every bucket but the first is out of reach of a random draw.
250+
let selfId = testKey(0)
251+
var rt = RoutingTable.new(selfId, RoutingTableConfig.new(maxBuckets = 16))
252+
253+
var inserted: seq[Key]
254+
while inserted.len < 3:
255+
let key = randomTestKey()
256+
if rt.bucketIndex(key) != 1:
257+
continue
258+
discard rt.insert(key)
259+
inserted.add(key)
260+
261+
check randomKeyInBucket(rt, 1, rng()).isNone()
262+
263+
let target = rt.refreshTarget(1, rng()).expect("bucket 1 holds peers")
264+
check:
265+
target in inserted
266+
rt.bucketIndex(target) == 1
267+
268+
test "refreshTarget returns none for an unreachable empty bucket":
269+
let selfId = testKey(0)
270+
var rt = RoutingTable.new(selfId, RoutingTableConfig.new(maxBuckets = 16))
271+
272+
check rt.refreshTarget(1, rng()).isNone()
273+
274+
test "refreshTarget prefers a random key when the bucket is reachable":
275+
let selfId = testKey(0)
276+
var rt = RoutingTable.new(selfId, RoutingTableConfig.new())
277+
let peer = rt.keyInBucket(TargetBucket)
278+
discard rt.insert(peer)
279+
280+
let target = rt.refreshTarget(TargetBucket, rng()).expect("bucket is reachable")
281+
check:
282+
target != peer
283+
rt.bucketIndex(target) == TargetBucket
284+
202285
test "randomKey returns none for empty bucket":
203286
var bucket: Bucket
204287
check randomKey(bucket, rng()).isNone()

0 commit comments

Comments
 (0)