logos-messaging-nim/tests/test_peer_store_extended.nim

642 lines
21 KiB
Nim
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{.used.}
import
std/[sequtils, times, random],
chronos,
libp2p/crypto/crypto,
libp2p/peerid,
libp2p/peerstore,
libp2p/multiaddress,
testutils/unittests
import
waku/
[node/peer_manager/peer_manager, node/peer_manager/waku_peer_store, waku_core/peers],
./testlib/wakucore
suite "Extended nim-libp2p Peer Store":
# Valid peerId missing the last digit. Useful for creating new peerIds
# basePeerId & "1"
# basePeerId & "2"
let basePeerId = "QmeuZJbXrszW2jdT7GdduSjQskPU3S7vvGWKtKgDfkDvW"
setup:
# Setup a nim-libp2p peerstore with some peers
let peerStore = PeerStore.new(nil, capacity = 50)
var p1, p2, p3, p4, p5, p6: PeerId
# create five peers basePeerId + [1-5]
require p1.init(basePeerId & "1")
require p2.init(basePeerId & "2")
require p3.init(basePeerId & "3")
require p4.init(basePeerId & "4")
require p5.init(basePeerId & "5")
# peer6 is not part of the peerstore
require p6.init(basePeerId & "6")
# Peer1: Connected
peerStore.addPeer(
RemotePeerInfo.init(
peerId = p1,
addrs = @[MultiAddress.init("/ip4/127.0.0.1/tcp/1").tryGet()],
protocols = @["/vac/waku/relay/2.0.0-beta1", "/vac/waku/store/2.0.0"],
publicKey = generateEcdsaKeyPair().pubkey,
agent = "nwaku",
protoVersion = "protoVersion1",
connectedness = Connected,
disconnectTime = 0,
origin = Discv5,
direction = Inbound,
lastFailedConn = Moment.init(1001, Second),
numberFailedConn = 1,
)
)
# Peer2: Connected
peerStore.addPeer(
RemotePeerInfo.init(
peerId = p2,
addrs = @[MultiAddress.init("/ip4/127.0.0.1/tcp/2").tryGet()],
protocols = @["/vac/waku/relay/2.0.0", "/vac/waku/store/2.0.0"],
publicKey = generateEcdsaKeyPair().pubkey,
agent = "nwaku",
protoVersion = "protoVersion2",
connectedness = Connected,
disconnectTime = 0,
origin = Discv5,
direction = Inbound,
lastFailedConn = Moment.init(1002, Second),
numberFailedConn = 2,
)
)
# Peer3: Connected
peerStore.addPeer(
RemotePeerInfo.init(
peerId = p3,
addrs = @[MultiAddress.init("/ip4/127.0.0.1/tcp/3").tryGet()],
protocols = @["/vac/waku/lightpush/2.0.0", "/vac/waku/store/2.0.0-beta1"],
publicKey = generateEcdsaKeyPair().pubkey,
agent = "gowaku",
protoVersion = "protoVersion3",
connectedness = Connected,
disconnectTime = 0,
origin = Discv5,
direction = Inbound,
lastFailedConn = Moment.init(1003, Second),
numberFailedConn = 3,
)
)
# Peer4: Added but never connected
peerStore.addPeer(
RemotePeerInfo.init(
peerId = p4,
addrs = @[MultiAddress.init("/ip4/127.0.0.1/tcp/4").tryGet()],
protocols = @[],
publicKey = generateEcdsaKeyPair().pubkey,
agent = "",
protoVersion = "",
connectedness = NotConnected,
disconnectTime = 0,
origin = Discv5,
direction = Inbound,
lastFailedConn = Moment.init(1004, Second),
numberFailedConn = 4,
)
)
# Peer5: Connected
peerStore.addPeer(
RemotePeerInfo.init(
peerId = p5,
addrs = @[MultiAddress.init("/ip4/127.0.0.1/tcp/5").tryGet()],
protocols = @["/vac/waku/swap/2.0.0", "/vac/waku/store/2.0.0-beta2"],
publicKey = generateEcdsaKeyPair().pubkey,
agent = "gowaku",
protoVersion = "protoVersion5",
connectedness = CanConnect,
disconnectTime = 1000,
origin = Discv5,
direction = Outbound,
lastFailedConn = Moment.init(1005, Second),
numberFailedConn = 5,
)
)
test "get() returns the correct StoredInfo for a given PeerId":
# When
let peer1 = peerStore.getPeer(p1)
let peer6 = peerStore.getPeer(p6)
# Then
check:
# regression on nim-libp2p fields
peer1.peerId == p1
peer1.addrs == @[MultiAddress.init("/ip4/127.0.0.1/tcp/1").tryGet()]
peer1.protocols == @["/vac/waku/relay/2.0.0-beta1", "/vac/waku/store/2.0.0"]
peer1.agent == "nwaku"
peer1.protoVersion == "protoVersion1"
# our extended fields
peer1.connectedness == Connected
peer1.disconnectTime == 0
peer1.origin == Discv5
peer1.numberFailedConn == 1
peer1.lastFailedConn == Moment.init(1001, Second)
check:
# fields are empty, not part of the peerstore
peer6.peerId == p6
peer6.addrs.len == 0
peer6.protocols.len == 0
peer6.agent == default(string)
peer6.protoVersion == default(string)
peer6.connectedness == default(Connectedness)
peer6.disconnectTime == default(int)
peer6.origin == default(PeerOrigin)
peer6.numberFailedConn == default(int)
peer6.lastFailedConn == default(Moment)
test "peers() returns all StoredInfo of the PeerStore":
# When
let allPeers = peerStore.peers()
# Then
check:
allPeers.len == 5
allPeers.anyIt(it.peerId == p1)
allPeers.anyIt(it.peerId == p2)
allPeers.anyIt(it.peerId == p3)
allPeers.anyIt(it.peerId == p4)
allPeers.anyIt(it.peerId == p5)
let p3 = allPeers.filterIt(it.peerId == p3)[0]
check:
# regression on nim-libp2p fields
p3.addrs == @[MultiAddress.init("/ip4/127.0.0.1/tcp/3").tryGet()]
p3.protocols == @["/vac/waku/lightpush/2.0.0", "/vac/waku/store/2.0.0-beta1"]
p3.agent == "gowaku"
p3.protoVersion == "protoVersion3"
# our extended fields
p3.connectedness == Connected
p3.disconnectTime == 0
p3.origin == Discv5
p3.numberFailedConn == 3
p3.lastFailedConn == Moment.init(1003, Second)
test "peers() returns all StoredInfo matching a specific protocol":
# When
let storePeers = peerStore.peers("/vac/waku/store/2.0.0")
let lpPeers = peerStore.peers("/vac/waku/lightpush/2.0.0")
# Then
check:
# Only p1 and p2 support that protocol
storePeers.len == 2
storePeers.anyIt(it.peerId == p1)
storePeers.anyIt(it.peerId == p2)
check:
# Only p3 supports that protocol
lpPeers.len == 1
lpPeers.anyIt(it.peerId == p3)
lpPeers[0].protocols ==
@["/vac/waku/lightpush/2.0.0", "/vac/waku/store/2.0.0-beta1"]
test "peers() returns all StoredInfo matching a given protocolMatcher":
# When
let pMatcherStorePeers = peerStore.peers(protocolMatcher("/vac/waku/store/2.0.0"))
let pMatcherSwapPeers = peerStore.peers(protocolMatcher("/vac/waku/swap/2.0.0"))
# Then
check:
# peers: 1,2,3,5 match /vac/waku/store/2.0.0/xxx
pMatcherStorePeers.len == 4
pMatcherStorePeers.anyIt(it.peerId == p1)
pMatcherStorePeers.anyIt(it.peerId == p2)
pMatcherStorePeers.anyIt(it.peerId == p3)
pMatcherStorePeers.anyIt(it.peerId == p5)
check:
pMatcherStorePeers.filterIt(it.peerId == p1)[0].protocols ==
@["/vac/waku/relay/2.0.0-beta1", "/vac/waku/store/2.0.0"]
pMatcherStorePeers.filterIt(it.peerId == p2)[0].protocols ==
@["/vac/waku/relay/2.0.0", "/vac/waku/store/2.0.0"]
pMatcherStorePeers.filterIt(it.peerId == p3)[0].protocols ==
@["/vac/waku/lightpush/2.0.0", "/vac/waku/store/2.0.0-beta1"]
pMatcherStorePeers.filterIt(it.peerId == p5)[0].protocols ==
@["/vac/waku/swap/2.0.0", "/vac/waku/store/2.0.0-beta2"]
check:
pMatcherSwapPeers.len == 1
pMatcherSwapPeers.anyIt(it.peerId == p5)
pMatcherSwapPeers[0].protocols ==
@["/vac/waku/swap/2.0.0", "/vac/waku/store/2.0.0-beta2"]
test "toRemotePeerInfo() converts a StoredInfo to a RemotePeerInfo":
# Given
let peer1 = peerStore.getPeer(p1)
# Then
check:
peer1.peerId == p1
peer1.addrs == @[MultiAddress.init("/ip4/127.0.0.1/tcp/1").tryGet()]
peer1.protocols == @["/vac/waku/relay/2.0.0-beta1", "/vac/waku/store/2.0.0"]
test "connectedness() returns the connection status of a given PeerId":
check:
# peers tracked in the peerstore
peerStore.connectedness(p1) == Connected
peerStore.connectedness(p2) == Connected
peerStore.connectedness(p3) == Connected
peerStore.connectedness(p4) == NotConnected
peerStore.connectedness(p5) == CanConnect
# peer not tracked in the peerstore
peerStore.connectedness(p6) == NotConnected
test "hasPeer() returns true if the peer supports a given protocol":
check:
peerStore.hasPeer(p1, "/vac/waku/relay/2.0.0-beta1")
peerStore.hasPeer(p1, "/vac/waku/store/2.0.0")
not peerStore.hasPeer(p1, "it-does-not-contain-this-protocol")
peerStore.hasPeer(p2, "/vac/waku/relay/2.0.0")
peerStore.hasPeer(p2, "/vac/waku/store/2.0.0")
peerStore.hasPeer(p3, "/vac/waku/lightpush/2.0.0")
peerStore.hasPeer(p3, "/vac/waku/store/2.0.0-beta1")
# we have no knowledge of p4 supported protocols
not peerStore.hasPeer(p4, "/vac/waku/lightpush/2.0.0")
peerStore.hasPeer(p5, "/vac/waku/swap/2.0.0")
peerStore.hasPeer(p5, "/vac/waku/store/2.0.0-beta2")
not peerStore.hasPeer(p5, "another-protocol-not-contained")
# peer 6 is not in the PeerStore
not peerStore.hasPeer(p6, "/vac/waku/lightpush/2.0.0")
test "hasPeers() returns true if any peer in the PeerStore supports a given protocol":
# Match specific protocols
check:
peerStore.hasPeers("/vac/waku/relay/2.0.0-beta1")
peerStore.hasPeers("/vac/waku/store/2.0.0")
peerStore.hasPeers("/vac/waku/lightpush/2.0.0")
not peerStore.hasPeers("/vac/waku/does-not-exist/2.0.0")
# Match protocolMatcher protocols
check:
peerStore.hasPeers(protocolMatcher("/vac/waku/store/2.0.0"))
not peerStore.hasPeers(protocolMatcher("/vac/waku/does-not-exist/2.0.0"))
test "getPeersByDirection()":
# When
let inPeers = peerStore.getPeersByDirection(Inbound)
let outPeers = peerStore.getPeersByDirection(Outbound)
# Then
check:
inPeers.len == 4
outPeers.len == 1
test "getDisconnectedPeers()":
# When
let disconnedtedPeers = peerStore.getDisconnectedPeers()
# Then
check:
disconnedtedPeers.len == 2
disconnedtedPeers.anyIt(it.peerId == p4)
disconnedtedPeers.anyIt(it.peerId == p5)
not disconnedtedPeers.anyIt(it.connectedness == Connected)
test "del() successfully deletes waku custom books":
# Given
let peerStore = PeerStore.new(nil, capacity = 5)
var p1: PeerId
require p1.init("QmeuZJbXrszW2jdT7GdduSjQskPU3S7vvGWKtKgDfkDvW1")
let remotePeer = RemotePeerInfo.init(
peerId = p1,
addrs = @[MultiAddress.init("/ip4/127.0.0.1/tcp/1").tryGet()],
protocols = @["proto"],
publicKey = generateEcdsaKeyPair().pubkey,
agent = "agent",
protoVersion = "version",
lastFailedConn = Moment.init(getTime().toUnix, Second),
numberFailedConn = 1,
connectedness = Connected,
disconnectTime = 0,
origin = Discv5,
direction = Inbound,
)
peerStore.addPeer(remotePeer)
# When
peerStore.delete(p1)
# Then
check:
peerStore[AddressBook][p1] == newSeq[MultiAddress](0)
peerStore[ProtoBook][p1] == newSeq[string](0)
peerStore[KeyBook][p1] == default(PublicKey)
peerStore[AgentBook][p1] == ""
peerStore[ProtoVersionBook][p1] == ""
peerStore[LastFailedConnBook][p1] == default(Moment)
peerStore[NumberFailedConnBook][p1] == 0
peerStore[ConnectionBook][p1] == default(Connectedness)
peerStore[DisconnectBook][p1] == 0
peerStore[SourceBook][p1] == default(PeerOrigin)
peerStore[DirectionBook][p1] == default(PeerDirection)
peerStore[GriefBook][p1] == default(GriefData)
suite "Extended nim-libp2p Peer Store: grief scores":
# These tests mock the clock and work better as a separate suite
var peerStore: PeerStore
var p1, p2, p3: PeerId
setup:
peerStore = PeerStore.new(nil, capacity = 50)
require p1.init(basePeerId & "1")
require p2.init(basePeerId & "2")
require p3.init(basePeerId & "3")
# Shorthand: one cooldown interval
let interval = GriefCooldownInterval
test "new peer has grief score 0":
check peerStore.getGriefScore(p1) == 0
test "griefPeer increases score":
let t0 = Moment.init(1000, Minute)
peerStore.griefPeer(p1, 5, t0)
check peerStore.getGriefScore(p1, t0) == 5
test "griefPeer accumulates":
let t0 = Moment.init(1000, Minute)
peerStore.griefPeer(p1, 3, t0)
peerStore.griefPeer(p1, 2, t0)
check peerStore.getGriefScore(p1, t0) == 5
test "grief cools down by 1 point per interval":
let t0 = Moment.init(1000, Minute)
peerStore.griefPeer(p1, 5, t0)
check peerStore.getGriefScore(p1, t0) == 5
check peerStore.getGriefScore(p1, t0 + interval * 1) == 4
check peerStore.getGriefScore(p1, t0 + interval * 2) == 3
check peerStore.getGriefScore(p1, t0 + interval * 3) == 2
check peerStore.getGriefScore(p1, t0 + interval * 4) == 1
check peerStore.getGriefScore(p1, t0 + interval * 5) == 0
test "grief floors at 0":
let t0 = Moment.init(1000, Minute)
peerStore.griefPeer(p1, 3, t0)
# Well past full cooldown, should be 0
check peerStore.getGriefScore(p1, t0 + interval * 10) == 0
test "cooldown preserves remainder":
let t0 = Moment.init(1000, Minute)
# Half an interval past 2 full intervals
let tHalf = t0 + interval * 2 + interval div 2
# Complete the 3rd interval
let t3 = t0 + interval * 3
peerStore.griefPeer(p1, 5, t0)
# After 2.5 intervals, score should be 3
check peerStore.getGriefScore(p1, tHalf) == 3
# After completing the 3rd interval, score should be 2
check peerStore.getGriefScore(p1, t3) == 2
test "grief after full cooldown restarts cooldown time":
let t0 = Moment.init(1000, Minute)
peerStore.griefPeer(p1, 2, t0)
# Fully cool down
check peerStore.getGriefScore(p1, t0 + interval * 5) == 0
# Grief again
let t1 = t0 + interval * 5
peerStore.griefPeer(p1, 3, t1)
check peerStore.getGriefScore(p1, t1) == 3
# 1 interval after second grief
check peerStore.getGriefScore(p1, t1 + interval) == 2
test "independent grief scores per peer":
let t0 = Moment.init(1000, Minute)
peerStore.griefPeer(p1, 10, t0)
peerStore.griefPeer(p2, 3, t0)
check peerStore.getGriefScore(p1, t0 + interval * 2) == 8
check peerStore.getGriefScore(p2, t0 + interval * 2) == 1
check peerStore.getGriefScore(p3, t0 + interval * 2) == 0
test "grief with default amount is 1":
let t0 = Moment.init(1000, Minute)
peerStore.griefPeer(p1, now = t0)
check peerStore.getGriefScore(p1, t0) == 1
test "griefPeer with zero or negative amount is ignored":
let t0 = Moment.init(1000, Minute)
peerStore.griefPeer(p1, 5, t0)
peerStore.griefPeer(p1, 0, t0)
peerStore.griefPeer(p1, -3, t0)
check peerStore.getGriefScore(p1, t0) == 5
test "grief added during partial cooldown does not reset cooldown time":
let t0 = Moment.init(1000, Minute)
let tHalf = t0 + interval * 2 + interval div 2
let t3 = t0 + interval * 3
let t4 = t0 + interval * 4
peerStore.griefPeer(p1, 5, t0)
# At 2.5 intervals: 2 consumed, score 3, half-interval remainder
check peerStore.getGriefScore(p1, tHalf) == 3
# Add more grief — cooldown time should NOT reset, remainder preserved
peerStore.griefPeer(p1, 4, tHalf)
check peerStore.getGriefScore(p1, tHalf) == 7
# Remainder completes another interval
check peerStore.getGriefScore(p1, t3) == 6
# And one more full interval
check peerStore.getGriefScore(p1, t4) == 5
test "multiple reads without time change are idempotent":
let t0 = Moment.init(1000, Minute)
peerStore.griefPeer(p1, 10, t0)
check peerStore.getGriefScore(p1, t0 + interval * 3) == 7
check peerStore.getGriefScore(p1, t0 + interval * 3) == 7
check peerStore.getGriefScore(p1, t0 + interval * 3) == 7
test "interleaved grief and cooldown across multiple peers":
let t0 = Moment.init(1000, Minute)
# Stagger grief: p1 at t0, p2 at t0+1interval, p3 at t0+2interval
peerStore.griefPeer(p1, 6, t0)
peerStore.griefPeer(p2, 4, t0 + interval)
peerStore.griefPeer(p3, 2, t0 + interval * 2)
# At t0+3*interval: p1 lost 3, p2 lost 2, p3 lost 1
check peerStore.getGriefScore(p1, t0 + interval * 3) == 3
check peerStore.getGriefScore(p2, t0 + interval * 3) == 2
check peerStore.getGriefScore(p3, t0 + interval * 3) == 1
# Grief p2 again at t0+3I
peerStore.griefPeer(p2, 10, t0 + interval * 3)
# At t0+5*interval: p1 lost 5 total, p2 lost 2 more since re-grief, p3 floored at 0
check peerStore.getGriefScore(p1, t0 + interval * 5) == 1
check peerStore.getGriefScore(p2, t0 + interval * 5) == 10
check peerStore.getGriefScore(p3, t0 + interval * 5) == 0
suite "Extended nim-libp2p Peer Store: grief-based peer selection":
# Tests for sortByGriefScore via selectPeers
const testProto = "/test/grief/1.0.0"
proc makePeer(port: int): RemotePeerInfo =
let key = generateSecp256k1Key()
RemotePeerInfo.init(
peerId = PeerId.init(key.getPublicKey().tryGet()).tryGet(),
addrs = @[MultiAddress.init("/ip4/127.0.0.1/tcp/" & $port).tryGet()],
protocols = @[testProto],
)
test "all peers at grief 0 returns all peers (shuffled)":
let switch = newTestSwitch()
let pm = PeerManager.new(switch)
let peerStore = switch.peerStore
let peers = (1..5).mapIt(makePeer(it + 10000))
for p in peers:
peerStore.addPeer(p)
let selected = pm.selectPeers(testProto)
check selected.len == 5
test "lower grief peers come before higher grief peers":
let switch = newTestSwitch()
let pm = PeerManager.new(switch)
let peerStore = switch.peerStore
let pA = makePeer(20001)
let pB = makePeer(20002)
let pC = makePeer(20003)
peerStore.addPeer(pA)
peerStore.addPeer(pB)
peerStore.addPeer(pC)
# pA: grief 0 (bucket 0), pB: grief 5 (bucket 1), pC: grief 15 (bucket 3)
peerStore.griefPeer(pB.peerId, 5)
peerStore.griefPeer(pC.peerId, 15)
# Run multiple times to account for shuffle within buckets
for i in 0 ..< 20:
let selected = pm.selectPeers(testProto)
check selected.len == 3
# pA (bucket 0) must always be first
check selected[0].peerId == pA.peerId
# pB (bucket 1) must always come before pC (bucket 3)
check selected[1].peerId == pB.peerId
check selected[2].peerId == pC.peerId
test "peers within same bucket are interchangeable":
let switch = newTestSwitch()
let pm = PeerManager.new(switch)
let peerStore = switch.peerStore
let pA = makePeer(30001)
let pB = makePeer(30002)
peerStore.addPeer(pA)
peerStore.addPeer(pB)
# Both within bucket 0 (scores 1 and 4, both div 5 == 0)
peerStore.griefPeer(pA.peerId, 1)
peerStore.griefPeer(pB.peerId, 4)
var sawAFirst = false
var sawBFirst = false
for i in 0 ..< 50:
let selected = pm.selectPeers(testProto)
check selected.len == 2
if selected[0].peerId == pA.peerId:
sawAFirst = true
else:
sawBFirst = true
# Both orderings should appear since they're in the same bucket
check sawAFirst
check sawBFirst
test "peers in different buckets never swap order":
let switch = newTestSwitch()
let pm = PeerManager.new(switch)
let peerStore = switch.peerStore
let pLow = makePeer(40001)
let pHigh = makePeer(40002)
peerStore.addPeer(pLow)
peerStore.addPeer(pHigh)
# pLow in bucket 0 (score 1), pHigh in bucket 1 (score 5)
peerStore.griefPeer(pLow.peerId, 1)
peerStore.griefPeer(pHigh.peerId, 5)
for i in 0 ..< 30:
let selected = pm.selectPeers(testProto)
check selected.len == 2
check selected[0].peerId == pLow.peerId
check selected[1].peerId == pHigh.peerId
test "zero-grief peers always come before grieved peers":
let switch = newTestSwitch()
let pm = PeerManager.new(switch)
let peerStore = switch.peerStore
let pClean1 = makePeer(50001)
let pClean2 = makePeer(50002)
let pGrieved = makePeer(50003)
peerStore.addPeer(pClean1)
peerStore.addPeer(pClean2)
peerStore.addPeer(pGrieved)
peerStore.griefPeer(pGrieved.peerId, 6)
for i in 0 ..< 20:
let selected = pm.selectPeers(testProto)
check selected.len == 3
# Grieved peer (bucket 1) must be last; clean peers (bucket 0) first
check selected[2].peerId == pGrieved.peerId
test "peers beyond MaxGriefBucket are excluded from selection":
let switch = newTestSwitch()
let pm = PeerManager.new(switch)
let peerStore = switch.peerStore
let pGood = makePeer(60001)
let pBad = makePeer(60002)
peerStore.addPeer(pGood)
peerStore.addPeer(pBad)
# pBad in bucket 4 (score 20, 20 div 5 = 4 > MaxGriefBucket)
peerStore.griefPeer(pBad.peerId, 20)
let selected = pm.selectPeers(testProto)
check selected.len == 1
check selected[0].peerId == pGood.peerId