Skip to content

Commit 93dfec1

Browse files
committed
fix: ensure refresh of nearby buckets
1 parent 7c57a8e commit 93dfec1

4 files changed

Lines changed: 70 additions & 12 deletions

File tree

.github/workflows/test_multiformat_exts.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ jobs:
5757
shell: ${{ needs.pick-platform.outputs.shell }}
5858
steps:
5959
- name: selected platform
60+
# msys2 is only installed by Setup Nim below, so it cannot be the shell here.
61+
shell: bash
6062
run: echo "(${{ needs.pick-platform.outputs.os }}-${{ needs.pick-platform.outputs.cpu }} / ${{ needs.pick-platform.outputs.cc }} / Nim ${{ needs.pick-platform.outputs.nim_ref }} / ${{ needs.pick-platform.outputs.nim_mm }})"
6163

6264
- name: Checkout

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: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,3 +237,15 @@ proc randomKey*(bucket: Bucket, rng: Rng): Opt[Key] =
237237
proc(e: NodeEntry): Key =
238238
e.nodeId
239239
)
240+
241+
proc refreshTarget*(rtable: RoutingTable, bucketIndex: int, rng: Rng): Opt[Key] =
242+
## Key to run a findNode against in order to refresh `bucketIndex`.
243+
let random = randomKeyInBucket(rtable, bucketIndex, rng)
244+
if random.isSome():
245+
return random
246+
247+
# Buckets near self need a shared prefix too long to draw at random. A peer
248+
# the bucket already holds has that prefix by construction.
249+
if bucketIndex notin 0 ..< rtable.buckets.len:
250+
return Opt.none(Key)
251+
rtable.buckets[bucketIndex].randomKey(rng)

tests/libp2p/kademlia/test_routing_table.nim

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,25 @@ 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

1831
proc keyInBucket(rt: RoutingTable, bucket: int): Key =
1932
randomKeyInBucket(rt, bucket, rng()).expect("bucket is reachable")
2033

21-
proc keyWithPrefix(leadingZeros: int): Key =
22-
## Key whose distance from `testKey(0)` has exactly `leadingZeros` leading zeros.
23-
var buf: array[IdLength, byte]
24-
buf[leadingZeros div 8] = 0b1000_0000'u8 shr (leadingZeros mod 8)
25-
return @buf
26-
2734
test "inserts single key in correct bucket":
2835
let selfId = testKey(0)
2936
var rt = RoutingTable.new(selfId)
@@ -184,7 +191,7 @@ suite "KadDHT Routing Table":
184191
RoutingTable.new(selfId, RoutingTableConfig.new(hasher = Opt.some(noOpHasher)))
185192

186193
for lz in 0 .. 8:
187-
check rt.bucketIndex(keyWithPrefix(lz)) == lz
194+
check rt.bucketIndex(keyWithLeadingZeros(lz)) == lz
188195

189196
test "small maxBuckets keeps prefix resolution and saturates the last bucket":
190197
let selfId = testKey(0)
@@ -196,11 +203,11 @@ suite "KadDHT Routing Table":
196203

197204
# Prefix lengths below the cap each get their own bucket.
198205
for lz in 0 ..< MaxBuckets - 1:
199-
check rt.bucketIndex(keyWithPrefix(lz)) == lz
206+
check rt.bucketIndex(keyWithLeadingZeros(lz)) == lz
200207

201208
# Deeper prefixes all fall into the last bucket.
202209
for lz in MaxBuckets - 1 .. MaxBuckets + 8:
203-
check rt.bucketIndex(keyWithPrefix(lz)) == MaxBuckets - 1
210+
check rt.bucketIndex(keyWithLeadingZeros(lz)) == MaxBuckets - 1
204211

205212
test "randomKeyInBucket targets the bucket under the default (hashing) hasher":
206213
# The bucket of a key is decided by its *hash*, so a target built by
@@ -232,6 +239,44 @@ suite "KadDHT Routing Table":
232239
# The last bucket needs 15 shared prefix bits — beyond the cap.
233240
check randomKeyInBucket(rt, MaxBuckets - 1, rng()).isNone()
234241

242+
test "refreshTarget falls back to a peer of a bucket too near to search for":
243+
# Buckets past the cap share too long a prefix with selfId to draw at random.
244+
const NearBucket = MaxRefreshLeadingZeros + 1
245+
let selfId = testKey(0)
246+
var rt = RoutingTable.new(selfId, RoutingTableConfig.new())
247+
248+
var inserted: seq[Key]
249+
while inserted.len < 3:
250+
let key = randomTestKey()
251+
if rt.bucketIndex(key) != NearBucket:
252+
continue
253+
discard rt.insert(key)
254+
inserted.add(key)
255+
256+
check randomKeyInBucket(rt, NearBucket, rng()).isNone()
257+
258+
let target = rt.refreshTarget(NearBucket, rng()).expect("bucket holds peers")
259+
check:
260+
target in inserted
261+
rt.bucketIndex(target) == NearBucket
262+
263+
test "refreshTarget returns none for an unreachable empty bucket":
264+
let selfId = testKey(0)
265+
var rt = RoutingTable.new(selfId, RoutingTableConfig.new())
266+
267+
check rt.refreshTarget(MaxRefreshLeadingZeros + 1, rng()).isNone()
268+
269+
test "refreshTarget prefers a random key when the bucket is reachable":
270+
let selfId = testKey(0)
271+
var rt = RoutingTable.new(selfId, RoutingTableConfig.new())
272+
let peer = rt.keyInBucket(TargetBucket)
273+
discard rt.insert(peer)
274+
275+
let target = rt.refreshTarget(TargetBucket, rng()).expect("bucket is reachable")
276+
check:
277+
target != peer
278+
rt.bucketIndex(target) == TargetBucket
279+
235280
test "randomKey returns none for empty bucket":
236281
var bucket: Bucket
237282
check randomKey(bucket, rng()).isNone()

0 commit comments

Comments
 (0)