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