@libp2p/gossipsub 14.1.1-6059227cb

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/README.md +85 -0
  2. package/dist/index.min.js +19 -0
  3. package/dist/index.min.js.map +7 -0
  4. package/dist/src/config.d.ts +32 -0
  5. package/dist/src/config.d.ts.map +1 -0
  6. package/dist/src/config.js +2 -0
  7. package/dist/src/config.js.map +1 -0
  8. package/dist/src/constants.d.ts +213 -0
  9. package/dist/src/constants.d.ts.map +1 -0
  10. package/dist/src/constants.js +217 -0
  11. package/dist/src/constants.js.map +1 -0
  12. package/dist/src/errors.d.ts +9 -0
  13. package/dist/src/errors.d.ts.map +1 -0
  14. package/dist/src/errors.js +15 -0
  15. package/dist/src/errors.js.map +1 -0
  16. package/dist/src/gossipsub.d.ts +419 -0
  17. package/dist/src/gossipsub.d.ts.map +1 -0
  18. package/dist/src/gossipsub.js +2520 -0
  19. package/dist/src/gossipsub.js.map +1 -0
  20. package/dist/src/index.d.ts +344 -0
  21. package/dist/src/index.d.ts.map +1 -0
  22. package/dist/src/index.js +43 -0
  23. package/dist/src/index.js.map +1 -0
  24. package/dist/src/message/decodeRpc.d.ts +11 -0
  25. package/dist/src/message/decodeRpc.d.ts.map +1 -0
  26. package/dist/src/message/decodeRpc.js +10 -0
  27. package/dist/src/message/decodeRpc.js.map +1 -0
  28. package/dist/src/message/index.d.ts +2 -0
  29. package/dist/src/message/index.d.ts.map +1 -0
  30. package/dist/src/message/index.js +2 -0
  31. package/dist/src/message/index.js.map +1 -0
  32. package/dist/src/message/rpc.d.ts +99 -0
  33. package/dist/src/message/rpc.d.ts.map +1 -0
  34. package/dist/src/message/rpc.js +663 -0
  35. package/dist/src/message/rpc.js.map +1 -0
  36. package/dist/src/message-cache.d.ts +80 -0
  37. package/dist/src/message-cache.d.ts.map +1 -0
  38. package/dist/src/message-cache.js +144 -0
  39. package/dist/src/message-cache.js.map +1 -0
  40. package/dist/src/metrics.d.ts +467 -0
  41. package/dist/src/metrics.d.ts.map +1 -0
  42. package/dist/src/metrics.js +896 -0
  43. package/dist/src/metrics.js.map +1 -0
  44. package/dist/src/score/compute-score.d.ts +4 -0
  45. package/dist/src/score/compute-score.d.ts.map +1 -0
  46. package/dist/src/score/compute-score.js +75 -0
  47. package/dist/src/score/compute-score.js.map +1 -0
  48. package/dist/src/score/index.d.ts +4 -0
  49. package/dist/src/score/index.d.ts.map +1 -0
  50. package/dist/src/score/index.js +4 -0
  51. package/dist/src/score/index.js.map +1 -0
  52. package/dist/src/score/message-deliveries.d.ts +45 -0
  53. package/dist/src/score/message-deliveries.d.ts.map +1 -0
  54. package/dist/src/score/message-deliveries.js +75 -0
  55. package/dist/src/score/message-deliveries.js.map +1 -0
  56. package/dist/src/score/peer-score-params.d.ts +125 -0
  57. package/dist/src/score/peer-score-params.d.ts.map +1 -0
  58. package/dist/src/score/peer-score-params.js +159 -0
  59. package/dist/src/score/peer-score-params.js.map +1 -0
  60. package/dist/src/score/peer-score-thresholds.d.ts +31 -0
  61. package/dist/src/score/peer-score-thresholds.d.ts.map +1 -0
  62. package/dist/src/score/peer-score-thresholds.js +32 -0
  63. package/dist/src/score/peer-score-thresholds.js.map +1 -0
  64. package/dist/src/score/peer-score.d.ts +119 -0
  65. package/dist/src/score/peer-score.d.ts.map +1 -0
  66. package/dist/src/score/peer-score.js +459 -0
  67. package/dist/src/score/peer-score.js.map +1 -0
  68. package/dist/src/score/peer-stats.d.ts +32 -0
  69. package/dist/src/score/peer-stats.d.ts.map +1 -0
  70. package/dist/src/score/peer-stats.js +2 -0
  71. package/dist/src/score/peer-stats.js.map +1 -0
  72. package/dist/src/score/scoreMetrics.d.ts +23 -0
  73. package/dist/src/score/scoreMetrics.d.ts.map +1 -0
  74. package/dist/src/score/scoreMetrics.js +155 -0
  75. package/dist/src/score/scoreMetrics.js.map +1 -0
  76. package/dist/src/stream.d.ts +30 -0
  77. package/dist/src/stream.d.ts.map +1 -0
  78. package/dist/src/stream.js +55 -0
  79. package/dist/src/stream.js.map +1 -0
  80. package/dist/src/tracer.d.ts +53 -0
  81. package/dist/src/tracer.d.ts.map +1 -0
  82. package/dist/src/tracer.js +155 -0
  83. package/dist/src/tracer.js.map +1 -0
  84. package/dist/src/types.d.ts +148 -0
  85. package/dist/src/types.d.ts.map +1 -0
  86. package/dist/src/types.js +90 -0
  87. package/dist/src/types.js.map +1 -0
  88. package/dist/src/utils/buildRawMessage.d.ts +20 -0
  89. package/dist/src/utils/buildRawMessage.d.ts.map +1 -0
  90. package/dist/src/utils/buildRawMessage.js +151 -0
  91. package/dist/src/utils/buildRawMessage.js.map +1 -0
  92. package/dist/src/utils/create-gossip-rpc.d.ts +7 -0
  93. package/dist/src/utils/create-gossip-rpc.d.ts.map +1 -0
  94. package/dist/src/utils/create-gossip-rpc.js +31 -0
  95. package/dist/src/utils/create-gossip-rpc.js.map +1 -0
  96. package/dist/src/utils/index.d.ts +4 -0
  97. package/dist/src/utils/index.d.ts.map +1 -0
  98. package/dist/src/utils/index.js +4 -0
  99. package/dist/src/utils/index.js.map +1 -0
  100. package/dist/src/utils/messageIdToString.d.ts +5 -0
  101. package/dist/src/utils/messageIdToString.d.ts.map +1 -0
  102. package/dist/src/utils/messageIdToString.js +8 -0
  103. package/dist/src/utils/messageIdToString.js.map +1 -0
  104. package/dist/src/utils/msgIdFn.d.ts +10 -0
  105. package/dist/src/utils/msgIdFn.d.ts.map +1 -0
  106. package/dist/src/utils/msgIdFn.js +23 -0
  107. package/dist/src/utils/msgIdFn.js.map +1 -0
  108. package/dist/src/utils/multiaddr.d.ts +3 -0
  109. package/dist/src/utils/multiaddr.d.ts.map +1 -0
  110. package/dist/src/utils/multiaddr.js +15 -0
  111. package/dist/src/utils/multiaddr.js.map +1 -0
  112. package/dist/src/utils/publishConfig.d.ts +8 -0
  113. package/dist/src/utils/publishConfig.d.ts.map +1 -0
  114. package/dist/src/utils/publishConfig.js +25 -0
  115. package/dist/src/utils/publishConfig.js.map +1 -0
  116. package/dist/src/utils/set.d.ts +14 -0
  117. package/dist/src/utils/set.d.ts.map +1 -0
  118. package/dist/src/utils/set.js +41 -0
  119. package/dist/src/utils/set.js.map +1 -0
  120. package/dist/src/utils/shuffle.d.ts +7 -0
  121. package/dist/src/utils/shuffle.d.ts.map +1 -0
  122. package/dist/src/utils/shuffle.js +21 -0
  123. package/dist/src/utils/shuffle.js.map +1 -0
  124. package/dist/src/utils/time-cache.d.ts +22 -0
  125. package/dist/src/utils/time-cache.d.ts.map +1 -0
  126. package/dist/src/utils/time-cache.js +54 -0
  127. package/dist/src/utils/time-cache.js.map +1 -0
  128. package/package.json +142 -0
  129. package/src/config.ts +31 -0
  130. package/src/constants.ts +261 -0
  131. package/src/errors.ts +17 -0
  132. package/src/gossipsub.ts +3061 -0
  133. package/src/index.ts +404 -0
  134. package/src/message/decodeRpc.ts +19 -0
  135. package/src/message/index.ts +1 -0
  136. package/src/message/rpc.proto +58 -0
  137. package/src/message/rpc.ts +848 -0
  138. package/src/message-cache.ts +196 -0
  139. package/src/metrics.ts +1014 -0
  140. package/src/score/compute-score.ts +98 -0
  141. package/src/score/index.ts +3 -0
  142. package/src/score/message-deliveries.ts +95 -0
  143. package/src/score/peer-score-params.ts +316 -0
  144. package/src/score/peer-score-thresholds.ts +70 -0
  145. package/src/score/peer-score.ts +565 -0
  146. package/src/score/peer-stats.ts +33 -0
  147. package/src/score/scoreMetrics.ts +215 -0
  148. package/src/stream.ts +79 -0
  149. package/src/tracer.ts +177 -0
  150. package/src/types.ts +178 -0
  151. package/src/utils/buildRawMessage.ts +174 -0
  152. package/src/utils/create-gossip-rpc.ts +34 -0
  153. package/src/utils/index.ts +3 -0
  154. package/src/utils/messageIdToString.ts +8 -0
  155. package/src/utils/msgIdFn.ts +24 -0
  156. package/src/utils/multiaddr.ts +19 -0
  157. package/src/utils/publishConfig.ts +33 -0
  158. package/src/utils/set.ts +43 -0
  159. package/src/utils/shuffle.ts +21 -0
  160. package/src/utils/time-cache.ts +71 -0
@@ -0,0 +1,565 @@
1
+ import { RejectReason } from '../types.js'
2
+ import { MapDef } from '../utils/set.js'
3
+ import { computeScore } from './compute-score.js'
4
+ import { MessageDeliveries, DeliveryRecordStatus } from './message-deliveries.js'
5
+ import { validatePeerScoreParams } from './peer-score-params.js'
6
+ import type { MsgIdStr, PeerIdStr, TopicStr, IPStr } from '../types.js'
7
+ import type { PeerScoreParams } from './peer-score-params.js'
8
+ import type { PeerStats, TopicStats } from './peer-stats.js'
9
+ import type { Metrics, ScorePenalty } from '../metrics.js'
10
+ import type { ComponentLogger, Logger } from '@libp2p/interface'
11
+
12
+ interface PeerScoreOpts {
13
+ /**
14
+ * Miliseconds to cache computed score per peer
15
+ */
16
+ scoreCacheValidityMs: number
17
+
18
+ computeScore?: typeof computeScore
19
+ }
20
+
21
+ interface ScoreCacheEntry {
22
+ /** The cached score */
23
+ score: number
24
+ /** Unix timestamp in miliseconds, the time after which the cached score for a peer is no longer valid */
25
+ cacheUntil: number
26
+ }
27
+
28
+ export type PeerScoreStatsDump = Record<PeerIdStr, PeerStats>
29
+
30
+ export class PeerScore {
31
+ /**
32
+ * Per-peer stats for score calculation
33
+ */
34
+ readonly peerStats = new Map<PeerIdStr, PeerStats>()
35
+ /**
36
+ * IP colocation tracking; maps IP => set of peers.
37
+ */
38
+ readonly peerIPs = new MapDef<PeerIdStr, Set<IPStr>>(() => new Set())
39
+ /**
40
+ * Cache score up to decayInterval if topic stats are unchanged.
41
+ */
42
+ readonly scoreCache = new Map<PeerIdStr, ScoreCacheEntry>()
43
+ /**
44
+ * Recent message delivery timing/participants
45
+ */
46
+ readonly deliveryRecords = new MessageDeliveries()
47
+
48
+ _backgroundInterval?: ReturnType<typeof setInterval>
49
+
50
+ private readonly scoreCacheValidityMs: number
51
+ private readonly computeScore: typeof computeScore
52
+ private readonly log: Logger
53
+
54
+ constructor (readonly params: PeerScoreParams, private readonly metrics: Metrics | null, componentLogger: ComponentLogger, opts: PeerScoreOpts) {
55
+ validatePeerScoreParams(params)
56
+ this.scoreCacheValidityMs = opts.scoreCacheValidityMs
57
+ this.computeScore = opts.computeScore ?? computeScore
58
+ this.log = componentLogger.forComponent('libp2p:gossipsub:score')
59
+ }
60
+
61
+ get size (): number {
62
+ return this.peerStats.size
63
+ }
64
+
65
+ /**
66
+ * Start PeerScore instance
67
+ */
68
+ start (): void {
69
+ if (this._backgroundInterval != null) {
70
+ this.log('Peer score already running')
71
+ return
72
+ }
73
+ this._backgroundInterval = setInterval(() => { this.background() }, this.params.decayInterval)
74
+ this.log('started')
75
+ }
76
+
77
+ /**
78
+ * Stop PeerScore instance
79
+ */
80
+ stop (): void {
81
+ if (this._backgroundInterval == null) {
82
+ this.log('Peer score already stopped')
83
+ return
84
+ }
85
+ clearInterval(this._backgroundInterval)
86
+ delete this._backgroundInterval
87
+ this.peerIPs.clear()
88
+ this.peerStats.clear()
89
+ this.deliveryRecords.clear()
90
+ this.log('stopped')
91
+ }
92
+
93
+ /**
94
+ * Periodic maintenance
95
+ */
96
+ background (): void {
97
+ this.refreshScores()
98
+ this.deliveryRecords.gc()
99
+ }
100
+
101
+ dumpPeerScoreStats (): PeerScoreStatsDump {
102
+ return Object.fromEntries(Array.from(this.peerStats.entries()).map(([peer, stats]) => [peer, stats]))
103
+ }
104
+
105
+ messageFirstSeenTimestampMs (msgIdStr: MsgIdStr): number | null {
106
+ const drec = this.deliveryRecords.getRecord(msgIdStr)
107
+ return (drec != null) ? drec.firstSeenTsMs : null
108
+ }
109
+
110
+ /**
111
+ * Decays scores, and purges score records for disconnected peers once their expiry has elapsed.
112
+ */
113
+ public refreshScores (): void {
114
+ const now = Date.now()
115
+ const decayToZero = this.params.decayToZero
116
+
117
+ this.peerStats.forEach((pstats, id) => {
118
+ if (!pstats.connected) {
119
+ // has the retention period expired?
120
+ if (now > pstats.expire) {
121
+ // yes, throw it away (but clean up the IP tracking first)
122
+ this.removeIPsForPeer(id, pstats.knownIPs)
123
+ this.peerStats.delete(id)
124
+ this.scoreCache.delete(id)
125
+ }
126
+
127
+ // we don't decay retained scores, as the peer is not active.
128
+ // this way the peer cannot reset a negative score by simply disconnecting and reconnecting,
129
+ // unless the retention period has elapsed.
130
+ // similarly, a well behaved peer does not lose its score by getting disconnected.
131
+ return
132
+ }
133
+
134
+ Object.entries(pstats.topics).forEach(([topic, tstats]) => {
135
+ const tparams = this.params.topics[topic]
136
+ if (tparams === undefined) {
137
+ // we are not scoring this topic
138
+ // should be unreachable, we only add scored topics to pstats
139
+ return
140
+ }
141
+
142
+ // decay counters
143
+ tstats.firstMessageDeliveries *= tparams.firstMessageDeliveriesDecay
144
+ if (tstats.firstMessageDeliveries < decayToZero) {
145
+ tstats.firstMessageDeliveries = 0
146
+ }
147
+
148
+ tstats.meshMessageDeliveries *= tparams.meshMessageDeliveriesDecay
149
+ if (tstats.meshMessageDeliveries < decayToZero) {
150
+ tstats.meshMessageDeliveries = 0
151
+ }
152
+
153
+ tstats.meshFailurePenalty *= tparams.meshFailurePenaltyDecay
154
+ if (tstats.meshFailurePenalty < decayToZero) {
155
+ tstats.meshFailurePenalty = 0
156
+ }
157
+
158
+ tstats.invalidMessageDeliveries *= tparams.invalidMessageDeliveriesDecay
159
+ if (tstats.invalidMessageDeliveries < decayToZero) {
160
+ tstats.invalidMessageDeliveries = 0
161
+ }
162
+
163
+ // update mesh time and activate mesh message delivery parameter if need be
164
+ if (tstats.inMesh) {
165
+ tstats.meshTime = now - tstats.graftTime
166
+ if (tstats.meshTime > tparams.meshMessageDeliveriesActivation) {
167
+ tstats.meshMessageDeliveriesActive = true
168
+ }
169
+ }
170
+ })
171
+
172
+ // decay P7 counter
173
+ pstats.behaviourPenalty *= this.params.behaviourPenaltyDecay
174
+ if (pstats.behaviourPenalty < decayToZero) {
175
+ pstats.behaviourPenalty = 0
176
+ }
177
+ })
178
+ }
179
+
180
+ /**
181
+ * Return the score for a peer
182
+ */
183
+ score (id: PeerIdStr): number {
184
+ this.metrics?.scoreFnCalls.inc()
185
+
186
+ const pstats = this.peerStats.get(id)
187
+ if (pstats == null) {
188
+ return 0
189
+ }
190
+
191
+ const now = Date.now()
192
+ const cacheEntry = this.scoreCache.get(id)
193
+
194
+ // Found cached score within validity period
195
+ if ((cacheEntry != null) && cacheEntry.cacheUntil > now) {
196
+ return cacheEntry.score
197
+ }
198
+
199
+ this.metrics?.scoreFnRuns.inc()
200
+
201
+ const score = this.computeScore(id, pstats, this.params, this.peerIPs)
202
+ const cacheUntil = now + this.scoreCacheValidityMs
203
+
204
+ if (cacheEntry != null) {
205
+ this.metrics?.scoreCachedDelta.observe(Math.abs(score - cacheEntry.score))
206
+ cacheEntry.score = score
207
+ cacheEntry.cacheUntil = cacheUntil
208
+ } else {
209
+ this.scoreCache.set(id, { score, cacheUntil })
210
+ }
211
+
212
+ return score
213
+ }
214
+
215
+ /**
216
+ * Apply a behavioural penalty to a peer
217
+ */
218
+ addPenalty (id: PeerIdStr, penalty: number, penaltyLabel: ScorePenalty): void {
219
+ const pstats = this.peerStats.get(id)
220
+ if (pstats != null) {
221
+ pstats.behaviourPenalty += penalty
222
+ this.metrics?.onScorePenalty(penaltyLabel)
223
+ }
224
+ }
225
+
226
+ addPeer (id: PeerIdStr): void {
227
+ // create peer stats (not including topic stats for each topic to be scored)
228
+ // topic stats will be added as needed
229
+ const pstats: PeerStats = {
230
+ connected: true,
231
+ expire: 0,
232
+ topics: {},
233
+ knownIPs: new Set(),
234
+ behaviourPenalty: 0
235
+ }
236
+ this.peerStats.set(id, pstats)
237
+ }
238
+
239
+ /** Adds a new IP to a peer, if the peer is not known the update is ignored */
240
+ addIP (id: PeerIdStr, ip: string): void {
241
+ const pstats = this.peerStats.get(id)
242
+ if (pstats != null) {
243
+ pstats.knownIPs.add(ip)
244
+ }
245
+
246
+ this.peerIPs.getOrDefault(ip).add(id)
247
+ }
248
+
249
+ /** Remove peer association with IP */
250
+ removeIP (id: PeerIdStr, ip: string): void {
251
+ const pstats = this.peerStats.get(id)
252
+ if (pstats != null) {
253
+ pstats.knownIPs.delete(ip)
254
+ }
255
+
256
+ const peersWithIP = this.peerIPs.get(ip)
257
+ if (peersWithIP != null) {
258
+ peersWithIP.delete(id)
259
+ if (peersWithIP.size === 0) {
260
+ this.peerIPs.delete(ip)
261
+ }
262
+ }
263
+ }
264
+
265
+ removePeer (id: PeerIdStr): void {
266
+ const pstats = this.peerStats.get(id)
267
+ if (pstats == null) {
268
+ return
269
+ }
270
+
271
+ // decide whether to retain the score; this currently only retains non-positive scores
272
+ // to dissuade attacks on the score function.
273
+ if (this.score(id) > 0) {
274
+ this.removeIPsForPeer(id, pstats.knownIPs)
275
+ this.peerStats.delete(id)
276
+ return
277
+ }
278
+
279
+ // furthermore, when we decide to retain the score, the firstMessageDelivery counters are
280
+ // reset to 0 and mesh delivery penalties applied.
281
+ Object.entries(pstats.topics).forEach(([topic, tstats]) => {
282
+ tstats.firstMessageDeliveries = 0
283
+
284
+ const threshold = this.params.topics[topic].meshMessageDeliveriesThreshold
285
+ if (tstats.inMesh && tstats.meshMessageDeliveriesActive && tstats.meshMessageDeliveries < threshold) {
286
+ const deficit = threshold - tstats.meshMessageDeliveries
287
+ tstats.meshFailurePenalty += deficit * deficit
288
+ }
289
+
290
+ tstats.inMesh = false
291
+ tstats.meshMessageDeliveriesActive = false
292
+ })
293
+
294
+ pstats.connected = false
295
+ pstats.expire = Date.now() + this.params.retainScore
296
+ }
297
+
298
+ /** Handles scoring functionality as a peer GRAFTs to a topic. */
299
+ graft (id: PeerIdStr, topic: TopicStr): void {
300
+ const pstats = this.peerStats.get(id)
301
+ if (pstats != null) {
302
+ const tstats = this.getPtopicStats(pstats, topic)
303
+ if (tstats != null) {
304
+ // if we are scoring the topic, update the mesh status.
305
+ tstats.inMesh = true
306
+ tstats.graftTime = Date.now()
307
+ tstats.meshTime = 0
308
+ tstats.meshMessageDeliveriesActive = false
309
+ }
310
+ }
311
+ }
312
+
313
+ /** Handles scoring functionality as a peer PRUNEs from a topic. */
314
+ prune (id: PeerIdStr, topic: TopicStr): void {
315
+ const pstats = this.peerStats.get(id)
316
+ if (pstats != null) {
317
+ const tstats = this.getPtopicStats(pstats, topic)
318
+ if (tstats != null) {
319
+ // sticky mesh delivery rate failure penalty
320
+ const threshold = this.params.topics[topic].meshMessageDeliveriesThreshold
321
+ if (tstats.meshMessageDeliveriesActive && tstats.meshMessageDeliveries < threshold) {
322
+ const deficit = threshold - tstats.meshMessageDeliveries
323
+ tstats.meshFailurePenalty += deficit * deficit
324
+ }
325
+ tstats.meshMessageDeliveriesActive = false
326
+ tstats.inMesh = false
327
+
328
+ // TODO: Consider clearing score cache on important penalties
329
+ // this.scoreCache.delete(id)
330
+ }
331
+ }
332
+ }
333
+
334
+ validateMessage (msgIdStr: MsgIdStr): void {
335
+ this.deliveryRecords.ensureRecord(msgIdStr)
336
+ }
337
+
338
+ deliverMessage (from: PeerIdStr, msgIdStr: MsgIdStr, topic: TopicStr): void {
339
+ this.markFirstMessageDelivery(from, topic)
340
+
341
+ const drec = this.deliveryRecords.ensureRecord(msgIdStr)
342
+ const now = Date.now()
343
+
344
+ // defensive check that this is the first delivery trace -- delivery status should be unknown
345
+ if (drec.status !== DeliveryRecordStatus.unknown) {
346
+ this.log(
347
+ 'unexpected delivery: message from %s was first seen %s ago and has delivery status %s',
348
+ from,
349
+ now - drec.firstSeenTsMs,
350
+ DeliveryRecordStatus[drec.status]
351
+ )
352
+ return
353
+ }
354
+
355
+ // mark the message as valid and reward mesh peers that have already forwarded it to us
356
+ drec.status = DeliveryRecordStatus.valid
357
+ drec.validated = now
358
+ drec.peers.forEach((p) => {
359
+ // this check is to make sure a peer can't send us a message twice and get a double count
360
+ // if it is a first delivery.
361
+ if (p !== from.toString()) {
362
+ this.markDuplicateMessageDelivery(p, topic)
363
+ }
364
+ })
365
+ }
366
+
367
+ /**
368
+ * Similar to `rejectMessage` except does not require the message id or reason for an invalid message.
369
+ */
370
+ rejectInvalidMessage (from: PeerIdStr, topic: TopicStr): void {
371
+ this.markInvalidMessageDelivery(from, topic)
372
+ }
373
+
374
+ rejectMessage (from: PeerIdStr, msgIdStr: MsgIdStr, topic: TopicStr, reason: RejectReason): void {
375
+ // eslint-disable-next-line default-case
376
+ switch (reason) {
377
+ // these messages are not tracked, but the peer is penalized as they are invalid
378
+ case RejectReason.Error:
379
+ this.markInvalidMessageDelivery(from, topic)
380
+ return
381
+
382
+ // we ignore those messages, so do nothing.
383
+ case RejectReason.Blacklisted:
384
+ return
385
+
386
+ // the rest are handled after record creation
387
+ }
388
+
389
+ const drec = this.deliveryRecords.ensureRecord(msgIdStr)
390
+
391
+ // defensive check that this is the first rejection -- delivery status should be unknown
392
+ if (drec.status !== DeliveryRecordStatus.unknown) {
393
+ this.log(
394
+ 'unexpected rejection: message from %s was first seen %s ago and has delivery status %d',
395
+ from,
396
+ Date.now() - drec.firstSeenTsMs,
397
+ DeliveryRecordStatus[drec.status]
398
+ )
399
+ return
400
+ }
401
+
402
+ if (reason === RejectReason.Ignore) {
403
+ // we were explicitly instructed by the validator to ignore the message but not penalize the peer
404
+ drec.status = DeliveryRecordStatus.ignored
405
+ drec.peers.clear()
406
+ return
407
+ }
408
+
409
+ // mark the message as invalid and penalize peers that have already forwarded it.
410
+ drec.status = DeliveryRecordStatus.invalid
411
+
412
+ this.markInvalidMessageDelivery(from, topic)
413
+ drec.peers.forEach((p) => {
414
+ this.markInvalidMessageDelivery(p, topic)
415
+ })
416
+
417
+ // release the delivery time tracking map to free some memory early
418
+ drec.peers.clear()
419
+ }
420
+
421
+ duplicateMessage (from: PeerIdStr, msgIdStr: MsgIdStr, topic: TopicStr): void {
422
+ const drec = this.deliveryRecords.ensureRecord(msgIdStr)
423
+
424
+ if (drec.peers.has(from)) {
425
+ // we have already seen this duplicate
426
+ return
427
+ }
428
+
429
+ // eslint-disable-next-line default-case
430
+ switch (drec.status) {
431
+ case DeliveryRecordStatus.unknown:
432
+ // the message is being validated; track the peer delivery and wait for
433
+ // the Deliver/Reject/Ignore notification.
434
+ drec.peers.add(from)
435
+ break
436
+
437
+ case DeliveryRecordStatus.valid:
438
+ // mark the peer delivery time to only count a duplicate delivery once.
439
+ drec.peers.add(from)
440
+ this.markDuplicateMessageDelivery(from, topic, drec.validated)
441
+ break
442
+
443
+ case DeliveryRecordStatus.invalid:
444
+ // we no longer track delivery time
445
+ this.markInvalidMessageDelivery(from, topic)
446
+ break
447
+
448
+ case DeliveryRecordStatus.ignored:
449
+ // the message was ignored; do nothing (we don't know if it was valid)
450
+ break
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Increments the "invalid message deliveries" counter for all scored topics the message is published in.
456
+ */
457
+ public markInvalidMessageDelivery (from: PeerIdStr, topic: TopicStr): void {
458
+ const pstats = this.peerStats.get(from)
459
+ if (pstats != null) {
460
+ const tstats = this.getPtopicStats(pstats, topic)
461
+ if (tstats != null) {
462
+ tstats.invalidMessageDeliveries += 1
463
+ }
464
+ }
465
+ }
466
+
467
+ /**
468
+ * Increments the "first message deliveries" counter for all scored topics the message is published in,
469
+ * as well as the "mesh message deliveries" counter, if the peer is in the mesh for the topic.
470
+ * Messages already known (with the seenCache) are counted with markDuplicateMessageDelivery()
471
+ */
472
+ public markFirstMessageDelivery (from: PeerIdStr, topic: TopicStr): void {
473
+ const pstats = this.peerStats.get(from)
474
+ if (pstats != null) {
475
+ const tstats = this.getPtopicStats(pstats, topic)
476
+ if (tstats != null) {
477
+ let cap = this.params.topics[topic].firstMessageDeliveriesCap
478
+ tstats.firstMessageDeliveries = Math.min(cap, tstats.firstMessageDeliveries + 1)
479
+
480
+ if (tstats.inMesh) {
481
+ cap = this.params.topics[topic].meshMessageDeliveriesCap
482
+ tstats.meshMessageDeliveries = Math.min(cap, tstats.meshMessageDeliveries + 1)
483
+ }
484
+ }
485
+ }
486
+ }
487
+
488
+ /**
489
+ * Increments the "mesh message deliveries" counter for messages we've seen before,
490
+ * as long the message was received within the P3 window.
491
+ */
492
+ public markDuplicateMessageDelivery (from: PeerIdStr, topic: TopicStr, validatedTime?: number): void {
493
+ const pstats = this.peerStats.get(from)
494
+ if (pstats != null) {
495
+ const now = validatedTime !== undefined ? Date.now() : 0
496
+
497
+ const tstats = this.getPtopicStats(pstats, topic)
498
+
499
+ if (tstats != null && tstats.inMesh) {
500
+ const tparams = this.params.topics[topic]
501
+
502
+ // check against the mesh delivery window -- if the validated time is passed as 0, then
503
+ // the message was received before we finished validation and thus falls within the mesh
504
+ // delivery window.
505
+ if (validatedTime !== undefined) {
506
+ const deliveryDelayMs = now - validatedTime
507
+ const isLateDelivery = deliveryDelayMs > tparams.meshMessageDeliveriesWindow
508
+ this.metrics?.onDuplicateMsgDelivery(topic, deliveryDelayMs, isLateDelivery)
509
+
510
+ if (isLateDelivery) {
511
+ return
512
+ }
513
+ }
514
+
515
+ const cap = tparams.meshMessageDeliveriesCap
516
+ tstats.meshMessageDeliveries = Math.min(cap, tstats.meshMessageDeliveries + 1)
517
+ }
518
+ }
519
+ }
520
+
521
+ /**
522
+ * Removes an IP list from the tracking list for a peer.
523
+ */
524
+ private removeIPsForPeer (id: PeerIdStr, ipsToRemove: Set<IPStr>): void {
525
+ for (const ipToRemove of ipsToRemove) {
526
+ const peerSet = this.peerIPs.get(ipToRemove)
527
+ if (peerSet != null) {
528
+ peerSet.delete(id)
529
+ if (peerSet.size === 0) {
530
+ this.peerIPs.delete(ipToRemove)
531
+ }
532
+ }
533
+ }
534
+ }
535
+
536
+ /**
537
+ * Returns topic stats if they exist, otherwise if the supplied parameters score the
538
+ * topic, inserts the default stats and returns a reference to those. If neither apply, returns None.
539
+ */
540
+ private getPtopicStats (pstats: PeerStats, topic: TopicStr): TopicStats | null {
541
+ let topicStats: TopicStats | undefined = pstats.topics[topic]
542
+
543
+ if (topicStats !== undefined) {
544
+ return topicStats
545
+ }
546
+
547
+ if (this.params.topics[topic] !== undefined) {
548
+ topicStats = {
549
+ inMesh: false,
550
+ graftTime: 0,
551
+ meshTime: 0,
552
+ firstMessageDeliveries: 0,
553
+ meshMessageDeliveries: 0,
554
+ meshMessageDeliveriesActive: false,
555
+ meshFailurePenalty: 0,
556
+ invalidMessageDeliveries: 0
557
+ }
558
+ pstats.topics[topic] = topicStats
559
+
560
+ return topicStats
561
+ }
562
+
563
+ return null
564
+ }
565
+ }
@@ -0,0 +1,33 @@
1
+ import type { TopicStr } from '../types.js'
2
+
3
+ export interface PeerStats {
4
+ /** true if the peer is currently connected */
5
+ connected: boolean
6
+ /** expiration time of the score stats for disconnected peers */
7
+ expire: number
8
+ /** per topic stats */
9
+ topics: Record<TopicStr, TopicStats>
10
+ /** IP tracking; store as set for easy processing */
11
+ knownIPs: Set<string>
12
+ /** behavioural pattern penalties (applied by the router) */
13
+ behaviourPenalty: number
14
+ }
15
+
16
+ export interface TopicStats {
17
+ /** true if the peer is in the mesh */
18
+ inMesh: boolean
19
+ /** time when the peer was (last) GRAFTed; valid only when in mesh */
20
+ graftTime: number
21
+ /** time in mesh (updated during refresh/decay to avoid calling gettimeofday on every score invocation) */
22
+ meshTime: number
23
+ /** first message deliveries */
24
+ firstMessageDeliveries: number
25
+ /** mesh message deliveries */
26
+ meshMessageDeliveries: number
27
+ /** true if the peer has been enough time in the mesh to activate mess message deliveries */
28
+ meshMessageDeliveriesActive: boolean
29
+ /** sticky mesh rate failure penalty counter */
30
+ meshFailurePenalty: number
31
+ /** invalid message counter */
32
+ invalidMessageDeliveries: number
33
+ }