@libp2p/gossipsub
Version:
A typescript implementation of gossipsub
178 lines (151 loc) • 5.36 kB
text/typescript
import { RejectReason } from './types.js'
import type { Metrics } from './metrics.js'
import type { MsgIdStr, MsgIdToStrFn, PeerIdStr } from './types.js'
/**
* IWantTracer is an internal tracer that tracks IWANT requests in order to penalize
* peers who don't follow up on IWANT requests after an IHAVE advertisement.
* The tracking of promises is probabilistic to avoid using too much memory.
*
* Note: Do not confuse these 'promises' with JS Promise objects.
* These 'promises' are merely expectations of a peer's behavior.
*/
export class IWantTracer {
/**
* Promises to deliver a message
* Map per message id, per peer, promise expiration time
*/
private readonly promises = new Map<MsgIdStr, Map<PeerIdStr, number>>()
/**
* First request time by msgId. Used for metrics to track expire times.
* Necessary to know if peers are actually breaking promises or simply sending them a bit later
*/
private readonly requestMsByMsg = new Map<MsgIdStr, number>()
private readonly requestMsByMsgExpire: number
constructor (
private readonly gossipsubIWantFollowupMs: number,
private readonly msgIdToStrFn: MsgIdToStrFn,
private readonly metrics: Metrics | null
) {
this.requestMsByMsgExpire = 10 * gossipsubIWantFollowupMs
}
get size (): number {
return this.promises.size
}
get requestMsByMsgSize (): number {
return this.requestMsByMsg.size
}
/**
* Track a promise to deliver a message from a list of msgIds we are requesting
*/
addPromise (from: PeerIdStr, msgIds: Uint8Array[]): void {
// pick msgId randomly from the list
const ix = Math.floor(Math.random() * msgIds.length)
const msgId = msgIds[ix]
const msgIdStr = this.msgIdToStrFn(msgId)
let expireByPeer = this.promises.get(msgIdStr)
if (expireByPeer == null) {
expireByPeer = new Map()
this.promises.set(msgIdStr, expireByPeer)
}
const now = Date.now()
// If a promise for this message id and peer already exists we don't update the expiry
if (!expireByPeer.has(from)) {
expireByPeer.set(from, now + this.gossipsubIWantFollowupMs)
if (this.metrics != null) {
this.metrics.iwantPromiseStarted.inc(1)
if (!this.requestMsByMsg.has(msgIdStr)) {
this.requestMsByMsg.set(msgIdStr, now)
}
}
}
}
/**
* Returns the number of broken promises for each peer who didn't follow up on an IWANT request.
*
* This should be called not too often relative to the expire times, since it iterates over the whole data.
*/
getBrokenPromises (): Map<PeerIdStr, number> {
const now = Date.now()
const result = new Map<PeerIdStr, number>()
let brokenPromises = 0
this.promises.forEach((expireByPeer, msgId) => {
expireByPeer.forEach((expire, p) => {
// the promise has been broken
if (expire < now) {
// add 1 to result
result.set(p, (result.get(p) ?? 0) + 1)
// delete from tracked promises
expireByPeer.delete(p)
// for metrics
brokenPromises++
}
})
// clean up empty promises for a msgId
if (expireByPeer.size === 0) {
this.promises.delete(msgId)
}
})
this.metrics?.iwantPromiseBroken.inc(brokenPromises)
return result
}
/**
* Someone delivered a message, stop tracking promises for it
*/
deliverMessage (msgIdStr: MsgIdStr, isDuplicate = false): void {
this.trackMessage(msgIdStr)
const expireByPeer = this.promises.get(msgIdStr)
// Expired promise, check requestMsByMsg
if (expireByPeer != null) {
this.promises.delete(msgIdStr)
if (this.metrics != null) {
this.metrics.iwantPromiseResolved.inc(1)
if (isDuplicate) { this.metrics.iwantPromiseResolvedFromDuplicate.inc(1) }
this.metrics.iwantPromiseResolvedPeers.inc(expireByPeer.size)
}
}
}
/**
* A message got rejected, so we can stop tracking promises and let the score penalty apply from invalid message delivery,
* unless its an obviously invalid message.
*/
rejectMessage (msgIdStr: MsgIdStr, reason: RejectReason): void {
this.trackMessage(msgIdStr)
// A message got rejected, so we can stop tracking promises and let the score penalty apply.
// With the expection of obvious invalid messages
switch (reason) {
case RejectReason.Error:
return
default:
break
}
this.promises.delete(msgIdStr)
}
clear (): void {
this.promises.clear()
}
prune (): void {
const maxMs = Date.now() - this.requestMsByMsgExpire
let count = 0
for (const [k, v] of this.requestMsByMsg.entries()) {
if (v < maxMs) {
// messages that stay too long in the requestMsByMsg map, delete
this.requestMsByMsg.delete(k)
count++
} else {
// recent messages, keep them
// sort by insertion order
break
}
}
this.metrics?.iwantMessagePruned.inc(count)
}
private trackMessage (msgIdStr: MsgIdStr): void {
if (this.metrics != null) {
const requestMs = this.requestMsByMsg.get(msgIdStr)
if (requestMs !== undefined) {
this.metrics.iwantPromiseDeliveryTime.observe((Date.now() - requestMs) / 1000)
this.requestMsByMsg.delete(msgIdStr)
}
}
}
}