libp2p_gossipsub/peer_score/
params.rs

1// Copyright 2020 Sigma Prime Pty Ltd.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a
4// copy of this software and associated documentation files (the "Software"),
5// to deal in the Software without restriction, including without limitation
6// the rights to use, copy, modify, merge, publish, distribute, sublicense,
7// and/or sell copies of the Software, and to permit persons to whom the
8// Software is furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in
11// all copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
14// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19// DEALINGS IN THE SOFTWARE.
20
21use std::{
22    collections::{HashMap, HashSet},
23    net::IpAddr,
24    time::Duration,
25};
26
27use crate::TopicHash;
28
29/// The default number of seconds for a decay interval.
30const DEFAULT_DECAY_INTERVAL: u64 = 1;
31/// The default rate to decay to 0.
32const DEFAULT_DECAY_TO_ZERO: f64 = 0.1;
33
34/// Computes the decay factor for a parameter, assuming the `decay_interval` is 1s
35/// and that the value decays to zero if it drops below 0.01.
36pub fn score_parameter_decay(decay: Duration) -> f64 {
37    score_parameter_decay_with_base(
38        decay,
39        Duration::from_secs(DEFAULT_DECAY_INTERVAL),
40        DEFAULT_DECAY_TO_ZERO,
41    )
42}
43
44/// Computes the decay factor for a parameter using base as the `decay_interval`.
45pub fn score_parameter_decay_with_base(decay: Duration, base: Duration, decay_to_zero: f64) -> f64 {
46    // the decay is linear, so after n ticks the value is factor^n
47    // so factor^n = decay_to_zero => factor = decay_to_zero^(1/n)
48    let ticks = decay.as_secs_f64() / base.as_secs_f64();
49    decay_to_zero.powf(1f64 / ticks)
50}
51
52#[derive(Debug, Clone)]
53pub struct PeerScoreThresholds {
54    /// The score threshold below which gossip propagation is suppressed;
55    /// should be negative.
56    pub gossip_threshold: f64,
57
58    /// The score threshold below which we shouldn't publish when using flood
59    /// publishing (also applies to fanout peers); should be negative and <= `gossip_threshold`.
60    pub publish_threshold: f64,
61
62    /// The score threshold below which message processing is suppressed altogether,
63    /// implementing an effective graylist according to peer score; should be negative and
64    /// <= `publish_threshold`.
65    pub graylist_threshold: f64,
66
67    /// The score threshold below which px will be ignored; this should be positive
68    /// and limited to scores attainable by bootstrappers and other trusted nodes.
69    pub accept_px_threshold: f64,
70
71    /// The median mesh score threshold before triggering opportunistic
72    /// grafting; this should have a small positive value.
73    pub opportunistic_graft_threshold: f64,
74}
75
76impl Default for PeerScoreThresholds {
77    fn default() -> Self {
78        PeerScoreThresholds {
79            gossip_threshold: -10.0,
80            publish_threshold: -50.0,
81            graylist_threshold: -80.0,
82            accept_px_threshold: 10.0,
83            opportunistic_graft_threshold: 20.0,
84        }
85    }
86}
87
88impl PeerScoreThresholds {
89    pub fn validate(&self) -> Result<(), &'static str> {
90        if self.gossip_threshold > 0f64 {
91            return Err("invalid gossip threshold; it must be <= 0");
92        }
93        if self.publish_threshold > 0f64 || self.publish_threshold > self.gossip_threshold {
94            return Err("Invalid publish threshold; it must be <= 0 and <= gossip threshold");
95        }
96        if self.graylist_threshold > 0f64 || self.graylist_threshold > self.publish_threshold {
97            return Err("Invalid graylist threshold; it must be <= 0 and <= publish threshold");
98        }
99        if self.accept_px_threshold < 0f64 {
100            return Err("Invalid accept px threshold; it must be >= 0");
101        }
102        if self.opportunistic_graft_threshold < 0f64 {
103            return Err("Invalid opportunistic grafting threshold; it must be >= 0");
104        }
105        Ok(())
106    }
107}
108
109#[derive(Debug, Clone)]
110pub struct PeerScoreParams {
111    /// Score parameters per topic.
112    pub topics: HashMap<TopicHash, TopicScoreParams>,
113
114    /// Aggregate topic score cap; this limits the total contribution of topics towards a positive
115    /// score. It must be positive (or 0 for no cap).
116    pub topic_score_cap: f64,
117
118    /// P5: Application-specific peer scoring
119    pub app_specific_weight: f64,
120
121    ///  P6: IP-colocation factor.
122    ///  The parameter has an associated counter which counts the number of peers with the same IP.
123    ///  If the number of peers in the same IP exceeds `ip_colocation_factor_threshold, then the
124    /// value  is the square of the difference, ie `(peers_in_same_ip -
125    /// ip_colocation_threshold)^2`.  If the number of peers in the same IP is less than the
126    /// threshold, then the value is 0.  The weight of the parameter MUST be negative, unless
127    /// you want to disable for testing.  Note: In order to simulate many IPs in a manageable
128    /// manner when testing, you can set the weight to 0        thus disabling the IP
129    /// colocation penalty.
130    pub ip_colocation_factor_weight: f64,
131    pub ip_colocation_factor_threshold: f64,
132    pub ip_colocation_factor_whitelist: HashSet<IpAddr>,
133
134    ///  P7: behavioural pattern penalties.
135    ///  This parameter has an associated counter which tracks misbehaviour as detected by the
136    ///  router. The router currently applies penalties for the following behaviors:
137    ///  - attempting to re-graft before the prune backoff time has elapsed.
138    ///  - not following up in IWANT requests for messages advertised with IHAVE.
139    ///
140    ///  The value of the parameter is the square of the counter over the threshold, which decays
141    ///  with BehaviourPenaltyDecay.
142    ///  The weight of the parameter MUST be negative (or zero to disable).
143    pub behaviour_penalty_weight: f64,
144    pub behaviour_penalty_threshold: f64,
145    pub behaviour_penalty_decay: f64,
146
147    /// The decay interval for parameter counters.
148    pub decay_interval: Duration,
149
150    /// Counter value below which it is considered 0.
151    pub decay_to_zero: f64,
152
153    /// Time to remember counters for a disconnected peer.
154    pub retain_score: Duration,
155
156    /// Slow peer penalty conditions,
157    /// by default `slow_peer_weight` is 50 times lower than `behaviour_penalty_weight`
158    /// i.e. 50 slow peer penalties match 1 behaviour penalty.
159    pub slow_peer_weight: f64,
160    pub slow_peer_threshold: f64,
161    pub slow_peer_decay: f64,
162}
163
164impl Default for PeerScoreParams {
165    fn default() -> Self {
166        PeerScoreParams {
167            topics: HashMap::new(),
168            topic_score_cap: 3600.0,
169            app_specific_weight: 10.0,
170            ip_colocation_factor_weight: -5.0,
171            ip_colocation_factor_threshold: 10.0,
172            ip_colocation_factor_whitelist: HashSet::new(),
173            behaviour_penalty_weight: -10.0,
174            behaviour_penalty_threshold: 0.0,
175            behaviour_penalty_decay: 0.2,
176            decay_interval: Duration::from_secs(DEFAULT_DECAY_INTERVAL),
177            decay_to_zero: DEFAULT_DECAY_TO_ZERO,
178            retain_score: Duration::from_secs(3600),
179            slow_peer_weight: -0.2,
180            slow_peer_threshold: 0.0,
181            slow_peer_decay: 0.2,
182        }
183    }
184}
185
186/// Peer score parameter validation
187impl PeerScoreParams {
188    pub fn validate(&self) -> Result<(), String> {
189        for (topic, params) in self.topics.iter() {
190            if let Err(e) = params.validate() {
191                return Err(format!("Invalid score parameters for topic {topic}: {e}"));
192            }
193        }
194
195        // check that the topic score is 0 or something positive
196        if self.topic_score_cap < 0f64 {
197            return Err("Invalid topic score cap; must be positive (or 0 for no cap)".into());
198        }
199
200        // check the IP colocation factor
201        if self.ip_colocation_factor_weight > 0f64 {
202            return Err(
203                "Invalid ip_colocation_factor_weight; must be negative (or 0 to disable)".into(),
204            );
205        }
206        if self.ip_colocation_factor_weight != 0f64 && self.ip_colocation_factor_threshold < 1f64 {
207            return Err("Invalid ip_colocation_factor_threshold; must be at least 1".into());
208        }
209
210        // check the behaviour penalty
211        if self.behaviour_penalty_weight > 0f64 {
212            return Err(
213                "Invalid behaviour_penalty_weight; must be negative (or 0 to disable)".into(),
214            );
215        }
216        if self.behaviour_penalty_weight != 0f64
217            && (self.behaviour_penalty_decay <= 0f64 || self.behaviour_penalty_decay >= 1f64)
218        {
219            return Err("invalid behaviour_penalty_decay; must be between 0 and 1".into());
220        }
221
222        if self.behaviour_penalty_threshold < 0f64 {
223            return Err("invalid behaviour_penalty_threshold; must be >= 0".into());
224        }
225
226        // check the decay parameters
227        if self.decay_interval < Duration::from_secs(1) {
228            return Err("Invalid decay_interval; must be at least 1s".into());
229        }
230        if self.decay_to_zero <= 0f64 || self.decay_to_zero >= 1f64 {
231            return Err("Invalid decay_to_zero; must be between 0 and 1".into());
232        }
233
234        // no need to check the score retention; a value of 0 means that we don't retain scores
235        Ok(())
236    }
237}
238
239#[derive(Debug, Clone)]
240pub struct TopicScoreParams {
241    /// The weight of the topic.
242    pub topic_weight: f64,
243
244    ///  P1: time in the mesh
245    ///  This is the time the peer has been grafted in the mesh.
246    ///  The value of the parameter is the `time/time_in_mesh_quantum`, capped by
247    /// `time_in_mesh_cap`  The weight of the parameter must be positive (or zero to disable).
248    pub time_in_mesh_weight: f64,
249    pub time_in_mesh_quantum: Duration,
250    pub time_in_mesh_cap: f64,
251
252    ///  P2: first message deliveries
253    ///  This is the number of message deliveries in the topic.
254    ///  The value of the parameter is a counter, decaying with `first_message_deliveries_decay`,
255    /// and capped  by `first_message_deliveries_cap`.
256    ///  The weight of the parameter MUST be positive (or zero to disable).
257    pub first_message_deliveries_weight: f64,
258    pub first_message_deliveries_decay: f64,
259    pub first_message_deliveries_cap: f64,
260
261    ///  P3: mesh message deliveries
262    ///  This is the number of message deliveries in the mesh, within the
263    ///  `mesh_message_deliveries_window` of message validation; deliveries during validation also
264    ///  count and are retroactively applied when validation succeeds.
265    ///  This window accounts for the minimum time before a hostile mesh peer trying to game the
266    ///  score could replay back a valid message we just sent them.
267    ///  It effectively tracks first and near-first deliveries, ie a message seen from a mesh peer
268    ///  before we have forwarded it to them.
269    ///  The parameter has an associated counter, decaying with `mesh_message_deliveries_decay`.
270    ///  If the counter exceeds the threshold, its value is 0.
271    ///  If the counter is below the `mesh_message_deliveries_threshold`, the value is the square
272    /// of  the deficit, ie (`message_deliveries_threshold - counter)^2`
273    ///  The penalty is only activated after `mesh_message_deliveries_activation` time in the mesh.
274    ///  The weight of the parameter MUST be negative (or zero to disable).
275    pub mesh_message_deliveries_weight: f64,
276    pub mesh_message_deliveries_decay: f64,
277    pub mesh_message_deliveries_cap: f64,
278    pub mesh_message_deliveries_threshold: f64,
279    pub mesh_message_deliveries_window: Duration,
280    pub mesh_message_deliveries_activation: Duration,
281
282    ///  P3b: sticky mesh propagation failures
283    ///  This is a sticky penalty that applies when a peer gets pruned from the mesh with an active
284    ///  mesh message delivery penalty.
285    ///  The weight of the parameter MUST be negative (or zero to disable)
286    pub mesh_failure_penalty_weight: f64,
287    pub mesh_failure_penalty_decay: f64,
288
289    ///  P4: invalid messages
290    ///  This is the number of invalid messages in the topic.
291    ///  The value of the parameter is the square of the counter, decaying with
292    ///  `invalid_message_deliveries_decay`.
293    ///  The weight of the parameter MUST be negative (or zero to disable).
294    pub invalid_message_deliveries_weight: f64,
295    pub invalid_message_deliveries_decay: f64,
296}
297
298/// NOTE: The topic score parameters are very network specific.
299///       For any production system, these values should be manually set.
300impl Default for TopicScoreParams {
301    fn default() -> Self {
302        TopicScoreParams {
303            topic_weight: 0.5,
304            // P1
305            time_in_mesh_weight: 1.0,
306            time_in_mesh_quantum: Duration::from_millis(1),
307            time_in_mesh_cap: 3600.0,
308            // P2
309            first_message_deliveries_weight: 1.0,
310            first_message_deliveries_decay: 0.5,
311            first_message_deliveries_cap: 2000.0,
312            // P3
313            mesh_message_deliveries_weight: -1.0,
314            mesh_message_deliveries_decay: 0.5,
315            mesh_message_deliveries_cap: 100.0,
316            mesh_message_deliveries_threshold: 20.0,
317            mesh_message_deliveries_window: Duration::from_millis(10),
318            mesh_message_deliveries_activation: Duration::from_secs(5),
319            // P3b
320            mesh_failure_penalty_weight: -1.0,
321            mesh_failure_penalty_decay: 0.5,
322            // P4
323            invalid_message_deliveries_weight: -1.0,
324            invalid_message_deliveries_decay: 0.3,
325        }
326    }
327}
328
329impl TopicScoreParams {
330    pub fn validate(&self) -> Result<(), &'static str> {
331        // make sure we have a sane topic weight
332        if self.topic_weight < 0f64 {
333            return Err("invalid topic weight; must be >= 0");
334        }
335
336        if self.time_in_mesh_quantum == Duration::from_secs(0) {
337            return Err("Invalid time_in_mesh_quantum; must be non zero");
338        }
339        if self.time_in_mesh_weight < 0f64 {
340            return Err("Invalid time_in_mesh_weight; must be positive (or 0 to disable)");
341        }
342        if self.time_in_mesh_weight != 0f64 && self.time_in_mesh_cap <= 0f64 {
343            return Err("Invalid time_in_mesh_cap must be positive");
344        }
345
346        if self.first_message_deliveries_weight < 0f64 {
347            return Err(
348                "Invalid first_message_deliveries_weight; must be positive (or 0 to disable)",
349            );
350        }
351        if self.first_message_deliveries_weight != 0f64
352            && (self.first_message_deliveries_decay <= 0f64
353                || self.first_message_deliveries_decay >= 1f64)
354        {
355            return Err("Invalid first_message_deliveries_decay; must be between 0 and 1");
356        }
357        if self.first_message_deliveries_weight != 0f64 && self.first_message_deliveries_cap <= 0f64
358        {
359            return Err("Invalid first_message_deliveries_cap must be positive");
360        }
361
362        if self.mesh_message_deliveries_weight > 0f64 {
363            return Err(
364                "Invalid mesh_message_deliveries_weight; must be negative (or 0 to disable)",
365            );
366        }
367        if self.mesh_message_deliveries_weight != 0f64
368            && (self.mesh_message_deliveries_decay <= 0f64
369                || self.mesh_message_deliveries_decay >= 1f64)
370        {
371            return Err("Invalid mesh_message_deliveries_decay; must be between 0 and 1");
372        }
373        if self.mesh_message_deliveries_weight != 0f64 && self.mesh_message_deliveries_cap <= 0f64 {
374            return Err("Invalid mesh_message_deliveries_cap must be positive");
375        }
376        if self.mesh_message_deliveries_weight != 0f64
377            && self.mesh_message_deliveries_threshold <= 0f64
378        {
379            return Err("Invalid mesh_message_deliveries_threshold; must be positive");
380        }
381        if self.mesh_message_deliveries_weight != 0f64
382            && self.mesh_message_deliveries_activation < Duration::from_secs(1)
383        {
384            return Err("Invalid mesh_message_deliveries_activation; must be at least 1s");
385        }
386
387        // check P3b
388        if self.mesh_failure_penalty_weight > 0f64 {
389            return Err("Invalid mesh_failure_penalty_weight; must be negative (or 0 to disable)");
390        }
391        if self.mesh_failure_penalty_weight != 0f64
392            && (self.mesh_failure_penalty_decay <= 0f64 || self.mesh_failure_penalty_decay >= 1f64)
393        {
394            return Err("Invalid mesh_failure_penalty_decay; must be between 0 and 1");
395        }
396
397        // check P4
398        if self.invalid_message_deliveries_weight > 0f64 {
399            return Err(
400                "Invalid invalid_message_deliveries_weight; must be negative (or 0 to disable)",
401            );
402        }
403        if self.invalid_message_deliveries_decay <= 0f64
404            || self.invalid_message_deliveries_decay >= 1f64
405        {
406            return Err("Invalid invalid_message_deliveries_decay; must be between 0 and 1");
407        }
408        Ok(())
409    }
410}