Skip to main content

hopr_ct_full_network/
config.rs

1#[cfg(feature = "serde")]
2use serde::{Deserialize, Serialize};
3use validator::{Validate, ValidationError, ValidationErrors};
4
5/// Configuration for the probing mechanism
6#[derive(Debug, Clone, Copy, PartialEq, smart_default::SmartDefault)]
7#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(deny_unknown_fields))]
8pub struct ProberConfig {
9    /// The delay between individual probing rounds for neighbor discovery
10    #[cfg_attr(
11        feature = "serde",
12        serde(default = "default_probing_interval", with = "humantime_serde")
13    )]
14    #[default(default_probing_interval())]
15    pub interval: std::time::Duration,
16
17    /// Weight for the staleness factor in probe priority (0.0–1.0).
18    ///
19    /// Higher values prioritize probing edges that haven't been measured recently.
20    /// Set to `0.0` to disable staleness-based probing (edges are not prioritized by age).
21    /// At least one of `staleness_weight`, `quality_weight`, or `base_priority` must be positive.
22    #[cfg_attr(feature = "serde", serde(default = "default_staleness_weight"))]
23    #[default(default_staleness_weight())]
24    pub staleness_weight: f64,
25
26    /// Weight for the inverse quality factor in probe priority (0.0–1.0).
27    ///
28    /// Higher values prioritize probing edges with poor quality scores.
29    /// Set to `0.0` to disable quality-based probing (edges are not prioritized by their score).
30    /// At least one of `staleness_weight`, `quality_weight`, or `base_priority` must be positive.
31    #[cfg_attr(feature = "serde", serde(default = "default_quality_weight"))]
32    #[default(default_quality_weight())]
33    pub quality_weight: f64,
34
35    /// Minimum probe chance added for all peers regardless of measurements (0.0–1.0).
36    ///
37    /// Ensures that even well-measured, recently-probed peers retain some chance of re-probing.
38    /// Set to `0.0` only when `staleness_weight` and/or `quality_weight` are sufficient to
39    /// guarantee all peers receive probe opportunities.
40    /// At least one of `staleness_weight`, `quality_weight`, or `base_priority` must be positive.
41    #[cfg_attr(feature = "serde", serde(default = "default_base_priority"))]
42    #[default(default_base_priority())]
43    pub base_priority: f64,
44
45    /// TTL for the cached weighted shuffle order.
46    ///
47    /// When expired, the graph is re-traversed and a new priority-ordered shuffle is computed.
48    /// Defaults to `2 × interval`.
49    #[cfg_attr(feature = "serde", serde(default = "default_shuffle_ttl", with = "humantime_serde"))]
50    #[default(default_shuffle_ttl())]
51    pub shuffle_ttl: std::time::Duration,
52
53    /// When `true`, neighbor probes are only sent to peers that have a
54    /// `Connected(true)` edge in the graph (i.e. the background discovery
55    /// process has already established a transport-level connection).
56    ///
57    /// When `false`, all known peers are probed regardless of connection
58    /// state — useful during bootstrap or when discovery runs out-of-band.
59    #[cfg_attr(feature = "serde", serde(default = "just_true"))]
60    #[default(just_true())]
61    pub probe_connected_only: bool,
62}
63
64impl Validate for ProberConfig {
65    fn validate(&self) -> Result<(), ValidationErrors> {
66        let mut errors = ValidationErrors::new();
67
68        if !(0.0..=1.0).contains(&self.staleness_weight) {
69            errors.add(
70                "staleness_weight",
71                ValidationError::new("staleness_weight must be between 0.0 and 1.0"),
72            );
73        }
74        if !(0.0..=1.0).contains(&self.quality_weight) {
75            errors.add(
76                "quality_weight",
77                ValidationError::new("quality_weight must be between 0.0 and 1.0"),
78            );
79        }
80        if !(0.0..=1.0).contains(&self.base_priority) {
81            errors.add(
82                "base_priority",
83                ValidationError::new("base_priority must be between 0.0 and 1.0"),
84            );
85        }
86
87        if self.staleness_weight + self.quality_weight + self.base_priority <= 0.0 {
88            errors.add(
89                "weights",
90                ValidationError::new("at least one priority weight must be positive"),
91            );
92        }
93
94        if errors.is_empty() { Ok(()) } else { Err(errors) }
95    }
96}
97
98#[inline]
99const fn default_staleness_weight() -> f64 {
100    0.4
101}
102
103#[inline]
104const fn default_quality_weight() -> f64 {
105    0.3
106}
107
108#[inline]
109const fn default_base_priority() -> f64 {
110    0.3
111}
112
113#[inline]
114const fn default_shuffle_ttl() -> std::time::Duration {
115    std::time::Duration::from_secs(default_probing_interval().as_secs() * 2)
116}
117
118#[inline]
119const fn default_probing_interval() -> std::time::Duration {
120    std::time::Duration::from_secs(30)
121}
122
123#[inline]
124const fn just_true() -> bool {
125    true
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn default_config_is_valid() {
134        let cfg = ProberConfig::default();
135        assert!(cfg.validate().is_ok());
136        assert!(cfg.probe_connected_only, "probe_connected_only should default to true");
137    }
138
139    #[test]
140    fn all_zero_weights_are_invalid() {
141        let cfg = ProberConfig {
142            staleness_weight: 0.0,
143            quality_weight: 0.0,
144            base_priority: 0.0,
145            ..Default::default()
146        };
147        let err = cfg.validate().unwrap_err();
148        assert!(err.field_errors().contains_key("weights"));
149    }
150
151    #[test]
152    fn zero_staleness_weight_alone_is_valid() {
153        let cfg = ProberConfig {
154            staleness_weight: 0.0,
155            ..Default::default()
156        };
157        assert!(cfg.validate().is_ok());
158    }
159
160    #[test]
161    fn zero_quality_weight_alone_is_valid() {
162        let cfg = ProberConfig {
163            quality_weight: 0.0,
164            ..Default::default()
165        };
166        assert!(cfg.validate().is_ok());
167    }
168
169    #[test]
170    fn zero_base_priority_alone_is_valid() {
171        let cfg = ProberConfig {
172            base_priority: 0.0,
173            ..Default::default()
174        };
175        assert!(cfg.validate().is_ok());
176    }
177
178    #[test]
179    fn staleness_weight_above_one_is_invalid() {
180        let cfg = ProberConfig {
181            staleness_weight: 1.1,
182            ..Default::default()
183        };
184        let err = cfg.validate().unwrap_err();
185        assert!(err.field_errors().contains_key("staleness_weight"));
186    }
187
188    #[test]
189    fn quality_weight_above_one_is_invalid() {
190        let cfg = ProberConfig {
191            quality_weight: 1.1,
192            ..Default::default()
193        };
194        let err = cfg.validate().unwrap_err();
195        assert!(err.field_errors().contains_key("quality_weight"));
196    }
197
198    #[test]
199    fn base_priority_above_one_is_invalid() {
200        let cfg = ProberConfig {
201            base_priority: 1.1,
202            ..Default::default()
203        };
204        let err = cfg.validate().unwrap_err();
205        assert!(err.field_errors().contains_key("base_priority"));
206    }
207
208    #[test]
209    fn negative_staleness_weight_is_invalid() {
210        let cfg = ProberConfig {
211            staleness_weight: -0.1,
212            quality_weight: 0.5,
213            base_priority: 0.5,
214            ..Default::default()
215        };
216        let err = cfg.validate().unwrap_err();
217        assert!(err.field_errors().contains_key("staleness_weight"));
218    }
219
220    #[test]
221    fn negative_quality_weight_is_invalid() {
222        let cfg = ProberConfig {
223            staleness_weight: 0.5,
224            quality_weight: -0.1,
225            base_priority: 0.5,
226            ..Default::default()
227        };
228        let err = cfg.validate().unwrap_err();
229        assert!(err.field_errors().contains_key("quality_weight"));
230    }
231
232    #[test]
233    fn negative_base_priority_is_invalid() {
234        let cfg = ProberConfig {
235            staleness_weight: 0.5,
236            quality_weight: 0.5,
237            base_priority: -0.1,
238            ..Default::default()
239        };
240        let err = cfg.validate().unwrap_err();
241        assert!(err.field_errors().contains_key("base_priority"));
242    }
243}