Files
nim-libp2p/tests/discovery/testrendezvous.nim
2025-09-03 18:01:23 +01:00

472 lines
17 KiB
Nim

{.used.}
# Nim-Libp2p
# Copyright (c) 2023 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
# at your option.
# This file may not be copied, modified, or distributed except according to
# those terms.
import sequtils, strutils
import chronos
import ../../libp2p/[protocols/rendezvous, switch]
import ../../libp2p/discovery/discoverymngr
import ../../libp2p/utils/offsettedseq
import ../helpers
import ./utils
suite "RendezVous":
teardown:
checkTrackers()
asyncTest "Request locally returns 0 for empty namespace":
let (nodes, rdvs) = setupNodes(1)
nodes.startAndDeferStop()
const namespace = ""
check rdvs[0].requestLocally(namespace).len == 0
asyncTest "Request locally returns registered peers":
let (nodes, rdvs) = setupNodes(1)
nodes.startAndDeferStop()
const namespace = "foo"
await rdvs[0].advertise(namespace)
let peerRecords = rdvs[0].requestLocally(namespace)
check:
peerRecords.len == 1
peerRecords[0] == nodes[0].peerInfo.signedPeerRecord.data
asyncTest "Unsubscribe Locally removes registered peer":
let (nodes, rdvs) = setupNodes(1)
nodes.startAndDeferStop()
const namespace = "foo"
await rdvs[0].advertise(namespace)
check rdvs[0].requestLocally(namespace).len == 1
rdvs[0].unsubscribeLocally(namespace)
check rdvs[0].requestLocally(namespace).len == 0
asyncTest "Request returns 0 for empty namespace from remote":
let (rendezvousNode, peerNodes, peerRdvs, _) = setupRendezvousNodeWithPeerNodes(1)
(rendezvousNode & peerNodes).startAndDeferStop()
await connectNodes(peerNodes[0], rendezvousNode)
const namespace = "empty"
check (await peerRdvs[0].request(Opt.some(namespace))).len == 0
asyncTest "Request returns registered peers from remote":
let (rendezvousNode, peerNodes, peerRdvs, _) = setupRendezvousNodeWithPeerNodes(1)
(rendezvousNode & peerNodes).startAndDeferStop()
await connectNodes(peerNodes[0], rendezvousNode)
const namespace = "foo"
await peerRdvs[0].advertise(namespace)
let peerRecords = await peerRdvs[0].request(Opt.some(namespace))
check:
peerRecords.len == 1
peerRecords[0] == peerNodes[0].peerInfo.signedPeerRecord.data
asyncTest "Unsubscribe removes registered peer from remote":
let (rendezvousNode, peerNodes, peerRdvs, _) = setupRendezvousNodeWithPeerNodes(1)
(rendezvousNode & peerNodes).startAndDeferStop()
await connectNodes(peerNodes[0], rendezvousNode)
const namespace = "foo"
await peerRdvs[0].advertise(namespace)
check (await peerRdvs[0].request(Opt.some(namespace))).len == 1
await peerRdvs[0].unsubscribe(namespace)
check (await peerRdvs[0].request(Opt.some(namespace))).len == 0
asyncTest "Consecutive requests with namespace returns peers with pagination":
let (rendezvousNode, peerNodes, peerRdvs, _) = setupRendezvousNodeWithPeerNodes(11)
(rendezvousNode & peerNodes).startAndDeferStop()
await connectNodesToRendezvousNode(peerNodes, rendezvousNode)
const namespace = "foo"
await allFutures(peerRdvs.mapIt(it.advertise(namespace)))
var data = peerNodes.mapIt(it.peerInfo.signedPeerRecord.data)
var peerRecords = await peerRdvs[0].request(Opt.some(namespace), 5)
check:
peerRecords.len == 5
peerRecords.allIt(it in data)
data.keepItIf(it notin peerRecords)
peerRecords = await peerRdvs[0].request(Opt.some(namespace))
check:
peerRecords.len == 6
peerRecords.allIt(it in data)
check (await peerRdvs[0].request(Opt.some(namespace))).len == 0
asyncTest "Request without namespace returns all registered peers":
let (rendezvousNode, peerNodes, peerRdvs, _) = setupRendezvousNodeWithPeerNodes(10)
(rendezvousNode & peerNodes).startAndDeferStop()
await connectNodesToRendezvousNode(peerNodes, rendezvousNode)
const namespaceFoo = "foo"
const namespaceBar = "Bar"
await allFutures(peerRdvs[0 ..< 5].mapIt(it.advertise(namespaceFoo)))
await allFutures(peerRdvs[5 ..< 10].mapIt(it.advertise(namespaceBar)))
check (await peerRdvs[0].request()).len == 10
check (await peerRdvs[0].request(Opt.none(string))).len == 10
asyncTest "Consecutive requests with namespace keep cookie and retun only new peers":
let (rendezvousNode, peerNodes, peerRdvs, _) = setupRendezvousNodeWithPeerNodes(2)
(rendezvousNode & peerNodes).startAndDeferStop()
await connectNodesToRendezvousNode(peerNodes, rendezvousNode)
let
rdv0 = peerRdvs[0]
rdv1 = peerRdvs[1]
const namespace = "foo"
await rdv0.advertise(namespace)
discard await rdv0.request(Opt.some(namespace))
await rdv1.advertise(namespace)
let peerRecords = await rdv0.request(Opt.some(namespace))
check:
peerRecords.len == 1
peerRecords[0] == peerNodes[1].peerInfo.signedPeerRecord.data
asyncTest "Request with namespace pagination with multiple namespaces":
let (rendezvousNode, peerNodes, peerRdvs, _) = setupRendezvousNodeWithPeerNodes(30)
(rendezvousNode & peerNodes).startAndDeferStop()
await connectNodesToRendezvousNode(peerNodes, rendezvousNode)
let rdv = peerRdvs[0]
# Register peers in different namespaces in mixed order
const
namespaceFoo = "foo"
namespaceBar = "bar"
await allFutures(peerRdvs[0 ..< 5].mapIt(it.advertise(namespaceFoo)))
await allFutures(peerRdvs[5 ..< 10].mapIt(it.advertise(namespaceBar)))
await allFutures(peerRdvs[10 ..< 15].mapIt(it.advertise(namespaceFoo)))
await allFutures(peerRdvs[15 ..< 20].mapIt(it.advertise(namespaceBar)))
var fooRecords = peerNodes[0 ..< 5].concat(peerNodes[10 ..< 15]).mapIt(
it.peerInfo.signedPeerRecord.data
)
var barRecords = peerNodes[5 ..< 10].concat(peerNodes[15 ..< 20]).mapIt(
it.peerInfo.signedPeerRecord.data
)
# Foo Page 1 with limit
var peerRecords = await rdv.request(Opt.some(namespaceFoo), 2)
check:
peerRecords.len == 2
peerRecords.allIt(it in fooRecords)
fooRecords.keepItIf(it notin peerRecords)
# Foo Page 2 with limit
peerRecords = await rdv.request(Opt.some(namespaceFoo), 5)
check:
peerRecords.len == 5
peerRecords.allIt(it in fooRecords)
fooRecords.keepItIf(it notin peerRecords)
# Foo Page 3 with the rest
peerRecords = await rdv.request(Opt.some(namespaceFoo))
check:
peerRecords.len == 3
peerRecords.allIt(it in fooRecords)
fooRecords.keepItIf(it notin peerRecords)
check fooRecords.len == 0
# Foo Page 4 empty
peerRecords = await rdv.request(Opt.some(namespaceFoo))
check peerRecords.len == 0
# Bar Page 1 with all
peerRecords = await rdv.request(Opt.some(namespaceBar), 30)
check:
peerRecords.len == 10
peerRecords.allIt(it in barRecords)
barRecords.keepItIf(it notin peerRecords)
check barRecords.len == 0
# Register new peers
await allFutures(peerRdvs[20 ..< 25].mapIt(it.advertise(namespaceFoo)))
await allFutures(peerRdvs[25 ..< 30].mapIt(it.advertise(namespaceBar)))
# Foo Page 5 only new peers
peerRecords = await rdv.request(Opt.some(namespaceFoo))
check:
peerRecords.len == 5
peerRecords.allIt(
it in peerNodes[20 ..< 25].mapIt(it.peerInfo.signedPeerRecord.data)
)
# Bar Page 2 only new peers
peerRecords = await rdv.request(Opt.some(namespaceBar))
check:
peerRecords.len == 5
peerRecords.allIt(
it in peerNodes[25 ..< 30].mapIt(it.peerInfo.signedPeerRecord.data)
)
# All records
peerRecords = await rdv.request(Opt.none(string))
check peerRecords.len == 30
asyncTest "Request with namespace with expired peers":
let (rendezvousNode, peerNodes, peerRdvs, rdv) =
setupRendezvousNodeWithPeerNodes(20)
(rendezvousNode & peerNodes).startAndDeferStop()
await connectNodesToRendezvousNode(peerNodes, rendezvousNode)
# Advertise peers
const
namespaceFoo = "foo"
namespaceBar = "bar"
await allFutures(peerRdvs[0 ..< 5].mapIt(it.advertise(namespaceFoo)))
await allFutures(peerRdvs[5 ..< 10].mapIt(it.advertise(namespaceBar)))
check:
(await peerRdvs[0].request(Opt.some(namespaceFoo))).len == 5
(await peerRdvs[0].request(Opt.some(namespaceBar))).len == 5
# Overwrite register timeout loop interval
discard rdv.deletesRegister(1.seconds)
# Overwrite expiration times
let now = Moment.now()
for reg in rdv.registered.s.mitems:
reg.expiration = now
# Wait for the deletion
checkUntilTimeout:
rdv.registered.offset == 10
rdv.registered.s.len == 0
(await peerRdvs[0].request(Opt.some(namespaceFoo))).len == 0
(await peerRdvs[0].request(Opt.some(namespaceBar))).len == 0
# Advertise new peers
await allFutures(peerRdvs[10 ..< 15].mapIt(it.advertise(namespaceFoo)))
await allFutures(peerRdvs[15 ..< 20].mapIt(it.advertise(namespaceBar)))
check:
rdv.registered.offset == 10
rdv.registered.s.len == 10
(await peerRdvs[0].request(Opt.some(namespaceFoo))).len == 5
(await peerRdvs[0].request(Opt.some(namespaceBar))).len == 5
asyncTest "Cookie offset is reset to end (returns empty) then new peers are discoverable":
let (rendezvousNode, peerNodes, peerRdvs, rdv) = setupRendezvousNodeWithPeerNodes(3)
(rendezvousNode & peerNodes).startAndDeferStop()
await connectNodesToRendezvousNode(peerNodes, rendezvousNode)
const namespace = "foo"
# Advertise two peers initially
await allFutures(peerRdvs[0 ..< 2].mapIt(it.advertise(namespace)))
# Build and inject overflow cookie: offset past current high()+1
let offset = (rdv.registered.high + 1000).uint64
let cookie = buildProtobufCookie(offset, namespace)
peerRdvs[0].injectCookieForPeer(rendezvousNode.peerInfo.peerId, namespace, cookie)
# First request should return empty due to clamping to high()+1
check (await peerRdvs[0].request(Opt.some(namespace))).len == 0
# Advertise a new peer, next request should return only the new one
await peerRdvs[2].advertise(namespace)
let peerRecords = await peerRdvs[0].request(Opt.some(namespace))
check:
peerRecords.len == 1
peerRecords[0] == peerNodes[2].peerInfo.signedPeerRecord.data
asyncTest "Cookie offset is reset to low after flush (returns current entries)":
let (rendezvousNode, peerNodes, peerRdvs, rdv) = setupRendezvousNodeWithPeerNodes(8)
(rendezvousNode & peerNodes).startAndDeferStop()
await connectNodesToRendezvousNode(peerNodes, rendezvousNode)
const namespace = "foo"
# Advertise 4 peers in namespace
await allFutures(peerRdvs[0 ..< 4].mapIt(it.advertise(namespace)))
# Expire all and flush to advance registered.offset
discard rdv.deletesRegister(1.seconds)
let now = Moment.now()
for reg in rdv.registered.s.mitems:
reg.expiration = now
checkUntilTimeout:
rdv.registered.s.len == 0
rdv.registered.offset == 4
# Advertise 4 new peers
await allFutures(peerRdvs[4 ..< 8].mapIt(it.advertise(namespace)))
# Build and inject underflow cookie: offset behind current low
let offset = 0'u64
let cookie = buildProtobufCookie(offset, namespace)
peerRdvs[0].injectCookieForPeer(rendezvousNode.peerInfo.peerId, namespace, cookie)
check (await peerRdvs[0].request(Opt.some(namespace))).len == 4
asyncTest "Cookie namespace mismatch resets to low (returns peers despite offset)":
let (rendezvousNode, peerNodes, peerRdvs, rdv) = setupRendezvousNodeWithPeerNodes(3)
(rendezvousNode & peerNodes).startAndDeferStop()
await connectNodesToRendezvousNode(peerNodes, rendezvousNode)
const namespace = "foo"
await allFutures(peerRdvs.mapIt(it.advertise(namespace)))
# Build and inject cookie with wrong namespace
let offset = 10.uint64
let cookie = buildProtobufCookie(offset, "other")
peerRdvs[0].injectCookieForPeer(rendezvousNode.peerInfo.peerId, namespace, cookie)
check (await peerRdvs[0].request(Opt.some(namespace))).len == 3
asyncTest "Peer default TTL is saved when advertised":
let (rendezvousNode, peerNodes, peerRdvs, rendezvousRdv) =
setupRendezvousNodeWithPeerNodes(1)
(rendezvousNode & peerNodes).startAndDeferStop()
await connectNodes(peerNodes[0], rendezvousNode)
const namespace = "foo"
let timeBefore = Moment.now()
await peerRdvs[0].advertise(namespace)
let timeAfter = Moment.now()
# expiration within [timeBefore + 2hours, timeAfter + 2hours]
check:
# Peer Node side
peerRdvs[0].registered.s[0].data.ttl.get == MinimumDuration.seconds.uint64
peerRdvs[0].registered.s[0].expiration >= timeBefore + MinimumDuration
peerRdvs[0].registered.s[0].expiration <= timeAfter + MinimumDuration
# Rendezvous Node side
rendezvousRdv.registered.s[0].data.ttl.get == MinimumDuration.seconds.uint64
rendezvousRdv.registered.s[0].expiration >= timeBefore + MinimumDuration
rendezvousRdv.registered.s[0].expiration <= timeAfter + MinimumDuration
asyncTest "Peer TTL is saved when advertised with TTL":
let (rendezvousNode, peerNodes, peerRdvs, rendezvousRdv) =
setupRendezvousNodeWithPeerNodes(1)
(rendezvousNode & peerNodes).startAndDeferStop()
await connectNodes(peerNodes[0], rendezvousNode)
const
namespace = "foo"
ttl = 3.hours
let timeBefore = Moment.now()
await peerRdvs[0].advertise(namespace, ttl)
let timeAfter = Moment.now()
# expiration within [timeBefore + ttl, timeAfter + ttl]
check:
# Peer Node side
peerRdvs[0].registered.s[0].data.ttl.get == ttl.seconds.uint64
peerRdvs[0].registered.s[0].expiration >= timeBefore + ttl
peerRdvs[0].registered.s[0].expiration <= timeAfter + ttl
# Rendezvous Node side
rendezvousRdv.registered.s[0].data.ttl.get == ttl.seconds.uint64
rendezvousRdv.registered.s[0].expiration >= timeBefore + ttl
rendezvousRdv.registered.s[0].expiration <= timeAfter + ttl
asyncTest "Peer can reregister to update its TTL before previous TTL expires":
let (rendezvousNode, peerNodes, peerRdvs, rendezvousRdv) =
setupRendezvousNodeWithPeerNodes(1)
(rendezvousNode & peerNodes).startAndDeferStop()
await connectNodes(peerNodes[0], rendezvousNode)
const namespace = "foo"
let now = Moment.now()
await peerRdvs[0].advertise(namespace)
check:
# Peer Node side
peerRdvs[0].registered.s.len == 1
peerRdvs[0].registered.s[0].expiration > now
# Rendezvous Node side
rendezvousRdv.registered.s.len == 1
rendezvousRdv.registered.s[0].expiration > now
await peerRdvs[0].advertise(namespace, 5.hours)
check:
# Added 2nd registration
# Updated expiration of the 1st one to the past
# Will be deleted on deletion heartbeat
# Peer Node side
peerRdvs[0].registered.s.len == 2
peerRdvs[0].registered.s[0].expiration < now
# Rendezvous Node side
rendezvousRdv.registered.s.len == 2
rendezvousRdv.registered.s[0].expiration < now
# Returns only one record
check (await peerRdvs[0].request(Opt.some(namespace))).len == 1
asyncTest "Peer registration is ignored if limit of 1000 registrations is reached":
let (rendezvousNode, peerNodes, peerRdvs, rendezvousRdv) =
setupRendezvousNodeWithPeerNodes(1)
(rendezvousNode & peerNodes).startAndDeferStop()
await connectNodes(peerNodes[0], rendezvousNode)
const namespace = "foo"
let peerRdv = peerRdvs[0]
# Create 999 registrations
await populatePeerRegistrations(
peerRdv, rendezvousRdv, namespace, RegistrationLimitPerPeer - 1
)
# 1000th registration allowed
await peerRdv.advertise(namespace)
check rendezvousRdv.registered.s.len == RegistrationLimitPerPeer
# 1001st registration ignored, limit reached
await peerRdv.advertise(namespace)
check rendezvousRdv.registered.s.len == RegistrationLimitPerPeer
asyncTest "Various local error":
let rdv = RendezVous.new(minDuration = 1.minutes, maxDuration = 72.hours)
expect AdvertiseError:
discard await rdv.request(Opt.some("A".repeat(300)))
expect AdvertiseError:
discard await rdv.request(Opt.some("A"), -1)
expect AdvertiseError:
discard await rdv.request(Opt.some("A"), 3000)
expect AdvertiseError:
await rdv.advertise("A".repeat(300))
expect AdvertiseError:
await rdv.advertise("A", 73.hours)
expect AdvertiseError:
await rdv.advertise("A", 30.seconds)
test "Various config error":
expect RendezVousError:
discard RendezVous.new(minDuration = 30.seconds)
expect RendezVousError:
discard RendezVous.new(maxDuration = 73.hours)
expect RendezVousError:
discard RendezVous.new(minDuration = 15.minutes, maxDuration = 10.minutes)