tor_guardmgr/vanguards/
set.rs

1//! Vanguard sets
2
3use std::cmp;
4use std::time::{Duration, SystemTime};
5
6use derive_deftly::{derive_deftly_adhoc, Deftly};
7use rand::{seq::IndexedRandom as _, RngCore};
8use serde::{Deserialize, Serialize};
9
10use tor_basic_utils::RngExt as _;
11use tor_error::internal;
12use tor_linkspec::{HasRelayIds as _, RelayIdSet, RelayIds};
13use tor_netdir::{NetDir, Relay};
14use tor_relay_selection::{LowLevelRelayPredicate as _, RelayExclusion, RelaySelector, RelayUsage};
15use tor_rtcompat::Runtime;
16use tracing::{debug, trace};
17
18use crate::{VanguardMgrError, VanguardMode};
19
20use super::VanguardParams;
21
22/// A vanguard relay.
23#[derive(Clone, amplify::Getters)]
24pub struct Vanguard<'a> {
25    /// The relay.
26    relay: Relay<'a>,
27}
28
29/// An identifier for a time-bound vanguard.
30///
31/// Each vanguard [`Layer`](crate::vanguards::Layer) consists of a [`VanguardSet`],
32/// which contains multiple `TimeBoundVanguard`s.
33///
34/// A [`VanguardSet`]'s `TimeBoundVanguard`s are rotated
35/// by [`VanguardMgr`](crate::vanguards::VanguardMgr) as soon as they expire.
36/// If [Full](crate::vanguards::VanguardMode) vanguards are in use,
37/// the `TimeBoundVanguard`s from all layers are persisted to disk.
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] //
39pub(crate) struct TimeBoundVanguard {
40    /// The ID of this relay.
41    pub(super) id: RelayIds,
42    /// When to stop using this relay as a vanguard.
43    pub(super) when: SystemTime,
44}
45
46/// A set of vanguards, for use in a particular [`Layer`](crate::vanguards::Layer).
47///
48/// `VanguardSet`s start out with a target size of `0`.
49///
50/// Upon obtaining a `NetDir`, users of this type should update the target
51/// based on the the current [`NetParameters`](tor_netdir::params::NetParameters).
52#[derive(Default, Debug, Clone, PartialEq)] //
53#[derive(Serialize, Deserialize)] //
54#[serde(transparent)]
55pub(super) struct VanguardSet {
56    /// The time-bound vanguards of a given [`Layer`](crate::vanguards::Layer).
57    vanguards: Vec<TimeBoundVanguard>,
58    /// The number of vanguards we would like to have in this set.
59    ///
60    /// We do not serialize this value, as it should be derived from, and kept up to date with,
61    /// the current [`NetParameters`](tor_netdir::params::NetParameters).
62    #[serde(skip)]
63    target: usize,
64}
65
66/// The L2 and L3 vanguard sets,
67/// stored in the same struct to simplify serialization.
68#[derive(Default, Debug, Clone, PartialEq)] //
69#[derive(Deftly, Serialize, Deserialize)] //
70#[derive_deftly_adhoc]
71pub(super) struct VanguardSets {
72    /// The L2 vanguard sets.
73    l2_vanguards: VanguardSet,
74    /// The L3 vanguard sets.
75    ///
76    /// Only used if full vanguards are enabled.
77    l3_vanguards: VanguardSet,
78}
79
80impl VanguardSets {
81    /// Find the timestamp of the vanguard that is due to expire next.
82    pub(super) fn next_expiry(&self) -> Option<SystemTime> {
83        let l2_expiry = self.l2_vanguards.next_expiry();
84        let l3_expiry = self.l3_vanguards.next_expiry();
85        match (l2_expiry, l3_expiry) {
86            (Some(e), None) | (None, Some(e)) => Some(e),
87            (Some(e1), Some(e2)) => Some(cmp::min(e1, e2)),
88            (None, None) => {
89                // Both vanguard sets are empty
90                None
91            }
92        }
93    }
94
95    /// Return a reference to the L2 [`VanguardSet`].
96    pub(super) fn l2(&self) -> &VanguardSet {
97        &self.l2_vanguards
98    }
99
100    /// Return a reference to the L3 [`VanguardSet`].
101    pub(super) fn l3(&self) -> &VanguardSet {
102        &self.l3_vanguards
103    }
104
105    /// Remove the vanguards that are expired at the specified timestamp.
106    ///
107    /// Returns the number of vanguards that were removed.
108    pub(super) fn remove_expired(&mut self, now: SystemTime) -> usize {
109        let l2_expired = self.l2_vanguards.remove_expired(now);
110        let l3_expired = self.l3_vanguards.remove_expired(now);
111
112        l2_expired + l3_expired
113    }
114
115    /// Remove the vanguards that are no longer listed in `netdir`.
116    ///
117    /// Returns whether either of the two sets have changed.
118    pub(super) fn remove_unlisted(&mut self, netdir: &NetDir) {
119        self.l2_vanguards.remove_unlisted(netdir);
120        self.l3_vanguards.remove_unlisted(netdir);
121    }
122
123    /// Replenish the vanguard sets if necessary, using the directory information
124    /// from the specified [`NetDir`].
125    ///
126    /// Note: the L3 set is only replenished if [`Full`](VanguardMode::Full) vanguards are enabled.
127    pub(super) fn replenish_vanguards<R: Runtime>(
128        &mut self,
129        runtime: &R,
130        netdir: &NetDir,
131        params: &VanguardParams,
132        mode: VanguardMode,
133    ) -> Result<(), VanguardMgrError> {
134        trace!("Replenishing vanguard sets");
135
136        // Resize the vanguard sets if necessary.
137        self.l2_vanguards.update_target(params.l2_pool_size());
138
139        let mut rng = rand::rng();
140        Self::replenish_set(
141            runtime,
142            &mut rng,
143            netdir,
144            &mut self.l2_vanguards,
145            params.l2_lifetime_min(),
146            params.l2_lifetime_max(),
147        )?;
148
149        if mode == VanguardMode::Full {
150            self.l3_vanguards.update_target(params.l3_pool_size());
151            Self::replenish_set(
152                runtime,
153                &mut rng,
154                netdir,
155                &mut self.l3_vanguards,
156                params.l3_lifetime_min(),
157                params.l3_lifetime_max(),
158            )?;
159        }
160
161        Ok(())
162    }
163
164    /// Replenish a single `VanguardSet` with however many vanguards it is short of.
165    fn replenish_set<R: Runtime, Rng: RngCore>(
166        runtime: &R,
167        rng: &mut Rng,
168        netdir: &NetDir,
169        vanguard_set: &mut VanguardSet,
170        min_lifetime: Duration,
171        max_lifetime: Duration,
172    ) -> Result<bool, VanguardMgrError> {
173        let mut set_changed = false;
174        let deficit = vanguard_set.deficit();
175        if deficit > 0 {
176            // Exclude the relays that are already in this vanguard set.
177            let exclude_ids = RelayIdSet::from(&*vanguard_set);
178            let exclude = RelayExclusion::exclude_identities(exclude_ids);
179            // Pick some vanguards to add to the vanguard_set.
180            let new_vanguards = Self::add_n_vanguards(
181                runtime,
182                rng,
183                netdir,
184                deficit,
185                exclude,
186                min_lifetime,
187                max_lifetime,
188            )?;
189
190            if !new_vanguards.is_empty() {
191                set_changed = true;
192            }
193
194            for v in new_vanguards {
195                vanguard_set.add_vanguard(v);
196            }
197        }
198
199        Ok(set_changed)
200    }
201
202    /// Select `n` relays to use as vanguards.
203    ///
204    /// Each selected vanguard will have a random lifetime
205    /// between `min_lifetime` and `max_lifetime`.
206    fn add_n_vanguards<R: Runtime, Rng: RngCore>(
207        runtime: &R,
208        rng: &mut Rng,
209        netdir: &NetDir,
210        n: usize,
211        exclude: RelayExclusion,
212        min_lifetime: Duration,
213        max_lifetime: Duration,
214    ) -> Result<Vec<TimeBoundVanguard>, VanguardMgrError> {
215        trace!(relay_count = n, "selecting relays to use as vanguards");
216
217        let vanguard_sel = RelaySelector::new(RelayUsage::vanguard(), exclude);
218
219        let (relays, _outcome) = vanguard_sel.select_n_relays(rng, n, netdir);
220
221        relays
222            .into_iter()
223            .map(|relay| {
224                // Pick an expiration for this vanguard.
225                let duration = select_lifetime(rng, min_lifetime, max_lifetime)?;
226                let when = runtime.wallclock() + duration;
227
228                Ok(TimeBoundVanguard {
229                    id: RelayIds::from_relay_ids(&relay),
230                    when,
231                })
232            })
233            .collect::<Result<Vec<_>, _>>()
234    }
235}
236
237/// Randomly select the lifetime of a vanguard from the `max(X,X)` distribution,
238/// where `X` is a uniform random value between `min_lifetime` and `max_lifetime`.
239///
240/// This ensures we are biased towards longer lifetimes.
241///
242/// See
243/// <https://spec.torproject.org/vanguards-spec/vanguards-stats.html>
244//
245// Note: the lifetimes of the vanguards (both L2 and L3) are selected
246// from the max(X,X) distribution.
247fn select_lifetime<Rng: RngCore>(
248    rng: &mut Rng,
249    min_lifetime: Duration,
250    max_lifetime: Duration,
251) -> Result<Duration, VanguardMgrError> {
252    let err = || internal!("invalid consensus: vanguard min_lifetime > max_lifetime");
253
254    let l1 = rng
255        .gen_range_checked(min_lifetime..=max_lifetime)
256        .ok_or_else(err)?;
257
258    let l2 = rng
259        .gen_range_checked(min_lifetime..=max_lifetime)
260        .ok_or_else(err)?;
261
262    Ok(std::cmp::max(l1, l2))
263}
264
265impl VanguardSet {
266    /// Pick a relay from this set.
267    ///
268    /// See [`VanguardMgr::select_vanguard`](crate::vanguards::VanguardMgr::select_vanguard)
269    /// for more information.
270    pub(super) fn pick_relay<'a, R: RngCore>(
271        &self,
272        rng: &mut R,
273        netdir: &'a NetDir,
274        relay_selector: &RelaySelector<'a>,
275    ) -> Option<Vanguard<'a>> {
276        let good_relays = self
277            .vanguards
278            .iter()
279            .filter_map(|vanguard| {
280                // Skip over any unusable relays
281                let relay = netdir.by_ids(&vanguard.id)?;
282                relay_selector
283                    .low_level_predicate_permits_relay(&relay)
284                    .then_some(relay)
285            })
286            .collect::<Vec<_>>();
287
288        // Note: We make a uniform choice instead of a weighted one,
289        // because we already made a bandwidth-weighted choice when we added
290        // the vanguards to this set in the first place.
291        good_relays.choose(rng).map(|relay| Vanguard {
292            relay: relay.clone(),
293        })
294    }
295
296    /// Whether this vanguard set is empty.
297    pub(super) fn is_empty(&self) -> bool {
298        self.vanguards.is_empty()
299    }
300
301    /// The number of vanguards we're missing.
302    fn deficit(&self) -> usize {
303        self.target.saturating_sub(self.vanguards.len())
304    }
305
306    /// Add a vanguard to this set.
307    fn add_vanguard(&mut self, v: TimeBoundVanguard) {
308        self.vanguards.push(v);
309    }
310
311    /// Remove the vanguards that are no longer listed in `netdir`
312    ///
313    /// Returns the number of vanguards that were unlisted.
314    fn remove_unlisted(&mut self, netdir: &NetDir) -> usize {
315        self.retain(|v| {
316            let cond = netdir.ids_listed(&v.id) != Some(false);
317
318            if !cond {
319                debug!(id=?v.id, "Removing newly-unlisted vanguard");
320            }
321
322            cond
323        })
324    }
325
326    /// Remove the vanguards that are expired at the specified timestamp.
327    ///
328    /// Returns the number of vanguards that expired.
329    fn remove_expired(&mut self, now: SystemTime) -> usize {
330        self.retain(|v| {
331            let cond = v.when > now;
332
333            if !cond {
334                debug!(id=?v.id, "Removing expired vanguard");
335            }
336
337            cond
338        })
339    }
340
341    /// A wrapper around [`Vec::retain`] that returns the number of discarded elements.
342    fn retain<F>(&mut self, f: F) -> usize
343    where
344        F: FnMut(&TimeBoundVanguard) -> bool,
345    {
346        let old_len = self.vanguards.len();
347        self.vanguards.retain(f);
348        old_len - self.vanguards.len()
349    }
350
351    /// Find the timestamp of the vanguard that is due to expire next.
352    fn next_expiry(&self) -> Option<SystemTime> {
353        self.vanguards.iter().map(|v| v.when).min()
354    }
355
356    /// Update the target size of this set, discarding or requesting additional vanguards if needed.
357    fn update_target(&mut self, target: usize) {
358        self.target = target;
359    }
360}
361
362impl From<&VanguardSet> for RelayIdSet {
363    fn from(vanguard_set: &VanguardSet) -> Self {
364        vanguard_set
365            .vanguards
366            .iter()
367            .flat_map(|vanguard| {
368                vanguard
369                    .id
370                    .clone()
371                    .identities()
372                    .map(|id| id.to_owned())
373                    .collect::<Vec<_>>()
374            })
375            .collect()
376    }
377}
378
379// Some accessors we need in the VanguardMgr tests.
380#[cfg(test)]
381derive_deftly_adhoc! {
382    VanguardSets expect items:
383
384    impl VanguardSets {
385        $(
386            #[doc = concat!("Return the ", stringify!($fname))]
387            pub(super) fn $fname(&self) -> &Vec<TimeBoundVanguard> {
388                &self.$fname.vanguards
389            }
390
391            #[doc = concat!("Return the target size of the ", stringify!($fname), " set")]
392            pub(super) fn $<$fname _target>(&self) -> usize {
393                self.$fname.target
394            }
395
396            #[doc = concat!("Return the deficit of the ", stringify!($fname), " set")]
397            pub(super) fn $<$fname _deficit>(&self) -> usize {
398                self.$fname.deficit()
399            }
400
401        )
402    }
403}