@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.
- package/README.md +85 -0
- package/dist/index.min.js +19 -0
- package/dist/index.min.js.map +7 -0
- package/dist/src/config.d.ts +32 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +2 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/constants.d.ts +213 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +217 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/errors.d.ts +9 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +15 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/gossipsub.d.ts +419 -0
- package/dist/src/gossipsub.d.ts.map +1 -0
- package/dist/src/gossipsub.js +2520 -0
- package/dist/src/gossipsub.js.map +1 -0
- package/dist/src/index.d.ts +344 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +43 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/message/decodeRpc.d.ts +11 -0
- package/dist/src/message/decodeRpc.d.ts.map +1 -0
- package/dist/src/message/decodeRpc.js +10 -0
- package/dist/src/message/decodeRpc.js.map +1 -0
- package/dist/src/message/index.d.ts +2 -0
- package/dist/src/message/index.d.ts.map +1 -0
- package/dist/src/message/index.js +2 -0
- package/dist/src/message/index.js.map +1 -0
- package/dist/src/message/rpc.d.ts +99 -0
- package/dist/src/message/rpc.d.ts.map +1 -0
- package/dist/src/message/rpc.js +663 -0
- package/dist/src/message/rpc.js.map +1 -0
- package/dist/src/message-cache.d.ts +80 -0
- package/dist/src/message-cache.d.ts.map +1 -0
- package/dist/src/message-cache.js +144 -0
- package/dist/src/message-cache.js.map +1 -0
- package/dist/src/metrics.d.ts +467 -0
- package/dist/src/metrics.d.ts.map +1 -0
- package/dist/src/metrics.js +896 -0
- package/dist/src/metrics.js.map +1 -0
- package/dist/src/score/compute-score.d.ts +4 -0
- package/dist/src/score/compute-score.d.ts.map +1 -0
- package/dist/src/score/compute-score.js +75 -0
- package/dist/src/score/compute-score.js.map +1 -0
- package/dist/src/score/index.d.ts +4 -0
- package/dist/src/score/index.d.ts.map +1 -0
- package/dist/src/score/index.js +4 -0
- package/dist/src/score/index.js.map +1 -0
- package/dist/src/score/message-deliveries.d.ts +45 -0
- package/dist/src/score/message-deliveries.d.ts.map +1 -0
- package/dist/src/score/message-deliveries.js +75 -0
- package/dist/src/score/message-deliveries.js.map +1 -0
- package/dist/src/score/peer-score-params.d.ts +125 -0
- package/dist/src/score/peer-score-params.d.ts.map +1 -0
- package/dist/src/score/peer-score-params.js +159 -0
- package/dist/src/score/peer-score-params.js.map +1 -0
- package/dist/src/score/peer-score-thresholds.d.ts +31 -0
- package/dist/src/score/peer-score-thresholds.d.ts.map +1 -0
- package/dist/src/score/peer-score-thresholds.js +32 -0
- package/dist/src/score/peer-score-thresholds.js.map +1 -0
- package/dist/src/score/peer-score.d.ts +119 -0
- package/dist/src/score/peer-score.d.ts.map +1 -0
- package/dist/src/score/peer-score.js +459 -0
- package/dist/src/score/peer-score.js.map +1 -0
- package/dist/src/score/peer-stats.d.ts +32 -0
- package/dist/src/score/peer-stats.d.ts.map +1 -0
- package/dist/src/score/peer-stats.js +2 -0
- package/dist/src/score/peer-stats.js.map +1 -0
- package/dist/src/score/scoreMetrics.d.ts +23 -0
- package/dist/src/score/scoreMetrics.d.ts.map +1 -0
- package/dist/src/score/scoreMetrics.js +155 -0
- package/dist/src/score/scoreMetrics.js.map +1 -0
- package/dist/src/stream.d.ts +30 -0
- package/dist/src/stream.d.ts.map +1 -0
- package/dist/src/stream.js +55 -0
- package/dist/src/stream.js.map +1 -0
- package/dist/src/tracer.d.ts +53 -0
- package/dist/src/tracer.d.ts.map +1 -0
- package/dist/src/tracer.js +155 -0
- package/dist/src/tracer.js.map +1 -0
- package/dist/src/types.d.ts +148 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +90 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/buildRawMessage.d.ts +20 -0
- package/dist/src/utils/buildRawMessage.d.ts.map +1 -0
- package/dist/src/utils/buildRawMessage.js +151 -0
- package/dist/src/utils/buildRawMessage.js.map +1 -0
- package/dist/src/utils/create-gossip-rpc.d.ts +7 -0
- package/dist/src/utils/create-gossip-rpc.d.ts.map +1 -0
- package/dist/src/utils/create-gossip-rpc.js +31 -0
- package/dist/src/utils/create-gossip-rpc.js.map +1 -0
- package/dist/src/utils/index.d.ts +4 -0
- package/dist/src/utils/index.d.ts.map +1 -0
- package/dist/src/utils/index.js +4 -0
- package/dist/src/utils/index.js.map +1 -0
- package/dist/src/utils/messageIdToString.d.ts +5 -0
- package/dist/src/utils/messageIdToString.d.ts.map +1 -0
- package/dist/src/utils/messageIdToString.js +8 -0
- package/dist/src/utils/messageIdToString.js.map +1 -0
- package/dist/src/utils/msgIdFn.d.ts +10 -0
- package/dist/src/utils/msgIdFn.d.ts.map +1 -0
- package/dist/src/utils/msgIdFn.js +23 -0
- package/dist/src/utils/msgIdFn.js.map +1 -0
- package/dist/src/utils/multiaddr.d.ts +3 -0
- package/dist/src/utils/multiaddr.d.ts.map +1 -0
- package/dist/src/utils/multiaddr.js +15 -0
- package/dist/src/utils/multiaddr.js.map +1 -0
- package/dist/src/utils/publishConfig.d.ts +8 -0
- package/dist/src/utils/publishConfig.d.ts.map +1 -0
- package/dist/src/utils/publishConfig.js +25 -0
- package/dist/src/utils/publishConfig.js.map +1 -0
- package/dist/src/utils/set.d.ts +14 -0
- package/dist/src/utils/set.d.ts.map +1 -0
- package/dist/src/utils/set.js +41 -0
- package/dist/src/utils/set.js.map +1 -0
- package/dist/src/utils/shuffle.d.ts +7 -0
- package/dist/src/utils/shuffle.d.ts.map +1 -0
- package/dist/src/utils/shuffle.js +21 -0
- package/dist/src/utils/shuffle.js.map +1 -0
- package/dist/src/utils/time-cache.d.ts +22 -0
- package/dist/src/utils/time-cache.d.ts.map +1 -0
- package/dist/src/utils/time-cache.js +54 -0
- package/dist/src/utils/time-cache.js.map +1 -0
- package/package.json +142 -0
- package/src/config.ts +31 -0
- package/src/constants.ts +261 -0
- package/src/errors.ts +17 -0
- package/src/gossipsub.ts +3061 -0
- package/src/index.ts +404 -0
- package/src/message/decodeRpc.ts +19 -0
- package/src/message/index.ts +1 -0
- package/src/message/rpc.proto +58 -0
- package/src/message/rpc.ts +848 -0
- package/src/message-cache.ts +196 -0
- package/src/metrics.ts +1014 -0
- package/src/score/compute-score.ts +98 -0
- package/src/score/index.ts +3 -0
- package/src/score/message-deliveries.ts +95 -0
- package/src/score/peer-score-params.ts +316 -0
- package/src/score/peer-score-thresholds.ts +70 -0
- package/src/score/peer-score.ts +565 -0
- package/src/score/peer-stats.ts +33 -0
- package/src/score/scoreMetrics.ts +215 -0
- package/src/stream.ts +79 -0
- package/src/tracer.ts +177 -0
- package/src/types.ts +178 -0
- package/src/utils/buildRawMessage.ts +174 -0
- package/src/utils/create-gossip-rpc.ts +34 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/messageIdToString.ts +8 -0
- package/src/utils/msgIdFn.ts +24 -0
- package/src/utils/multiaddr.ts +19 -0
- package/src/utils/publishConfig.ts +33 -0
- package/src/utils/set.ts +43 -0
- package/src/utils/shuffle.ts +21 -0
- package/src/utils/time-cache.ts +71 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import type { PeerScoreParams } from './peer-score-params.js'
|
|
2
|
+
import type { PeerStats } from './peer-stats.js'
|
|
3
|
+
|
|
4
|
+
type TopicLabel = string
|
|
5
|
+
type TopicStr = string
|
|
6
|
+
type TopicStrToLabel = Map<TopicStr, TopicLabel>
|
|
7
|
+
|
|
8
|
+
export interface TopicScoreWeights<T> {
|
|
9
|
+
p1w: T
|
|
10
|
+
p2w: T
|
|
11
|
+
p3w: T
|
|
12
|
+
p3bw: T
|
|
13
|
+
p4w: T
|
|
14
|
+
}
|
|
15
|
+
export interface ScoreWeights<T> {
|
|
16
|
+
byTopic: Map<TopicLabel, TopicScoreWeights<T>>
|
|
17
|
+
p5w: T
|
|
18
|
+
p6w: T
|
|
19
|
+
p7w: T
|
|
20
|
+
score: T
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function computeScoreWeights (
|
|
24
|
+
peer: string,
|
|
25
|
+
pstats: PeerStats,
|
|
26
|
+
params: PeerScoreParams,
|
|
27
|
+
peerIPs: Map<string, Set<string>>,
|
|
28
|
+
topicStrToLabel: TopicStrToLabel
|
|
29
|
+
): ScoreWeights<number> {
|
|
30
|
+
let score = 0
|
|
31
|
+
|
|
32
|
+
const byTopic = new Map<TopicLabel, TopicScoreWeights<number>>()
|
|
33
|
+
|
|
34
|
+
// topic stores
|
|
35
|
+
Object.entries(pstats.topics).forEach(([topic, tstats]) => {
|
|
36
|
+
// the topic parameters
|
|
37
|
+
// Aggregate by known topicLabel or throw to 'unknown'. This prevent too high cardinality
|
|
38
|
+
const topicLabel = topicStrToLabel.get(topic) ?? 'unknown'
|
|
39
|
+
const topicParams = params.topics[topic]
|
|
40
|
+
if (topicParams === undefined) {
|
|
41
|
+
// we are not scoring this topic
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let topicScores = byTopic.get(topicLabel)
|
|
46
|
+
if (topicScores == null) {
|
|
47
|
+
topicScores = {
|
|
48
|
+
p1w: 0,
|
|
49
|
+
p2w: 0,
|
|
50
|
+
p3w: 0,
|
|
51
|
+
p3bw: 0,
|
|
52
|
+
p4w: 0
|
|
53
|
+
}
|
|
54
|
+
byTopic.set(topicLabel, topicScores)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let p1w = 0
|
|
58
|
+
let p2w = 0
|
|
59
|
+
let p3w = 0
|
|
60
|
+
let p3bw = 0
|
|
61
|
+
let p4w = 0
|
|
62
|
+
|
|
63
|
+
// P1: time in Mesh
|
|
64
|
+
if (tstats.inMesh) {
|
|
65
|
+
const p1 = Math.max(tstats.meshTime / topicParams.timeInMeshQuantum, topicParams.timeInMeshCap)
|
|
66
|
+
p1w += p1 * topicParams.timeInMeshWeight
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// P2: first message deliveries
|
|
70
|
+
let p2 = tstats.firstMessageDeliveries
|
|
71
|
+
if (p2 > topicParams.firstMessageDeliveriesCap) {
|
|
72
|
+
p2 = topicParams.firstMessageDeliveriesCap
|
|
73
|
+
}
|
|
74
|
+
p2w += p2 * topicParams.firstMessageDeliveriesWeight
|
|
75
|
+
|
|
76
|
+
// P3: mesh message deliveries
|
|
77
|
+
if (
|
|
78
|
+
tstats.meshMessageDeliveriesActive &&
|
|
79
|
+
tstats.meshMessageDeliveries < topicParams.meshMessageDeliveriesThreshold
|
|
80
|
+
) {
|
|
81
|
+
const deficit = topicParams.meshMessageDeliveriesThreshold - tstats.meshMessageDeliveries
|
|
82
|
+
const p3 = deficit * deficit
|
|
83
|
+
p3w += p3 * topicParams.meshMessageDeliveriesWeight
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// P3b:
|
|
87
|
+
// NOTE: the weight of P3b is negative (validated in validateTopicScoreParams) so this detracts
|
|
88
|
+
const p3b = tstats.meshFailurePenalty
|
|
89
|
+
p3bw += p3b * topicParams.meshFailurePenaltyWeight
|
|
90
|
+
|
|
91
|
+
// P4: invalid messages
|
|
92
|
+
// NOTE: the weight of P4 is negative (validated in validateTopicScoreParams) so this detracts
|
|
93
|
+
const p4 = tstats.invalidMessageDeliveries * tstats.invalidMessageDeliveries
|
|
94
|
+
p4w += p4 * topicParams.invalidMessageDeliveriesWeight
|
|
95
|
+
|
|
96
|
+
// update score, mixing with topic weight
|
|
97
|
+
score += (p1w + p2w + p3w + p3bw + p4w) * topicParams.topicWeight
|
|
98
|
+
|
|
99
|
+
topicScores.p1w += p1w
|
|
100
|
+
topicScores.p2w += p2w
|
|
101
|
+
topicScores.p3w += p3w
|
|
102
|
+
topicScores.p3bw += p3bw
|
|
103
|
+
topicScores.p4w += p4w
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// apply the topic score cap, if any
|
|
107
|
+
if (params.topicScoreCap > 0 && score > params.topicScoreCap) {
|
|
108
|
+
score = params.topicScoreCap
|
|
109
|
+
|
|
110
|
+
// Proportionally apply cap to all individual contributions
|
|
111
|
+
const capF = params.topicScoreCap / score
|
|
112
|
+
for (const ws of byTopic.values()) {
|
|
113
|
+
ws.p1w *= capF
|
|
114
|
+
ws.p2w *= capF
|
|
115
|
+
ws.p3w *= capF
|
|
116
|
+
ws.p3bw *= capF
|
|
117
|
+
ws.p4w *= capF
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let p5w = 0
|
|
122
|
+
let p6w = 0
|
|
123
|
+
let p7w = 0
|
|
124
|
+
|
|
125
|
+
// P5: application-specific score
|
|
126
|
+
const p5 = params.appSpecificScore(peer)
|
|
127
|
+
p5w += p5 * params.appSpecificWeight
|
|
128
|
+
|
|
129
|
+
// P6: IP colocation factor
|
|
130
|
+
pstats.knownIPs.forEach((ip) => {
|
|
131
|
+
if (params.IPColocationFactorWhitelist.has(ip)) {
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// P6 has a cliff (IPColocationFactorThreshold)
|
|
136
|
+
// It's only applied if at least that many peers are connected to us from that source IP addr.
|
|
137
|
+
// It is quadratic, and the weight is negative (validated in validatePeerScoreParams)
|
|
138
|
+
const peersInIP = peerIPs.get(ip)
|
|
139
|
+
const numPeersInIP = (peersInIP != null) ? peersInIP.size : 0
|
|
140
|
+
if (numPeersInIP > params.IPColocationFactorThreshold) {
|
|
141
|
+
const surplus = numPeersInIP - params.IPColocationFactorThreshold
|
|
142
|
+
const p6 = surplus * surplus
|
|
143
|
+
p6w += p6 * params.IPColocationFactorWeight
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// P7: behavioural pattern penalty
|
|
148
|
+
const p7 = pstats.behaviourPenalty * pstats.behaviourPenalty
|
|
149
|
+
p7w += p7 * params.behaviourPenaltyWeight
|
|
150
|
+
|
|
151
|
+
score += p5w + p6w + p7w
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
byTopic,
|
|
155
|
+
p5w,
|
|
156
|
+
p6w,
|
|
157
|
+
p7w,
|
|
158
|
+
score
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function computeAllPeersScoreWeights (
|
|
163
|
+
peerIdStrs: Iterable<string>,
|
|
164
|
+
peerStats: Map<string, PeerStats>,
|
|
165
|
+
params: PeerScoreParams,
|
|
166
|
+
peerIPs: Map<string, Set<string>>,
|
|
167
|
+
topicStrToLabel: TopicStrToLabel
|
|
168
|
+
): ScoreWeights<number[]> {
|
|
169
|
+
const sw: ScoreWeights<number[]> = {
|
|
170
|
+
byTopic: new Map(),
|
|
171
|
+
p5w: [],
|
|
172
|
+
p6w: [],
|
|
173
|
+
p7w: [],
|
|
174
|
+
score: []
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const peerIdStr of peerIdStrs) {
|
|
178
|
+
const pstats = peerStats.get(peerIdStr)
|
|
179
|
+
if (pstats != null) {
|
|
180
|
+
const swPeer = computeScoreWeights(peerIdStr, pstats, params, peerIPs, topicStrToLabel)
|
|
181
|
+
|
|
182
|
+
for (const [topic, swPeerTopic] of swPeer.byTopic) {
|
|
183
|
+
let swTopic = sw.byTopic.get(topic)
|
|
184
|
+
if (swTopic == null) {
|
|
185
|
+
swTopic = {
|
|
186
|
+
p1w: [],
|
|
187
|
+
p2w: [],
|
|
188
|
+
p3w: [],
|
|
189
|
+
p3bw: [],
|
|
190
|
+
p4w: []
|
|
191
|
+
}
|
|
192
|
+
sw.byTopic.set(topic, swTopic)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
swTopic.p1w.push(swPeerTopic.p1w)
|
|
196
|
+
swTopic.p2w.push(swPeerTopic.p2w)
|
|
197
|
+
swTopic.p3w.push(swPeerTopic.p3w)
|
|
198
|
+
swTopic.p3bw.push(swPeerTopic.p3bw)
|
|
199
|
+
swTopic.p4w.push(swPeerTopic.p4w)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
sw.p5w.push(swPeer.p5w)
|
|
203
|
+
sw.p6w.push(swPeer.p6w)
|
|
204
|
+
sw.p7w.push(swPeer.p7w)
|
|
205
|
+
sw.score.push(swPeer.score)
|
|
206
|
+
} else {
|
|
207
|
+
sw.p5w.push(0)
|
|
208
|
+
sw.p6w.push(0)
|
|
209
|
+
sw.p7w.push(0)
|
|
210
|
+
sw.score.push(0)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return sw
|
|
215
|
+
}
|
package/src/stream.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { pipe } from '@libp2p/utils'
|
|
2
|
+
import { encode, decode } from 'it-length-prefixed'
|
|
3
|
+
import type { AbortOptions, Stream } from '@libp2p/interface'
|
|
4
|
+
import type { Uint8ArrayList } from 'uint8arraylist'
|
|
5
|
+
|
|
6
|
+
interface OutboundStreamOpts {
|
|
7
|
+
/** Max size in bytes for pushable buffer. If full, will throw on .push */
|
|
8
|
+
maxBufferSize?: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface InboundStreamOpts {
|
|
12
|
+
/** Max size in bytes for reading messages from the stream */
|
|
13
|
+
maxDataLength?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class OutboundStream {
|
|
17
|
+
constructor (private readonly rawStream: Stream, errCallback: (e: Error) => void, opts: OutboundStreamOpts) {
|
|
18
|
+
if (opts.maxBufferSize != null) {
|
|
19
|
+
rawStream.maxWriteBufferLength = opts.maxBufferSize
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
rawStream.addEventListener('close', (evt) => {
|
|
23
|
+
if (evt.error != null) {
|
|
24
|
+
errCallback(evt.error)
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get protocol (): string {
|
|
30
|
+
return this.rawStream.protocol
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async push (data: Uint8Array): Promise<void> {
|
|
34
|
+
return this.pushPrefixed(encode.single(data))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Same to push() but this is prefixed data so no need to encode length prefixed again
|
|
39
|
+
*/
|
|
40
|
+
pushPrefixed (data: Uint8ArrayList): void {
|
|
41
|
+
// TODO: backpressure
|
|
42
|
+
this.rawStream.send(data)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async close (options?: AbortOptions): Promise<void> {
|
|
46
|
+
await this.rawStream.close(options)
|
|
47
|
+
.catch(err => {
|
|
48
|
+
this.rawStream.abort(err)
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class InboundStream {
|
|
54
|
+
public readonly source: AsyncIterable<Uint8ArrayList>
|
|
55
|
+
|
|
56
|
+
private readonly rawStream: Stream
|
|
57
|
+
private readonly closeController: AbortController
|
|
58
|
+
|
|
59
|
+
constructor (rawStream: Stream, opts: InboundStreamOpts = {}) {
|
|
60
|
+
this.rawStream = rawStream
|
|
61
|
+
this.closeController = new AbortController()
|
|
62
|
+
|
|
63
|
+
this.closeController.signal.addEventListener('abort', () => {
|
|
64
|
+
rawStream.close()
|
|
65
|
+
.catch(err => {
|
|
66
|
+
rawStream.abort(err)
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
this.source = pipe(
|
|
71
|
+
this.rawStream,
|
|
72
|
+
(source) => decode(source, opts)
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async close (): Promise<void> {
|
|
77
|
+
this.closeController.abort()
|
|
78
|
+
}
|
|
79
|
+
}
|
package/src/tracer.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { RejectReason } from './types.js'
|
|
2
|
+
import type { Metrics } from './metrics.js'
|
|
3
|
+
import type { MsgIdStr, MsgIdToStrFn, PeerIdStr } from './types.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* IWantTracer is an internal tracer that tracks IWANT requests in order to penalize
|
|
7
|
+
* peers who don't follow up on IWANT requests after an IHAVE advertisement.
|
|
8
|
+
* The tracking of promises is probabilistic to avoid using too much memory.
|
|
9
|
+
*
|
|
10
|
+
* Note: Do not confuse these 'promises' with JS Promise objects.
|
|
11
|
+
* These 'promises' are merely expectations of a peer's behavior.
|
|
12
|
+
*/
|
|
13
|
+
export class IWantTracer {
|
|
14
|
+
/**
|
|
15
|
+
* Promises to deliver a message
|
|
16
|
+
* Map per message id, per peer, promise expiration time
|
|
17
|
+
*/
|
|
18
|
+
private readonly promises = new Map<MsgIdStr, Map<PeerIdStr, number>>()
|
|
19
|
+
/**
|
|
20
|
+
* First request time by msgId. Used for metrics to track expire times.
|
|
21
|
+
* Necessary to know if peers are actually breaking promises or simply sending them a bit later
|
|
22
|
+
*/
|
|
23
|
+
private readonly requestMsByMsg = new Map<MsgIdStr, number>()
|
|
24
|
+
private readonly requestMsByMsgExpire: number
|
|
25
|
+
|
|
26
|
+
constructor (
|
|
27
|
+
private readonly gossipsubIWantFollowupMs: number,
|
|
28
|
+
private readonly msgIdToStrFn: MsgIdToStrFn,
|
|
29
|
+
private readonly metrics: Metrics | null
|
|
30
|
+
) {
|
|
31
|
+
this.requestMsByMsgExpire = 10 * gossipsubIWantFollowupMs
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get size (): number {
|
|
35
|
+
return this.promises.size
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get requestMsByMsgSize (): number {
|
|
39
|
+
return this.requestMsByMsg.size
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Track a promise to deliver a message from a list of msgIds we are requesting
|
|
44
|
+
*/
|
|
45
|
+
addPromise (from: PeerIdStr, msgIds: Uint8Array[]): void {
|
|
46
|
+
// pick msgId randomly from the list
|
|
47
|
+
const ix = Math.floor(Math.random() * msgIds.length)
|
|
48
|
+
const msgId = msgIds[ix]
|
|
49
|
+
const msgIdStr = this.msgIdToStrFn(msgId)
|
|
50
|
+
|
|
51
|
+
let expireByPeer = this.promises.get(msgIdStr)
|
|
52
|
+
if (expireByPeer == null) {
|
|
53
|
+
expireByPeer = new Map()
|
|
54
|
+
this.promises.set(msgIdStr, expireByPeer)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const now = Date.now()
|
|
58
|
+
|
|
59
|
+
// If a promise for this message id and peer already exists we don't update the expiry
|
|
60
|
+
if (!expireByPeer.has(from)) {
|
|
61
|
+
expireByPeer.set(from, now + this.gossipsubIWantFollowupMs)
|
|
62
|
+
|
|
63
|
+
if (this.metrics != null) {
|
|
64
|
+
this.metrics.iwantPromiseStarted.inc(1)
|
|
65
|
+
if (!this.requestMsByMsg.has(msgIdStr)) {
|
|
66
|
+
this.requestMsByMsg.set(msgIdStr, now)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Returns the number of broken promises for each peer who didn't follow up on an IWANT request.
|
|
74
|
+
*
|
|
75
|
+
* This should be called not too often relative to the expire times, since it iterates over the whole data.
|
|
76
|
+
*/
|
|
77
|
+
getBrokenPromises (): Map<PeerIdStr, number> {
|
|
78
|
+
const now = Date.now()
|
|
79
|
+
const result = new Map<PeerIdStr, number>()
|
|
80
|
+
|
|
81
|
+
let brokenPromises = 0
|
|
82
|
+
|
|
83
|
+
this.promises.forEach((expireByPeer, msgId) => {
|
|
84
|
+
expireByPeer.forEach((expire, p) => {
|
|
85
|
+
// the promise has been broken
|
|
86
|
+
if (expire < now) {
|
|
87
|
+
// add 1 to result
|
|
88
|
+
result.set(p, (result.get(p) ?? 0) + 1)
|
|
89
|
+
// delete from tracked promises
|
|
90
|
+
expireByPeer.delete(p)
|
|
91
|
+
// for metrics
|
|
92
|
+
brokenPromises++
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
// clean up empty promises for a msgId
|
|
96
|
+
if (expireByPeer.size === 0) {
|
|
97
|
+
this.promises.delete(msgId)
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
this.metrics?.iwantPromiseBroken.inc(brokenPromises)
|
|
102
|
+
|
|
103
|
+
return result
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Someone delivered a message, stop tracking promises for it
|
|
108
|
+
*/
|
|
109
|
+
deliverMessage (msgIdStr: MsgIdStr, isDuplicate = false): void {
|
|
110
|
+
this.trackMessage(msgIdStr)
|
|
111
|
+
|
|
112
|
+
const expireByPeer = this.promises.get(msgIdStr)
|
|
113
|
+
|
|
114
|
+
// Expired promise, check requestMsByMsg
|
|
115
|
+
if (expireByPeer != null) {
|
|
116
|
+
this.promises.delete(msgIdStr)
|
|
117
|
+
|
|
118
|
+
if (this.metrics != null) {
|
|
119
|
+
this.metrics.iwantPromiseResolved.inc(1)
|
|
120
|
+
if (isDuplicate) { this.metrics.iwantPromiseResolvedFromDuplicate.inc(1) }
|
|
121
|
+
this.metrics.iwantPromiseResolvedPeers.inc(expireByPeer.size)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* A message got rejected, so we can stop tracking promises and let the score penalty apply from invalid message delivery,
|
|
128
|
+
* unless its an obviously invalid message.
|
|
129
|
+
*/
|
|
130
|
+
rejectMessage (msgIdStr: MsgIdStr, reason: RejectReason): void {
|
|
131
|
+
this.trackMessage(msgIdStr)
|
|
132
|
+
|
|
133
|
+
// A message got rejected, so we can stop tracking promises and let the score penalty apply.
|
|
134
|
+
// With the expection of obvious invalid messages
|
|
135
|
+
switch (reason) {
|
|
136
|
+
case RejectReason.Error:
|
|
137
|
+
return
|
|
138
|
+
default:
|
|
139
|
+
break
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
this.promises.delete(msgIdStr)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
clear (): void {
|
|
146
|
+
this.promises.clear()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
prune (): void {
|
|
150
|
+
const maxMs = Date.now() - this.requestMsByMsgExpire
|
|
151
|
+
let count = 0
|
|
152
|
+
|
|
153
|
+
for (const [k, v] of this.requestMsByMsg.entries()) {
|
|
154
|
+
if (v < maxMs) {
|
|
155
|
+
// messages that stay too long in the requestMsByMsg map, delete
|
|
156
|
+
this.requestMsByMsg.delete(k)
|
|
157
|
+
count++
|
|
158
|
+
} else {
|
|
159
|
+
// recent messages, keep them
|
|
160
|
+
// sort by insertion order
|
|
161
|
+
break
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this.metrics?.iwantMessagePruned.inc(count)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private trackMessage (msgIdStr: MsgIdStr): void {
|
|
169
|
+
if (this.metrics != null) {
|
|
170
|
+
const requestMs = this.requestMsByMsg.get(msgIdStr)
|
|
171
|
+
if (requestMs !== undefined) {
|
|
172
|
+
this.metrics.iwantPromiseDeliveryTime.observe((Date.now() - requestMs) / 1000)
|
|
173
|
+
this.requestMsByMsg.delete(msgIdStr)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { TopicValidatorResult } from './index.ts'
|
|
2
|
+
import type { Message } from './index.ts'
|
|
3
|
+
import type { RPC } from './message/rpc.js'
|
|
4
|
+
import type { PrivateKey, PeerId } from '@libp2p/interface'
|
|
5
|
+
import type { Multiaddr } from '@multiformats/multiaddr'
|
|
6
|
+
|
|
7
|
+
export type MsgIdStr = string
|
|
8
|
+
export type PeerIdStr = string
|
|
9
|
+
export type TopicStr = string
|
|
10
|
+
export type IPStr = string
|
|
11
|
+
|
|
12
|
+
export interface AddrInfo {
|
|
13
|
+
id: PeerId
|
|
14
|
+
addrs: Multiaddr[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Compute a local non-spec'ed msg-id for faster de-duplication of seen messages.
|
|
19
|
+
* Used exclusively for a local seen_cache
|
|
20
|
+
*/
|
|
21
|
+
export interface FastMsgIdFn { (msg: RPC.Message): string | number }
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* By default, gossipsub only provide a browser friendly function to convert Uint8Array message id to string.
|
|
25
|
+
* Application could use this option to provide a more efficient function.
|
|
26
|
+
*/
|
|
27
|
+
export interface MsgIdToStrFn { (msgId: Uint8Array): string }
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Compute spec'ed msg-id. Used for IHAVE / IWANT messages
|
|
31
|
+
*/
|
|
32
|
+
export interface MsgIdFn {
|
|
33
|
+
(msg: Message): Promise<Uint8Array> | Uint8Array
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DataTransform {
|
|
37
|
+
/**
|
|
38
|
+
* Takes the data published by peers on a topic and transforms the data.
|
|
39
|
+
* Should be the reverse of outboundTransform(). Example:
|
|
40
|
+
* - `inboundTransform()`: decompress snappy payload
|
|
41
|
+
* - `outboundTransform()`: compress snappy payload
|
|
42
|
+
*/
|
|
43
|
+
inboundTransform(topic: TopicStr, data: Uint8Array): Uint8Array
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Takes the data to be published (a topic and associated data) transforms the data. The
|
|
47
|
+
* transformed data will then be used to create a `RawGossipsubMessage` to be sent to peers.
|
|
48
|
+
*/
|
|
49
|
+
outboundTransform(topic: TopicStr, data: Uint8Array): Uint8Array
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export enum SignaturePolicy {
|
|
53
|
+
/**
|
|
54
|
+
* On the producing side:
|
|
55
|
+
* - Build messages with the signature, key (from may be enough for certain inlineable public key types), from and seqno fields.
|
|
56
|
+
*
|
|
57
|
+
* On the consuming side:
|
|
58
|
+
* - Enforce the fields to be present, reject otherwise.
|
|
59
|
+
* - Propagate only if the fields are valid and signature can be verified, reject otherwise.
|
|
60
|
+
*/
|
|
61
|
+
StrictSign = 'StrictSign',
|
|
62
|
+
/**
|
|
63
|
+
* On the producing side:
|
|
64
|
+
* - Build messages without the signature, key, from and seqno fields.
|
|
65
|
+
* - The corresponding protobuf key-value pairs are absent from the marshalled message, not just empty.
|
|
66
|
+
*
|
|
67
|
+
* On the consuming side:
|
|
68
|
+
* - Enforce the fields to be absent, reject otherwise.
|
|
69
|
+
* - Propagate only if the fields are absent, reject otherwise.
|
|
70
|
+
* - A message_id function will not be able to use the above fields, and should instead rely on the data field. A commonplace strategy is to calculate a hash.
|
|
71
|
+
*/
|
|
72
|
+
StrictNoSign = 'StrictNoSign'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface PublishOpts {
|
|
76
|
+
/**
|
|
77
|
+
* Do not throw `PublishError.NoPeersSubscribedToTopic` error if there are no
|
|
78
|
+
* peers listening on the topic.
|
|
79
|
+
*
|
|
80
|
+
* N.B. if you sent this option to true, and you publish a message on a topic
|
|
81
|
+
* with no peers listening on that topic, no other network node will ever
|
|
82
|
+
* receive the message.
|
|
83
|
+
*/
|
|
84
|
+
allowPublishToZeroTopicPeers?: boolean
|
|
85
|
+
ignoreDuplicatePublishError?: boolean
|
|
86
|
+
/** serialize message once and send to all peers without control messages */
|
|
87
|
+
batchPublish?: boolean
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export enum PublishConfigType {
|
|
91
|
+
Signing,
|
|
92
|
+
Anonymous
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type PublishConfig =
|
|
96
|
+
| {
|
|
97
|
+
type: PublishConfigType.Signing
|
|
98
|
+
author: PeerId
|
|
99
|
+
key: Uint8Array
|
|
100
|
+
privateKey: PrivateKey
|
|
101
|
+
}
|
|
102
|
+
| { type: PublishConfigType.Anonymous }
|
|
103
|
+
|
|
104
|
+
export type RejectReasonObj =
|
|
105
|
+
| { reason: RejectReason.Error, error: ValidateError }
|
|
106
|
+
| { reason: Exclude<RejectReason, RejectReason.Error> }
|
|
107
|
+
|
|
108
|
+
export enum RejectReason {
|
|
109
|
+
/**
|
|
110
|
+
* The message failed the configured validation during decoding.
|
|
111
|
+
* SelfOrigin is considered a ValidationError
|
|
112
|
+
*/
|
|
113
|
+
Error = 'error',
|
|
114
|
+
/**
|
|
115
|
+
* Custom validator fn reported status IGNORE.
|
|
116
|
+
*/
|
|
117
|
+
Ignore = 'ignore',
|
|
118
|
+
/**
|
|
119
|
+
* Custom validator fn reported status REJECT.
|
|
120
|
+
*/
|
|
121
|
+
Reject = 'reject',
|
|
122
|
+
/**
|
|
123
|
+
* The peer that sent the message OR the source from field is blacklisted.
|
|
124
|
+
* Causes messages to be ignored, not penalized, neither do score record creation.
|
|
125
|
+
*/
|
|
126
|
+
Blacklisted = 'blacklisted'
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export enum ValidateError {
|
|
130
|
+
/// The message has an invalid signature,
|
|
131
|
+
InvalidSignature = 'invalid_signature',
|
|
132
|
+
/// The sequence number was the incorrect size
|
|
133
|
+
InvalidSeqno = 'invalid_seqno',
|
|
134
|
+
/// The PeerId was invalid
|
|
135
|
+
InvalidPeerId = 'invalid_peerid',
|
|
136
|
+
/// Signature existed when validation has been sent to
|
|
137
|
+
/// [`crate::behaviour::MessageAuthenticity::Anonymous`].
|
|
138
|
+
SignaturePresent = 'signature_present',
|
|
139
|
+
/// Sequence number existed when validation has been sent to
|
|
140
|
+
/// [`crate::behaviour::MessageAuthenticity::Anonymous`].
|
|
141
|
+
SeqnoPresent = 'seqno_present',
|
|
142
|
+
/// Message source existed when validation has been sent to
|
|
143
|
+
/// [`crate::behaviour::MessageAuthenticity::Anonymous`].
|
|
144
|
+
FromPresent = 'from_present',
|
|
145
|
+
/// The data transformation failed.
|
|
146
|
+
TransformFailed = 'transform_failed'
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export enum MessageStatus {
|
|
150
|
+
duplicate = 'duplicate',
|
|
151
|
+
invalid = 'invalid',
|
|
152
|
+
valid = 'valid'
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Store both Uint8Array and string message id so that we don't have to convert data between the two.
|
|
157
|
+
* See https://github.com/ChainSafe/js-libp2p-gossipsub/pull/274
|
|
158
|
+
*/
|
|
159
|
+
export interface MessageId {
|
|
160
|
+
msgId: Uint8Array
|
|
161
|
+
msgIdStr: MsgIdStr
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Typesafe conversion of MessageAcceptance -> RejectReason. TS ensures all values covered
|
|
166
|
+
*/
|
|
167
|
+
export function rejectReasonFromAcceptance (
|
|
168
|
+
acceptance: Exclude<TopicValidatorResult, TopicValidatorResult.Accept>
|
|
169
|
+
): RejectReason.Ignore | RejectReason.Reject {
|
|
170
|
+
switch (acceptance) {
|
|
171
|
+
case TopicValidatorResult.Ignore:
|
|
172
|
+
return RejectReason.Ignore
|
|
173
|
+
case TopicValidatorResult.Reject:
|
|
174
|
+
return RejectReason.Reject
|
|
175
|
+
default:
|
|
176
|
+
throw new Error('Unreachable')
|
|
177
|
+
}
|
|
178
|
+
}
|