Skip to content

Commit 5481add

Browse files
authored
feat: gossipsub 1.2: IDONTWANT (#498)
* feat: gossipsub 1.2: IDONTWANT * chore: add unit test * chore: remove packageManager from package.json * chore: add idontwants cacheSize metric * chore: fix lint error * chore: make test less flaky * chore: fix comment
1 parent 6326e4d commit 5481add

10 files changed

+725
-16
lines changed

package-lock.json

+360
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/constants.ts

+10
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ export const GossipsubIDv10 = '/meshsub/1.0.0'
1818
*/
1919
export const GossipsubIDv11 = '/meshsub/1.1.0'
2020

21+
/**
22+
* The protocol ID for version 1.2.0 of the Gossipsub protocol
23+
* See the spec for details about how v1.2.0 compares to v1.1.0:
24+
* https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.2.md
25+
*/
26+
export const GossipsubIDv12 = '/meshsub/1.2.0'
27+
2128
// Overlay parameters
2229

2330
/**
@@ -249,3 +256,6 @@ export const DEFAULT_METRIC_MESH_MESSAGE_DELIVERIES_WINDOWS = 1000
249256

250257
/** Wait for 1 more heartbeats before clearing a backoff */
251258
export const BACKOFF_SLACK = 1
259+
260+
export const GossipsubIdontwantMinDataSize = 512
261+
export const GossipsubIdontwantMaxMessages = 512

src/index.ts

+123-7
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ type ReceivedMessageResult =
8888
| ({ code: MessageStatus.invalid, msgIdStr?: MsgIdStr } & RejectReasonObj)
8989
| { code: MessageStatus.valid, messageId: MessageId, msg: Message }
9090

91-
export const multicodec: string = constants.GossipsubIDv11
91+
export const multicodec: string = constants.GossipsubIDv12
9292

9393
export interface GossipsubOpts extends GossipsubOptsSpec, PubSubInit {
9494
/** if dial should fallback to floodsub */
@@ -211,6 +211,20 @@ export interface GossipsubOpts extends GossipsubOptsSpec, PubSubInit {
211211
* It should be a number between 0 and 1, with a reasonable default of 0.25
212212
*/
213213
gossipFactor: number
214+
215+
/**
216+
* The minimum message size in bytes to be considered for sending IDONTWANT messages
217+
*
218+
* @default 512
219+
*/
220+
idontwantMinDataSize?: number
221+
222+
/**
223+
* The maximum number of IDONTWANT messages per heartbeat per peer
224+
*
225+
* @default 512
226+
*/
227+
idontwantMaxMessages?: number
214228
}
215229

216230
export interface GossipsubMessage {
@@ -274,7 +288,7 @@ export class GossipSub extends TypedEventEmitter<GossipsubEvents> implements Pub
274288
* The signature policy to follow by default
275289
*/
276290
public readonly globalSignaturePolicy: typeof StrictSign | typeof StrictNoSign
277-
public multicodecs: string[] = [constants.GossipsubIDv11, constants.GossipsubIDv10]
291+
public multicodecs: string[] = [constants.GossipsubIDv12, constants.GossipsubIDv11, constants.GossipsubIDv10]
278292

279293
private publishConfig: PublishConfig | undefined
280294

@@ -409,11 +423,24 @@ export class GossipSub extends TypedEventEmitter<GossipsubEvents> implements Pub
409423
*/
410424
readonly gossipTracer: IWantTracer
411425

426+
/**
427+
* Tracks IDONTWANT messages received by peers in the current heartbeat
428+
*/
429+
private readonly idontwantCounts = new Map<PeerIdStr, number>()
430+
431+
/**
432+
* Tracks IDONTWANT messages received by peers and the heartbeat they were received in
433+
*
434+
* idontwants are stored for `mcacheLength` heartbeats before being pruned,
435+
* so this map is bounded by peerCount * idontwantMaxMessages * mcacheLength
436+
*/
437+
private readonly idontwants = new Map<PeerIdStr, Map<MsgIdStr, number>>()
438+
412439
private readonly components: GossipSubComponents
413440

414441
private directPeerInitial: ReturnType<typeof setTimeout> | null = null
415442

416-
public static multicodec: string = constants.GossipsubIDv11
443+
public static multicodec: string = constants.GossipsubIDv12
417444

418445
// Options
419446
readonly opts: Required<GossipOptions>
@@ -462,6 +489,8 @@ export class GossipSub extends TypedEventEmitter<GossipsubEvents> implements Pub
462489
opportunisticGraftTicks: constants.GossipsubOpportunisticGraftTicks,
463490
directConnectTicks: constants.GossipsubDirectConnectTicks,
464491
gossipFactor: constants.GossipsubGossipFactor,
492+
idontwantMinDataSize: constants.GossipsubIdontwantMinDataSize,
493+
idontwantMaxMessages: constants.GossipsubIdontwantMaxMessages,
465494
...options,
466495
scoreParams: createPeerScoreParams(options.scoreParams),
467496
scoreThresholds: createPeerScoreThresholds(options.scoreThresholds)
@@ -750,6 +779,8 @@ export class GossipSub extends TypedEventEmitter<GossipsubEvents> implements Pub
750779
this.seenCache.clear()
751780
if (this.fastMsgIdCache != null) this.fastMsgIdCache.clear()
752781
if (this.directPeerInitial != null) clearTimeout(this.directPeerInitial)
782+
this.idontwantCounts.clear()
783+
this.idontwants.clear()
753784

754785
this.log('stopped')
755786
}
@@ -956,6 +987,9 @@ export class GossipSub extends TypedEventEmitter<GossipsubEvents> implements Pub
956987
this.control.delete(id)
957988
// Remove from backoff mapping
958989
this.outbound.delete(id)
990+
// Remove from idontwant tracking
991+
this.idontwantCounts.delete(id)
992+
this.idontwants.delete(id)
959993

960994
// Remove from peer scoring
961995
this.score.removePeer(id)
@@ -1019,6 +1053,10 @@ export class GossipSub extends TypedEventEmitter<GossipsubEvents> implements Pub
10191053
prune: this.decodeRpcLimits.maxControlMessages,
10201054
prune$: {
10211055
peers: this.decodeRpcLimits.maxPeerInfos
1056+
},
1057+
idontwant: this.decodeRpcLimits.maxControlMessages,
1058+
idontwant$: {
1059+
messageIDs: this.decodeRpcLimits.maxIdontwantMessageIDs
10221060
}
10231061
}
10241062
}
@@ -1310,6 +1348,11 @@ export class GossipSub extends TypedEventEmitter<GossipsubEvents> implements Pub
13101348
this.seenCache.put(msgIdStr)
13111349
}
13121350

1351+
// possibly send IDONTWANTs to mesh peers
1352+
if ((rpcMsg.data?.length ?? 0) >= this.opts.idontwantMinDataSize) {
1353+
this.sendIDontWants(msgId, rpcMsg.topic, propagationSource.toString())
1354+
}
1355+
13131356
// (Optional) Provide custom validation here with dynamic validators per topic
13141357
// NOTE: This custom topicValidator() must resolve fast (< 100ms) to allow scores
13151358
// to not penalize peers for long validation times.
@@ -1359,10 +1402,11 @@ export class GossipSub extends TypedEventEmitter<GossipsubEvents> implements Pub
13591402
return
13601403
}
13611404

1362-
const iwant = (controlMsg.ihave != null) ? this.handleIHave(id, controlMsg.ihave) : []
1363-
const ihave = (controlMsg.iwant != null) ? this.handleIWant(id, controlMsg.iwant) : []
1364-
const prune = (controlMsg.graft != null) ? await this.handleGraft(id, controlMsg.graft) : []
1365-
;(controlMsg.prune != null) && (await this.handlePrune(id, controlMsg.prune))
1405+
const iwant = (controlMsg.ihave?.length > 0) ? this.handleIHave(id, controlMsg.ihave) : []
1406+
const ihave = (controlMsg.iwant?.length > 0) ? this.handleIWant(id, controlMsg.iwant) : []
1407+
const prune = (controlMsg.graft?.length > 0) ? await this.handleGraft(id, controlMsg.graft) : []
1408+
;(controlMsg.prune?.length > 0) && (await this.handlePrune(id, controlMsg.prune))
1409+
;(controlMsg.idontwant?.length > 0) && this.handleIdontwant(id, controlMsg.idontwant)
13661410

13671411
if ((iwant.length === 0) && (ihave.length === 0) && (prune.length === 0)) {
13681412
return
@@ -1691,6 +1735,39 @@ export class GossipSub extends TypedEventEmitter<GossipsubEvents> implements Pub
16911735
}
16921736
}
16931737

1738+
private handleIdontwant (id: PeerIdStr, idontwant: RPC.ControlIDontWant[]): void {
1739+
let idontwantCount = this.idontwantCounts.get(id) ?? 0
1740+
// return early if we have already received too many IDONTWANT messages from the peer
1741+
if (idontwantCount >= this.opts.idontwantMaxMessages) {
1742+
return
1743+
}
1744+
const startIdontwantCount = idontwantCount
1745+
1746+
let idontwants = this.idontwants.get(id)
1747+
if (idontwants == null) {
1748+
idontwants = new Map()
1749+
this.idontwants.set(id, idontwants)
1750+
}
1751+
let idonthave = 0
1752+
// eslint-disable-next-line no-labels
1753+
out: for (const { messageIDs } of idontwant) {
1754+
for (const msgId of messageIDs) {
1755+
if (idontwantCount >= this.opts.idontwantMaxMessages) {
1756+
// eslint-disable-next-line no-labels
1757+
break out
1758+
}
1759+
idontwantCount++
1760+
1761+
const msgIdStr = this.msgIdToStrFn(msgId)
1762+
idontwants.set(msgIdStr, this.heartbeatTicks)
1763+
if (!this.mcache.msgs.has(msgIdStr)) idonthave++
1764+
}
1765+
}
1766+
this.idontwantCounts.set(id, idontwantCount)
1767+
const total = idontwantCount - startIdontwantCount
1768+
this.metrics?.onIdontwantRcv(total, idonthave)
1769+
}
1770+
16941771
/**
16951772
* Add standard backoff log for a peer in a topic
16961773
*/
@@ -2353,6 +2430,27 @@ export class GossipSub extends TypedEventEmitter<GossipsubEvents> implements Pub
23532430
this.sendRpc(id, out)
23542431
}
23552432

2433+
private sendIDontWants (msgId: Uint8Array, topic: string, source: PeerIdStr): void {
2434+
const ids = this.mesh.get(topic)
2435+
if (ids == null) {
2436+
return
2437+
}
2438+
2439+
// don't send IDONTWANT to:
2440+
// - the source
2441+
// - peers that don't support v1.2
2442+
const tosend = new Set(ids)
2443+
tosend.delete(source)
2444+
for (const id of tosend) {
2445+
if (this.streamsOutbound.get(id)?.protocol !== constants.GossipsubIDv12) {
2446+
tosend.delete(id)
2447+
}
2448+
}
2449+
2450+
const idontwantRpc = createGossipRpc([], { idontwant: [{ messageIDs: [msgId] }] })
2451+
this.sendRpcInBatch(tosend, idontwantRpc)
2452+
}
2453+
23562454
/**
23572455
* Send an rpc object to a peer
23582456
*/
@@ -2701,6 +2799,18 @@ export class GossipSub extends TypedEventEmitter<GossipsubEvents> implements Pub
27012799
// apply IWANT request penalties
27022800
this.applyIwantPenalties()
27032801

2802+
// clean up IDONTWANT counters
2803+
this.idontwantCounts.clear()
2804+
2805+
// clean up old tracked IDONTWANTs
2806+
for (const idontwants of this.idontwants.values()) {
2807+
for (const [msgId, heartbeatTick] of idontwants) {
2808+
if (this.heartbeatTicks - heartbeatTick >= this.opts.mcacheLength) {
2809+
idontwants.delete(msgId)
2810+
}
2811+
}
2812+
}
2813+
27042814
// ensure direct peers are connected
27052815
if (this.heartbeatTicks % this.opts.directConnectTicks === 0) {
27062816
// we only do this every few ticks to allow pending connections to complete and account for restarts/downtime
@@ -3069,6 +3179,12 @@ export class GossipSub extends TypedEventEmitter<GossipsubEvents> implements Pub
30693179
}
30703180
metrics.cacheSize.set({ cache: 'backoff' }, backoffSize)
30713181

3182+
let idontwantsCount = 0
3183+
for (const idontwant of this.idontwants.values()) {
3184+
idontwantsCount += idontwant.size
3185+
}
3186+
metrics.cacheSize.set({ cache: 'idontwants' }, idontwantsCount)
3187+
30723188
// Peer counts
30733189

30743190
for (const [topicStr, peers] of this.topics) {

src/message/decodeRpc.ts

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export interface DecodeRPCLimits {
33
maxMessages: number
44
maxIhaveMessageIDs: number
55
maxIwantMessageIDs: number
6+
maxIdontwantMessageIDs: number
67
maxControlMessages: number
78
maxPeerInfos: number
89
}
@@ -12,6 +13,7 @@ export const defaultDecodeRpcLimits: DecodeRPCLimits = {
1213
maxMessages: Infinity,
1314
maxIhaveMessageIDs: Infinity,
1415
maxIwantMessageIDs: Infinity,
16+
maxIdontwantMessageIDs: Infinity,
1517
maxControlMessages: Infinity,
1618
maxPeerInfos: Infinity
1719
}

src/message/rpc.proto

+6
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ message RPC {
2424
repeated ControlIWant iwant = 2;
2525
repeated ControlGraft graft = 3;
2626
repeated ControlPrune prune = 4;
27+
repeated ControlIDontWant idontwant = 5;
2728
}
2829

2930
message ControlIHave {
@@ -49,4 +50,9 @@ message RPC {
4950
optional bytes peerID = 1;
5051
optional bytes signedPeerRecord = 2;
5152
}
53+
54+
message ControlIDontWant {
55+
repeated bytes messageIDs = 1;
56+
}
57+
5258
}

0 commit comments

Comments
 (0)