1
#![cfg_attr(docsrs, feature(doc_cfg))]
2
#![doc = include_str!("../README.md")]
3
// @@ begin lint list maintained by maint/add_warning @@
4
#![allow(renamed_and_removed_lints)] // @@REMOVE_WHEN(ci_arti_stable)
5
#![allow(unknown_lints)] // @@REMOVE_WHEN(ci_arti_nightly)
6
#![warn(missing_docs)]
7
#![warn(noop_method_call)]
8
#![warn(unreachable_pub)]
9
#![warn(clippy::all)]
10
#![deny(clippy::await_holding_lock)]
11
#![deny(clippy::cargo_common_metadata)]
12
#![deny(clippy::cast_lossless)]
13
#![deny(clippy::checked_conversions)]
14
#![allow(clippy::cognitive_complexity)] // See arti#2556
15
#![deny(clippy::debug_assert_with_mut_call)]
16
#![deny(clippy::exhaustive_enums)]
17
#![deny(clippy::exhaustive_structs)]
18
#![deny(clippy::expl_impl_clone_on_copy)]
19
#![deny(clippy::fallible_impl_from)]
20
#![deny(clippy::implicit_clone)]
21
#![deny(clippy::large_stack_arrays)]
22
#![warn(clippy::manual_ok_or)]
23
#![deny(clippy::missing_docs_in_private_items)]
24
#![warn(clippy::needless_borrow)]
25
#![warn(clippy::needless_pass_by_value)]
26
#![warn(clippy::option_option)]
27
#![warn(clippy::or_fun_call)]
28
#![deny(clippy::print_stderr)]
29
#![deny(clippy::print_stdout)]
30
#![warn(clippy::rc_buffer)]
31
#![deny(clippy::ref_option_ref)]
32
#![warn(clippy::semicolon_if_nothing_returned)]
33
#![warn(clippy::trait_duplication_in_bounds)]
34
#![deny(clippy::unchecked_time_subtraction)]
35
#![deny(clippy::unnecessary_wraps)]
36
#![warn(clippy::unseparated_literal_suffix)]
37
#![deny(clippy::unwrap_used)]
38
#![deny(clippy::mod_module_files)]
39
#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness
40
#![allow(clippy::uninlined_format_args)]
41
#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
42
#![allow(clippy::result_large_err)] // temporary workaround for arti#587
43
#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
44
#![allow(clippy::needless_lifetimes)] // See arti#1765
45
#![allow(mismatched_lifetime_syntaxes)] // temporary workaround for arti#2060
46
#![allow(clippy::collapsible_if)] // See arti#2342
47
#![deny(clippy::unused_async)]
48
#![deny(clippy::string_slice)] // See arti#2571
49
#![allow(recursion_depth_exceeding_limit)] // arti#2715, rust/issues/159228
50
//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
51

            
52
pub mod details;
53
mod err;
54
#[cfg(feature = "hs-common")]
55
mod hsdir_params;
56
#[cfg(feature = "hs-common")]
57
mod hsdir_ring;
58
pub mod params;
59
mod weight;
60

            
61
#[cfg(any(test, feature = "testing"))]
62
pub mod testnet;
63
#[cfg(feature = "testing")]
64
pub mod testprovider;
65

            
66
use async_trait::async_trait;
67
#[cfg(feature = "hs-service")]
68
use itertools::chain;
69
use tor_error::warn_report;
70
#[cfg(feature = "hs-common")]
71
use tor_linkspec::OwnedCircTarget;
72
use tor_linkspec::{
73
    ChanTarget, DirectChanMethodsHelper, HasAddrs, HasRelayIds, RelayIdRef, RelayIdType,
74
};
75
use tor_llcrypto as ll;
76
use tor_llcrypto::pk::{ed25519::Ed25519Identity, rsa::RsaIdentity};
77
use tor_netdoc::doc::microdesc::{MdDigest, MicrodescAndHash};
78
use tor_netdoc::doc::netstatus::{self, MdConsensus, MdRouterStatus};
79
#[cfg(feature = "hs-common")]
80
use {hsdir_ring::HsDirRing, std::iter};
81

            
82
use derive_more::{From, Into};
83
use futures::{StreamExt, stream::BoxStream};
84
use num_enum::{IntoPrimitive, TryFromPrimitive};
85
use rand::seq::{IndexedRandom as _, SliceRandom as _, WeightError};
86
use serde::Deserialize;
87
use std::collections::HashMap;
88
use std::net::IpAddr;
89
use std::ops::Deref;
90
use std::sync::Arc;
91
use std::time::SystemTime;
92
use strum::{EnumCount, EnumIter};
93
use tracing::warn;
94
use typed_index_collections::{TiSlice, TiVec};
95

            
96
#[cfg(feature = "hs-common")]
97
use {
98
    itertools::Itertools,
99
    std::collections::HashSet,
100
    std::result::Result as StdResult,
101
    tor_error::{Bug, internal},
102
    tor_hscrypto::{pk::HsBlindId, time::TimePeriod},
103
    tor_linkspec::{OwnedChanTargetBuilder, verbatim::VerbatimLinkSpecCircTarget},
104
    tor_llcrypto::pk::curve25519,
105
};
106

            
107
pub use err::Error;
108
pub use weight::WeightRole;
109
/// A Result using the Error type from the tor-netdir crate
110
pub type Result<T> = std::result::Result<T, Error>;
111

            
112
#[cfg(feature = "hs-common")]
113
pub use err::{OnionDirLookupError, VerbatimCircTargetDecodeError};
114

            
115
use params::NetParameters;
116
#[cfg(feature = "geoip")]
117
use tor_geoip::{CountryCode, GeoipDb, HasCountryCode};
118

            
119
#[cfg(feature = "hs-common")]
120
pub use hsdir_params::HsDirParams;
121

            
122
/// Index into the consensus relays
123
///
124
/// This is an index into the list of relays returned by
125
/// [`.c_relays()`](ConsensusRelays::c_relays)
126
/// (on the corresponding consensus or netdir).
127
///
128
/// This is just a `usize` inside, but using a newtype prevents getting a relay index
129
/// confused with other kinds of slice indices or counts.
130
///
131
/// If you are in a part of the code which needs to work with multiple consensuses,
132
/// the typechecking cannot tell if you try to index into the wrong consensus.
133
#[derive(Debug, From, Into, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
134
pub(crate) struct RouterStatusIdx(usize);
135

            
136
/// Extension trait to provide index-type-safe `.c_relays()` method
137
//
138
// TODO: Really it would be better to have MdConsensns::relays() return TiSlice,
139
// but that would be an API break there.
140
pub(crate) trait ConsensusRelays {
141
    /// Obtain the list of relays in the consensus
142
    //
143
    fn c_relays(&self) -> &TiSlice<RouterStatusIdx, MdRouterStatus>;
144
}
145
impl ConsensusRelays for MdConsensus {
146
53266291
    fn c_relays(&self) -> &TiSlice<RouterStatusIdx, MdRouterStatus> {
147
53266291
        TiSlice::from_ref(MdConsensus::relays(self))
148
53266291
    }
149
}
150
impl ConsensusRelays for NetDir {
151
53168192
    fn c_relays(&self) -> &TiSlice<RouterStatusIdx, MdRouterStatus> {
152
53168192
        self.consensus.c_relays()
153
53168192
    }
154
}
155

            
156
/// Configuration for determining when two relays have addresses "too close" in
157
/// the network.
158
///
159
/// Used by `Relay::low_level_details().in_same_subnet()`.
160
#[derive(Deserialize, Debug, Clone, Copy, Eq, PartialEq)]
161
#[serde(deny_unknown_fields)]
162
pub struct SubnetConfig {
163
    /// Consider IPv4 nodes in the same /x to be the same family.
164
    ///
165
    /// If this value is 0, all nodes with IPv4 addresses will be in the
166
    /// same family.  If this value is above 32, then no nodes will be
167
    /// placed im the same family based on their IPv4 addresses.
168
    subnets_family_v4: u8,
169
    /// Consider IPv6 nodes in the same /x to be the same family.
170
    ///
171
    /// If this value is 0, all nodes with IPv6 addresses will be in the
172
    /// same family.  If this value is above 128, then no nodes will be
173
    /// placed im the same family based on their IPv6 addresses.
174
    subnets_family_v6: u8,
175
}
176

            
177
impl Default for SubnetConfig {
178
570159
    fn default() -> Self {
179
570159
        Self::new(16, 32)
180
570159
    }
181
}
182

            
183
impl SubnetConfig {
184
    /// Construct a new SubnetConfig from a pair of bit prefix lengths.
185
    ///
186
    /// The values are clamped to the appropriate ranges if they are
187
    /// out-of-bounds.
188
1731952
    pub fn new(subnets_family_v4: u8, subnets_family_v6: u8) -> Self {
189
1731952
        Self {
190
1731952
            subnets_family_v4,
191
1731952
            subnets_family_v6,
192
1731952
        }
193
1731952
    }
194

            
195
    /// Construct a new SubnetConfig such that addresses are not in the same
196
    /// family with anything--not even with themselves.
197
3488764
    pub fn no_addresses_match() -> SubnetConfig {
198
3488764
        SubnetConfig {
199
3488764
            subnets_family_v4: 33,
200
3488764
            subnets_family_v6: 129,
201
3488764
        }
202
3488764
    }
203

            
204
    /// Return true if the two addresses in the same subnet, according to this
205
    /// configuration.
206
48639864
    pub fn addrs_in_same_subnet(&self, a: &IpAddr, b: &IpAddr) -> bool {
207
48639864
        match (a, b) {
208
48639848
            (IpAddr::V4(a), IpAddr::V4(b)) => {
209
48639848
                let bits = self.subnets_family_v4;
210
48639848
                if bits > 32 {
211
6960
                    return false;
212
48632888
                }
213
48632888
                let a = u32::from_be_bytes(a.octets());
214
48632888
                let b = u32::from_be_bytes(b.octets());
215
48632888
                (a >> (32 - bits)) == (b >> (32 - bits))
216
            }
217
10
            (IpAddr::V6(a), IpAddr::V6(b)) => {
218
10
                let bits = self.subnets_family_v6;
219
10
                if bits > 128 {
220
2
                    return false;
221
8
                }
222
8
                let a = u128::from_be_bytes(a.octets());
223
8
                let b = u128::from_be_bytes(b.octets());
224
8
                (a >> (128 - bits)) == (b >> (128 - bits))
225
            }
226
6
            _ => false,
227
        }
228
48639864
    }
229

            
230
    /// Return true if any of the addresses in `a` shares a subnet with any of
231
    /// the addresses in `b`, according to this configuration.
232
48639839
    pub fn any_addrs_in_same_subnet<T, U>(&self, a: &T, b: &U) -> bool
233
48639839
    where
234
48639839
        T: tor_linkspec::HasAddrs,
235
48639839
        U: tor_linkspec::HasAddrs,
236
    {
237
48639842
        a.addrs().any(|aa| {
238
48639842
            b.addrs()
239
48639846
                .any(|bb| self.addrs_in_same_subnet(&aa.ip(), &bb.ip()))
240
48639842
        })
241
48639839
    }
242

            
243
    /// Return a new subnet configuration that is the union of `self` and
244
    /// `other`.
245
    ///
246
    /// That is, return a subnet configuration that puts all addresses in the
247
    /// same subnet if and only if at least one of `self` and `other` would put
248
    /// them in the same subnet.
249
1741259
    pub fn union(&self, other: &Self) -> Self {
250
        use std::cmp::min;
251
1741259
        Self {
252
1741259
            subnets_family_v4: min(self.subnets_family_v4, other.subnets_family_v4),
253
1741259
            subnets_family_v6: min(self.subnets_family_v6, other.subnets_family_v6),
254
1741259
        }
255
1741259
    }
256
}
257

            
258
/// Configuration for which listed family information to use when deciding
259
/// whether relays belong to the same family.
260
///
261
/// Derived from network parameters.
262
#[derive(Clone, Copy, Debug)]
263
pub struct FamilyRules {
264
    /// If true, we use family information from lists of family members.
265
    use_family_lists: bool,
266
    /// If true, we use family information from lists of family IDs and from family certs.
267
    use_family_ids: bool,
268
}
269

            
270
impl<'a> From<&'a NetParameters> for FamilyRules {
271
580826
    fn from(params: &'a NetParameters) -> Self {
272
580826
        FamilyRules {
273
580826
            use_family_lists: bool::from(params.use_family_lists),
274
580826
            use_family_ids: bool::from(params.use_family_ids),
275
580826
        }
276
580826
    }
277
}
278

            
279
impl FamilyRules {
280
    /// Return a `FamilyRules` that will use all recognized kinds of family information.
281
894
    pub fn all_family_info() -> Self {
282
894
        Self {
283
894
            use_family_lists: true,
284
894
            use_family_ids: true,
285
894
        }
286
894
    }
287

            
288
    /// Return a `FamilyRules` that will ignore all family information declared by relays.
289
3488857
    pub fn ignore_declared_families() -> Self {
290
3488857
        Self {
291
3488857
            use_family_lists: false,
292
3488857
            use_family_ids: false,
293
3488857
        }
294
3488857
    }
295

            
296
    /// Configure this `FamilyRules` to use (or not use) family information from
297
    /// lists of family members.
298
47
    pub fn use_family_lists(&mut self, val: bool) -> &mut Self {
299
47
        self.use_family_lists = val;
300
47
        self
301
47
    }
302

            
303
    /// Configure this `FamilyRules` to use (or not use) family information from
304
    /// family IDs and family certs.
305
47
    pub fn use_family_ids(&mut self, val: bool) -> &mut Self {
306
47
        self.use_family_ids = val;
307
47
        self
308
47
    }
309

            
310
    /// Return a `FamilyRules` that will look at every source of information
311
    /// requested by `self` or by `other`.
312
1741256
    pub fn union(&self, other: &Self) -> Self {
313
        Self {
314
1741256
            use_family_lists: self.use_family_lists || other.use_family_lists,
315
1741256
            use_family_ids: self.use_family_ids || other.use_family_ids,
316
        }
317
1741256
    }
318
}
319

            
320
/// An opaque type representing the weight with which a relay or set of
321
/// relays will be selected for a given role.
322
///
323
/// Most users should ignore this type, and just use pick_relay instead.
324
#[derive(
325
    Copy,
326
    Clone,
327
    Debug,
328
    derive_more::Add,
329
    derive_more::Sum,
330
    derive_more::AddAssign,
331
    Eq,
332
    PartialEq,
333
    Ord,
334
    PartialOrd,
335
)]
336
pub struct RelayWeight(u64);
337

            
338
impl RelayWeight {
339
    /// Try to divide this weight by `rhs`.
340
    ///
341
    /// Return a ratio on success, or None on division-by-zero.
342
3574
    pub fn checked_div(&self, rhs: RelayWeight) -> Option<f64> {
343
3574
        if rhs.0 == 0 {
344
1
            None
345
        } else {
346
3573
            Some((self.0 as f64) / (rhs.0 as f64))
347
        }
348
3574
    }
349

            
350
    /// Compute a ratio `frac` of this weight.
351
    ///
352
    /// Return None if frac is less than zero, since negative weights
353
    /// are impossible.
354
27262
    pub fn ratio(&self, frac: f64) -> Option<RelayWeight> {
355
27262
        let product = (self.0 as f64) * frac;
356
27262
        if product >= 0.0 && product.is_finite() {
357
27261
            Some(RelayWeight(product as u64))
358
        } else {
359
1
            None
360
        }
361
27262
    }
362
}
363

            
364
impl From<u64> for RelayWeight {
365
7144
    fn from(val: u64) -> Self {
366
7144
        RelayWeight(val)
367
7144
    }
368
}
369

            
370
/// An operation for which we might be requesting a hidden service directory.
371
#[derive(Copy, Clone, Debug, PartialEq)]
372
// TODO: make this pub(crate) once NetDir::hs_dirs is removed
373
#[non_exhaustive]
374
pub enum HsDirOp {
375
    /// Uploading an onion service descriptor.
376
    #[cfg(feature = "hs-service")]
377
    Upload,
378
    /// Downloading an onion service descriptor.
379
    Download,
380
}
381

            
382
/// A view of the Tor directory, suitable for use in building circuits.
383
///
384
/// Abstractly, a [`NetDir`] is a set of usable public [`Relay`]s, each of which
385
/// has its own properties, identity, and correct weighted probability for use
386
/// under different circumstances.
387
///
388
/// A [`NetDir`] is constructed by making a [`PartialNetDir`] from a consensus
389
/// document, and then adding enough microdescriptors to that `PartialNetDir` so
390
/// that it can be used to build paths. (Thus, if you have a NetDir, it is
391
/// definitely adequate to build paths.)
392
///
393
/// # "Usable" relays
394
///
395
/// Many methods on NetDir are defined in terms of <a name="usable">"Usable"</a> relays.  Unless
396
/// otherwise stated, a relay is "usable" if it is listed in the consensus,
397
/// if we have full directory information for that relay (including a
398
/// microdescriptor), and if that relay does not have any flags indicating that
399
/// we should never use it. (Currently, `NoEdConsensus` is the only such flag.)
400
///
401
/// # Limitations
402
///
403
/// The current NetDir implementation assumes fairly strongly that every relay
404
/// has an Ed25519 identity and an RSA identity, that the consensus is indexed
405
/// by RSA identities, and that the Ed25519 identities are stored in
406
/// microdescriptors.
407
///
408
/// If these assumptions someday change, then we'll have to revise the
409
/// implementation.
410
#[derive(Debug, Clone)]
411
pub struct NetDir {
412
    /// A microdescriptor consensus that lists the members of the network,
413
    /// and maps each one to a 'microdescriptor' that has more information
414
    /// about it
415
    consensus: Arc<MdConsensus>,
416
    /// A map from keys to integer values, distributed in the consensus,
417
    /// and clamped to certain defaults.
418
    params: NetParameters,
419
    /// Map from routerstatus index, to that routerstatus's microdescriptor (if we have one.)
420
    mds: TiVec<RouterStatusIdx, Option<Arc<MicrodescAndHash>>>,
421
    /// Map from SHA256 of _missing_ microdescriptors to the index of their
422
    /// corresponding routerstatus.
423
    rsidx_by_missing: HashMap<MdDigest, RouterStatusIdx>,
424
    /// Map from ed25519 identity to index of the routerstatus.
425
    ///
426
    /// Note that we don't know the ed25519 identity of a relay until
427
    /// we get the microdescriptor for it, so this won't be filled in
428
    /// until we get the microdescriptors.
429
    ///
430
    /// # Implementation note
431
    ///
432
    /// For this field, and for `rsidx_by_rsa`,
433
    /// it might be cool to have references instead.
434
    /// But that would make this into a self-referential structure,
435
    /// which isn't possible in safe rust.
436
    rsidx_by_ed: HashMap<Ed25519Identity, RouterStatusIdx>,
437
    /// Map from RSA identity to index of the routerstatus.
438
    ///
439
    /// This is constructed at the same time as the NetDir object, so it
440
    /// can be immutable.
441
    rsidx_by_rsa: Arc<HashMap<RsaIdentity, RouterStatusIdx>>,
442

            
443
    /// Hash ring(s) describing the onion service directory.
444
    ///
445
    /// This is empty in a PartialNetDir, and is filled in before the NetDir is
446
    /// built.
447
    //
448
    // TODO hs: It is ugly to have this exist in a partially constructed state
449
    // in a PartialNetDir.
450
    // Ideally, a PartialNetDir would contain only an HsDirs<HsDirParams>,
451
    // or perhaps nothing at all, here.
452
    #[cfg(feature = "hs-common")]
453
    hsdir_rings: Arc<HsDirs<HsDirRing>>,
454

            
455
    /// Weight values to apply to a given relay when deciding how frequently
456
    /// to choose it for a given role.
457
    weights: weight::WeightSet,
458

            
459
    #[cfg(feature = "geoip")]
460
    /// Country codes for each router in our consensus.
461
    ///
462
    /// This is indexed by the `RouterStatusIdx` (i.e. a router idx of zero has
463
    /// the country code at position zero in this array).
464
    country_codes: Vec<Option<CountryCode>>,
465
}
466

            
467
/// Collection of hidden service directories (or parameters for them)
468
///
469
/// In [`NetDir`] this is used to store the actual hash rings.
470
/// (But, in a NetDir in a [`PartialNetDir`], it contains [`HsDirRing`]s
471
/// where only the `params` are populated, and the `ring` is empty.)
472
///
473
/// This same generic type is used as the return type from
474
/// [`HsDirParams::compute`](HsDirParams::compute),
475
/// where it contains the *parameters* for the primary and secondary rings.
476
#[derive(Debug, Clone)]
477
#[cfg(feature = "hs-common")]
478
pub(crate) struct HsDirs<D> {
479
    /// The current ring
480
    ///
481
    /// It corresponds to the time period containing the `valid-after` time in
482
    /// the consensus. Its SRV is whatever SRV was most current at the time when
483
    /// that time period began.
484
    ///
485
    /// This is the hash ring that we should use whenever we are fetching an
486
    /// onion service descriptor.
487
    current: D,
488

            
489
    /// Secondary rings (based on the parameters for the previous and next time periods)
490
    ///
491
    /// Onion services upload to positions on these ring as well, based on how
492
    /// far into the current time period this directory is, so that
493
    /// not-synchronized clients can still find their descriptor.
494
    ///
495
    /// Note that with the current (2023) network parameters, with
496
    /// `hsdir_interval = SRV lifetime = 24 hours` at most one of these
497
    /// secondary rings will be active at a time.  We have two here in order
498
    /// to conform with a more flexible regime in proposal 342.
499
    //
500
    // TODO: hs clients never need this; so I've made it not-present for them.
501
    // But does that risk too much with respect to side channels?
502
    //
503
    // TODO: Perhaps we should refactor this so that it is clear that these
504
    // are immutable?  On the other hand, the documentation for this type
505
    // declares that it is immutable, so we are likely okay.
506
    //
507
    // TODO: this `Vec` is only ever 0,1,2 elements.
508
    // Maybe it should be an ArrayVec or something.
509
    #[cfg(feature = "hs-service")]
510
    secondary: Vec<D>,
511
}
512

            
513
#[cfg(feature = "hs-common")]
514
impl<D> HsDirs<D> {
515
    /// Convert an `HsDirs<D>` to `HsDirs<D2>` by mapping each contained `D`
516
19491
    pub(crate) fn map<D2>(self, mut f: impl FnMut(D) -> D2) -> HsDirs<D2> {
517
19491
        HsDirs {
518
19491
            current: f(self.current),
519
19491
            #[cfg(feature = "hs-service")]
520
19491
            secondary: self.secondary.into_iter().map(f).collect(),
521
19491
        }
522
19491
    }
523

            
524
    /// Iterate over some of the contained hsdirs, according to `secondary`
525
    ///
526
    /// The current ring is always included.
527
    /// Secondary rings are included iff `secondary` and the `hs-service` feature is enabled.
528
754
    fn iter_filter_secondary(&self, secondary: bool) -> impl Iterator<Item = &D> {
529
754
        let i = iter::once(&self.current);
530

            
531
        // With "hs-service" disabled, there are no secondary rings,
532
        // so we don't care.
533
754
        let _ = secondary;
534

            
535
        #[cfg(feature = "hs-service")]
536
754
        let i = chain!(i, self.secondary.iter().filter(move |_| secondary));
537

            
538
754
        i
539
754
    }
540

            
541
    /// Iterate over all the contained hsdirs
542
752
    pub(crate) fn iter(&self) -> impl Iterator<Item = &D> {
543
752
        self.iter_filter_secondary(true)
544
752
    }
545

            
546
    /// Iterate over the hsdirs relevant for `op`
547
2
    pub(crate) fn iter_for_op(&self, op: HsDirOp) -> impl Iterator<Item = &D> {
548
2
        self.iter_filter_secondary(match op {
549
            #[cfg(feature = "hs-service")]
550
1
            HsDirOp::Upload => true,
551
1
            HsDirOp::Download => false,
552
        })
553
2
    }
554
}
555

            
556
/// An event that a [`NetDirProvider`] can broadcast to indicate that a change in
557
/// the status of its directory.
558
#[derive(
559
    Debug, Clone, Copy, PartialEq, Eq, EnumIter, EnumCount, IntoPrimitive, TryFromPrimitive,
560
)]
561
#[non_exhaustive]
562
#[repr(u16)]
563
pub enum DirEvent {
564
    /// A new consensus has been received, and has enough information to be
565
    /// used.
566
    ///
567
    /// This event is also broadcast when a new set of consensus parameters is
568
    /// available, even if that set of parameters comes from a configuration
569
    /// change rather than from the latest consensus.
570
    NewConsensus,
571

            
572
    /// New descriptors have been received for the current consensus.
573
    ///
574
    /// (This event is _not_ broadcast when receiving new descriptors for a
575
    /// consensus which is not yet ready to replace the current consensus.)
576
    NewDescriptors,
577

            
578
    /// We have received updated recommendations and requirements
579
    /// for which subprotocols we should have to use the network.
580
    NewProtocolRecommendation,
581
}
582

            
583
/// The network directory provider is shutting down without giving us the
584
/// netdir we asked for.
585
#[derive(Clone, Copy, Debug, thiserror::Error)]
586
#[error("Network directory provider is shutting down")]
587
#[non_exhaustive]
588
pub struct NetdirProviderShutdown;
589

            
590
impl tor_error::HasKind for NetdirProviderShutdown {
591
    fn kind(&self) -> tor_error::ErrorKind {
592
        tor_error::ErrorKind::ArtiShuttingDown
593
    }
594
}
595

            
596
/// How "timely" must a network directory be?
597
///
598
/// This enum is used as an argument when requesting a [`NetDir`] object from
599
/// [`NetDirProvider`] and other APIs, to specify how recent the information
600
/// must be in order to be useful.
601
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
602
#[allow(clippy::exhaustive_enums)]
603
pub enum Timeliness {
604
    /// The network directory must be strictly timely.
605
    ///
606
    /// That is, it must be based on a consensus that valid right now, with no
607
    /// tolerance for skew or consensus problems.
608
    ///
609
    /// Avoid using this option if you could use [`Timeliness::Timely`] instead.
610
    Strict,
611
    /// The network directory must be roughly timely.
612
    ///
613
    /// This is, it must be based on a consensus that is not _too_ far in the
614
    /// future, and not _too_ far in the past.
615
    ///
616
    /// (The tolerances for "too far" will depend on configuration.)
617
    ///
618
    /// This is almost always the option that you want to use.
619
    Timely,
620
    /// Any network directory is permissible, regardless of how untimely.
621
    ///
622
    /// Avoid using this option if you could use [`Timeliness::Timely`] instead.
623
    Unchecked,
624
}
625

            
626
/// An object that can provide [`NetDir`]s, as well as inform consumers when
627
/// they might have changed.
628
///
629
/// It is the responsibility of the implementor of `NetDirProvider`
630
/// to try to obtain an up-to-date `NetDir`,
631
/// and continuously to maintain and update it.
632
///
633
/// In usual configurations, Arti uses `tor_dirmgr::DirMgr`
634
/// as its `NetDirProvider`.
635
#[async_trait]
636
pub trait NetDirProvider: UpcastArcNetDirProvider + Send + Sync {
637
    /// Return a network directory that's live according to the provided
638
    /// `timeliness`.
639
    fn netdir(&self, timeliness: Timeliness) -> Result<Arc<NetDir>>;
640

            
641
    /// Return a reasonable netdir for general usage.
642
    ///
643
    /// This is an alias for
644
    /// [`NetDirProvider::netdir`]`(`[`Timeliness::Timely`]`)`.
645
390
    fn timely_netdir(&self) -> Result<Arc<NetDir>> {
646
390
        self.netdir(Timeliness::Timely)
647
390
    }
648

            
649
    /// Return a new asynchronous stream that will receive notification
650
    /// whenever the consensus has changed.
651
    ///
652
    /// Multiple events may be batched up into a single item: each time
653
    /// this stream yields an event, all you can assume is that the event has
654
    /// occurred at least once.
655
    fn events(&self) -> BoxStream<'static, DirEvent>;
656

            
657
    /// Return the latest network parameters.
658
    ///
659
    /// If we have no directory, return a reasonable set of defaults.
660
    fn params(&self) -> Arc<dyn AsRef<NetParameters>>;
661

            
662
    /// Get a NetDir from `provider`, waiting until one exists.
663
    async fn wait_for_netdir(
664
        &self,
665
        timeliness: Timeliness,
666
32
    ) -> std::result::Result<Arc<NetDir>, NetdirProviderShutdown> {
667
        if let Ok(nd) = self.netdir(timeliness) {
668
            return Ok(nd);
669
        }
670

            
671
        let mut stream = self.events();
672
        loop {
673
            // We need to retry `self.netdir()` before waiting for any stream events, to
674
            // avoid deadlock.
675
            //
676
            // We ignore all errors here: they can all potentially be fixed by
677
            // getting a fresh consensus, and they will all get warned about
678
            // by the NetDirProvider itself.
679
            if let Ok(nd) = self.netdir(timeliness) {
680
                return Ok(nd);
681
            }
682
            match stream.next().await {
683
                Some(_) => {}
684
                None => {
685
                    return Err(NetdirProviderShutdown);
686
                }
687
            }
688
        }
689
32
    }
690

            
691
    /// Wait until `provider` lists `target`.
692
    ///
693
    /// NOTE: This might potentially wait indefinitely, if `target` is never actually
694
    /// becomes listed in the directory.  It will exit if the `NetDirProvider` shuts down.
695
    async fn wait_for_netdir_to_list(
696
        &self,
697
        target: &tor_linkspec::RelayIds,
698
        timeliness: Timeliness,
699
    ) -> std::result::Result<(), NetdirProviderShutdown> {
700
        let mut events = self.events();
701
        loop {
702
            // See if the desired relay is in the netdir.
703
            //
704
            // We do this before waiting for any events, to avoid race conditions.
705
            {
706
                let netdir = self.wait_for_netdir(timeliness).await?;
707
                if netdir.ids_listed(target) == Some(true) {
708
                    return Ok(());
709
                }
710
                // If we reach this point, then ids_listed returned `Some(false)`,
711
                // meaning "This relay is definitely not in the current directory";
712
                // or it returned `None`, meaning "waiting for more information
713
                // about this network directory.
714
                // In both cases, it's reasonable to just wait for another netdir
715
                // event and try again.
716
            }
717
            // We didn't find the relay; wait for the provider to have a new netdir
718
            // or more netdir information.
719
            if events.next().await.is_none() {
720
                // The event stream is closed; the provider has shut down.
721
                return Err(NetdirProviderShutdown);
722
            }
723
        }
724
    }
725

            
726
    /// Return the latest set of recommended and required protocols, if there is one.
727
    ///
728
    /// This may be more recent (or more available) than this provider's associated NetDir.
729
    fn protocol_statuses(&self) -> Option<(SystemTime, Arc<netstatus::ProtoStatuses>)>;
730
}
731

            
732
#[async_trait]
733
impl<T> NetDirProvider for Arc<T>
734
where
735
    T: NetDirProvider,
736
{
737
    fn netdir(&self, timeliness: Timeliness) -> Result<Arc<NetDir>> {
738
        self.deref().netdir(timeliness)
739
    }
740

            
741
    fn timely_netdir(&self) -> Result<Arc<NetDir>> {
742
        self.deref().timely_netdir()
743
    }
744

            
745
    fn events(&self) -> BoxStream<'static, DirEvent> {
746
        self.deref().events()
747
    }
748

            
749
    fn params(&self) -> Arc<dyn AsRef<NetParameters>> {
750
        self.deref().params()
751
    }
752

            
753
    async fn wait_for_netdir(
754
        &self,
755
        timeliness: Timeliness,
756
    ) -> std::result::Result<Arc<NetDir>, NetdirProviderShutdown> {
757
        self.deref().wait_for_netdir(timeliness).await
758
    }
759

            
760
    async fn wait_for_netdir_to_list(
761
        &self,
762
        target: &tor_linkspec::RelayIds,
763
        timeliness: Timeliness,
764
    ) -> std::result::Result<(), NetdirProviderShutdown> {
765
        self.deref()
766
            .wait_for_netdir_to_list(target, timeliness)
767
            .await
768
    }
769

            
770
    fn protocol_statuses(&self) -> Option<(SystemTime, Arc<netstatus::ProtoStatuses>)> {
771
        self.deref().protocol_statuses()
772
    }
773
}
774

            
775
/// Helper trait: allows any `Arc<X>` to be upcast to a `Arc<dyn
776
/// NetDirProvider>` if X is an implementation or supertrait of NetDirProvider.
777
///
778
/// This trait exists to work around a limitation in rust: when trait upcasting
779
/// coercion is stable, this will be unnecessary.
780
///
781
/// The Rust tracking issue is <https://github.com/rust-lang/rust/issues/65991>.
782
pub trait UpcastArcNetDirProvider {
783
    /// Return a view of this object as an `Arc<dyn NetDirProvider>`
784
    fn upcast_arc<'a>(self: Arc<Self>) -> Arc<dyn NetDirProvider + 'a>
785
    where
786
        Self: 'a;
787
}
788

            
789
impl<T> UpcastArcNetDirProvider for T
790
where
791
    T: NetDirProvider + Sized,
792
{
793
4
    fn upcast_arc<'a>(self: Arc<Self>) -> Arc<dyn NetDirProvider + 'a>
794
4
    where
795
4
        Self: 'a,
796
    {
797
4
        self
798
4
    }
799
}
800

            
801
impl AsRef<NetParameters> for NetDir {
802
1598
    fn as_ref(&self) -> &NetParameters {
803
1598
        self.params()
804
1598
    }
805
}
806

            
807
/// A partially build NetDir -- it can't be unwrapped until it has
808
/// enough information to build safe paths.
809
#[derive(Debug, Clone)]
810
pub struct PartialNetDir {
811
    /// The netdir that's under construction.
812
    netdir: NetDir,
813

            
814
    /// The previous netdir, if we had one
815
    ///
816
    /// Used as a cache, so we can reuse information
817
    #[cfg(feature = "hs-common")]
818
    prev_netdir: Option<Arc<NetDir>>,
819
}
820

            
821
/// A view of a relay on the Tor network, suitable for building circuits.
822
// TODO: This should probably be a more specific struct, with a trait
823
// that implements it.
824
#[derive(Clone)]
825
pub struct Relay<'a> {
826
    /// A router descriptor for this relay.
827
    rs: &'a netstatus::MdRouterStatus,
828
    /// A microdescriptor for this relay.
829
    md: &'a MicrodescAndHash,
830
    /// The country code this relay is in, if we know one.
831
    #[cfg(feature = "geoip")]
832
    cc: Option<CountryCode>,
833
}
834

            
835
/// A relay that we haven't checked for validity or usability in
836
/// routing.
837
#[derive(Debug)]
838
pub struct UncheckedRelay<'a> {
839
    /// A router descriptor for this relay.
840
    rs: &'a netstatus::MdRouterStatus,
841
    /// A microdescriptor for this relay, if there is one.
842
    md: Option<&'a MicrodescAndHash>,
843
    /// The country code this relay is in, if we know one.
844
    #[cfg(feature = "geoip")]
845
    cc: Option<CountryCode>,
846
}
847

            
848
/// A partial or full network directory that we can download
849
/// microdescriptors for.
850
pub trait MdReceiver {
851
    /// Return an iterator over the digests for all of the microdescriptors
852
    /// that this netdir is missing.
853
    fn missing_microdescs(&self) -> Box<dyn Iterator<Item = &MdDigest> + '_>;
854
    /// Add a microdescriptor to this netdir, if it was wanted.
855
    ///
856
    /// Return true if it was indeed wanted.
857
    fn add_microdesc(&mut self, md: MicrodescAndHash) -> bool;
858
    /// Return the number of missing microdescriptors.
859
    fn n_missing(&self) -> usize;
860
}
861

            
862
impl PartialNetDir {
863
    /// Create a new PartialNetDir with a given consensus, and no
864
    /// microdescriptors loaded.
865
    ///
866
    /// If `replacement_params` is provided, override network parameters from
867
    /// the consensus with those from `replacement_params`.
868
9653
    pub fn new(
869
9653
        consensus: MdConsensus,
870
9653
        replacement_params: Option<&netstatus::NetParams<i32>>,
871
9653
    ) -> Self {
872
9653
        Self::new_inner(
873
9653
            consensus,
874
9653
            replacement_params,
875
            #[cfg(feature = "geoip")]
876
9653
            None,
877
        )
878
9653
    }
879

            
880
    /// Create a new PartialNetDir with GeoIP support.
881
    ///
882
    /// This does the same thing as `new()`, except the provided GeoIP database is used to add
883
    /// country codes to relays.
884
    #[cfg(feature = "geoip")]
885
142
    pub fn new_with_geoip(
886
142
        consensus: MdConsensus,
887
142
        replacement_params: Option<&netstatus::NetParams<i32>>,
888
142
        geoip_db: &GeoipDb,
889
142
    ) -> Self {
890
142
        Self::new_inner(consensus, replacement_params, Some(geoip_db))
891
142
    }
892

            
893
    /// Implementation of the `new()` functions.
894
9795
    fn new_inner(
895
9795
        consensus: MdConsensus,
896
9795
        replacement_params: Option<&netstatus::NetParams<i32>>,
897
9795
        #[cfg(feature = "geoip")] geoip_db: Option<&GeoipDb>,
898
9795
    ) -> Self {
899
9795
        let mut params = NetParameters::default();
900

            
901
        // (We ignore unrecognized options here, since they come from
902
        // the consensus, and we don't expect to recognize everything
903
        // there.)
904
9795
        let _ = params.saturating_update(consensus.params().iter());
905

            
906
        // Now see if the user has any parameters to override.
907
        // (We have to do this now, or else changes won't be reflected in our
908
        // weights.)
909
9795
        if let Some(replacement) = replacement_params {
910
9789
            for u in params.saturating_update(replacement.iter()) {
911
1
                warn!("Unrecognized option: override_net_params.{}", u);
912
            }
913
6
        }
914

            
915
        // Compute the weights we'll want to use for these relays.
916
9795
        let weights = weight::WeightSet::from_consensus(&consensus, &params);
917

            
918
9795
        let n_relays = consensus.c_relays().len();
919

            
920
9795
        let rsidx_by_missing = consensus
921
9795
            .c_relays()
922
9795
            .iter_enumerated()
923
370415
            .map(|(rsidx, rs)| (*rs.md_digest(), rsidx))
924
9795
            .collect();
925

            
926
9795
        let rsidx_by_rsa = consensus
927
9795
            .c_relays()
928
9795
            .iter_enumerated()
929
370415
            .map(|(rsidx, rs)| (*rs.rsa_identity(), rsidx))
930
9795
            .collect();
931

            
932
        #[cfg(feature = "geoip")]
933
9795
        let country_codes = if let Some(db) = geoip_db {
934
142
            consensus
935
142
                .c_relays()
936
142
                .iter()
937
698
                .map(|rs| {
938
985
                    db.lookup_country_code_multi(rs.addrs().map(|x| x.ip()))
939
698
                        .cloned()
940
698
                })
941
142
                .collect()
942
        } else {
943
9653
            Default::default()
944
        };
945

            
946
        #[cfg(feature = "hs-common")]
947
9795
        let hsdir_rings = Arc::new({
948
9795
            let params = HsDirParams::compute(&consensus, &params).expect("Invalid consensus!");
949
            // TODO: It's a bit ugly to use expect above, but this function does
950
            // not return a Result. On the other hand, the error conditions under which
951
            // HsDirParams::compute can return Err are _very_ narrow and hard to
952
            // hit; see documentation in that function.  As such, we probably
953
            // don't need to have this return a Result.
954

            
955
9795
            params.map(HsDirRing::empty_from_params)
956
        });
957

            
958
9795
        let netdir = NetDir {
959
9795
            consensus: Arc::new(consensus),
960
9795
            params,
961
9795
            mds: vec![None; n_relays].into(),
962
9795
            rsidx_by_missing,
963
9795
            rsidx_by_rsa: Arc::new(rsidx_by_rsa),
964
9795
            rsidx_by_ed: HashMap::with_capacity(n_relays),
965
9795
            #[cfg(feature = "hs-common")]
966
9795
            hsdir_rings,
967
9795
            weights,
968
9795
            #[cfg(feature = "geoip")]
969
9795
            country_codes,
970
9795
        };
971

            
972
9795
        PartialNetDir {
973
9795
            netdir,
974
9795
            #[cfg(feature = "hs-common")]
975
9795
            prev_netdir: None,
976
9795
        }
977
9795
    }
978

            
979
    /// Return the declared lifetime of this PartialNetDir.
980
1
    pub fn lifetime(&self) -> &netstatus::Lifetime {
981
1
        self.netdir.lifetime()
982
1
    }
983

            
984
    /// Record a previous netdir, which can be used for reusing cached information
985
    //
986
    // Fills in as many missing microdescriptors as possible in this
987
    // netdir, using the microdescriptors from the previous netdir.
988
    //
989
    // With HS enabled, stores the netdir for reuse of relay hash ring index values.
990
    #[allow(clippy::needless_pass_by_value)] // prev might, or might not, be stored
991
1
    pub fn fill_from_previous_netdir(&mut self, prev: Arc<NetDir>) {
992
38
        for md in prev.mds.iter().flatten() {
993
38
            self.netdir.add_arc_microdesc(md.clone());
994
38
        }
995

            
996
        #[cfg(feature = "hs-common")]
997
1
        {
998
1
            self.prev_netdir = Some(prev);
999
1
        }
1
    }
    /// Compute the hash ring(s) for this NetDir
    #[cfg(feature = "hs-common")]
9696
    fn compute_rings(&mut self) {
9696
        let params = HsDirParams::compute(&self.netdir.consensus, &self.netdir.params)
9696
            .expect("Invalid consensus");
        // TODO: see TODO by similar expect in new()
9696
        self.netdir.hsdir_rings =
10072
            Arc::new(params.map(|params| {
10072
                HsDirRing::compute(params, &self.netdir, self.prev_netdir.as_deref())
10072
            }));
9696
    }
    /// Return true if this are enough information in this directory
    /// to build multihop paths.
2
    pub fn have_enough_paths(&self) -> bool {
2
        self.netdir.have_enough_paths()
2
    }
    /// If this directory has enough information to build multihop
    /// circuits, return it.
9886
    pub fn unwrap_if_sufficient(
9886
        #[allow(unused_mut)] mut self,
9886
    ) -> std::result::Result<NetDir, PartialNetDir> {
9886
        if self.netdir.have_enough_paths() {
            #[cfg(feature = "hs-common")]
9696
            self.compute_rings();
9696
            Ok(self.netdir)
        } else {
190
            Err(self)
        }
9886
    }
}
impl MdReceiver for PartialNetDir {
98
    fn missing_microdescs(&self) -> Box<dyn Iterator<Item = &MdDigest> + '_> {
98
        self.netdir.missing_microdescs()
98
    }
386118
    fn add_microdesc(&mut self, md: MicrodescAndHash) -> bool {
386118
        self.netdir.add_microdesc(md)
386118
    }
282
    fn n_missing(&self) -> usize {
282
        self.netdir.n_missing()
282
    }
}
impl NetDir {
    /// Return the declared lifetime of this NetDir.
330
    pub fn lifetime(&self) -> &netstatus::Lifetime {
330
        self.consensus.lifetime()
330
    }
    /// Add `md` to this NetDir.
    ///
    /// Return true if we wanted it, and false otherwise.
386156
    fn add_arc_microdesc(&mut self, md: Arc<MicrodescAndHash>) -> bool {
386156
        if let Some(rsidx) = self.rsidx_by_missing.remove(md.digest()) {
369753
            assert_eq!(self.c_relays()[rsidx].md_digest(), md.digest());
            // There should never be two approved MDs in the same
            // consensus listing the same ID... but if there is,
            // we'll let the most recent one win.
369753
            self.rsidx_by_ed.insert(*md.ed25519_id(), rsidx);
            // Happy path: we did indeed want this one.
369753
            self.mds[rsidx] = Some(md);
            // Save some space in the missing-descriptor list.
369753
            if self.rsidx_by_missing.len() < self.rsidx_by_missing.capacity() / 4 {
18597
                self.rsidx_by_missing.shrink_to_fit();
351156
            }
369753
            return true;
16403
        }
        // Either we already had it, or we never wanted it at all.
16403
        false
386156
    }
    /// Construct a (possibly invalid) Relay object from a routerstatus and its
    /// index within the consensus.
50626089
    fn relay_from_rs_and_rsidx<'a>(
50626089
        &'a self,
50626089
        rs: &'a netstatus::MdRouterStatus,
50626089
        rsidx: RouterStatusIdx,
50626089
    ) -> UncheckedRelay<'a> {
50626089
        debug_assert_eq!(self.c_relays()[rsidx].rsa_identity(), rs.rsa_identity());
50626089
        let md = self.mds[rsidx].as_deref();
50626089
        if let Some(md) = md {
50622653
            debug_assert_eq!(rs.md_digest(), md.digest());
3436
        }
50626089
        UncheckedRelay {
50626089
            rs,
50626089
            md,
50626089
            #[cfg(feature = "geoip")]
50626089
            cc: self.country_codes.get(rsidx.0).copied().flatten(),
50626089
        }
50626089
    }
    /// Return the value of the hsdir_n_replicas param.
    #[cfg(feature = "hs-common")]
752
    fn n_replicas(&self) -> u8 {
752
        self.params
752
            .hsdir_n_replicas
752
            .get()
752
            .try_into()
752
            .expect("BoundedInt did not enforce bounds")
752
    }
    /// Return the spread parameter for the specified `op`.
    #[cfg(feature = "hs-common")]
752
    fn spread(&self, op: HsDirOp) -> usize {
752
        let spread = match op {
376
            HsDirOp::Download => self.params.hsdir_spread_fetch,
            #[cfg(feature = "hs-service")]
376
            HsDirOp::Upload => self.params.hsdir_spread_store,
        };
752
        spread
752
            .get()
752
            .try_into()
752
            .expect("BoundedInt did not enforce bounds!")
752
    }
    /// Select `spread` hsdir relays for the specified `hsid` from a given `ring`.
    ///
    /// Algorithm:
    ///
    /// for idx in 1..=n_replicas:
    ///       - let H = hsdir_ring::onion_service_index(id, replica, rand,
    ///         period).
    ///       - Find the position of H within hsdir_ring.
    ///       - Take elements from hsdir_ring starting at that position,
    ///         adding them to Dirs until we have added `spread` new elements
    ///         that were not there before.
    #[cfg(feature = "hs-common")]
752
    fn select_hsdirs<'h, 'r: 'h>(
752
        &'r self,
752
        hsid: HsBlindId,
752
        ring: &'h HsDirRing,
752
        spread: usize,
752
    ) -> impl Iterator<Item = Relay<'r>> + 'h {
752
        let n_replicas = self.n_replicas();
752
        (1..=n_replicas) // 1-indexed !
752
            .flat_map({
752
                let mut selected_nodes = HashSet::new();
32
                move |replica: u8| {
32
                    let hsdir_idx = hsdir_ring::service_hsdir_index(&hsid, replica, ring.params());
160
                    ring.ring_items_at(hsdir_idx, spread, |(hsdir_idx, _)| {
                        // According to rend-spec 2.2.3:
                        //                                                  ... If any of those
                        // nodes have already been selected for a lower-numbered replica of the
                        // service, any nodes already chosen are disregarded (i.e. skipped over)
                        // when choosing a replica's hsdir_spread_store nodes.
160
                        selected_nodes.insert(*hsdir_idx)
160
                    })
32
                    .collect::<Vec<_>>()
32
                }
            })
832
            .filter_map(move |(_hsdir_idx, rs_idx)| {
                // This ought not to be None but let's not panic or bail if it is
112
                self.relay_by_rs_idx(*rs_idx)
112
            })
752
    }
    /// Replace the overridden parameters in this netdir with `new_replacement`.
    ///
    /// After this function is done, the netdir's parameters will be those in
    /// the consensus, overridden by settings from `new_replacement`.  Any
    /// settings in the old replacement parameters will be discarded.
    pub fn replace_overridden_parameters(&mut self, new_replacement: &netstatus::NetParams<i32>) {
        // TODO(nickm): This is largely duplicate code from PartialNetDir::new().
        let mut new_params = NetParameters::default();
        let _ = new_params.saturating_update(self.consensus.params().iter());
        for u in new_params.saturating_update(new_replacement.iter()) {
            warn!("Unrecognized option: override_net_params.{}", u);
        }
        self.params = new_params;
    }
    /// Return an iterator over all Relay objects, including invalid ones
    /// that we can't use.
1277401
    pub fn all_relays(&self) -> impl Iterator<Item = UncheckedRelay<'_>> {
        // TODO: I'd like if we could memoize this so we don't have to
        // do so many hashtable lookups.
1277401
        self.c_relays()
1277401
            .iter_enumerated()
49361706
            .map(move |(rsidx, rs)| self.relay_from_rs_and_rsidx(rs, rsidx))
1277401
    }
    /// Return an iterator over all [usable](NetDir#usable) Relays.
1210579
    pub fn relays(&self) -> impl Iterator<Item = Relay<'_>> {
1210579
        self.all_relays().filter_map(UncheckedRelay::into_relay)
1210579
    }
    /// Look up a relay's [`MicrodescAndHash`] by its [`RouterStatusIdx`]
    #[cfg_attr(not(feature = "hs-common"), allow(dead_code))]
    pub(crate) fn md_by_rsidx(&self, rsidx: RouterStatusIdx) -> Option<&MicrodescAndHash> {
        self.mds.get(rsidx)?.as_deref()
    }
    /// Return a relay matching a given identity, if we have a
    /// _usable_ relay with that key.
    ///
    /// (Does not return [unusable](NetDir#usable) relays.)
    ///
    ///
    /// Note that a `None` answer is not always permanent: if a microdescriptor
    /// is subsequently added for a relay with this ID, the ID may become usable
    /// even if it was not usable before.
608362
    pub fn by_id<'a, T>(&self, id: T) -> Option<Relay<'_>>
608362
    where
608362
        T: Into<RelayIdRef<'a>>,
    {
608362
        let id = id.into();
608362
        let answer = match id {
606237
            RelayIdRef::Ed25519(ed25519) => {
606237
                let rsidx = *self.rsidx_by_ed.get(ed25519)?;
599182
                let rs = self.c_relays().get(rsidx).expect("Corrupt index");
599182
                self.relay_from_rs_and_rsidx(rs, rsidx).into_relay()?
            }
2125
            RelayIdRef::Rsa(rsa) => self
2125
                .by_rsa_id_unchecked(rsa)
2125
                .and_then(UncheckedRelay::into_relay)?,
            other_type => self.relays().find(|r| r.has_identity(other_type))?,
        };
599189
        assert!(answer.has_identity(id));
599189
        Some(answer)
608362
    }
    /// Obtain a `Relay` given a `RouterStatusIdx`
    ///
    /// Differs from `relay_from_rs_and_rsi` as follows:
    ///  * That function expects the caller to already have an `MdRouterStatus`;
    ///    it checks with `debug_assert` that the relay in the netdir matches.
    ///  * That function panics if the `RouterStatusIdx` is invalid; this one returns `None`.
    ///  * That function returns an `UncheckedRelay`; this one a `Relay`.
    ///
    /// `None` could be returned here, even with a valid `rsi`,
    /// if `rsi` refers to an [unusable](NetDir#usable) relay.
    #[cfg_attr(not(feature = "hs-common"), allow(dead_code))]
5278
    pub(crate) fn relay_by_rs_idx(&self, rs_idx: RouterStatusIdx) -> Option<Relay<'_>> {
5278
        let rs = self.c_relays().get(rs_idx)?;
5278
        let md = self.mds.get(rs_idx)?.as_deref();
5278
        UncheckedRelay {
5278
            rs,
5278
            md,
5278
            #[cfg(feature = "geoip")]
5278
            cc: self.country_codes.get(rs_idx.0).copied().flatten(),
5278
        }
5278
        .into_relay()
5278
    }
    /// Return a relay with the same identities as those in `target`, if one
    /// exists.
    ///
    /// Does not return [unusable](NetDir#usable) relays.
    ///
    /// Note that a negative result from this method is not necessarily permanent:
    /// it may be the case that a relay exists,
    /// but we don't yet have enough information about it to know all of its IDs.
    /// To test whether a relay is *definitely* absent,
    /// use [`by_ids_detailed`](Self::by_ids_detailed)
    /// or [`ids_listed`](Self::ids_listed).
    ///
    /// # Limitations
    ///
    /// This will be very slow if `target` does not have an Ed25519 or RSA
    /// identity.
545882
    pub fn by_ids<T>(&self, target: &T) -> Option<Relay<'_>>
545882
    where
545882
        T: HasRelayIds + ?Sized,
    {
545882
        let mut identities = target.identities();
        // Don't try if there are no identities.
545882
        let first_id = identities.next()?;
        // Since there is at most one relay with each given ID type,
        // we only need to check the first relay we find.
545882
        let candidate = self.by_id(first_id)?;
545776
        if identities.all(|wanted_id| candidate.has_identity(wanted_id)) {
545775
            Some(candidate)
        } else {
1
            None
        }
545882
    }
    /// Check whether there is a relay that has at least one identity from
    /// `target`, and which _could_ have every identity from `target`.
    /// If so, return such a relay.
    ///
    /// Return `Ok(None)` if we did not find a relay with any identity from `target`.
    ///
    /// Return `RelayLookupError::Impossible` if we found a relay with at least
    /// one identity from `target`, but that relay's other identities contradict
    /// what we learned from `target`.
    ///
    /// Does not return [unusable](NetDir#usable) relays.
    ///
    /// (This function is only useful if you need to distinguish the
    /// "impossible" case from the "no such relay known" case.)
    ///
    /// # Limitations
    ///
    /// This will be very slow if `target` does not have an Ed25519 or RSA
    /// identity.
    //
    // TODO HS: This function could use a better name.
    //
    // TODO: We could remove the feature restriction here once we think this API is
    // stable.
    #[cfg(feature = "hs-common")]
2122
    pub fn by_ids_detailed<T>(
2122
        &self,
2122
        target: &T,
2122
    ) -> std::result::Result<Option<Relay<'_>>, RelayLookupError>
2122
    where
2122
        T: HasRelayIds + ?Sized,
    {
2122
        let candidate = target
2122
            .identities()
            // Find all the relays that share any identity with this set of identities.
4242
            .filter_map(|id| self.by_id(id))
            // We might find the same relay more than once under a different
            // identity, so we remove the duplicates.
            //
            // Since there is at most one relay per rsa identity per consensus,
            // this is a true uniqueness check under current construction rules.
2122
            .unique_by(|r| r.rs.rsa_identity())
            // If we find two or more distinct relays, then have a contradiction.
2122
            .at_most_one()
2122
            .map_err(|_| RelayLookupError::Impossible)?;
        // If we have no candidate, return None early.
2122
        let candidate = match candidate {
5
            Some(relay) => relay,
2117
            None => return Ok(None),
        };
        // Now we know we have a single candidate.  Make sure that it does not have any
        // identity that does not match the target.
5
        if target
5
            .identities()
7
            .all(|wanted_id| match candidate.identity(wanted_id.id_type()) {
                None => true,
7
                Some(id) => id == wanted_id,
7
            })
        {
3
            Ok(Some(candidate))
        } else {
2
            Err(RelayLookupError::Impossible)
        }
2122
    }
    /// Return a boolean if this consensus definitely has (or does not have) a
    /// relay matching the listed identities.
    ///
    /// `Some(true)` indicates that the relay exists.
    /// `Some(false)` indicates that the relay definitely does not exist.
    /// `None` indicates that we can't yet tell whether such a relay exists,
    ///  due to missing information.
24866
    fn id_pair_listed(&self, ed_id: &Ed25519Identity, rsa_id: &RsaIdentity) -> Option<bool> {
24866
        let r = self.by_rsa_id_unchecked(rsa_id);
24866
        match r {
24583
            Some(unchecked) => {
24583
                if !unchecked.rs.ed25519_id_is_usable() {
                    return Some(false);
24583
                }
                // If md is present, then it's listed iff we have the right
                // ed id.  Otherwise we don't know if it's listed.
24583
                unchecked.md.map(|md| md.ed25519_id() == ed_id)
            }
            None => {
                // Definitely not listed.
283
                Some(false)
            }
        }
24866
    }
    /// Check whether a relay exists (or may exist)
    /// with the same identities as those in `target`.
    ///
    /// `Some(true)` indicates that the relay exists.
    /// `Some(false)` indicates that the relay definitely does not exist.
    /// `None` indicates that we can't yet tell whether such a relay exists,
    ///  due to missing information.
8226
    pub fn ids_listed<T>(&self, target: &T) -> Option<bool>
8226
    where
8226
        T: HasRelayIds + ?Sized,
    {
8226
        let rsa_id = target.rsa_identity();
8226
        let ed25519_id = target.ed_identity();
        // TODO: If we later support more identity key types, this will
        // become incorrect.  This assertion might help us recognize that case.
        const _: () = assert!(RelayIdType::COUNT == 2);
8226
        match (rsa_id, ed25519_id) {
8226
            (Some(r), Some(e)) => self.id_pair_listed(e, r),
            (Some(r), None) => Some(self.rsa_id_is_listed(r)),
            (None, Some(e)) => {
                if self.rsidx_by_ed.contains_key(e) {
                    Some(true)
                } else {
                    None
                }
            }
            (None, None) => None,
        }
8226
    }
    /// Return a (possibly [unusable](NetDir#usable)) relay with a given RSA identity.
    ///
    /// This API can be used to find information about a relay that is listed in
    /// the current consensus, even if we don't yet have enough information
    /// (like a microdescriptor) about the relay to use it.
278403
    #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
278403
    #[cfg_attr(docsrs, doc(cfg(feature = "experimental-api")))]
278403
    fn by_rsa_id_unchecked(&self, rsa_id: &RsaIdentity) -> Option<UncheckedRelay<'_>> {
278403
        let rsidx = *self.rsidx_by_rsa.get(rsa_id)?;
276000
        let rs = self.c_relays().get(rsidx).expect("Corrupt index");
276000
        assert_eq!(rs.rsa_identity(), rsa_id);
276000
        Some(self.relay_from_rs_and_rsidx(rs, rsidx))
278403
    }
    /// Return the relay with a given RSA identity, if we have one
    /// and it is [usable](NetDir#usable).
6
    fn by_rsa_id(&self, rsa_id: &RsaIdentity) -> Option<Relay<'_>> {
6
        self.by_rsa_id_unchecked(rsa_id)?.into_relay()
6
    }
    /// Return true if `rsa_id` is listed in this directory, even if it isn't
    /// currently usable.
    ///
    /// (An "[unusable](NetDir#usable)" relay in this context is one for which we don't have full
    /// directory information.)
3
    #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
3
    #[cfg_attr(docsrs, doc(cfg(feature = "experimental-api")))]
3
    fn rsa_id_is_listed(&self, rsa_id: &RsaIdentity) -> bool {
3
        self.by_rsa_id_unchecked(rsa_id).is_some()
3
    }
    /// List the hsdirs in this NetDir, that should be in the HSDir rings
    ///
    /// The results are not returned in any particular order.
    #[cfg(feature = "hs-common")]
10072
    fn all_hsdirs(&self) -> impl Iterator<Item = (RouterStatusIdx, Relay<'_>)> {
384785
        self.c_relays().iter_enumerated().filter_map(|(rsidx, rs)| {
384785
            let relay = self.relay_from_rs_and_rsidx(rs, rsidx);
384785
            relay.is_hsdir_for_ring().then_some(())?;
94328
            let relay = relay.into_relay()?;
94324
            Some((rsidx, relay))
384785
        })
10072
    }
    /// Return the parameters from the consensus, clamped to the
    /// correct ranges, with defaults filled in.
    ///
    /// NOTE: that unsupported parameters aren't returned here; only those
    /// values configured in the `params` module are available.
610125
    pub fn params(&self) -> &NetParameters {
610125
        &self.params
610125
    }
    /// Return a [`ProtoStatus`](netstatus::ProtoStatus) that lists the
    /// network's current requirements and recommendations for the list of
    /// protocols that every relay must implement.
    //
    // TODO HS: I am not sure this is the right API; other alternatives would be:
    //    * To expose the _required_ relay protocol list instead (since that's all that
    //      onion service implementations need).
    //    * To expose the client protocol list as well (for symmetry).
    //    * To expose the MdConsensus instead (since that's more general, although
    //      it restricts the future evolution of this API).
    //
    // I think that this is a reasonably good compromise for now, but I'm going
    // to put it behind the `hs-common` feature to give us time to consider more.
    #[cfg(feature = "hs-common")]
2115
    pub fn relay_protocol_status(&self) -> &netstatus::ProtoStatus {
2115
        self.consensus.relay_protocol_status()
2115
    }
    /// Return a [`ProtoStatus`](netstatus::ProtoStatus) that lists the
    /// network's current requirements and recommendations for the list of
    /// protocols that every relay must implement.
    //
    // TODO HS: See notes on relay_protocol_status above.
    #[cfg(feature = "hs-common")]
    pub fn client_protocol_status(&self) -> &netstatus::ProtoStatus {
        self.consensus.client_protocol_status()
    }
    /// Construct a `CircTarget` from an externally provided list of link specifiers,
    /// and an externally provided onion key.
    ///
    /// This method is used in the onion service protocol,
    /// where introduction points and rendezvous points are specified using these inputs.
    ///
    /// This function is a member of `NetDir` so that it can provide a reasonable list of
    /// [`Protocols`](tor_protover::Protocols) capabilities for the generated `CircTarget`.
    /// It does not (and should not!) look up anything else from the directory.
    #[cfg(feature = "hs-common")]
2115
    pub fn circ_target_from_verbatim_linkspecs(
2115
        &self,
2115
        linkspecs: &[tor_linkspec::EncodedLinkSpec],
2115
        ntor_onion_key: &curve25519::PublicKey,
2115
    ) -> StdResult<VerbatimLinkSpecCircTarget<OwnedCircTarget>, VerbatimCircTargetDecodeError> {
        use VerbatimCircTargetDecodeError as E;
        use tor_linkspec::CircTarget as _;
        use tor_linkspec::decode::Strictness;
2115
        let mut bld = OwnedCircTarget::builder();
        use tor_error::into_internal;
2115
        *bld.chan_target() =
2115
            OwnedChanTargetBuilder::from_encoded_linkspecs(Strictness::Standard, linkspecs)?;
2115
        let protocols = {
2115
            let chan_target = bld.chan_target().build().map_err(into_internal!(
                "from_encoded_linkspecs gave an invalid output"
            ))?;
2115
            match self
2115
                .by_ids_detailed(&chan_target)
2115
                .map_err(E::ImpossibleIds)?
            {
                Some(relay) => relay.protovers().clone(),
2115
                None => self.relay_protocol_status().required_protocols().clone(),
            }
        };
2115
        bld.protocols(protocols);
2115
        bld.ntor_onion_key(*ntor_onion_key);
2115
        Ok(VerbatimLinkSpecCircTarget::new(
2115
            bld.build()
2115
                .map_err(into_internal!("Failed to construct a valid circtarget"))?,
2115
            linkspecs.to_vec(),
        ))
2115
    }
    /// Return weighted the fraction of relays we can use.  We only
    /// consider relays that match the predicate `usable`.  We weight
    /// this bandwidth according to the provided `role`.
    ///
    /// If _no_ matching relays in the consensus have a nonzero
    /// weighted bandwidth value, we fall back to looking at the
    /// unweighted fraction of matching relays.
    ///
    /// If there are no matching relays in the consensus, we return 0.0.
29059
    fn frac_for_role<'a, F>(&'a self, role: WeightRole, usable: F) -> f64
29059
    where
29059
        F: Fn(&UncheckedRelay<'a>) -> bool,
    {
29059
        let mut total_weight = 0_u64;
29059
        let mut have_weight = 0_u64;
29059
        let mut have_count = 0_usize;
29059
        let mut total_count = 0_usize;
1104174
        for r in self.all_relays() {
1104174
            if !usable(&r) {
365990
                continue;
738184
            }
738184
            let w = self.weights.weight_rs_for_role(r.rs, role);
738184
            total_weight += w;
738184
            total_count += 1;
738184
            if r.is_usable() {
735479
                have_weight += w;
735479
                have_count += 1;
735479
            }
        }
29059
        if total_weight > 0 {
            // The consensus lists some weighted bandwidth so return the
            // fraction of the weighted bandwidth for which we have
            // descriptors.
29059
            (have_weight as f64) / (total_weight as f64)
        } else if total_count > 0 {
            // The consensus lists no weighted bandwidth for these relays,
            // but at least it does list relays. Return the fraction of
            // relays for which it we have descriptors.
            (have_count as f64) / (total_count as f64)
        } else {
            // There are no relays of this kind in the consensus.  Return
            // 0.0, to avoid dividing by zero and giving NaN.
            0.0
        }
29059
    }
    /// Return the estimated fraction of possible paths that we have
    /// enough microdescriptors to build.
9889
    fn frac_usable_paths(&self) -> f64 {
        // TODO #504, TODO SPEC: We may want to add a set of is_flagged_fast() and/or
        // is_flagged_stable() checks here.  This will require spec clarification.
370791
        let f_g = self.frac_for_role(WeightRole::Guard, |u| {
370791
            u.low_level_details().is_suitable_as_guard()
370791
        });
9889
        let f_m = self.frac_for_role(WeightRole::Middle, |_| true);
108027
        let f_e = if self.all_relays().any(|u| u.rs.is_flagged_exit()) {
362472
            self.frac_for_role(WeightRole::Exit, |u| u.rs.is_flagged_exit())
        } else {
            // If there are no exits at all, we use f_m here.
611
            f_m
        };
9889
        f_g * f_m * f_e
9889
    }
    /// Return true if there is enough information in this NetDir to build
    /// multihop circuits.
9888
    fn have_enough_paths(&self) -> bool {
        // TODO-A001: This should check for our guards as well, and
        // make sure that if they're listed in the consensus, we have
        // the descriptors for them.
        // If we can build a randomly chosen path with at least this
        // probability, we know enough information to participate
        // on the network.
9888
        let min_frac_paths: f64 = self.params().min_circuit_path_threshold.as_fraction();
        // What fraction of paths can we build?
9888
        let available = self.frac_usable_paths();
9888
        available >= min_frac_paths
9888
    }
    /// Choose a relay at random.
    ///
    /// Each relay is chosen with probability proportional to its weight
    /// in the role `role`, and is only selected if the predicate `usable`
    /// returns true for it.
    ///
    /// This function returns None if (and only if) there are no relays
    /// where `usable` returned true.
    ///
    /// A relay with zero weight will be chosen iff all `usable` relays have
    /// zero weight.
    //
    // TODO this API, with the `usable` closure, invites mistakes where we fail to
    // check conditions that are implied by the role we have selected for the relay:
    // call sites must include a call to `Relay::is_polarity_inverter()` or whatever.
    // IMO the `WeightRole` ought to imply a condition (and it should therefore probably
    // be renamed.)  -Diziet
29802
    pub fn pick_relay<'a, R, P>(
29802
        &'a self,
29802
        rng: &mut R,
29802
        role: WeightRole,
29802
        usable: P,
29802
    ) -> Option<Relay<'a>>
29802
    where
29802
        R: rand::Rng,
29802
        P: FnMut(&Relay<'a>) -> bool,
    {
29802
        let relays: Vec<_> = self.relays().filter(usable).collect();
29802
        tracing::trace!(?role, "picking from {} relays", relays.len());
        // Preemptively check for and handle an empty sequence ourselves, since it's
        // cheap to do so and the `choose_weighted` behavior for this edge-case
        // is a bit unpredictable.
        // See e.g. <https://github.com/rust-random/rand/issues/1783>
29802
        if relays.is_empty() {
33
            tracing::debug!(?role, "No eligible relays");
33
            return None;
29769
        }
        // This algorithm uses rand::distr::WeightedIndex, and uses
        // gives O(n) time and space  to build the index, plus O(log n)
        // sampling time.
        //
        // We might be better off building a WeightedIndex in advance
        // for each `role`, and then sampling it repeatedly until we
        // get a relay that satisfies `usable`.  Or we might not --
        // that depends heavily on the actual particulars of our
        // inputs.  We probably shouldn't make any changes there
        // unless profiling tells us that this function is in a hot
        // path.
        //
        // The C Tor sampling implementation goes through some trouble
        // here to try to make its path selection constant-time.  I
        // believe that there is no actual remotely exploitable
        // side-channel here however.  It could be worth analyzing in
        // the future.
        //
        // This code will give the wrong result if the total of all weights
        // can exceed u64::MAX.  We make sure that can't happen when we
        // set up `self.weights`.
523590
        match relays[..].choose_weighted(rng, |r| {
523590
            let weight = self.weights.weight_rs_for_role(r.rs, role);
523590
            tracing::trace!("relay:{id:?} role:{role:?} weight:{weight}", id = r.id());
523590
            weight
523590
        }) {
29769
            Ok(relay) => Some(relay.clone()),
            Err(WeightError::InsufficientNonZero) => {
                warn!(?self.weights, ?role,
                        "After filtering, all {} relays had zero weight. Choosing one at random. See bug #1907.",
                        relays.len());
                relays.choose(rng).cloned()
            }
            Err(e) => {
                warn_report!(
                    e,
                    "Unexpected error while choosing from {} relays for role {:?}",
                    relays.len(),
                    role
                );
                None
            }
        }
29802
    }
    /// Choose `n` items (relays) at random, using the provided weights.
    ///
    /// This is intended as an internal, easier-to-test, implementation of
    /// `pick_n_relays`. `T` is generic for testing, but intended to be `Relay`.
    ///
    /// Items are chosen without replacement: no item will be returned twice.
    ///
    /// If *all* items have zero-weight, then up to `n` will be chosen randomly
    /// and returned. Otherwise, never returns items with zero-weight.
    ///
    /// May return fewer than `n` items if there are fewer than `n` with non-zero weight
    /// (or all have zero-weight but there are fewer than `n` total).
27776
    fn pick_n_weighted<R, T>(rng: &mut R, n: usize, weighted_items: &[(T, u64)]) -> Vec<T>
27776
    where
27776
        R: rand::Rng,
27776
        T: Clone,
    {
27776
        let mut sampled_items = match weighted_items[..]
178474
            .sample_weighted(rng, n, |(_r, w)| *w as f64)
        {
            Err(e) => {
                warn_report!(e, "Unexpected error while sampling a set of items");
                Vec::new()
            }
27776
            Ok(sampled_items) => {
27776
                if sampled_items.len() < n {
                    // Too few items had nonzero weights: return all of those that are okay.
24967
                    let nonzero_weight_items: Vec<_> = weighted_items
24967
                        .iter()
111183
                        .filter_map(|(i, w)| if *w > 0 { Some(i) } else { None })
24967
                        .cloned()
24967
                        .collect();
24967
                    if nonzero_weight_items.is_empty() {
12506
                        tracing::debug!(
                            "All {} items had zero weight! Picking some at random. See bug #1907.",
                            weighted_items.len()
                        );
12506
                        let items: Vec<_> =
12512
                            weighted_items.iter().map(|(i, _w)| i.clone()).collect();
12506
                        if items.len() >= n {
2
                            items.sample(rng, n).cloned().collect()
                        } else {
12504
                            items
                        }
                    } else {
12461
                        tracing::debug!(
                            "After filtering, only had {}/{} items with nonzero weight. Returning them all. See bug #1907.",
                            nonzero_weight_items.len(),
                            weighted_items.len()
                        );
12461
                        nonzero_weight_items
                    }
                } else {
19813
                    sampled_items.map(|(i, _w)| i.clone()).collect()
                }
            }
        };
27776
        sampled_items.shuffle(rng);
27776
        sampled_items
27776
    }
    /// Choose `n` relay at random.
    ///
    /// Each relay is chosen with probability proportional to its weight
    /// in the role `role`, and is only selected if the predicate `usable`
    /// returns true for it.
    ///
    /// Relays are chosen without replacement: no relay will be
    /// returned twice. Therefore, the resulting vector may be smaller
    /// than `n` if we happen to have fewer than `n` appropriate relays.
    ///
    /// Relays with zero-weight will be chosen only if there are *no* usable
    /// relays with nonzero-weight.
    ///
    /// This function returns an empty vector if (and only if) there are no
    /// relays where `usable` returned true.
27770
    pub fn pick_n_relays<'a, R, P>(
27770
        &'a self,
27770
        rng: &mut R,
27770
        n: usize,
27770
        role: WeightRole,
27770
        usable: P,
27770
    ) -> Vec<Relay<'a>>
27770
    where
27770
        R: rand::Rng,
27770
        P: FnMut(&Relay<'a>) -> bool,
    {
27770
        let filtered_weighted_relays: Vec<(Relay<'a>, u64)> = self
27770
            .relays()
27770
            .filter(usable)
178453
            .map(|r| (r.clone(), self.weights.weight_rs_for_role(r.rs, role)))
27770
            .collect();
27770
        let res = NetDir::pick_n_weighted(rng, n, filtered_weighted_relays.as_slice());
27770
        let n_found = res.len();
27770
        if n_found < n {
24962
            warn!(?self.weights, ?role,
                "Requested {n} relays, but only {n_usable} were usable, and only {n_found} were chosen after weighting {role:?}.",
                n_usable=filtered_weighted_relays.len(),
            );
2808
        }
27770
        res
27770
    }
    /// Compute the weight with which `relay` will be selected for a given
    /// `role`.
35721
    pub fn relay_weight<'a>(&'a self, relay: &Relay<'a>, role: WeightRole) -> RelayWeight {
35721
        RelayWeight(self.weights.weight_rs_for_role(relay.rs, role))
35721
    }
    /// Compute the total weight with which any relay matching `usable`
    /// will be selected for a given `role`.
    ///
    /// Note: because this function is used to assess the total
    /// properties of the consensus, the `usable` predicate takes a
    /// [`MdRouterStatus`] rather than a [`Relay`].
23630
    pub fn total_weight<P>(&self, role: WeightRole, usable: P) -> RelayWeight
23630
    where
23630
        P: Fn(&UncheckedRelay<'_>) -> bool,
    {
23630
        self.all_relays()
529048
            .filter_map(|unchecked| {
529048
                if usable(&unchecked) {
226914
                    Some(RelayWeight(
226914
                        self.weights.weight_rs_for_role(unchecked.rs, role),
226914
                    ))
                } else {
302134
                    None
                }
529048
            })
23630
            .sum()
23630
    }
    /// Compute the weight with which a relay with ID `rsa_id` would be
    /// selected for a given `role`.
    ///
    /// Note that weight returned by this function assumes that the
    /// relay with that ID is actually [usable](NetDir#usable); if it isn't usable,
    /// then other weight-related functions will call its weight zero.
251217
    pub fn weight_by_rsa_id(&self, rsa_id: &RsaIdentity, role: WeightRole) -> Option<RelayWeight> {
251217
        self.by_rsa_id_unchecked(rsa_id)
251217
            .map(|unchecked| RelayWeight(self.weights.weight_rs_for_role(unchecked.rs, role)))
251217
    }
    /// Return all relays in this NetDir known to be in the same family as
    /// `relay`.
    ///
    /// This list of members will **not** necessarily include `relay` itself.
    ///
    /// # Limitations
    ///
    /// Two relays only belong to the same family if _each_ relay
    /// claims to share a family with the other.  But if we are
    /// missing a microdescriptor for one of the relays listed by this
    /// relay, we cannot know whether it acknowledges family
    /// membership with this relay or not.  Therefore, this function
    /// can omit family members for which there is not (as yet) any
    /// Relay object.
2
    pub fn known_family_members<'a>(
2
        &'a self,
2
        relay: &'a Relay<'a>,
2
    ) -> impl Iterator<Item = Relay<'a>> {
2
        let relay_rsa_id = relay.rsa_id();
4
        relay.md.family().members().filter_map(move |other_rsa_id| {
4
            self.by_rsa_id(other_rsa_id)
4
                .filter(|other_relay| other_relay.md.family().contains(relay_rsa_id))
4
        })
2
    }
    /// Return the current hidden service directory "time period".
    ///
    /// Specifically, this returns the time period that contains the beginning
    /// of the validity period of this `NetDir`'s consensus.  That time period
    /// is the one we use when acting as an hidden service client.
    #[cfg(feature = "hs-common")]
1739
    pub fn hs_time_period(&self) -> TimePeriod {
1739
        self.hsdir_rings.current.time_period()
1739
    }
    /// Return the [`HsDirParams`] of all the relevant hidden service directory "time periods"
    ///
    /// This includes the current time period (as from
    /// [`.hs_time_period`](NetDir::hs_time_period))
    /// plus additional time periods that we publish descriptors for when we are
    /// acting as a hidden service.
    #[cfg(feature = "hs-service")]
376
    pub fn hs_all_time_periods(&self) -> Vec<HsDirParams> {
376
        self.hsdir_rings
376
            .iter()
752
            .map(|r| r.params().clone())
376
            .collect()
376
    }
    /// Return the relays in this network directory that will be used as hidden service directories
    ///
    /// These are suitable to retrieve a given onion service's descriptor at a given time period.
    #[cfg(feature = "hs-common")]
8
    pub fn hs_dirs_download<'r, R>(
8
        &'r self,
8
        hsid: HsBlindId,
8
        period: TimePeriod,
8
        rng: &mut R,
8
    ) -> std::result::Result<Vec<Relay<'r>>, Bug>
8
    where
8
        R: rand::Rng,
    {
        // Algorithm:
        //
        // 1. Determine which HsDirRing to use, based on the time period.
        // 2. Find the shared random value that's associated with that HsDirRing.
        // 3. Choose spread = the parameter `hsdir_spread_fetch`
        // 4. Let n_replicas = the parameter `hsdir_n_replicas`.
        // 5. Initialize Dirs = []
        // 6. for idx in 1..=n_replicas:
        //       - let H = hsdir_ring::onion_service_index(id, replica, rand,
        //         period).
        //       - Find the position of H within hsdir_ring.
        //       - Take elements from hsdir_ring starting at that position,
        //         adding them to Dirs until we have added `spread` new elements
        //         that were not there before.
        // 7. Shuffle Dirs
        // 8. return Dirs.
8
        let spread = self.spread(HsDirOp::Download);
        // When downloading, only look at relays on current ring.
8
        let ring = &self.hsdir_rings.current;
8
        if ring.params().time_period != period {
            return Err(internal!(
                "our current ring is not associated with the requested time period!"
            ));
8
        }
8
        let mut hs_dirs = self.select_hsdirs(hsid, ring, spread).collect_vec();
        // When downloading, the order of the returned relays is random.
8
        hs_dirs.shuffle(rng);
8
        Ok(hs_dirs)
8
    }
    /// Return the relays in this network directory that will be used as hidden service directories
    ///
    /// Returns the relays that are suitable for storing a given onion service's descriptors at the
    /// given time period.
    #[cfg(feature = "hs-service")]
376
    pub fn hs_dirs_upload(
376
        &self,
376
        hsid: HsBlindId,
376
        period: TimePeriod,
376
    ) -> std::result::Result<impl Iterator<Item = Relay<'_>>, Bug> {
        // Algorithm:
        //
        // 1. Choose spread = the parameter `hsdir_spread_store`
        // 2. Determine which HsDirRing to use, based on the time period.
        // 3. Find the shared random value that's associated with that HsDirRing.
        // 4. Let n_replicas = the parameter `hsdir_n_replicas`.
        // 5. Initialize Dirs = []
        // 6. for idx in 1..=n_replicas:
        //       - let H = hsdir_ring::onion_service_index(id, replica, rand,
        //         period).
        //       - Find the position of H within hsdir_ring.
        //       - Take elements from hsdir_ring starting at that position,
        //         adding them to Dirs until we have added `spread` new elements
        //         that were not there before.
        // 3. return Dirs.
376
        let spread = self.spread(HsDirOp::Upload);
        // For each HsBlindId, determine which HsDirRing to use.
376
        let rings = self
376
            .hsdir_rings
376
            .iter()
376
            .filter_map(move |ring| {
                // Make sure the ring matches the TP of the hsid it's matched with.
376
                (ring.params().time_period == period).then_some((ring, hsid, period))
376
            })
376
            .collect::<Vec<_>>();
        // The specified period should have an associated ring.
376
        if !rings.iter().any(|(_, _, tp)| *tp == period) {
            return Err(internal!(
                "the specified time period does not have an associated ring"
            ));
376
        };
        // Now that we've matched each `hsid` with the ring associated with its TP, we can start
        // selecting replicas from each ring.
376
        Ok(rings.into_iter().flat_map(move |(ring, hsid, period)| {
8
            assert_eq!(period, ring.params().time_period());
8
            self.select_hsdirs(hsid, ring, spread)
8
        }))
376
    }
    /// Return the relays in this network directory that will be used as hidden service directories
    ///
    /// Depending on `op`,
    /// these are suitable to either store, or retrieve, a
    /// given onion service's descriptor at a given time period.
    ///
    /// When `op` is `Download`, the order is random.
    /// When `op` is `Upload`, the order is not specified.
    ///
    /// Return an error if the time period is not one returned by
    /// `onion_service_time_period` or `onion_service_secondary_time_periods`.
    //
    // TODO: make HsDirOp pub(crate) once this is removed
    #[cfg(feature = "hs-common")]
    #[deprecated(note = "Use hs_dirs_upload or hs_dirs_download instead")]
2
    pub fn hs_dirs<'r, R>(&'r self, hsid: &HsBlindId, op: HsDirOp, rng: &mut R) -> Vec<Relay<'r>>
2
    where
2
        R: rand::Rng,
    {
        // Algorithm:
        //
        // 1. Determine which HsDirRing to use, based on the time period.
        // 2. Find the shared random value that's associated with that HsDirRing.
        // 3. Choose spread = the parameter `hsdir_spread_store` or
        //    `hsdir_spread_fetch` based on `op`.
        // 4. Let n_replicas = the parameter `hsdir_n_replicas`.
        // 5. Initialize Dirs = []
        // 6. for idx in 1..=n_replicas:
        //       - let H = hsdir_ring::onion_service_index(id, replica, rand,
        //         period).
        //       - Find the position of H within hsdir_ring.
        //       - Take elements from hsdir_ring starting at that position,
        //         adding them to Dirs until we have added `spread` new elements
        //         that were not there before.
        // 7. return Dirs.
2
        let n_replicas = self
2
            .params
2
            .hsdir_n_replicas
2
            .get()
2
            .try_into()
2
            .expect("BoundedInt did not enforce bounds");
2
        let spread = match op {
1
            HsDirOp::Download => self.params.hsdir_spread_fetch,
            #[cfg(feature = "hs-service")]
1
            HsDirOp::Upload => self.params.hsdir_spread_store,
        };
2
        let spread = spread
2
            .get()
2
            .try_into()
2
            .expect("BoundedInt did not enforce bounds!");
        // TODO: I may be wrong here but I suspect that this function may
        // need refactoring so that it does not look at _all_ of the HsDirRings,
        // but only at the ones that corresponds to time periods for which
        // HsBlindId is valid.  Or I could be mistaken, in which case we should
        // have a comment to explain why I am, since the logic is subtle.
        // (For clients, there is only one ring.) -nickm
        //
        // (Actually, there is no need to follow through with the above TODO,
        // since this function is deprecated, and not used anywhere but the
        // tests.)
2
        let mut hs_dirs = self
2
            .hsdir_rings
2
            .iter_for_op(op)
2
            .cartesian_product(1..=n_replicas) // 1-indexed !
2
            .flat_map({
2
                let mut selected_nodes = HashSet::new();
4
                move |(ring, replica): (&HsDirRing, u8)| {
4
                    let hsdir_idx = hsdir_ring::service_hsdir_index(hsid, replica, ring.params());
21
                    ring.ring_items_at(hsdir_idx, spread, |(hsdir_idx, _)| {
                        // According to rend-spec 2.2.3:
                        //                                                  ... If any of those
                        // nodes have already been selected for a lower-numbered replica of the
                        // service, any nodes already chosen are disregarded (i.e. skipped over)
                        // when choosing a replica's hsdir_spread_store nodes.
21
                        selected_nodes.insert(*hsdir_idx)
21
                    })
4
                    .collect::<Vec<_>>()
4
                }
            })
14
            .filter_map(|(_hsdir_idx, rs_idx)| {
                // This ought not to be None but let's not panic or bail if it is
14
                self.relay_by_rs_idx(*rs_idx)
14
            })
2
            .collect_vec();
2
        match op {
1
            HsDirOp::Download => {
1
                // When `op` is `Download`, the order is random.
1
                hs_dirs.shuffle(rng);
1
            }
            #[cfg(feature = "hs-service")]
1
            HsDirOp::Upload => {
1
                // When `op` is `Upload`, the order is not specified.
1
            }
        }
2
        hs_dirs
2
    }
}
impl MdReceiver for NetDir {
146
    fn missing_microdescs(&self) -> Box<dyn Iterator<Item = &MdDigest> + '_> {
146
        Box::new(self.rsidx_by_missing.keys())
146
    }
386118
    fn add_microdesc(&mut self, md: MicrodescAndHash) -> bool {
386118
        self.add_arc_microdesc(Arc::new(md))
386118
    }
282
    fn n_missing(&self) -> usize {
282
        self.rsidx_by_missing.len()
282
    }
}
impl<'a> UncheckedRelay<'a> {
    /// Return an [`UncheckedRelayDetails`](details::UncheckedRelayDetails) for this relay.
    ///
    /// Callers should generally avoid using this information directly if they can;
    /// it's better to use a higher-level function that exposes semantic information
    /// rather than these properties.
1016665
    pub fn low_level_details(&self) -> details::UncheckedRelayDetails<'_> {
1016665
        details::UncheckedRelayDetails(self)
1016665
    }
    /// Return true if this relay is valid and [usable](NetDir#usable).
    ///
    /// This function should return `true` for every Relay we expose
    /// to the user.
48936778
    pub fn is_usable(&self) -> bool {
        // No need to check for 'valid' or 'running': they are implicit.
48936778
        self.md.is_some() && self.rs.ed25519_id_is_usable()
48936778
    }
    /// If this is [usable](NetDir#usable), return a corresponding Relay object.
48198594
    pub fn into_relay(self) -> Option<Relay<'a>> {
48198594
        if self.is_usable() {
            Some(Relay {
48198582
                rs: self.rs,
48198582
                md: self.md?,
                #[cfg(feature = "geoip")]
48198582
                cc: self.cc,
            })
        } else {
12
            None
        }
48198594
    }
    /// Return true if this relay is a hidden service directory
    ///
    /// Ie, if it is to be included in the hsdir ring.
    #[cfg(feature = "hs-common")]
384785
    pub(crate) fn is_hsdir_for_ring(&self) -> bool {
        // TODO are there any other flags should we check?
        // rend-spec-v3 2.2.3 says just
        //   "each node listed in the current consensus with the HSDir flag"
        // Do we need to check ed25519_id_is_usable ?
        // See also https://gitlab.torproject.org/tpo/core/arti/-/issues/504
384785
        self.rs.is_flagged_hsdir()
384785
    }
}
impl<'a> Relay<'a> {
    /// Return a [`RelayDetails`](details::RelayDetails) for this relay.
    ///
    /// Callers should generally avoid using this information directly if they can;
    /// it's better to use a higher-level function that exposes semantic information
    /// rather than these properties.
100020186
    pub fn low_level_details(&self) -> details::RelayDetails<'_> {
100020186
        details::RelayDetails(self)
100020186
    }
    /// Return the Ed25519 ID for this relay.
148031788
    pub fn id(&self) -> &Ed25519Identity {
148031788
        self.md.ed25519_id()
148031788
    }
    /// Return the RsaIdentity for this relay.
96067410
    pub fn rsa_id(&self) -> &RsaIdentity {
96067410
        self.rs.rsa_identity()
96067410
    }
    /// Return a reference to this relay's "router status" entry in
    /// the consensus.
    ///
    /// The router status entry contains information about the relay
    /// that the authorities voted on directly.  For most use cases,
    /// you shouldn't need them.
    ///
    /// This function is only available if the crate was built with
    /// its `experimental-api` feature.
    #[cfg(feature = "experimental-api")]
2
    pub fn rs(&self) -> &netstatus::MdRouterStatus {
2
        self.rs
2
    }
    /// Return a reference to this relay's "microdescriptor" entry in
    /// the consensus.
    ///
    /// A "microdescriptor" is a synopsis of the information about a relay,
    /// used to determine its capabilities and route traffic through it.
    /// For most use cases, you shouldn't need it.
    ///
    /// This function is only available if the crate was built with
    /// its `experimental-api` feature.
    #[cfg(feature = "experimental-api")]
2
    pub fn md(&self) -> &MicrodescAndHash {
2
        self.md
2
    }
}
/// An error value returned from [`NetDir::by_ids_detailed`].
#[cfg(feature = "hs-common")]
#[derive(Clone, Debug, thiserror::Error)]
#[non_exhaustive]
pub enum RelayLookupError {
    /// We found a relay whose presence indicates that the provided set of
    /// identities is impossible to resolve.
    #[error("Provided set of identities is impossible according to consensus.")]
    Impossible,
}
impl<'a> HasAddrs for Relay<'a> {
135042630
    fn addrs(&self) -> impl Iterator<Item = std::net::SocketAddr> {
135042630
        self.rs.addrs()
135042630
    }
}
#[cfg(feature = "geoip")]
impl<'a> HasCountryCode for Relay<'a> {
846
    fn country_code(&self) -> Option<CountryCode> {
846
        self.cc
846
    }
}
impl<'a> tor_linkspec::HasRelayIdsLegacy for Relay<'a> {
148019468
    fn ed_identity(&self) -> &Ed25519Identity {
148019468
        self.id()
148019468
    }
45071128
    fn rsa_identity(&self) -> &RsaIdentity {
45071128
        self.rsa_id()
45071128
    }
}
impl<'a> HasRelayIds for UncheckedRelay<'a> {
    fn identity(&self, key_type: RelayIdType) -> Option<RelayIdRef<'_>> {
        match key_type {
            RelayIdType::Ed25519 if self.rs.ed25519_id_is_usable() => {
                self.md.map(|m| m.ed25519_id().into())
            }
            RelayIdType::Rsa => Some(self.rs.rsa_identity().into()),
            _ => None,
        }
    }
}
#[cfg(feature = "geoip")]
impl<'a> HasCountryCode for UncheckedRelay<'a> {
    fn country_code(&self) -> Option<CountryCode> {
        self.cc
    }
}
impl<'a> DirectChanMethodsHelper for Relay<'a> {}
impl<'a> ChanTarget for Relay<'a> {}
impl<'a> tor_linkspec::CircTarget for Relay<'a> {
1764286
    fn ntor_onion_key(&self) -> &ll::pk::curve25519::PublicKey {
1764286
        self.md.ntor_key()
1764286
    }
1760291
    fn protovers(&self) -> &tor_protover::Protocols {
1760291
        self.rs.protovers()
1760291
    }
}
#[cfg(test)]
mod test {
    // @@ begin test lint list maintained by maint/add_warning @@
    #![allow(clippy::bool_assert_comparison)]
    #![allow(clippy::clone_on_copy)]
    #![allow(clippy::dbg_macro)]
    #![allow(clippy::mixed_attributes_style)]
    #![allow(clippy::print_stderr)]
    #![allow(clippy::print_stdout)]
    #![allow(clippy::single_char_pattern)]
    #![allow(clippy::unwrap_used)]
    #![allow(clippy::unchecked_time_subtraction)]
    #![allow(clippy::useless_vec)]
    #![allow(clippy::needless_pass_by_value)]
    #![allow(clippy::string_slice)] // See arti#2571
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
    use super::*;
    use crate::testnet::*;
    use float_eq::assert_float_eq;
    use std::collections::HashSet;
    use std::time::Duration;
    use tor_basic_utils::test_rng::{self, testing_rng};
    use tor_linkspec::{RelayIdType, RelayIds};
    #[cfg(feature = "hs-common")]
    fn dummy_hs_blind_id() -> HsBlindId {
        let hsid = [2, 1, 1, 1].iter().cycle().take(32).cloned().collect_vec();
        let hsid = Ed25519Identity::new(hsid[..].try_into().unwrap());
        HsBlindId::from(hsid)
    }
    // Basic functionality for a partial netdir: Add microdescriptors,
    // then you have a netdir.
    #[test]
    fn partial_netdir() {
        let (consensus, microdescs) = construct_network().unwrap();
        let dir = PartialNetDir::new(consensus, None);
        // Check the lifetime
        let lifetime = dir.lifetime();
        assert_eq!(
            lifetime
                .valid_until()
                .duration_since(lifetime.valid_after())
                .unwrap(),
            Duration::new(86400, 0)
        );
        // No microdescriptors, so we don't have enough paths, and can't
        // advance.
        assert!(!dir.have_enough_paths());
        let mut dir = match dir.unwrap_if_sufficient() {
            Ok(_) => panic!(),
            Err(d) => d,
        };
        let missing: HashSet<_> = dir.missing_microdescs().collect();
        assert_eq!(missing.len(), 40);
        assert_eq!(missing.len(), dir.netdir.c_relays().len());
        for md in &microdescs {
            assert!(missing.contains(md.digest()));
        }
        // Now add all the mds and try again.
        for md in microdescs {
            let wanted = dir.add_microdesc(md);
            assert!(wanted);
        }
        let missing: HashSet<_> = dir.missing_microdescs().collect();
        assert!(missing.is_empty());
        assert!(dir.have_enough_paths());
        let _complete = match dir.unwrap_if_sufficient() {
            Ok(d) => d,
            Err(_) => panic!(),
        };
    }
    #[test]
    fn override_params() {
        let (consensus, _microdescs) = construct_network().unwrap();
        let override_p = "bwweightscale=2 doesnotexist=77 circwindow=500"
            .parse()
            .unwrap();
        let dir = PartialNetDir::new(consensus.clone(), Some(&override_p));
        let params = &dir.netdir.params;
        assert_eq!(params.bw_weight_scale.get(), 2);
        assert_eq!(params.circuit_window.get(), 500_i32);
        // try again without the override.
        let dir = PartialNetDir::new(consensus, None);
        let params = &dir.netdir.params;
        assert_eq!(params.bw_weight_scale.get(), 1_i32);
        assert_eq!(params.circuit_window.get(), 1000_i32);
    }
    #[test]
    fn fill_from_previous() {
        let (consensus, microdescs) = construct_network().unwrap();
        let mut dir = PartialNetDir::new(consensus.clone(), None);
        for md in microdescs.iter().skip(2) {
            let wanted = dir.add_microdesc(md.clone());
            assert!(wanted);
        }
        let dir1 = dir.unwrap_if_sufficient().unwrap();
        assert_eq!(dir1.missing_microdescs().count(), 2);
        let mut dir = PartialNetDir::new(consensus, None);
        assert_eq!(dir.missing_microdescs().count(), 40);
        dir.fill_from_previous_netdir(Arc::new(dir1));
        assert_eq!(dir.missing_microdescs().count(), 2);
    }
    #[test]
    fn path_count() {
        let low_threshold = "min_paths_for_circs_pct=64".parse().unwrap();
        let high_threshold = "min_paths_for_circs_pct=65".parse().unwrap();
        let (consensus, microdescs) = construct_network().unwrap();
        let mut dir = PartialNetDir::new(consensus.clone(), Some(&low_threshold));
        for (pos, md) in microdescs.iter().enumerate() {
            if pos % 7 == 2 {
                continue; // skip a few relays.
            }
            dir.add_microdesc(md.clone());
        }
        let dir = dir.unwrap_if_sufficient().unwrap();
        // We  have 40 relays that we know about from the consensus.
        assert_eq!(dir.all_relays().count(), 40);
        // But only 34 are usable.
        assert_eq!(dir.relays().count(), 34);
        // For guards: mds 20..=39 correspond to Guard relays.
        // Their bandwidth is 2*(1000+2000+...10000) = 110_000.
        // We skipped 23, 30, and 37.  They have bandwidth
        // 4000 + 1000 + 8000 = 13_000.  So our fractional bandwidth
        // should be (110-13)/110.
        let f = dir.frac_for_role(WeightRole::Guard, |u| u.rs.is_flagged_guard());
        assert!(((97.0 / 110.0) - f).abs() < 0.000001);
        // For exits: mds 10..=19 and 30..=39 correspond to Exit relays.
        // We skipped 16, 30,  and 37. Per above our fractional bandwidth is
        // (110-16)/110.
        let f = dir.frac_for_role(WeightRole::Exit, |u| u.rs.is_flagged_exit());
        assert!(((94.0 / 110.0) - f).abs() < 0.000001);
        // For middles: all relays are middles. We skipped 2, 9, 16,
        // 23, 30, and 37. Per above our fractional bandwidth is
        // (220-33)/220
        let f = dir.frac_for_role(WeightRole::Middle, |_| true);
        assert!(((187.0 / 220.0) - f).abs() < 0.000001);
        // Multiplying those together, we get the fraction of paths we can
        // build at ~0.64052066, which is above the threshold we set above for
        // MinPathsForCircsPct.
        let f = dir.frac_usable_paths();
        assert!((f - 0.64052066).abs() < 0.000001);
        // But if we try again with a slightly higher threshold...
        let mut dir = PartialNetDir::new(consensus, Some(&high_threshold));
        for (pos, md) in microdescs.into_iter().enumerate() {
            if pos % 7 == 2 {
                continue; // skip a few relays.
            }
            dir.add_microdesc(md);
        }
        assert!(dir.unwrap_if_sufficient().is_err());
    }
    /// Return a 3-tuple for use by `test_pick_*()` of an Rng, a number of
    /// iterations, and a tolerance.
    ///
    /// If the Rng is deterministic (the default), we can use a faster setup,
    /// with a higher tolerance and fewer iterations.  But if you've explicitly
    /// opted into randomization (or are replaying a seed from an earlier
    /// randomized test), we give you more iterations and a tighter tolerance.
    fn testing_rng_with_tolerances() -> (impl rand::Rng, usize, f64) {
        // Use a deterministic RNG if none is specified, since this is slow otherwise.
        let config = test_rng::Config::from_env().unwrap_or(test_rng::Config::Deterministic);
        let (iters, tolerance) = match config {
            test_rng::Config::Deterministic => (5000, 0.02),
            _ => (50000, 0.01),
        };
        (config.into_rng(), iters, tolerance)
    }
    #[test]
    fn test_pick() {
        let (consensus, microdescs) = construct_network().unwrap();
        let mut dir = PartialNetDir::new(consensus, None);
        for md in microdescs.into_iter() {
            let wanted = dir.add_microdesc(md.clone());
            assert!(wanted);
        }
        let dir = dir.unwrap_if_sufficient().unwrap();
        let (mut rng, total, tolerance) = testing_rng_with_tolerances();
        let mut picked = [0_isize; 40];
        for _ in 0..total {
            let r = dir.pick_relay(&mut rng, WeightRole::Middle, |r| {
                r.low_level_details().supports_exit_port_ipv4(80)
            });
            let r = r.unwrap();
            let id_byte = r.identity(RelayIdType::Rsa).unwrap().as_bytes()[0];
            picked[id_byte as usize] += 1;
        }
        // non-exits should never get picked.
        picked[0..10].iter().for_each(|x| assert_eq!(*x, 0));
        picked[20..30].iter().for_each(|x| assert_eq!(*x, 0));
        let picked_f: Vec<_> = picked.iter().map(|x| *x as f64 / total as f64).collect();
        // We didn't we any non-default weights, so the other relays get
        // weighted proportional to their bandwidth.
        assert_float_eq!(picked_f[19], (10.0 / 110.0), abs <= tolerance);
        assert_float_eq!(picked_f[38], (9.0 / 110.0), abs <= tolerance);
        assert_float_eq!(picked_f[39], (10.0 / 110.0), abs <= tolerance);
    }
    #[test]
    fn test_pick_multiple() {
        // This is mostly a copy of test_pick, except that it uses
        // pick_n_relays to pick several relays at once.
        let dir = construct_netdir().unwrap_if_sufficient().unwrap();
        let (mut rng, total, tolerance) = testing_rng_with_tolerances();
        let mut picked = [0_isize; 40];
        for _ in 0..total / 4 {
            let relays = dir.pick_n_relays(&mut rng, 4, WeightRole::Middle, |r| {
                r.low_level_details().supports_exit_port_ipv4(80)
            });
            assert_eq!(relays.len(), 4);
            for r in relays {
                let id_byte = r.identity(RelayIdType::Rsa).unwrap().as_bytes()[0];
                picked[id_byte as usize] += 1;
            }
        }
        // non-exits should never get picked.
        picked[0..10].iter().for_each(|x| assert_eq!(*x, 0));
        picked[20..30].iter().for_each(|x| assert_eq!(*x, 0));
        let picked_f: Vec<_> = picked.iter().map(|x| *x as f64 / total as f64).collect();
        // We didn't we any non-default weights, so the other relays get
        // weighted proportional to their bandwidth.
        assert_float_eq!(picked_f[19], (10.0 / 110.0), abs <= tolerance);
        assert_float_eq!(picked_f[36], (7.0 / 110.0), abs <= tolerance);
        assert_float_eq!(picked_f[39], (10.0 / 110.0), abs <= tolerance);
    }
    #[test]
    fn test_pick_multiple_from_insufficient() {
        // This is intended to test `NetDir::pick_n_relays`, but targets the internal,
        // easier-to-test `NetDir::pick_n_weighted` that is used to implement it.
        let mut rng = testing_rng();
        // If *any* item (relay) has non-zero weight, then we return only those, even if we asked for more.
        let mostly_zeros = vec![("dud1", 0), ("dud2", 0), ("ok", 1), ("dud3", 0)];
        for n in [1, 2, 10] {
            assert_eq!(
                NetDir::pick_n_weighted(&mut rng, n, &mostly_zeros[..]),
                vec!["ok"],
                "where n={n}"
            );
        }
        // If *all* items have zero weight, and we ask for as many or more than
        // we have, we get back all of them.
        let all_zeros = vec![("dud1", 0), ("dud2", 0), ("dud3", 0)];
        let all_zeros_items = all_zeros.iter().map(|(x, _w)| *x).collect::<Vec<_>>();
        for n in [all_zeros.len(), all_zeros.len() + 10] {
            let mut res = NetDir::pick_n_weighted(&mut rng, n, &all_zeros[..]);
            res.sort();
            assert_eq!(res, all_zeros_items, "where n={n}");
        }
        // If *all* items have zero weight, and we ask for fewer than we have,
        // we get back as many as we asked for.
        let n = all_zeros.len() / 2;
        let res = NetDir::pick_n_weighted(&mut rng, n, &all_zeros[..]);
        assert_eq!(res.len(), n);
    }
    #[test]
    fn subnets() {
        let cfg = SubnetConfig::default();
        fn same_net(cfg: &SubnetConfig, a: &str, b: &str) -> bool {
            cfg.addrs_in_same_subnet(&a.parse().unwrap(), &b.parse().unwrap())
        }
        assert!(same_net(&cfg, "127.15.3.3", "127.15.9.9"));
        assert!(!same_net(&cfg, "127.15.3.3", "127.16.9.9"));
        assert!(!same_net(&cfg, "127.15.3.3", "127::"));
        assert!(same_net(&cfg, "ffff:ffff:90:33::", "ffff:ffff:91:34::"));
        assert!(!same_net(&cfg, "ffff:ffff:90:33::", "ffff:fffe:91:34::"));
        let cfg = SubnetConfig {
            subnets_family_v4: 32,
            subnets_family_v6: 128,
        };
        assert!(!same_net(&cfg, "127.15.3.3", "127.15.9.9"));
        assert!(!same_net(&cfg, "ffff:ffff:90:33::", "ffff:ffff:91:34::"));
        assert!(same_net(&cfg, "127.0.0.1", "127.0.0.1"));
        assert!(!same_net(&cfg, "127.0.0.1", "127.0.0.2"));
        assert!(same_net(&cfg, "ffff:ffff:90:33::", "ffff:ffff:90:33::"));
        let cfg = SubnetConfig {
            subnets_family_v4: 33,
            subnets_family_v6: 129,
        };
        assert!(!same_net(&cfg, "127.0.0.1", "127.0.0.1"));
        assert!(!same_net(&cfg, "::", "::"));
    }
    #[test]
    fn subnet_union() {
        let cfg1 = SubnetConfig {
            subnets_family_v4: 16,
            subnets_family_v6: 64,
        };
        let cfg2 = SubnetConfig {
            subnets_family_v4: 24,
            subnets_family_v6: 32,
        };
        let a1 = "1.2.3.4".parse().unwrap();
        let a2 = "1.2.10.10".parse().unwrap();
        let a3 = "ffff:ffff::7".parse().unwrap();
        let a4 = "ffff:ffff:1234::8".parse().unwrap();
        assert_eq!(cfg1.addrs_in_same_subnet(&a1, &a2), true);
        assert_eq!(cfg2.addrs_in_same_subnet(&a1, &a2), false);
        assert_eq!(cfg1.addrs_in_same_subnet(&a3, &a4), false);
        assert_eq!(cfg2.addrs_in_same_subnet(&a3, &a4), true);
        let cfg_u = cfg1.union(&cfg2);
        assert_eq!(
            cfg_u,
            SubnetConfig {
                subnets_family_v4: 16,
                subnets_family_v6: 32,
            }
        );
        assert_eq!(cfg_u.addrs_in_same_subnet(&a1, &a2), true);
        assert_eq!(cfg_u.addrs_in_same_subnet(&a3, &a4), true);
        assert_eq!(cfg1.union(&cfg1), cfg1);
        assert_eq!(cfg1.union(&SubnetConfig::no_addresses_match()), cfg1);
    }
    #[test]
    fn relay_funcs() {
        let (consensus, microdescs) = construct_custom_network(
            |pos, nb, _| {
                if pos == 15 {
                    nb.rs.add_or_port("[f0f0::30]:9001".parse().unwrap());
                } else if pos == 20 {
                    nb.rs.add_or_port("[f0f0::3131]:9001".parse().unwrap());
                }
            },
            None,
        )
        .unwrap();
        let subnet_config = SubnetConfig::default();
        let all_family_info = FamilyRules::all_family_info();
        let mut dir = PartialNetDir::new(consensus, None);
        for md in microdescs.into_iter() {
            let wanted = dir.add_microdesc(md.clone());
            assert!(wanted);
        }
        let dir = dir.unwrap_if_sufficient().unwrap();
        // Pick out a few relays by ID.
        let k0 = Ed25519Identity::from([0; 32]);
        let k1 = Ed25519Identity::from([1; 32]);
        let k2 = Ed25519Identity::from([2; 32]);
        let k3 = Ed25519Identity::from([3; 32]);
        let k10 = Ed25519Identity::from([10; 32]);
        let k15 = Ed25519Identity::from([15; 32]);
        let k20 = Ed25519Identity::from([20; 32]);
        let r0 = dir.by_id(&k0).unwrap();
        let r1 = dir.by_id(&k1).unwrap();
        let r2 = dir.by_id(&k2).unwrap();
        let r3 = dir.by_id(&k3).unwrap();
        let r10 = dir.by_id(&k10).unwrap();
        let r15 = dir.by_id(&k15).unwrap();
        let r20 = dir.by_id(&k20).unwrap();
        assert_eq!(r0.id(), &[0; 32].into());
        assert_eq!(r0.rsa_id(), &[0; 20].into());
        assert_eq!(r1.id(), &[1; 32].into());
        assert_eq!(r1.rsa_id(), &[1; 20].into());
        assert!(r0.same_relay_ids(&r0));
        assert!(r1.same_relay_ids(&r1));
        assert!(!r1.same_relay_ids(&r0));
        assert!(r0.low_level_details().is_dir_cache());
        assert!(!r1.low_level_details().is_dir_cache());
        assert!(r2.low_level_details().is_dir_cache());
        assert!(!r3.low_level_details().is_dir_cache());
        assert!(!r0.low_level_details().supports_exit_port_ipv4(80));
        assert!(!r1.low_level_details().supports_exit_port_ipv4(80));
        assert!(!r2.low_level_details().supports_exit_port_ipv4(80));
        assert!(!r3.low_level_details().supports_exit_port_ipv4(80));
        assert!(!r0.low_level_details().policies_allow_some_port());
        assert!(!r1.low_level_details().policies_allow_some_port());
        assert!(!r2.low_level_details().policies_allow_some_port());
        assert!(!r3.low_level_details().policies_allow_some_port());
        assert!(r10.low_level_details().policies_allow_some_port());
        assert!(r0.low_level_details().in_same_family(&r0, all_family_info));
        assert!(r0.low_level_details().in_same_family(&r1, all_family_info));
        assert!(r1.low_level_details().in_same_family(&r0, all_family_info));
        assert!(r1.low_level_details().in_same_family(&r1, all_family_info));
        assert!(!r0.low_level_details().in_same_family(&r2, all_family_info));
        assert!(!r2.low_level_details().in_same_family(&r0, all_family_info));
        assert!(r2.low_level_details().in_same_family(&r2, all_family_info));
        assert!(r2.low_level_details().in_same_family(&r3, all_family_info));
        assert!(r0.low_level_details().in_same_subnet(&r10, &subnet_config));
        assert!(r10.low_level_details().in_same_subnet(&r10, &subnet_config));
        assert!(r0.low_level_details().in_same_subnet(&r0, &subnet_config));
        assert!(r1.low_level_details().in_same_subnet(&r1, &subnet_config));
        assert!(!r1.low_level_details().in_same_subnet(&r2, &subnet_config));
        assert!(!r2.low_level_details().in_same_subnet(&r3, &subnet_config));
        // Make sure IPv6 families work.
        let subnet_config = SubnetConfig {
            subnets_family_v4: 128,
            subnets_family_v6: 96,
        };
        assert!(r15.low_level_details().in_same_subnet(&r20, &subnet_config));
        assert!(!r15.low_level_details().in_same_subnet(&r1, &subnet_config));
        // Make sure that subnet configs can be disabled.
        let subnet_config = SubnetConfig {
            subnets_family_v4: 255,
            subnets_family_v6: 255,
        };
        assert!(!r15.low_level_details().in_same_subnet(&r20, &subnet_config));
    }
    #[test]
    fn test_badexit() {
        // make a netdir where relays 10-19 are badexit, and everybody
        // exits to 443 on IPv6.
        use tor_netdoc::types::relay_flags::RelayFlag;
        let netdir = construct_custom_netdir(|pos, nb, _| {
            if (10..20).contains(&pos) {
                nb.rs.add_flags(RelayFlag::BadExit);
            }
            nb.md.parse_ipv6_policy("accept 443").unwrap();
        })
        .unwrap()
        .unwrap_if_sufficient()
        .unwrap();
        let e12 = netdir.by_id(&Ed25519Identity::from([12; 32])).unwrap();
        let e32 = netdir.by_id(&Ed25519Identity::from([32; 32])).unwrap();
        assert!(!e12.low_level_details().supports_exit_port_ipv4(80));
        assert!(e32.low_level_details().supports_exit_port_ipv4(80));
        assert!(!e12.low_level_details().supports_exit_port_ipv6(443));
        assert!(e32.low_level_details().supports_exit_port_ipv6(443));
        assert!(!e32.low_level_details().supports_exit_port_ipv6(555));
        assert!(!e12.low_level_details().policies_allow_some_port());
        assert!(e32.low_level_details().policies_allow_some_port());
        assert!(!e12.low_level_details().ipv4_policy().allows_some_port());
        assert!(!e12.low_level_details().ipv6_policy().allows_some_port());
        assert!(e32.low_level_details().ipv4_policy().allows_some_port());
        assert!(e32.low_level_details().ipv6_policy().allows_some_port());
        assert!(
            e12.low_level_details()
                .ipv4_declared_policy()
                .allows_some_port()
        );
        assert!(
            e12.low_level_details()
                .ipv6_declared_policy()
                .allows_some_port()
        );
    }
    #[cfg(feature = "experimental-api")]
    #[test]
    fn test_accessors() {
        let netdir = construct_netdir().unwrap_if_sufficient().unwrap();
        let r4 = netdir.by_id(&Ed25519Identity::from([4; 32])).unwrap();
        let r16 = netdir.by_id(&Ed25519Identity::from([16; 32])).unwrap();
        assert!(!r4.md().ipv4_policy().allows_some_port());
        assert!(r16.md().ipv4_policy().allows_some_port());
        assert!(!r4.rs().is_flagged_exit());
        assert!(r16.rs().is_flagged_exit());
    }
    #[test]
    fn test_by_id() {
        // Make a netdir that omits the microdescriptor for 0xDDDDDD...
        let netdir = construct_custom_netdir(|pos, nb, _| {
            nb.omit_md = pos == 13;
        })
        .unwrap();
        let netdir = netdir.unwrap_if_sufficient().unwrap();
        let r = netdir.by_id(&Ed25519Identity::from([0; 32])).unwrap();
        assert_eq!(r.id().as_bytes(), &[0; 32]);
        assert!(netdir.by_id(&Ed25519Identity::from([13; 32])).is_none());
        let r = netdir.by_rsa_id(&[12; 20].into()).unwrap();
        assert_eq!(r.rsa_id().as_bytes(), &[12; 20]);
        assert!(netdir.rsa_id_is_listed(&[12; 20].into()));
        assert!(netdir.by_rsa_id(&[13; 20].into()).is_none());
        assert!(netdir.by_rsa_id_unchecked(&[99; 20].into()).is_none());
        assert!(!netdir.rsa_id_is_listed(&[99; 20].into()));
        let r = netdir.by_rsa_id_unchecked(&[13; 20].into()).unwrap();
        assert_eq!(r.rs.rsa_identity().as_bytes(), &[13; 20]);
        assert!(netdir.rsa_id_is_listed(&[13; 20].into()));
        let pair_13_13 = RelayIds::builder()
            .ed_identity([13; 32].into())
            .rsa_identity([13; 20].into())
            .build()
            .unwrap();
        let pair_14_14 = RelayIds::builder()
            .ed_identity([14; 32].into())
            .rsa_identity([14; 20].into())
            .build()
            .unwrap();
        let pair_14_99 = RelayIds::builder()
            .ed_identity([14; 32].into())
            .rsa_identity([99; 20].into())
            .build()
            .unwrap();
        let r = netdir.by_ids(&pair_13_13);
        assert!(r.is_none());
        let r = netdir.by_ids(&pair_14_14).unwrap();
        assert_eq!(r.identity(RelayIdType::Rsa).unwrap().as_bytes(), &[14; 20]);
        assert_eq!(
            r.identity(RelayIdType::Ed25519).unwrap().as_bytes(),
            &[14; 32]
        );
        let r = netdir.by_ids(&pair_14_99);
        assert!(r.is_none());
        assert_eq!(
            netdir.id_pair_listed(&[13; 32].into(), &[13; 20].into()),
            None
        );
        assert_eq!(
            netdir.id_pair_listed(&[15; 32].into(), &[15; 20].into()),
            Some(true)
        );
        assert_eq!(
            netdir.id_pair_listed(&[15; 32].into(), &[99; 20].into()),
            Some(false)
        );
    }
    #[test]
    #[cfg(feature = "hs-common")]
    fn test_by_ids_detailed() {
        // Make a netdir that omits the microdescriptor for 0xDDDDDD...
        let netdir = construct_custom_netdir(|pos, nb, _| {
            nb.omit_md = pos == 13;
        })
        .unwrap();
        let netdir = netdir.unwrap_if_sufficient().unwrap();
        let id13_13 = RelayIds::builder()
            .ed_identity([13; 32].into())
            .rsa_identity([13; 20].into())
            .build()
            .unwrap();
        let id15_15 = RelayIds::builder()
            .ed_identity([15; 32].into())
            .rsa_identity([15; 20].into())
            .build()
            .unwrap();
        let id15_99 = RelayIds::builder()
            .ed_identity([15; 32].into())
            .rsa_identity([99; 20].into())
            .build()
            .unwrap();
        let id99_15 = RelayIds::builder()
            .ed_identity([99; 32].into())
            .rsa_identity([15; 20].into())
            .build()
            .unwrap();
        let id99_99 = RelayIds::builder()
            .ed_identity([99; 32].into())
            .rsa_identity([99; 20].into())
            .build()
            .unwrap();
        let id15_xx = RelayIds::builder()
            .ed_identity([15; 32].into())
            .build()
            .unwrap();
        let idxx_15 = RelayIds::builder()
            .rsa_identity([15; 20].into())
            .build()
            .unwrap();
        assert!(matches!(netdir.by_ids_detailed(&id13_13), Ok(None)));
        assert!(matches!(netdir.by_ids_detailed(&id15_15), Ok(Some(_))));
        assert!(matches!(
            netdir.by_ids_detailed(&id15_99),
            Err(RelayLookupError::Impossible)
        ));
        assert!(matches!(
            netdir.by_ids_detailed(&id99_15),
            Err(RelayLookupError::Impossible)
        ));
        assert!(matches!(netdir.by_ids_detailed(&id99_99), Ok(None)));
        assert!(matches!(netdir.by_ids_detailed(&id15_xx), Ok(Some(_))));
        assert!(matches!(netdir.by_ids_detailed(&idxx_15), Ok(Some(_))));
    }
    #[test]
    fn weight_type() {
        let r0 = RelayWeight(0);
        let r100 = RelayWeight(100);
        let r200 = RelayWeight(200);
        let r300 = RelayWeight(300);
        assert_eq!(r100 + r200, r300);
        assert_eq!(r100.checked_div(r200), Some(0.5));
        assert!(r100.checked_div(r0).is_none());
        assert_eq!(r200.ratio(0.5), Some(r100));
        assert!(r200.ratio(-1.0).is_none());
    }
    #[test]
    fn weight_accessors() {
        // Make a netdir that omits the microdescriptor for 0xDDDDDD...
        let netdir = construct_netdir().unwrap_if_sufficient().unwrap();
        let g_total = netdir.total_weight(WeightRole::Guard, |r| r.rs.is_flagged_guard());
        // This is just the total guard weight, since all our Wxy = 1.
        assert_eq!(g_total, RelayWeight(110_000));
        let g_total = netdir.total_weight(WeightRole::Guard, |_| false);
        assert_eq!(g_total, RelayWeight(0));
        let relay = netdir.by_id(&Ed25519Identity::from([35; 32])).unwrap();
        assert!(relay.rs.is_flagged_guard());
        let w = netdir.relay_weight(&relay, WeightRole::Guard);
        assert_eq!(w, RelayWeight(6_000));
        let w = netdir
            .weight_by_rsa_id(&[33; 20].into(), WeightRole::Guard)
            .unwrap();
        assert_eq!(w, RelayWeight(4_000));
        assert!(
            netdir
                .weight_by_rsa_id(&[99; 20].into(), WeightRole::Guard)
                .is_none()
        );
    }
    #[test]
    fn family_list() {
        let netdir = construct_custom_netdir(|pos, n, _| {
            if pos == 0x0a {
                n.md.family(
                    "$0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B \
                     $0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C \
                     $0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D"
                        .parse()
                        .unwrap(),
                );
            } else if pos == 0x0c {
                n.md.family("$0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A".parse().unwrap());
            }
        })
        .unwrap()
        .unwrap_if_sufficient()
        .unwrap();
        // In the testing netdir, adjacent members are in the same family by default...
        let r0 = netdir.by_id(&Ed25519Identity::from([0; 32])).unwrap();
        let family: Vec<_> = netdir.known_family_members(&r0).collect();
        assert_eq!(family.len(), 1);
        assert_eq!(family[0].id(), &Ed25519Identity::from([1; 32]));
        // But we've made this relay claim membership with several others.
        let r10 = netdir.by_id(&Ed25519Identity::from([10; 32])).unwrap();
        let family: HashSet<_> = netdir.known_family_members(&r10).map(|r| *r.id()).collect();
        assert_eq!(family.len(), 2);
        assert!(family.contains(&Ed25519Identity::from([11; 32])));
        assert!(family.contains(&Ed25519Identity::from([12; 32])));
        // Note that 13 doesn't get put in, even though it's listed, since it doesn't claim
        //  membership with 10.
    }
    #[test]
    #[cfg(feature = "geoip")]
    fn relay_has_country_code() {
        let src_v6 = r#"
        fe80:dead:beef::,fe80:dead:ffff::,US
        fe80:feed:eeee::1,fe80:feed:eeee::1,AT
        fe80:feed:eeee::2,fe80:feed:ffff::,DE
        "#;
        let db = GeoipDb::new_from_legacy_format("", src_v6, true).unwrap();
        let netdir = construct_custom_netdir_with_geoip(
            |pos, n, _| {
                if pos == 0x01 {
                    n.rs.add_or_port("[fe80:dead:beef::1]:42".parse().unwrap());
                }
                if pos == 0x02 {
                    n.rs.add_or_port("[fe80:feed:eeee::1]:42".parse().unwrap());
                    n.rs.add_or_port("[fe80:feed:eeee::2]:42".parse().unwrap());
                }
                if pos == 0x03 {
                    n.rs.add_or_port("[fe80:dead:beef::1]:42".parse().unwrap());
                    n.rs.add_or_port("[fe80:dead:beef::2]:42".parse().unwrap());
                }
            },
            &db,
        )
        .unwrap()
        .unwrap_if_sufficient()
        .unwrap();
        // No GeoIP data available -> None
        let r0 = netdir.by_id(&Ed25519Identity::from([0; 32])).unwrap();
        assert_eq!(r0.cc, None);
        // Exactly one match -> Some
        let r1 = netdir.by_id(&Ed25519Identity::from([1; 32])).unwrap();
        assert_eq!(r1.cc.as_ref().map(|x| x.as_ref()), Some("US"));
        // Conflicting matches -> None
        let r2 = netdir.by_id(&Ed25519Identity::from([2; 32])).unwrap();
        assert_eq!(r2.cc, None);
        // Multiple agreeing matches -> Some
        let r3 = netdir.by_id(&Ed25519Identity::from([3; 32])).unwrap();
        assert_eq!(r3.cc.as_ref().map(|x| x.as_ref()), Some("US"));
    }
    #[test]
    #[cfg(feature = "hs-common")]
    #[allow(deprecated)]
    fn hs_dirs_selection() {
        use tor_basic_utils::test_rng::testing_rng;
        const HSDIR_SPREAD_STORE: i32 = 6;
        const HSDIR_SPREAD_FETCH: i32 = 2;
        const PARAMS: [(&str, i32); 2] = [
            ("hsdir_spread_store", HSDIR_SPREAD_STORE),
            ("hsdir_spread_fetch", HSDIR_SPREAD_FETCH),
        ];
        let netdir: Arc<NetDir> =
            crate::testnet::construct_custom_netdir_with_params(|_, _, _| {}, PARAMS, None)
                .unwrap()
                .unwrap_if_sufficient()
                .unwrap()
                .into();
        let hsid = dummy_hs_blind_id();
        const OP_RELAY_COUNT: &[(HsDirOp, usize)] = &[
            // We can't upload to (hsdir_n_replicas * hsdir_spread_store) = 12, relays because there
            // are only 10 relays with the HsDir flag in the consensus.
            #[cfg(feature = "hs-service")]
            (HsDirOp::Upload, 10),
            (HsDirOp::Download, 4),
        ];
        for (op, relay_count) in OP_RELAY_COUNT {
            let relays = netdir.hs_dirs(&hsid, *op, &mut testing_rng());
            assert_eq!(relays.len(), *relay_count);
            // There should be no duplicates (the filtering function passed to
            // HsDirRing::ring_items_at() ensures the relays that are already in use for
            // lower-numbered replicas aren't considered a second time for a higher-numbered
            // replica).
            let unique = relays
                .iter()
                .map(|relay| relay.ed_identity())
                .collect::<HashSet<_>>();
            assert_eq!(unique.len(), relays.len());
        }
        // TODO: come up with a test that checks that HsDirRing::ring_items_at() skips over the
        // expected relays.
        //
        // For example, let's say we have the following hsdir ring:
        //
        //         A  -  B
        //        /       \
        //       F         C
        //        \       /
        //         E  -  D
        //
        // Let's also assume that:
        //
        //   * hsdir_spread_store = 3
        //   * the ordering of the relays on the ring is [A, B, C, D, E, F]
        //
        // If we use relays [A, B, C] for replica 1, and hs_index(2) = E, then replica 2 _must_ get
        // relays [E, F, D]. We should have a test that checks this.
    }
    #[test]
    fn zero_weights() {
        // Here we check the behavior of IndexedRandom::choose_weighted
        // in the presence of items whose weight is 0.
        //
        // We think that the behavior is:
        //   - If all items have weight 0, choose_weighted returns an error.
        //   - If any items have non-zero weight, one of them will be returned.
        let items = vec![1, 2, 3];
        let mut rng = testing_rng();
        let a = items.choose_weighted(&mut rng, |_| 0);
        assert!(matches!(a, Err(WeightError::InsufficientNonZero)));
        let only_one = |n: &i32| if *n == 1 { 1 } else { 0 };
        for _ in 0..100 {
            let a = items.choose_weighted(&mut rng, only_one);
            assert_eq!(a.unwrap(), &1);
        }
    }
}