test(gossipsub): behaviourPenalty tests (#1469)

This commit is contained in:
Radosław Kamiński
2025-06-24 16:07:14 +01:00
committed by GitHub
parent d1cfbb35d3
commit abb2c43667
2 changed files with 148 additions and 44 deletions

View File

@@ -1,6 +1,6 @@
{.used.}
import std/[sequtils]
import std/[sequtils, tables]
import stew/byteutils
import utils
import chronicles
@@ -9,14 +9,15 @@ import ../../libp2p/protocols/pubsub/rpc/[message]
import ../helpers
import ../utils/[futures]
const MsgIdSuccess = "msg id gen success"
suite "GossipSub Behavior":
const
topic = "foobar"
MsgIdSuccess = "msg id gen success"
teardown:
checkTrackers()
asyncTest "handleIHave - peers with no budget should not request messages":
let topic = "foobar"
var (gossipSub, conns, peers) = setupGossipSubWithPeers(1, topic)
defer:
await teardownGossipSub(gossipSub, conns)
@@ -46,7 +47,6 @@ suite "GossipSub Behavior":
gossipSub.mcache.msgs.len == 1
asyncTest "handleIHave - peers with budget should request messages":
let topic = "foobar"
var (gossipSub, conns, peers) = setupGossipSubWithPeers(1, topic)
defer:
await teardownGossipSub(gossipSub, conns)
@@ -78,9 +78,7 @@ suite "GossipSub Behavior":
gossipSub.mcache.msgs.len == 1
asyncTest "handleIHave - do not handle IHave if peer score is below GossipThreshold threshold":
const
topic = "foobar"
gossipThreshold = -100.0
const gossipThreshold = -100.0
let
(gossipSub, conns, peers) = setupGossipSubWithPeers(1, topic)
peer = peers[0]
@@ -103,7 +101,6 @@ suite "GossipSub Behavior":
iWant.messageIDs.len == 0
asyncTest "handleIWant - peers with budget should request messages":
let topic = "foobar"
var (gossipSub, conns, peers) = setupGossipSubWithPeers(1, topic)
defer:
await teardownGossipSub(gossipSub, conns)
@@ -131,9 +128,7 @@ suite "GossipSub Behavior":
gossipSub.mcache.msgs.len == 1
asyncTest "handleIWant - do not handle IWant if peer score is below GossipThreshold threshold":
const
topic = "foobar"
gossipThreshold = -100.0
const gossipThreshold = -100.0
let
(gossipSub, conns, peers) = setupGossipSubWithPeers(1, topic)
peer = peers[0]
@@ -159,11 +154,7 @@ suite "GossipSub Behavior":
asyncTest "handleIDontWant - Max IDONTWANT messages per heartbeat per peer":
# Given GossipSub node with 1 peer
let
topic = "foobar"
totalPeers = 1
let (gossipSub, conns, peers) = setupGossipSubWithPeers(totalPeers, topic)
let (gossipSub, conns, peers) = setupGossipSubWithPeers(1, topic)
defer:
await teardownGossipSub(gossipSub, conns)
@@ -187,9 +178,7 @@ suite "GossipSub Behavior":
peer.iDontWants[0].len == IDontWantMaxCount
asyncTest "handlePrune - do not trigger PeerExchange on Prune if peer score is below GossipThreshold threshold":
const
topic = "foobar"
gossipThreshold = -100.0
const gossipThreshold = -100.0
let
(gossipSub, conns, peers) = setupGossipSubWithPeers(1, topic)
peer = peers[0]
@@ -221,9 +210,7 @@ suite "GossipSub Behavior":
result.isCancelled()
asyncTest "handleGraft - do not graft when peer score below PublishThreshold threshold":
const
topic = "foobar"
publishThreshold = -100.0
const publishThreshold = -100.0
let
(gossipSub, conns, peers) = setupGossipSubWithPeers(1, topic)
peer = peers[0]
@@ -245,8 +232,58 @@ suite "GossipSub Behavior":
gossipSub.mesh[topic].len == 0
prunes.len == 0
asyncTest "handleGraft - penalizes direct peer attempting to graft":
# Given a GossipSub instance with one direct peer
let
(gossipSub, conns, peers) = setupGossipSubWithPeers(1, topic)
peer = peers[0]
defer:
await teardownGossipSub(gossipSub, conns)
# And the peer is configured as a direct peer
gossipSub.parameters.directPeers[peer.peerId] = @[]
# And initial behavior penalty is zero
check:
peer.behaviourPenalty == 0.0
# When a GRAFT message is handled
let graftMsg = ControlGraft(topicID: topic)
let prunes = gossipSub.handleGraft(peer, @[graftMsg])
# Then the peer is penalized with behavior penalty
# And receives PRUNE in response
check:
peer.behaviourPenalty == 0.1
prunes.len == 1
asyncTest "handleGraft - penalizes peer for grafting during backoff period":
# Given a GossipSub instance with one peer
let
(gossipSub, conns, peers) = setupGossipSubWithPeers(1, topic)
peer = peers[0]
defer:
await teardownGossipSub(gossipSub, conns)
# And the peer is in backoff period for the topic
gossipSub.backingOff.mgetOrPut(topic, initTable[PeerId, Moment]())[peer.peerId] =
Moment.now() + 1.hours
# And initial behavior penalty is zero
check:
peer.behaviourPenalty == 0.0
# When a GRAFT message is handled
let graftMsg = ControlGraft(topicID: topic)
let prunes = gossipSub.handleGraft(peer, @[graftMsg])
# Then the peer is penalized with behavior penalty
# And receives PRUNE in response
check:
peer.behaviourPenalty == 0.1
prunes.len == 1
asyncTest "replenishFanout - Degree Lo":
let topic = "foobar"
let (gossipSub, conns, peers) =
setupGossipSubWithPeers(15, topic, populateGossipsub = true)
defer:
@@ -257,7 +294,6 @@ suite "GossipSub Behavior":
check gossipSub.fanout[topic].len == gossipSub.parameters.d
asyncTest "dropFanoutPeers - drop expired fanout topics":
let topic = "foobar"
let (gossipSub, conns, peers) =
setupGossipSubWithPeers(6, topic, populateGossipsub = true, populateFanout = true)
defer:
@@ -272,7 +308,7 @@ suite "GossipSub Behavior":
check topic notin gossipSub.fanout
asyncTest "dropFanoutPeers - leave unexpired fanout topics":
let
const
topic1 = "foobar1"
topic2 = "foobar2"
let (gossipSub, conns, peers) = setupGossipSubWithPeers(
@@ -293,7 +329,6 @@ suite "GossipSub Behavior":
check topic2 in gossipSub.fanout
asyncTest "getGossipPeers - should gather up to degree D non intersecting peers":
let topic = "foobar"
let (gossipSub, conns, peers) = setupGossipSubWithPeers(45, topic)
defer:
await teardownGossipSub(gossipSub, conns)
@@ -331,7 +366,6 @@ suite "GossipSub Behavior":
check not gossipSub.mesh.hasPeerId(topic, p.peerId)
asyncTest "getGossipPeers - should not crash on missing topics in mesh":
let topic = "foobar"
let (gossipSub, conns, peers) = setupGossipSubWithPeers(30, topic)
defer:
await teardownGossipSub(gossipSub, conns)
@@ -355,7 +389,6 @@ suite "GossipSub Behavior":
check gossipPeers.len == gossipSub.parameters.d
asyncTest "getGossipPeers - should not crash on missing topics in fanout":
let topic = "foobar"
let (gossipSub, conns, peers) = setupGossipSubWithPeers(30, topic)
defer:
await teardownGossipSub(gossipSub, conns)
@@ -380,7 +413,6 @@ suite "GossipSub Behavior":
check gossipPeers.len == gossipSub.parameters.d
asyncTest "getGossipPeers - should not crash on missing topics in gossip":
let topic = "foobar"
let (gossipSub, conns, peers) = setupGossipSubWithPeers(30, topic)
defer:
await teardownGossipSub(gossipSub, conns)
@@ -405,9 +437,7 @@ suite "GossipSub Behavior":
check gossipPeers.len == 0
asyncTest "getGossipPeers - do not select peer for IHave broadcast if peer score is below GossipThreshold threshold":
const
topic = "foobar"
gossipThreshold = -100.0
const gossipThreshold = -100.0
let
(gossipSub, conns, peers) =
setupGossipSubWithPeers(1, topic, populateGossipsub = true)
@@ -431,7 +461,6 @@ suite "GossipSub Behavior":
gossipPeers.len == 0
asyncTest "rebalanceMesh - Degree Lo":
let topic = "foobar"
let (gossipSub, conns, peers) =
setupGossipSubWithPeers(15, topic, populateGossipsub = true)
defer:
@@ -442,7 +471,6 @@ suite "GossipSub Behavior":
check gossipSub.mesh[topic].len == gossipSub.parameters.d
asyncTest "rebalanceMesh - bad peers":
let topic = "foobar"
let (gossipSub, conns, peers) =
setupGossipSubWithPeers(15, topic, populateGossipsub = true)
defer:
@@ -461,7 +489,6 @@ suite "GossipSub Behavior":
check peer.score >= 0.0
asyncTest "rebalanceMesh - Degree Hi":
let topic = "foobar"
let (gossipSub, conns, peers) =
setupGossipSubWithPeers(15, topic, populateGossipsub = true, populateMesh = true)
defer:
@@ -473,7 +500,6 @@ suite "GossipSub Behavior":
gossipSub.parameters.d + gossipSub.parameters.dScore
asyncTest "rebalanceMesh - fail due to backoff":
let topic = "foobar"
let (gossipSub, conns, peers) =
setupGossipSubWithPeers(15, topic, populateGossipsub = true)
defer:
@@ -493,7 +519,6 @@ suite "GossipSub Behavior":
check gossipSub.mesh[topic].len == 0
asyncTest "rebalanceMesh - fail due to backoff - remote":
let topic = "foobar"
let (gossipSub, conns, peers) =
setupGossipSubWithPeers(15, topic, populateGossipsub = true, populateMesh = true)
defer:
@@ -519,8 +544,7 @@ suite "GossipSub Behavior":
check topic notin gossipSub.mesh
asyncTest "rebalanceMesh - Degree Hi - audit scenario":
let
topic = "foobar"
const
numInPeers = 6
numOutPeers = 7
totalPeers = numInPeers + numOutPeers
@@ -562,10 +586,7 @@ suite "GossipSub Behavior":
asyncTest "rebalanceMesh - Degree Hi - dScore controls number of peers to retain by score when pruning":
# Given GossipSub node starting with 13 peers in mesh
let
topic = "foobar"
totalPeers = 13
const totalPeers = 13
let (gossipSub, conns, peers) = setupGossipSubWithPeers(
totalPeers, topic, populateGossipsub = true, populateMesh = true
)

View File

@@ -127,6 +127,89 @@ suite "GossipSub":
check:
currentRateLimitHits("unknown") == rateLimitHits + 1
asyncTest "Peer is disconnected and rate limit is hit when overhead rate limit is exceeded when decodeRpcMsg fails":
# Given a GossipSub instance with one peer
let
(gossipSub, conns, peers) = setupGossipSubWithPeers(1, topic)
peer = peers[0]
rateLimitHits = currentRateLimitHits("unknown")
defer:
await teardownGossipSub(gossipSub, conns)
# And peer disconnection is enabled when rate limit is exceeded
gossipSub.parameters.disconnectPeerAboveRateLimit = true
# And low overheadRateLimit is set
const
bytes = 1
interval = 1.millis
overheadRateLimit = Opt.some((bytes, interval))
gossipSub.parameters.overheadRateLimit = overheadRateLimit
peer.overheadRateLimitOpt = Opt.some(TokenBucket.new(bytes, interval))
# When invalid RPC data is sent that fails to decode
expect(PeerRateLimitError):
await gossipSub.rpcHandler(peer, @[byte 1, 2, 3])
# And the rate limit hit counter is incremented
check:
currentRateLimitHits("unknown") == rateLimitHits + 1
asyncTest "Peer is punished and rate limit is hit when overhead rate limit is exceeded when decodeRpcMsg fails":
# Given a GossipSub instance with one peer
let
(gossipSub, conns, peers) = setupGossipSubWithPeers(1, topic)
peer = peers[0]
rateLimitHits = currentRateLimitHits("unknown")
defer:
await teardownGossipSub(gossipSub, conns)
# And peer disconnection is disabled when rate limit is exceeded to not raise PeerRateLimitError
gossipSub.parameters.disconnectPeerAboveRateLimit = false
# And low overheadRateLimit is set
const
bytes = 1
interval = 1.millis
overheadRateLimit = Opt.some((bytes, interval))
gossipSub.parameters.overheadRateLimit = overheadRateLimit
peer.overheadRateLimitOpt = Opt.some(TokenBucket.new(bytes, interval))
# And initial behavior penalty is zero
check:
peer.behaviourPenalty == 0.0
# When invalid RPC data is sent that fails to decode
expect(PeerMessageDecodeError):
await gossipSub.rpcHandler(peer, @[byte 1, 2, 3])
# And the rate limit hit counter is incremented
check:
currentRateLimitHits("unknown") == rateLimitHits + 1
peer.behaviourPenalty == 0.1
asyncTest "Peer is punished when decodeRpcMsg fails":
# Given a GossipSub instance with one peer
let
(gossipSub, conns, peers) = setupGossipSubWithPeers(1, topic)
peer = peers[0]
defer:
await teardownGossipSub(gossipSub, conns)
# And initial behavior penalty is zero
check:
peer.behaviourPenalty == 0.0
# When invalid RPC data is sent that fails to decode
expect(PeerMessageDecodeError):
await gossipSub.rpcHandler(peer, @[byte 1, 2, 3])
# Then the peer is penalized with behavior penalty
check:
peer.behaviourPenalty == 0.1
asyncTest "Peer is punished when message contains invalid sequence number":
# Given a GossipSub instance with one peer
let