Skip to content

Commit 502a36a

Browse files
SionoiStinniaru3005github-actions[bot]
authored
fix(service-disco): wait time cap (#2834)
Co-authored-by: Arunima Chaudhuri <tinniarunima66@gmail.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 460a759 commit 502a36a

2 files changed

Lines changed: 132 additions & 5 deletions

File tree

libp2p/protocols/service_discovery/registrar.nim

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ proc pruneExpiredEntries[K](
101101
bounds.del(k)
102102

103103
proc pruneExpiredAds*(registrar: Registrar, advertExpiry: Duration) =
104-
let now = Moment.now()
104+
#Always use seconds granularity
105+
let now = Moment.init(Moment.now().epochSeconds, Second)
105106

106107
var expiredCount = 0
107108

@@ -156,8 +157,7 @@ proc waitingTime*(
156157
(serviceSim + discoConfig.ipSimCoefficient * ipSim + discoConfig.safetyParam)
157158

158159
# Bound & Quantize W
159-
w = max(0.0, w)
160-
w = min(w, float64(uint32.high))
160+
w = w.clamp(0.0, float64(uint32.high))
161161
w = round(w)
162162

163163
var waitDuration = w.int64.secs
@@ -458,7 +458,8 @@ proc registration*(disco: ServiceDiscovery, peerId: PeerId, inMsg: Message): Mes
458458

459459
return msg
460460

461-
let now = Moment.now()
461+
#Always use seconds granularity
462+
let now = Moment.init(Moment.now().epochSeconds, Second)
462463

463464
let ticketOpt = disco.isValidTicket(regMsg, now).valueOr:
464465
error "invalid ticket", error
@@ -486,6 +487,8 @@ proc registration*(disco: ServiceDiscovery, peerId: PeerId, inMsg: Message): Mes
486487

487488
return msg
488489

490+
tWait = min(disco.discoConfig.advertExpiry, tWait)
491+
489492
disco.registrar.updateLowerBounds(serviceId, ad, tWait, now)
490493

491494
var ticket = Ticket(

tests/libp2p/service_discovery/test_registrar.nim

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,87 @@ suite "Service Discovery Registrar - Waiting Time Calculation":
226226
# ipSim ≈ 0.97 for same /24; w should be in the hundreds of seconds
227227
check w > 500.seconds
228228

229+
suite "Service Discovery Registrar - advertExpiry cap":
230+
test "registration caps the offered tWaitFor at advertExpiry":
231+
let advertExpiry = 100.secs
232+
let conf = ServiceDiscoveryConfig.new(
233+
advertExpiry = advertExpiry, safetyParam = 1.0, advertCacheCap = 10
234+
)
235+
let disco = setupServiceDiscoveryNode(discoConfig = conf)
236+
let serviceName = "service"
237+
let serviceId = serviceName.hashServiceId()
238+
let advertiserKey = PrivateKey.random(rng()).get()
239+
let advertiserId = PeerId.init(advertiserKey).get()
240+
let adBytes = makeAdvertisement(serviceName, advertiserKey).encode().get()
241+
242+
# Saturate the cache: occupancy pins at 100.0, so w = 100*100*1.0 = 10000s.
243+
for i in 0 ..< 10:
244+
disco.registrar.cacheTimestamps[(peerId: randomPeerId(), seqNo: uint64(i))] =
245+
Moment.now()
246+
247+
let inMsg = kadprotobuf.Message(
248+
msgType: kadprotobuf.MessageType.register,
249+
key: serviceId,
250+
register: Opt.some(
251+
kadprotobuf.RegisterMessage(
252+
advertisement: adBytes,
253+
status: Opt.none(kadprotobuf.RegistrationStatus),
254+
ticket: Opt.none(Ticket),
255+
)
256+
),
257+
)
258+
let reply = disco.registration(advertiserId, inMsg).register.get()
259+
260+
check reply.status.get() == kadprotobuf.RegistrationStatus.Wait
261+
check reply.ticket.get().tWaitFor.get() == advertExpiry
262+
263+
test "sustained overload keeps offering advertExpiry-length waits across retries":
264+
let advertExpiry = 100.secs
265+
let registrationWindow = 10.secs
266+
let conf = ServiceDiscoveryConfig.new(
267+
advertExpiry = advertExpiry,
268+
safetyParam = 1.0,
269+
advertCacheCap = 10,
270+
registrationWindow = registrationWindow,
271+
)
272+
let disco = setupServiceDiscoveryNode(discoConfig = conf)
273+
let serviceName = "service"
274+
let serviceId = serviceName.hashServiceId()
275+
let advertiserKey = PrivateKey.random(rng()).get()
276+
let advertiserId = PeerId.init(advertiserKey).get()
277+
let adBytes = makeAdvertisement(serviceName, advertiserKey).encode().get()
278+
279+
for i in 0 ..< 10:
280+
disco.registrar.cacheTimestamps[(peerId: randomPeerId(), seqNo: uint64(i))] =
281+
Moment.now()
282+
283+
let firstAttemptTime =
284+
Moment.init((Moment.now() - advertExpiry).epochSeconds, Second)
285+
var retryTicket = Ticket(
286+
advertisement: adBytes,
287+
tInit: firstAttemptTime,
288+
tMod: firstAttemptTime,
289+
tWaitFor: advertExpiry,
290+
signature: Opt.none(seq[byte]),
291+
)
292+
check retryTicket.sign(disco.switch.peerInfo.privateKey).isOk()
293+
294+
let inMsg = kadprotobuf.Message(
295+
msgType: kadprotobuf.MessageType.register,
296+
key: serviceId,
297+
register: Opt.some(
298+
kadprotobuf.RegisterMessage(
299+
advertisement: adBytes,
300+
status: Opt.none(kadprotobuf.RegistrationStatus),
301+
ticket: Opt.some(retryTicket),
302+
)
303+
),
304+
)
305+
let reply = disco.registration(advertiserId, inMsg).register.get()
306+
307+
check reply.status.get() == kadprotobuf.RegistrationStatus.Wait
308+
check reply.ticket.get().tWaitFor.get() == advertExpiry
309+
229310
suite "Service Discovery Registrar - Lower Bound Enforcement":
230311
test "waitingTime enforces service lower bound when exists":
231312
let registrar = Registrar.new()
@@ -278,7 +359,7 @@ suite "Service Discovery Registrar - Lower Bound Enforcement":
278359

279360
test "waitingTime uses most restrictive lower bound":
280361
let registrar = Registrar.new()
281-
let discoConfig = ServiceDiscoveryConfig.new()
362+
let discoConfig = ServiceDiscoveryConfig.new(advertExpiry = 2500.secs)
282363
let serviceId = makeServiceId()
283364
let ip1 = "192.168.1.1"
284365
let ip2 = "10.0.0.1"
@@ -1291,6 +1372,49 @@ suite "Service Discovery Registrar - registration response":
12911372
ticket.tWaitFor.get() > ZeroDuration
12921373
ticket.verify(registrarPubKey)
12931374

1375+
test "registration quantizes now to whole-second granularity":
1376+
# `now` is read once in registration() and truncated to whole seconds, so
1377+
# every time value it emits — the ticket's tInit/tMod and the recorded
1378+
# service timestamp — must land exactly on a second boundary. Using the
1379+
# raw Moment.now() (sub-second nanoseconds) would break these checks.
1380+
let config = ServiceDiscoveryConfig.new(safetyParam = 1.0) # forces a Wait
1381+
let disco = setupServiceDiscoveryNode(discoConfig = config)
1382+
let serviceName = "service"
1383+
let serviceId = serviceName.hashServiceId()
1384+
let ad = makeAdvertisement(serviceName)
1385+
let adBytes = ad.encode().get()
1386+
let advertiserId = ad.data.peerId
1387+
1388+
let inMsg = kadprotobuf.Message(
1389+
msgType: kadprotobuf.MessageType.register,
1390+
key: serviceId,
1391+
register: Opt.some(
1392+
kadprotobuf.RegisterMessage(
1393+
advertisement: adBytes,
1394+
status: Opt.none(kadprotobuf.RegistrationStatus),
1395+
ticket: Opt.none(Ticket),
1396+
)
1397+
),
1398+
)
1399+
1400+
let reply = disco.registration(advertiserId, inMsg).register.get()
1401+
1402+
check reply.status.get() == kadprotobuf.RegistrationStatus.Wait
1403+
check reply.ticket.isSome()
1404+
1405+
# Reconstructing a Moment from its own epochSeconds is a fixed point only
1406+
# when the moment already has no sub-second part.
1407+
let ticket = reply.ticket.get()
1408+
let tInit = ticket.tInit.get()
1409+
let tMod = ticket.tMod.get()
1410+
check tInit == Moment.init(tInit.epochSeconds, Second)
1411+
check tMod == Moment.init(tMod.epochSeconds, Second)
1412+
1413+
# The recorded service timestamp mirrors the same truncated `now`.
1414+
check serviceId in disco.registrar.timestampService
1415+
let ts = disco.registrar.timestampService[serviceId]
1416+
check ts == Moment.init(ts.epochSeconds, Second)
1417+
12941418
test "retrying with a valid ticket inside the window caches the ad":
12951419
let conf = ServiceDiscoveryConfig.new(registrationWindow = 10.secs)
12961420
let disco = setupServiceDiscoveryNode(discoConfig = conf)

0 commit comments

Comments
 (0)