1package pubsub 2 3import ( 4 "fmt" 5 "math" 6 "time" 7 8 "github.com/libp2p/go-libp2p-core/peer" 9) 10 11type 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 33func (p *PeerScoreThresholds) validate() error { 34 if p.GossipThreshold > 0 { 35 return fmt.Errorf("invalid gossip threshold; it must be <= 0") 36 } 37 if p.PublishThreshold > 0 || p.PublishThreshold > p.GossipThreshold { 38 return fmt.Errorf("invalid publish threshold; it must be <= 0 and <= gossip threshold") 39 } 40 if p.GraylistThreshold > 0 || p.GraylistThreshold > p.PublishThreshold { 41 return fmt.Errorf("invalid graylist threshold; it must be <= 0 and <= publish threshold") 42 } 43 if p.AcceptPXThreshold < 0 { 44 return fmt.Errorf("invalid accept PX threshold; it must be >= 0") 45 } 46 if p.OpportunisticGraftThreshold < 0 { 47 return fmt.Errorf("invalid opportunistic grafting threshold; it must be >= 0") 48 } 49 return nil 50} 51 52type 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 97type 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 150func (p *PeerScoreParams) validate() error { 151 for topic, params := range p.Topics { 152 err := params.validate() 153 if err != nil { 154 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 if p.TopicScoreCap < 0 { 160 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 if p.AppSpecificScore == nil { 165 return fmt.Errorf("missing application specific score function") 166 } 167 168 // check the IP colocation factor 169 if p.IPColocationFactorWeight > 0 { 170 return fmt.Errorf("invalid IPColocationFactorWeight; must be negative (or 0 to disable)") 171 } 172 if p.IPColocationFactorWeight != 0 && p.IPColocationFactorThreshold < 1 { 173 return fmt.Errorf("invalid IPColocationFactorThreshold; must be at least 1") 174 } 175 176 // check the behaviour penalty 177 if p.BehaviourPenaltyWeight > 0 { 178 return fmt.Errorf("invalid BehaviourPenaltyWeight; must be negative (or 0 to disable)") 179 } 180 if p.BehaviourPenaltyWeight != 0 && (p.BehaviourPenaltyDecay <= 0 || p.BehaviourPenaltyDecay >= 1) { 181 return fmt.Errorf("invalid BehaviourPenaltyDecay; must be between 0 and 1") 182 } 183 if p.BehaviourPenaltyThreshold < 0 { 184 return fmt.Errorf("invalid BehaviourPenaltyThreshold; must be >= 0") 185 } 186 187 // check the decay parameters 188 if p.DecayInterval < time.Second { 189 return fmt.Errorf("invalid DecayInterval; must be at least 1s") 190 } 191 if p.DecayToZero <= 0 || p.DecayToZero >= 1 { 192 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 return nil 197} 198 199func (p *TopicScoreParams) validate() error { 200 // make sure we have a sane topic weight 201 if p.TopicWeight < 0 { 202 return fmt.Errorf("invalid topic weight; must be >= 0") 203 } 204 205 // check P1 206 if p.TimeInMeshQuantum == 0 { 207 return fmt.Errorf("invalid TimeInMeshQuantum; must be non zero") 208 } 209 if p.TimeInMeshWeight < 0 { 210 return fmt.Errorf("invalid TimeInMeshWeight; must be positive (or 0 to disable)") 211 } 212 if p.TimeInMeshWeight != 0 && p.TimeInMeshQuantum <= 0 { 213 return fmt.Errorf("invalid TimeInMeshQuantum; must be positive") 214 } 215 if p.TimeInMeshWeight != 0 && p.TimeInMeshCap <= 0 { 216 return fmt.Errorf("invalid TimeInMeshCap; must be positive") 217 } 218 219 // check P2 220 if p.FirstMessageDeliveriesWeight < 0 { 221 return fmt.Errorf("invallid FirstMessageDeliveriesWeight; must be positive (or 0 to disable)") 222 } 223 if p.FirstMessageDeliveriesWeight != 0 && (p.FirstMessageDeliveriesDecay <= 0 || p.FirstMessageDeliveriesDecay >= 1) { 224 return fmt.Errorf("invalid FirstMessageDeliveriesDecay; must be between 0 and 1") 225 } 226 if p.FirstMessageDeliveriesWeight != 0 && p.FirstMessageDeliveriesCap <= 0 { 227 return fmt.Errorf("invalid FirstMessageDeliveriesCap; must be positive") 228 } 229 230 // check P3 231 if p.MeshMessageDeliveriesWeight > 0 { 232 return fmt.Errorf("invalid MeshMessageDeliveriesWeight; must be negative (or 0 to disable)") 233 } 234 if p.MeshMessageDeliveriesWeight != 0 && (p.MeshMessageDeliveriesDecay <= 0 || p.MeshMessageDeliveriesDecay >= 1) { 235 return fmt.Errorf("invalid MeshMessageDeliveriesDecay; must be between 0 and 1") 236 } 237 if p.MeshMessageDeliveriesWeight != 0 && p.MeshMessageDeliveriesCap <= 0 { 238 return fmt.Errorf("invalid MeshMessageDeliveriesCap; must be positive") 239 } 240 if p.MeshMessageDeliveriesWeight != 0 && p.MeshMessageDeliveriesThreshold <= 0 { 241 return fmt.Errorf("invalid MeshMessageDeliveriesThreshold; must be positive") 242 } 243 if p.MeshMessageDeliveriesWindow < 0 { 244 return fmt.Errorf("invalid MeshMessageDeliveriesWindow; must be non-negative") 245 } 246 if p.MeshMessageDeliveriesWeight != 0 && p.MeshMessageDeliveriesActivation < time.Second { 247 return fmt.Errorf("invalid MeshMessageDeliveriesActivation; must be at least 1s") 248 } 249 250 // check P3b 251 if p.MeshFailurePenaltyWeight > 0 { 252 return fmt.Errorf("invalid MeshFailurePenaltyWeight; must be negative (or 0 to disable)") 253 } 254 if p.MeshFailurePenaltyWeight != 0 && (p.MeshFailurePenaltyDecay <= 0 || p.MeshFailurePenaltyDecay >= 1) { 255 return fmt.Errorf("invalid MeshFailurePenaltyDecay; must be between 0 and 1") 256 } 257 258 // check P4 259 if p.InvalidMessageDeliveriesWeight > 0 { 260 return fmt.Errorf("invalid InvalidMessageDeliveriesWeight; must be negative (or 0 to disable)") 261 } 262 if p.InvalidMessageDeliveriesDecay <= 0 || p.InvalidMessageDeliveriesDecay >= 1 { 263 return fmt.Errorf("invalid InvalidMessageDeliveriesDecay; must be between 0 and 1") 264 } 265 266 return nil 267} 268 269const ( 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 276func ScoreParameterDecay(decay time.Duration) float64 { 277 return ScoreParameterDecayWithBase(decay, DefaultDecayInterval, DefaultDecayToZero) 278} 279 280// ScoreParameterDecay computes the decay factor for a parameter using base as the DecayInterval 281func 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 ticks := float64(decay / base) 285 return math.Pow(decayToZero, 1/ticks) 286} 287