Skip to main content

tor_guardmgr/vanguards/
set.rs

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