1
package pubsub
2

3
import (
4
	"fmt"
5
	"math"
6
	"time"
7

8
	"github.com/libp2p/go-libp2p-core/peer"
9
)
10

11
type PeerScoreThresholds struct {
12
	// GossipThreshold is the score threshold below which gossip propagation is supressed;
13
	// should be negative.
14
	GossipThreshold float64
15

16
	// PublishThreshold is the score threshold below which we shouldn't publish when using flood
17
	// publishing (also applies to fanout and floodsub peers); should be negative and <= GossipThreshold.
18
	PublishThreshold float64
19

20
	// GraylistThreshold is the score threshold below which message processing is supressed altogether,
21
	// implementing an effective graylist according to peer score; should be negative and <= PublisThreshold.
22
	GraylistThreshold float64
23

24
	// AcceptPXThreshold is the score threshold below which PX will be ignored; this should be positive
25
	// and limited to scores attainable by bootstrappers and other trusted nodes.
26
	AcceptPXThreshold float64
27

28
	// OpportunisticGraftThreshold is the median mesh score threshold before triggering opportunistic
29
	// grafting; this should have a small positive value.
30
	OpportunisticGraftThreshold float64
31
}
32

33
func (p *PeerScoreThresholds) validate() error {
34 1
	if p.GossipThreshold > 0 {
35 1
		return fmt.Errorf("invalid gossip threshold; it must be <= 0")
36
	}
37 1
	if p.PublishThreshold > 0 || p.PublishThreshold > p.GossipThreshold {
38 1
		return fmt.Errorf("invalid publish threshold; it must be <= 0 and <= gossip threshold")
39
	}
40 1
	if p.GraylistThreshold > 0 || p.GraylistThreshold > p.PublishThreshold {
41 1
		return fmt.Errorf("invalid graylist threshold; it must be <= 0 and <= publish threshold")
42
	}
43 1
	if p.AcceptPXThreshold < 0 {
44 1
		return fmt.Errorf("invalid accept PX threshold; it must be >= 0")
45
	}
46 1
	if p.OpportunisticGraftThreshold < 0 {
47 1
		return fmt.Errorf("invalid opportunistic grafting threshold; it must be >= 0")
48
	}
49 1
	return nil
50
}
51

52
type PeerScoreParams struct {
53
	// Score parameters per topic.
54
	Topics map[string]*TopicScoreParams
55

56
	// Aggregate topic score cap; this limits the total contribution of topics towards a positive
57
	// score. It must be positive (or 0 for no cap).
58
	TopicScoreCap float64
59

60
	// P5: Application-specific peer scoring
61
	AppSpecificScore  func(p peer.ID) float64
62
	AppSpecificWeight float64
63

64
	// P6: IP-colocation factor.
65
	// The parameter has an associated counter which counts the number of peers with the same IP.
66
	// If the number of peers in the same IP exceeds IPColocationFactorThreshold, then the value
67
	// is the square of the difference, ie (PeersInSameIP - IPColocationThreshold)^2.
68
	// If the number of peers in the same IP is less than the threshold, then the value is 0.
69
	// The weight of the parameter MUST be negative, unless you want to disable for testing.
70
	// Note: In order to simulate many IPs in a managable manner when testing, you can set the weight to 0
71
	//       thus disabling the IP colocation penalty.
72
	IPColocationFactorWeight    float64
73
	IPColocationFactorThreshold int
74
	IPColocationFactorWhitelist map[string]struct{}
75

76
	// P7: behavioural pattern penalties.
77
	// This parameter has an associated counter which tracks misbehaviour as detected by the
78
	// router. The router currently applies penalties for the following behaviors:
79
	// - attempting to re-graft before the prune backoff time has elapsed.
80
	// - not following up in IWANT requests for messages advertised with IHAVE.
81
	//
82
	// The value of the parameter is the square of the counter over the threshold, which decays with
83
	// BehaviourPenaltyDecay.
84
	// The weight of the parameter MUST be negative (or zero to disable).
85
	BehaviourPenaltyWeight, BehaviourPenaltyThreshold, BehaviourPenaltyDecay float64
86

87
	// the decay interval for parameter counters.
88
	DecayInterval time.Duration
89

90
	// counter value below which it is considered 0.
91
	DecayToZero float64
92

93
	// time to remember counters for a disconnected peer.
94
	RetainScore time.Duration
95
}
96

97
type TopicScoreParams struct {
98
	// The weight of the topic.
99
	TopicWeight float64
100

101
	// P1: time in the mesh
102
	// This is the time the peer has ben grafted in the mesh.
103
	// The value of of the parameter is the time/TimeInMeshQuantum, capped by TimeInMeshCap
104
	// The weight of the parameter MUST be positive (or zero to disable).
105
	TimeInMeshWeight  float64
106
	TimeInMeshQuantum time.Duration
107
	TimeInMeshCap     float64
108

109
	// P2: first message deliveries
110
	// This is the number of message deliveries in the topic.
111
	// The value of the parameter is a counter, decaying with FirstMessageDeliveriesDecay, and capped
112
	// by FirstMessageDeliveriesCap.
113
	// The weight of the parameter MUST be positive (or zero to disable).
114
	FirstMessageDeliveriesWeight, FirstMessageDeliveriesDecay float64
115
	FirstMessageDeliveriesCap                                 float64
116

117
	// P3: mesh message deliveries
118
	// This is the number of message deliveries in the mesh, within the MeshMessageDeliveriesWindow of
119
	// message validation; deliveries during validation also count and are retroactively applied
120
	// when validation succeeds.
121
	// This window accounts for the minimum time before a hostile mesh peer trying to game the score
122
	// could replay back a valid message we just sent them.
123
	// It effectively tracks first and near-first deliveries, ie a message seen from a mesh peer
124
	// before we have forwarded it to them.
125
	// The parameter has an associated counter, decaying with MeshMessageDeliveriesDecay.
126
	// If the counter exceeds the threshold, its value is 0.
127
	// If the counter is below the MeshMessageDeliveriesThreshold, the value is the square of
128
	// the deficit, ie (MessageDeliveriesThreshold - counter)^2
129
	// The penalty is only activated after MeshMessageDeliveriesActivation time in the mesh.
130
	// The weight of the parameter MUST be negative (or zero to disable).
131
	MeshMessageDeliveriesWeight, MeshMessageDeliveriesDecay      float64
132
	MeshMessageDeliveriesCap, MeshMessageDeliveriesThreshold     float64
133
	MeshMessageDeliveriesWindow, MeshMessageDeliveriesActivation time.Duration
134

135
	// P3b: sticky mesh propagation failures
136
	// This is a sticky penalty that applies when a peer gets pruned from the mesh with an active
137
	// mesh message delivery penalty.
138
	// The weight of the parameter MUST be negative (or zero to disable)
139
	MeshFailurePenaltyWeight, MeshFailurePenaltyDecay float64
140

141
	// P4: invalid messages
142
	// This is the number of invalid messages in the topic.
143
	// The value of the parameter is the square of the counter, decaying with
144
	// InvalidMessageDeliveriesDecay.
145
	// The weight of the parameter MUST be negative (or zero to disable).
146
	InvalidMessageDeliveriesWeight, InvalidMessageDeliveriesDecay float64
147
}
148

149
// peer score parameter validation
150
func (p *PeerScoreParams) validate() error {
151
	for topic, params := range p.Topics {
152 1
		err := params.validate()
153 1
		if err != nil {
154 1
			return fmt.Errorf("invalid score parameters for topic %s: %w", topic, err)
155
		}
156
	}
157

158
	// check that the topic score is 0 or something positive
159 1
	if p.TopicScoreCap < 0 {
160 1
		return fmt.Errorf("invalid topic score cap; must be positive (or 0 for no cap)")
161
	}
162

163
	// check that we have an app specific score; the weight can be anything (but expected positive)
164 1
	if p.AppSpecificScore == nil {
165 1
		return fmt.Errorf("missing application specific score function")
166
	}
167

168
	// check the IP colocation factor
169 1
	if p.IPColocationFactorWeight > 0 {
170 1
		return fmt.Errorf("invalid IPColocationFactorWeight; must be negative (or 0 to disable)")
171
	}
172 1
	if p.IPColocationFactorWeight != 0 && p.IPColocationFactorThreshold < 1 {
173 1
		return fmt.Errorf("invalid IPColocationFactorThreshold; must be at least 1")
174
	}
175

176
	// check the behaviour penalty
177 1
	if p.BehaviourPenaltyWeight > 0 {
178 1
		return fmt.Errorf("invalid BehaviourPenaltyWeight; must be negative (or 0 to disable)")
179
	}
180 1
	if p.BehaviourPenaltyWeight != 0 && (p.BehaviourPenaltyDecay <= 0 || p.BehaviourPenaltyDecay >= 1) {
181 1
		return fmt.Errorf("invalid BehaviourPenaltyDecay; must be between 0 and 1")
182
	}
183 1
	if p.BehaviourPenaltyThreshold < 0 {
184 0
		return fmt.Errorf("invalid BehaviourPenaltyThreshold; must be >= 0")
185
	}
186

187
	// check the decay parameters
188 1
	if p.DecayInterval < time.Second {
189 1
		return fmt.Errorf("invalid DecayInterval; must be at least 1s")
190
	}
191 1
	if p.DecayToZero <= 0 || p.DecayToZero >= 1 {
192 1
		return fmt.Errorf("invalid DecayToZero; must be between 0 and 1")
193
	}
194

195
	// no need to check the score retention; a value of 0 means that we don't retain scores
196 1
	return nil
197
}
198

199
func (p *TopicScoreParams) validate() error {
200
	// make sure we have a sane topic weight
201 1
	if p.TopicWeight < 0 {
202 1
		return fmt.Errorf("invalid topic weight; must be >= 0")
203
	}
204

205
	// check P1
206 1
	if p.TimeInMeshQuantum == 0 {
207 1
		return fmt.Errorf("invalid TimeInMeshQuantum; must be non zero")
208
	}
209 1
	if p.TimeInMeshWeight < 0 {
210 1
		return fmt.Errorf("invalid TimeInMeshWeight; must be positive (or 0 to disable)")
211
	}
212 1
	if p.TimeInMeshWeight != 0 && p.TimeInMeshQuantum <= 0 {
213 1
		return fmt.Errorf("invalid TimeInMeshQuantum; must be positive")
214
	}
215 1
	if p.TimeInMeshWeight != 0 && p.TimeInMeshCap <= 0 {
216 1
		return fmt.Errorf("invalid TimeInMeshCap; must be positive")
217
	}
218

219
	// check P2
220 1
	if p.FirstMessageDeliveriesWeight < 0 {
221 1
		return fmt.Errorf("invallid FirstMessageDeliveriesWeight; must be positive (or 0 to disable)")
222
	}
223 1
	if p.FirstMessageDeliveriesWeight != 0 && (p.FirstMessageDeliveriesDecay <= 0 || p.FirstMessageDeliveriesDecay >= 1) {
224 1
		return fmt.Errorf("invalid FirstMessageDeliveriesDecay; must be between 0 and 1")
225
	}
226 1
	if p.FirstMessageDeliveriesWeight != 0 && p.FirstMessageDeliveriesCap <= 0 {
227 1
		return fmt.Errorf("invalid FirstMessageDeliveriesCap; must be positive")
228
	}
229

230
	// check P3
231 1
	if p.MeshMessageDeliveriesWeight > 0 {
232 1
		return fmt.Errorf("invalid MeshMessageDeliveriesWeight; must be negative (or 0 to disable)")
233
	}
234 1
	if p.MeshMessageDeliveriesWeight != 0 && (p.MeshMessageDeliveriesDecay <= 0 || p.MeshMessageDeliveriesDecay >= 1) {
235 1
		return fmt.Errorf("invalid MeshMessageDeliveriesDecay; must be between 0 and 1")
236
	}
237 1
	if p.MeshMessageDeliveriesWeight != 0 && p.MeshMessageDeliveriesCap <= 0 {
238 1
		return fmt.Errorf("invalid MeshMessageDeliveriesCap; must be positive")
239
	}
240 1
	if p.MeshMessageDeliveriesWeight != 0 && p.MeshMessageDeliveriesThreshold <= 0 {
241 1
		return fmt.Errorf("invalid MeshMessageDeliveriesThreshold; must be positive")
242
	}
243 1
	if p.MeshMessageDeliveriesWindow < 0 {
244 1
		return fmt.Errorf("invalid MeshMessageDeliveriesWindow; must be non-negative")
245
	}
246 1
	if p.MeshMessageDeliveriesWeight != 0 && p.MeshMessageDeliveriesActivation < time.Second {
247 1
		return fmt.Errorf("invalid MeshMessageDeliveriesActivation; must be at least 1s")
248
	}
249

250
	// check P3b
251 1
	if p.MeshFailurePenaltyWeight > 0 {
252 1
		return fmt.Errorf("invalid MeshFailurePenaltyWeight; must be negative (or 0 to disable)")
253
	}
254 1
	if p.MeshFailurePenaltyWeight != 0 && (p.MeshFailurePenaltyDecay <= 0 || p.MeshFailurePenaltyDecay >= 1) {
255 1
		return fmt.Errorf("invalid MeshFailurePenaltyDecay; must be between 0 and 1")
256
	}
257

258
	// check P4
259 1
	if p.InvalidMessageDeliveriesWeight > 0 {
260 1
		return fmt.Errorf("invalid InvalidMessageDeliveriesWeight; must be negative (or 0 to disable)")
261
	}
262 1
	if p.InvalidMessageDeliveriesDecay <= 0 || p.InvalidMessageDeliveriesDecay >= 1 {
263 1
		return fmt.Errorf("invalid InvalidMessageDeliveriesDecay; must be between 0 and 1")
264
	}
265

266 1
	return nil
267
}
268

269
const (
270
	DefaultDecayInterval = time.Second
271
	DefaultDecayToZero   = 0.01
272
)
273

274
// ScoreParameterDecay computes the decay factor for a parameter, assuming the DecayInterval is 1s
275
// and that the value decays to zero if it drops below 0.01
276
func ScoreParameterDecay(decay time.Duration) float64 {
277 1
	return ScoreParameterDecayWithBase(decay, DefaultDecayInterval, DefaultDecayToZero)
278
}
279

280
// ScoreParameterDecay computes the decay factor for a parameter using base as the DecayInterval
281
func ScoreParameterDecayWithBase(decay time.Duration, base time.Duration, decayToZero float64) float64 {
282
	// the decay is linear, so after n ticks the value is factor^n
283
	// so factor^n = decayToZero => factor = decayToZero^(1/n)
284 1
	ticks := float64(decay / base)
285 1
	return math.Pow(decayToZero, 1/ticks)
286
}

Read our documentation on viewing source code .

Loading