diff --git a/packages/gossipsub/src/constants.ts b/packages/gossipsub/src/constants.ts index 0ffb39417b..40b2204acf 100644 --- a/packages/gossipsub/src/constants.ts +++ b/packages/gossipsub/src/constants.ts @@ -198,6 +198,12 @@ export const GossipsubPrunePeers = 16 */ export const GossipsubPruneBackoff = minute +/** + * GossipsubMaxPruneBackoff is the maximum backoff time we accept from a peer in a PRUNE message. + * Larger values are clamped to this. + */ +export const GossipsubMaxPruneBackoff = 60 * minute + /** * Backoff to use when unsubscribing from a topic. Should not resubscribe to this topic before it expired. */ diff --git a/packages/gossipsub/src/gossipsub.ts b/packages/gossipsub/src/gossipsub.ts index fb9055078d..f41a9194d3 100644 --- a/packages/gossipsub/src/gossipsub.ts +++ b/packages/gossipsub/src/gossipsub.ts @@ -1619,7 +1619,7 @@ export class GossipSub extends TypedEventEmitter implements Typ const peersInMesh = this.mesh.get(topicID) if (peersInMesh == null) { - return + continue } this.log('PRUNE: Remove mesh link to %s in %s', id, topicID) @@ -1631,7 +1631,7 @@ export class GossipSub extends TypedEventEmitter implements Typ // is there a backoff specified by the peer? if so obey it if (typeof backoff === 'number' && backoff > 0) { - this.doAddBackoff(id, topicID, backoff * 1000) + this.doAddBackoff(id, topicID, Math.min(backoff * 1000, constants.GossipsubMaxPruneBackoff)) } else { this.addBackoff(id, topicID) } diff --git a/packages/gossipsub/test/prune.spec.ts b/packages/gossipsub/test/prune.spec.ts new file mode 100644 index 0000000000..3ab64904bd --- /dev/null +++ b/packages/gossipsub/test/prune.spec.ts @@ -0,0 +1,48 @@ +import { stop } from '@libp2p/interface' +import { expect } from 'aegir/chai' +import { GossipsubMaxPruneBackoff } from '../src/constants.ts' +import { createComponents } from './utils/create-pubsub.ts' +import { createPeerId } from './utils/index.ts' +import type { GossipSubAndComponents } from './utils/create-pubsub.ts' + +const TOPIC = 'prune-topic' + +describe('handlePrune', () => { + let node: GossipSubAndComponents + let peer: string + + beforeEach(async () => { + node = await createComponents({ init: {} }) + peer = (await createPeerId()).toString() + }) + + afterEach(async () => { + await stop(node.pubsub, ...Object.values(node.components)) + }) + + it('caps the backoff accepted from a peer at GossipsubMaxPruneBackoff', async () => { + const gs = node.pubsub as any + gs.mesh.set(TOPIC, new Set([peer])) + + const before = Date.now() + await gs.handlePrune(peer, [{ topicID: TOPIC, peers: [], backoff: Number.MAX_SAFE_INTEGER }]) + const after = Date.now() + + const expire = gs.backoff.get(TOPIC)?.get(peer) + expect(expire).to.be.at.least(before + GossipsubMaxPruneBackoff) + expect(expire).to.be.at.most(after + GossipsubMaxPruneBackoff) + }) + + it('processes remaining entries after a topic that is not in the mesh', async () => { + const gs = node.pubsub as any + gs.mesh.set(TOPIC, new Set([peer])) + + await gs.handlePrune(peer, [ + { topicID: 'not-joined-topic', peers: [] }, + { topicID: TOPIC, peers: [] } + ]) + + expect(node.pubsub.mesh.get(TOPIC)?.has(peer)).to.equal(false) + expect(gs.backoff.get(TOPIC)?.has(peer)).to.equal(true) + }) +})