Skip to content

Commit ff5ee99

Browse files
authored
test: enable gossipsub tests (#3424)
1 parent 4580b64 commit ff5ee99

File tree

5 files changed

+288
-94
lines changed

5 files changed

+288
-94
lines changed

packages/gossipsub/package.json

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,16 @@
4747
"release": "aegir release --no-types",
4848
"build": "aegir build",
4949
"generate": "protons ./src/message/rpc.proto",
50-
"pretest": "npm run build",
51-
"pretest:e2e": "npm run build",
52-
"benchmark": "yarn benchmark:files 'test/benchmark/**/*.test.ts'",
50+
"benchmark": "yarn benchmark:files test/benchmark/**/*.test.ts",
5351
"benchmark:files": "NODE_OPTIONS='--max-old-space-size=4096 --loader=ts-node/esm' benchmark --config .benchrc.yaml --defaultBranch master",
54-
"test": "aegir test -f './dist/test/*.spec.js'",
55-
"test:unit": "aegir test -f './dist/test/unit/*.test.js' --target node",
56-
"test:e2e": "aegir test -f './dist/test/e2e/*.spec.js'",
57-
"test:browser": "npm run test -- --target browser"
52+
"test": "aegir test -f dist/test/*.spec.js",
53+
"test:node": "aegir test -t node -f dist/test/*.spec.js -f dist/test/unit/*.spec.js -f dist/test/e2e/*.spec.js",
54+
"test:chrome": "aegir test -f dist/test/*.spec.js -t browser",
55+
"test:chrome-webworker": "aegir test -f dist/test/*.spec.js -t webworker",
56+
"test:firefox": "aegir test -f dist/test/*.spec.js -t browser -- --browser firefox",
57+
"test:firefox-webworker": "aegir test -f dist/test/*.spec.js -t webworker -- --browser firefox",
58+
"test:electron-main": "aegir test -f dist/test/*.spec.js -t electron-main",
59+
"test:webkit": "aegir test -f dist/test/*.spec.js -t browser -- --browser webkit"
5860
},
5961
"repository": {
6062
"type": "git",

packages/gossipsub/test/2-nodes.spec.ts

Lines changed: 49 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,15 @@ async function nodesArePubSubPeers (node0: GossipSubAndComponents, node1: Gossip
2727
.getPeers()
2828
.map((p) => p.toString())
2929
.includes(node0.components.peerId.toString())
30-
return node0SeesNode1 && node1SeesNode0
30+
31+
const node0StreamsReady =
32+
node0.pubsub.streamsOutbound.has(node1.components.peerId.toString()) &&
33+
node0.pubsub.streamsInbound.has(node1.components.peerId.toString())
34+
const node1StreamsReady =
35+
node1.pubsub.streamsOutbound.has(node0.components.peerId.toString()) &&
36+
node1.pubsub.streamsInbound.has(node0.components.peerId.toString())
37+
38+
return node0SeesNode1 && node1SeesNode0 && node0StreamsReady && node1StreamsReady
3139
},
3240
{
3341
timeout
@@ -93,14 +101,16 @@ describe('2 nodes', () => {
93101
it('Subscribe to a topic', async () => {
94102
const topic = 'test_topic'
95103

104+
const subscriptionChanges = [
105+
pEvent<'subscription-change', CustomEvent<SubscriptionChangeData>>(nodes[0].pubsub, 'subscription-change'),
106+
pEvent<'subscription-change', CustomEvent<SubscriptionChangeData>>(nodes[1].pubsub, 'subscription-change')
107+
]
108+
96109
nodes[0].pubsub.subscribe(topic)
97110
nodes[1].pubsub.subscribe(topic)
98111

99112
// await subscription change
100-
const [evt0] = await Promise.all([
101-
pEvent<'subscription-change', CustomEvent<SubscriptionChangeData>>(nodes[0].pubsub, 'subscription-change'),
102-
pEvent<'subscription-change', CustomEvent<SubscriptionChangeData>>(nodes[1].pubsub, 'subscription-change')
103-
])
113+
const [evt0] = await Promise.all(subscriptionChanges)
104114

105115
const { peerId: changedPeerId, subscriptions: changedSubs } = evt0.detail
106116

@@ -118,14 +128,11 @@ describe('2 nodes', () => {
118128
expect(changedSubs[0].topic).to.equal(topic)
119129
expect(changedSubs[0].subscribe).to.equal(true)
120130

121-
// await heartbeats
122-
await Promise.all([
123-
pEvent(nodes[0].pubsub, 'gossipsub:heartbeat'),
124-
pEvent(nodes[1].pubsub, 'gossipsub:heartbeat')
125-
])
126-
127-
expect((nodes[0].pubsub).mesh.get(topic)?.has(nodes[1].components.peerId.toString())).to.be.true()
128-
expect((nodes[1].pubsub).mesh.get(topic)?.has(nodes[0].components.peerId.toString())).to.be.true()
131+
// await mesh propagation
132+
await pWaitFor(() => {
133+
return (nodes[0].pubsub).mesh.get(topic)?.has(nodes[1].components.peerId.toString()) === true &&
134+
(nodes[1].pubsub).mesh.get(topic)?.has(nodes[0].components.peerId.toString()) === true
135+
}, { timeout: 10000 })
129136
})
130137
})
131138

@@ -141,15 +148,22 @@ describe('2 nodes', () => {
141148
})
142149

143150
// Create subscriptions
151+
const subscriptionChanges = [
152+
pEvent(nodes[0].pubsub, 'subscription-change'),
153+
pEvent(nodes[1].pubsub, 'subscription-change')
154+
]
155+
const heartbeats = [
156+
pEvent(nodes[0].pubsub, 'gossipsub:heartbeat'),
157+
pEvent(nodes[1].pubsub, 'gossipsub:heartbeat')
158+
]
159+
144160
nodes[0].pubsub.subscribe(topic)
145161
nodes[1].pubsub.subscribe(topic)
146162

147163
// await subscription change and heartbeat
148164
await Promise.all([
149-
pEvent(nodes[0].pubsub, 'subscription-change'),
150-
pEvent(nodes[1].pubsub, 'subscription-change'),
151-
pEvent(nodes[0].pubsub, 'gossipsub:heartbeat'),
152-
pEvent(nodes[1].pubsub, 'gossipsub:heartbeat')
165+
...subscriptionChanges,
166+
...heartbeats
153167
])
154168
})
155169

@@ -240,35 +254,40 @@ describe('2 nodes', () => {
240254
await connectAllPubSubNodes(nodes)
241255

242256
// Create subscriptions
243-
nodes[0].pubsub.subscribe(topic)
244-
nodes[1].pubsub.subscribe(topic)
245-
246-
// await subscription change and heartbeat
247-
await Promise.all([
257+
const subscriptionChanges = [
248258
pEvent(nodes[0].pubsub, 'subscription-change'),
249259
pEvent(nodes[1].pubsub, 'subscription-change')
250-
])
251-
await Promise.all([
260+
]
261+
const heartbeats = [
252262
pEvent(nodes[0].pubsub, 'gossipsub:heartbeat'),
253263
pEvent(nodes[1].pubsub, 'gossipsub:heartbeat')
254-
])
264+
]
265+
266+
nodes[0].pubsub.subscribe(topic)
267+
nodes[1].pubsub.subscribe(topic)
268+
269+
// await subscription change and heartbeat
270+
await Promise.all([...subscriptionChanges, ...heartbeats])
255271
})
256272

257273
afterEach(async () => {
258274
await stop(...nodes.reduce<any[]>((acc, curr) => acc.concat(curr.pubsub, ...Object.entries(curr.components)), []))
259275
})
260276

261277
it('Unsubscribe from a topic', async () => {
262-
nodes[0].pubsub.unsubscribe(topic)
263-
expect(nodes[0].pubsub.getTopics()).to.be.empty()
264-
265-
const evt = await pEvent<'subscription-change', CustomEvent<SubscriptionChangeData>>(
278+
const subscriptionChangePromise = pEvent<'subscription-change', CustomEvent<SubscriptionChangeData>>(
266279
nodes[1].pubsub,
267280
'subscription-change'
268281
)
282+
const heartbeatPromise = pEvent(nodes[1].pubsub, 'gossipsub:heartbeat')
283+
284+
nodes[0].pubsub.unsubscribe(topic)
285+
expect(nodes[0].pubsub.getTopics()).to.be.empty()
286+
287+
const evt = await subscriptionChangePromise
269288
const { peerId: changedPeerId, subscriptions: changedSubs } = evt.detail
270289

271-
await pEvent(nodes[1].pubsub, 'gossipsub:heartbeat')
290+
await heartbeatPromise
272291

273292
expect(nodes[1].pubsub.getPeers()).to.have.lengthOf(1)
274293
expect(nodes[1].pubsub.getSubscribers(topic)).to.be.empty()

packages/gossipsub/test/gossip.spec.ts

Lines changed: 73 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,10 @@ describe('gossip', () => {
8686
})
8787

8888
it('should send idontwant to peers in topic', async function () {
89-
// This test checks that idontwants and idontwantsCounts are correctly incrmemented
90-
// - idontwantCounts should track the number of idontwant messages received from a peer for a single heartbeat
91-
// - it should increment on receive of idontwant msgs (up to limit)
92-
// - it should be emptied after heartbeat
93-
// - idontwants should track the idontwant messages received from a peer along with the heartbeatId when received
94-
// - it should increment on receive of idontwant msgs (up to limit)
95-
// - it should be emptied after mcacheLength heartbeats
89+
// This integration test checks IDONTWANT lifecycle behavior under network traffic:
90+
// - publishing messages in a connected topic causes peers to track IDONTWANT state
91+
// - retained idontwants stay bounded while entries are tracked across heartbeats
92+
// - idontwantCounts are cleared at the next heartbeat
9693
this.timeout(10e4)
9794
const nodeA = nodes[0]
9895
const otherNodes = nodes.slice(1)
@@ -121,70 +118,99 @@ describe('gossip', () => {
121118
])
122119
await nodeA.pubsub.publish(topic, msg)
123120
}
124-
// track the heartbeat when each node received the last message
125-
126-
const ticks = otherNodes.map((n) => n.pubsub['heartbeatTicks'])
127-
128-
// there's no event currently implemented to await, so just wait a bit - flaky :(
129-
// TODO figure out something more robust
130-
await new Promise((resolve) => setTimeout(resolve, 200))
121+
// wait for one heartbeat so IDONTWANT handling has happened on all peers
122+
await Promise.all(otherNodes.map(async (n) => pEvent(n.pubsub, 'gossipsub:heartbeat')))
131123

132-
// other nodes should have received idontwant messages
133-
// check that idontwants <= GossipsubIdontwantMaxMessages
124+
// other nodes should have tracked idontwant messages
125+
// check retained idontwants are bounded over mcacheLength heartbeats
134126
for (let i = 0; i < otherNodes.length; i++) {
135127
const node = otherNodes[i]
136128

137-
const currentTick = node.pubsub['heartbeatTicks']
138-
139-
const idontwantCounts = node.pubsub['idontwantCounts']
140-
let minCount = Infinity
141-
let maxCount = 0
142-
for (const count of idontwantCounts.values()) {
143-
minCount = Math.min(minCount, count)
144-
maxCount = Math.max(maxCount, count)
145-
}
146-
// expect(minCount).to.be.greaterThan(0)
147-
expect(maxCount).to.be.lessThanOrEqual(idontwantMaxMessages)
148-
149129
const idontwants = node.pubsub['idontwants']
150-
let minIdontwants = Infinity
151130
let maxIdontwants = 0
152131
for (const idontwant of idontwants.values()) {
153-
minIdontwants = Math.min(minIdontwants, idontwant.size)
154132
maxIdontwants = Math.max(maxIdontwants, idontwant.size)
155133
}
156-
// expect(minIdontwants).to.be.greaterThan(0)
157-
expect(maxIdontwants).to.be.lessThanOrEqual(idontwantMaxMessages)
158-
159-
// sanity check that the idontwantCount matches idontwants.size
160-
// only the case if there hasn't been a heartbeat
161-
if (currentTick === ticks[i]) {
162-
expect(minCount).to.be.equal(minIdontwants)
163-
expect(maxCount).to.be.equal(maxIdontwants)
164-
}
134+
135+
expect(maxIdontwants).to.be.lessThanOrEqual(idontwantMaxMessages * node.pubsub.opts.mcacheLength)
165136
}
166137

167138
await Promise.all(otherNodes.map(async (n) => pEvent(n.pubsub, 'gossipsub:heartbeat')))
168139

169140
// after a heartbeat
170141
// idontwants are still tracked
171142
// but idontwantCounts have been cleared
172-
for (const node of nodes) {
143+
for (const node of otherNodes) {
173144
const idontwantCounts = node.pubsub['idontwantCounts']
174-
for (const count of idontwantCounts.values()) {
175-
expect(count).to.be.equal(0)
176-
}
145+
expect(idontwantCounts.size).to.equal(0)
177146

178147
const idontwants = node.pubsub['idontwants']
179-
let minIdontwants = Infinity
180148
let maxIdontwants = 0
181149
for (const idontwant of idontwants.values()) {
182-
minIdontwants = Math.min(minIdontwants, idontwant.size)
183150
maxIdontwants = Math.max(maxIdontwants, idontwant.size)
184151
}
185-
// expect(minIdontwants).to.be.greaterThan(0)
186-
expect(maxIdontwants).to.be.lessThanOrEqual(idontwantMaxMessages)
152+
expect(maxIdontwants).to.be.lessThanOrEqual(idontwantMaxMessages * node.pubsub.opts.mcacheLength)
153+
}
154+
})
155+
156+
it('should cap idontwant tracking per peer per heartbeat', async function () {
157+
// `should send idontwant to peers in topic` exercises this path indirectly, this
158+
// test verifies the cap deterministically with controlled input and exact assertions.
159+
// This test directly exercises handleIdontwant to verify per-heartbeat cap semantics:
160+
// - idontwantCounts and idontwants stop growing at idontwantMaxMessages
161+
// - counts reset on heartbeat and start again next heartbeat
162+
const nodeA = nodes[0]
163+
const pubsub = nodeA.pubsub as unknown as Partial<GossipSubClass> & {
164+
handleIdontwant: GossipSubClass['handleIdontwant']
165+
idontwantCounts: Map<string, number>
166+
idontwants: Map<string, Map<string, number>>
187167
}
168+
const peerId = 'peer-a'
169+
const idontwantMaxMessages = nodeA.pubsub.opts.idontwantMaxMessages
170+
171+
pubsub.handleIdontwant(peerId, [{
172+
messageIDs: Array.from({ length: idontwantMaxMessages * 2 }, (_, i) => uint8ArrayFromString(`msg-${i}`))
173+
}])
174+
175+
expect(pubsub.idontwantCounts.get(peerId)).to.equal(idontwantMaxMessages)
176+
expect(pubsub.idontwants.get(peerId)?.size).to.equal(idontwantMaxMessages)
177+
178+
pubsub.handleIdontwant(peerId, [{ messageIDs: [uint8ArrayFromString('overflow')] }])
179+
180+
expect(pubsub.idontwantCounts.get(peerId)).to.equal(idontwantMaxMessages)
181+
expect(pubsub.idontwants.get(peerId)?.size).to.equal(idontwantMaxMessages)
182+
183+
await nodeA.pubsub.heartbeat()
184+
185+
expect(pubsub.idontwantCounts.get(peerId)).to.equal(undefined)
186+
187+
pubsub.handleIdontwant(peerId, [{ messageIDs: [uint8ArrayFromString('next-heartbeat')] }])
188+
189+
expect(pubsub.idontwantCounts.get(peerId)).to.equal(1)
190+
})
191+
192+
it('should prune tracked idontwants after mcacheLength heartbeats', async function () {
193+
const nodeA = nodes[0]
194+
const pubsub = nodeA.pubsub as unknown as Partial<GossipSubClass> & {
195+
handleIdontwant: GossipSubClass['handleIdontwant']
196+
idontwants: Map<string, Map<string, number>>
197+
}
198+
const peerId = 'peer-b'
199+
const mcacheLength = nodeA.pubsub.opts.mcacheLength
200+
201+
pubsub.handleIdontwant(peerId, [{ messageIDs: [uint8ArrayFromString('msg-to-prune')] }])
202+
expect(pubsub.idontwants.get(peerId)?.size).to.equal(1)
203+
204+
for (let i = 0; i < mcacheLength - 1; i++) {
205+
await nodeA.pubsub.heartbeat()
206+
}
207+
208+
if (mcacheLength > 1) {
209+
expect(pubsub.idontwants.get(peerId)?.size).to.equal(1)
210+
}
211+
212+
await nodeA.pubsub.heartbeat()
213+
expect(pubsub.idontwants.get(peerId)?.size).to.equal(0)
188214
})
189215

190216
it('Should allow publishing to zero peers if flag is passed', async function () {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ describe('Set util', function () {
1212
{ id: 'remove even numbers - need 0', ineed: 0, fn: (item) => item % 2 === 0, result: new Set([]) },
1313
{ id: 'remove even numbers - need 1', ineed: 1, fn: (item) => item % 2 === 0, result: new Set([2]) },
1414
{ id: 'remove even numbers - need 2', ineed: 2, fn: (item) => item % 2 === 0, result: new Set([2, 4]) },
15-
{ id: 'remove even numbers - need 10', ineed: 2, fn: (item) => item % 2 === 0, result: new Set([2, 4]) }
15+
{ id: 'remove even numbers - need 10', ineed: 10, fn: (item) => item % 2 === 0, result: new Set([2, 4]) }
1616
]
1717

1818
for (const { id, ineed, fn, result } of testCases) {

0 commit comments

Comments
 (0)