libp2p_gossipsub/peer_score/
params.rs1use std::{
22    collections::{HashMap, HashSet},
23    net::IpAddr,
24    time::Duration,
25};
26
27use crate::TopicHash;
28
29const DEFAULT_DECAY_INTERVAL: u64 = 1;
31const DEFAULT_DECAY_TO_ZERO: f64 = 0.1;
33
34pub 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
44pub fn score_parameter_decay_with_base(decay: Duration, base: Duration, decay_to_zero: f64) -> f64 {
46    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    pub gossip_threshold: f64,
57
58    pub publish_threshold: f64,
61
62    pub graylist_threshold: f64,
66
67    pub accept_px_threshold: f64,
70
71    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    pub topics: HashMap<TopicHash, TopicScoreParams>,
113
114    pub topic_score_cap: f64,
117
118    pub app_specific_weight: f64,
120
121    pub ip_colocation_factor_weight: f64,
131    pub ip_colocation_factor_threshold: f64,
132    pub ip_colocation_factor_whitelist: HashSet<IpAddr>,
133
134    pub behaviour_penalty_weight: f64,
144    pub behaviour_penalty_threshold: f64,
145    pub behaviour_penalty_decay: f64,
146
147    pub decay_interval: Duration,
149
150    pub decay_to_zero: f64,
152
153    pub retain_score: Duration,
155
156    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
186impl 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        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        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        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        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        Ok(())
236    }
237}
238
239#[derive(Debug, Clone)]
240pub struct TopicScoreParams {
241    pub topic_weight: f64,
243
244    pub time_in_mesh_weight: f64,
249    pub time_in_mesh_quantum: Duration,
250    pub time_in_mesh_cap: f64,
251
252    pub first_message_deliveries_weight: f64,
258    pub first_message_deliveries_decay: f64,
259    pub first_message_deliveries_cap: f64,
260
261    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    pub mesh_failure_penalty_weight: f64,
287    pub mesh_failure_penalty_decay: f64,
288
289    pub invalid_message_deliveries_weight: f64,
295    pub invalid_message_deliveries_decay: f64,
296}
297
298impl Default for TopicScoreParams {
301    fn default() -> Self {
302        TopicScoreParams {
303            topic_weight: 0.5,
304            time_in_mesh_weight: 1.0,
306            time_in_mesh_quantum: Duration::from_millis(1),
307            time_in_mesh_cap: 3600.0,
308            first_message_deliveries_weight: 1.0,
310            first_message_deliveries_decay: 0.5,
311            first_message_deliveries_cap: 2000.0,
312            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            mesh_failure_penalty_weight: -1.0,
321            mesh_failure_penalty_decay: 0.5,
322            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        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        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        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}