@@ -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 ] = 0x 80 'u8 shr (n mod 8 )
26+ return @ buf
27+
1528suite " 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