diff --git a/Cargo.lock b/Cargo.lock index 67ae5e80c..e1f2d8c35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4052,17 +4052,22 @@ dependencies = [ "bitflags", "derive_builder_fork_arti", "derive_more", + "digest 0.10.6", "float_eq", "futures", "hex", + "hex-literal", + "humantime 2.1.0", "num_enum", "rand 0.8.5", "rand_chacha 0.3.1", "serde", "signature 1.6.4", + "smallvec", "static_assertions", "strum", "thiserror", + "time", "tor-basic-utils", "tor-checkable", "tor-config", diff --git a/crates/tor-hscrypto/src/time.rs b/crates/tor-hscrypto/src/time.rs index 5231bc98c..4ce718a96 100644 --- a/crates/tor-hscrypto/src/time.rs +++ b/crates/tor-hscrypto/src/time.rs @@ -125,6 +125,22 @@ impl TimePeriod { (SystemTime::UNIX_EPOCH + epoch_offset).checked_add(Duration::from_secs(end_sec))?; Some(start..end) } + + /// Return the numeric index of this time period. + /// + /// This function should only be used when encoding the time period for + /// cryptographic purposes. + pub fn interval_num(&self) -> u64 { + self.interval_num + } + + /// Return the length of this time period as a number of seconds. + /// + /// This function should only be used when encoding the time period for + /// cryptographic purposes. + pub fn length_in_sec(&self) -> u64 { + self.length_in_sec.into() + } } #[cfg(test)] diff --git a/crates/tor-netdir/Cargo.toml b/crates/tor-netdir/Cargo.toml index f6541064d..bc4b84a6c 100644 --- a/crates/tor-netdir/Cargo.toml +++ b/crates/tor-netdir/Cargo.toml @@ -23,7 +23,7 @@ experimental = ["experimental-api", "onion-service", "onion-client"] experimental-api = [] onion-client = ["onion-common"] onion-service = ["onion-common"] -onion-common = ["tor-hscrypto"] +onion-common = ["digest", "time", "smallvec", "tor-hscrypto"] # Enable testing-only APIs. APIs under this feature are not # covered by semver. @@ -33,15 +33,19 @@ testing = ["hex", "tor-netdoc/build_docs"] bitflags = "1" derive_builder = { version = "0.11.2", package = "derive_builder_fork_arti" } derive_more = "0.99.3" +digest = { version = "0.10.0", optional = true } futures = "0.3.14" hex = { version = "0.4", optional = true } +humantime = "2" num_enum = "0.5" rand = "0.8" serde = { version = "1.0.103", features = ["derive"] } signature = "1" +smallvec = { version = "1.10.0", optional = true } static_assertions = "1" strum = { version = "0.24", features = ["derive"] } thiserror = "1" +time = { version = "0.3.17", features = ["macros"], optional = true } tor-checkable = { path = "../tor-checkable", version = "0.4.0" } tor-config = { path = "../tor-config", version = "0.7.0" } tor-error = { path = "../tor-error", version = "0.4.0" } @@ -56,6 +60,7 @@ tracing = "0.1.18" [dev-dependencies] float_eq = "1.0.0" hex = "0.4" +hex-literal = "0.3" rand_chacha = "0.3" tor-basic-utils = { path = "../tor-basic-utils", version = "0.5.0" } tor-netdoc = { path = "../tor-netdoc", version = "0.6.0", features = ["build_docs"] } diff --git a/crates/tor-netdir/src/err.rs b/crates/tor-netdir/src/err.rs index 4a745fd78..b8b67f916 100644 --- a/crates/tor-netdir/src/err.rs +++ b/crates/tor-netdir/src/err.rs @@ -19,6 +19,9 @@ pub enum Error { /// We have directory information, but it is too expired to use. #[error("Directory is published too far in the future: Your clock is probably wrong")] DirNotYetValid, + /// We received a consensus document that should be impossible. + #[error("Invalid information from consensus document: {0}")] + InvalidConsensus(&'static str), } impl HasKind for Error { @@ -29,6 +32,7 @@ impl HasKind for Error { E::DirExpired => EK::DirectoryExpired, E::DirNotYetValid => EK::ClockSkew, E::NotEnoughInfo | E::NoInfo => EK::BootstrapRequired, + E::InvalidConsensus(_) => EK::TorProtocolViolation, } } } diff --git a/crates/tor-netdir/src/hsdir_params.rs b/crates/tor-netdir/src/hsdir_params.rs new file mode 100644 index 000000000..ecf30fb8f --- /dev/null +++ b/crates/tor-netdir/src/hsdir_params.rs @@ -0,0 +1,463 @@ +//! Compute which time period and shared random value from a consensus to use at +//! any given time. +//! +//! This is, unfortunately, a bit complex. It works as follows: +//! +//! * The _current_ time period is the one that contains the valid-after time +//! for the consensus... +//! * but to compute the time period interval, you need to look at the +//! consensus parameters, +//! * and to compute the time period offset, you need to know the consensus +//! voting interval. +//! +//! * The SRV for any given time period is the one that that was the most +//! recent at the _start_ of the time period... +//! * but to know when an SRV was most recent, you need to read a timestamp +//! from it that won't be there until proposal 342 is implemented... +//! * and until then, you have to compute the start of the UTC day when the +//! consensus became valid. +//! +//! This module could conceivably be part of `tor-netdoc`, but it seems better +//! to make it part of `tor-netdir`: this is where we put our complexity. +use std::time::{Duration, SystemTime}; + +use crate::{params::NetParameters, Error, Result}; +use time::{OffsetDateTime, UtcOffset}; +use tor_hscrypto::time::TimePeriod; +use tor_netdoc::doc::netstatus::{Lifetime, MdConsensus, SharedRandVal}; + +/// Parameters for generating and using an HsDir ring. +/// +/// These parameters are derived from the shared random values and time +/// parameters in the consensus, and are used to determine the +/// position of each HsDir within the ring. +#[derive(Clone, Debug)] +pub(crate) struct HsRingParams { + /// The time period for this ring. It's used to ensure that blinded onion + /// keys rotate in a _predictable_ way over time. + pub(crate) time_period: TimePeriod, + /// The SharedRandVal for this ring. It's used to ensure that the position + /// of each HsDir within the ring rotates _unpredictably_ over time. + pub(crate) shared_rand: SharedRandVal, +} + +/// By how many voting periods do we offset the beginning of our first time +/// period from the epoch? +/// +/// We do this so that each of our time periods begins at a time when the SRV is +/// not rotating. +const VOTING_PERIODS_IN_OFFSET: u32 = 12; + +/// How many voting periods make up an entire round of the shared random value +/// commit-and-reveal protocol? +/// +/// We use this to compute an SRV lifetime if one of the SRV values is missing. +const VOTING_PERIODS_IN_SRV_ROUND: u32 = 24; + +/// One day. +const ONE_DAY: Duration = Duration::new(86400, 0); + +/// Compute the `HsRingParams` for the current time period, according to a given +/// consensus. +/// +/// Return the ring parameters for the current period (which clients use when +/// fetching onion service descriptors), along with a SmallVec of ring +/// parameters for any secondary periods that onion services should additionally +/// use when publishing their descriptors. +/// +/// Note that "current" here is always relative to a given consensus, not the +/// current wall-clock time. +/// +/// (This function's return type is a bit cumbersome; these parameters are +/// bundled together because it is efficient to compute them all at once.) +pub(crate) fn compute_ring_parameters( + consensus: &MdConsensus, + params: &NetParameters, +) -> Result<(HsRingParams, Vec)> { + let srvs = extract_srvs(consensus)?; + let tp_length: Duration = params.hsdir_timeperiod_length.try_into().map_err(|_| { + Error::InvalidConsensus("Minutes in hsdir timeperiod could not be converted to a Duration") + })?; + let offset = voting_period(consensus.lifetime())? * VOTING_PERIODS_IN_OFFSET; + let cur_period = TimePeriod::new(tp_length, consensus.lifetime().valid_after(), offset) + .expect("Consensus valid-after did not fall in a time period"); + let cur_period_start = cur_period + .range() + .ok_or(Error::InvalidConsensus( + "HsDir time period in consensus could not be represented as a SystemTime range.", + ))? + .start; + + let cur_srv = + find_srv_for_time(&srvs[..], cur_period_start).unwrap_or_else(|| disaster_srv(cur_period)); + let main_ring = HsRingParams { + time_period: cur_period, + shared_rand: cur_srv, + }; + + // When computing secondary rings, we don't try so many fallback operations: + // if they aren't available, they aren't available. + let mut other_rings = Vec::new(); + for period in [cur_period.prev(), cur_period.next()].iter().flatten() { + if let Some(period_range) = period.range() { + if let Some(srv) = find_srv_for_time(&srvs[..], period_range.start) { + other_rings.push(HsRingParams { + time_period: *period, + shared_rand: srv, + }); + } + } + } + + Ok((main_ring, other_rings)) +} + +/// Compute the "Disaster SRV" for a given time period. +/// +/// This SRV is used if the authorities do not list any shared random value for +/// that time period, but we need to compute an HsDir ring for it anyway. +fn disaster_srv(period: TimePeriod) -> SharedRandVal { + use digest::Digest; + let mut d = tor_llcrypto::d::Sha3_256::new(); + d.update(b"shared-random-disaster"); + d.update((period.length_in_sec() / 60).to_be_bytes()); + d.update(period.interval_num().to_be_bytes()); + + let v: [u8; 32] = d.finalize().into(); + v.into() +} + +/// Helper type: A `SharedRandVal`, and the time range over which it is the most +/// recent. +type SrvInfo = (SharedRandVal, std::ops::Range); + +/// Given a list of SrvInfo, return the SharedRandVal (if any) that is the most +/// recent SRV at `when`. +fn find_srv_for_time(info: &[SrvInfo], when: SystemTime) -> Option { + info.iter() + .find(|(_, range)| range.contains(&when)) + .map(|(srv, _)| *srv) +} + +/// Return every SRV from a consensus, along with a duration over which it is +/// most recent SRV. +fn extract_srvs(consensus: &MdConsensus) -> Result> { + let mut v = Vec::new(); + let consensus_ts = consensus.lifetime().valid_after(); + let srv_interval = srv_interval(consensus)?; + + if let Some(cur) = consensus.shared_rand_cur() { + let ts_begin = cur + .timestamp() + .unwrap_or_else(|| start_of_day_containing(consensus_ts)); + let ts_end = ts_begin + srv_interval; + v.push((*cur.value(), ts_begin..ts_end)); + } + if let Some(prev) = consensus.shared_rand_prev() { + let ts_begin = prev + .timestamp() + .unwrap_or_else(|| start_of_day_containing(consensus_ts) - ONE_DAY); + let ts_end = ts_begin + srv_interval; + v.push((*prev.value(), ts_begin..ts_end)); + } + + Ok(v) +} + +/// Return the length of time for which a single SRV value is valid. +fn srv_interval(consensus: &MdConsensus) -> Result { + // What we _want_ to do, ideally, is is to learn the duration from the + // difference between the declared time for the previous value and the + // declared time for the current one. + // + // (This assumes that proposal 342 is implemented.) + if let (Some(cur), Some(prev)) = (consensus.shared_rand_cur(), consensus.shared_rand_prev()) { + if let (Some(cur_ts), Some(prev_ts)) = (cur.timestamp(), prev.timestamp()) { + if let Ok(d) = cur_ts.duration_since(prev_ts) { + return Ok(d); + } + } + } + + // But if one of those values is missing, or if it has no timestamp, we have + // to fall back to admitting that we know the schedule for the voting + // algorithm. + voting_period(consensus.lifetime()).map(|d| d * VOTING_PERIODS_IN_SRV_ROUND) +} + +/// Return the length of the voting period in the consensus. +/// +/// (The "voting period" is the length of time between between one consensus and the next.) +fn voting_period(lifetime: &Lifetime) -> Result { + // TODO hs: consider moving this function to be a method of Lifetime. + let valid_after = lifetime.valid_after(); + let fresh_until = lifetime.fresh_until(); + fresh_until + .duration_since(valid_after) + .map_err(|_| Error::InvalidConsensus("Mis-formed lifetime")) +} + +/// Return a time at the start of the UTC day containing `t`. +fn start_of_day_containing(t: SystemTime) -> SystemTime { + OffsetDateTime::from(t) + .to_offset(UtcOffset::UTC) + .replace_time(time::macros::time!(00:00)) + .into() +} + +#[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::print_stderr)] + #![allow(clippy::print_stdout)] + #![allow(clippy::single_char_pattern)] + #![allow(clippy::unwrap_used)] + #![allow(clippy::unchecked_duration_subtraction)] + //! + use super::*; + use hex_literal::hex; + use tor_netdoc::doc::netstatus::{ConsensusBuilder, MdConsensusRouterStatus}; + + /// Helper: parse an rfc3339 time. + /// + /// # Panics + /// + /// Panics if the time is invalid. + fn t(s: &str) -> SystemTime { + humantime::parse_rfc3339(s).unwrap() + } + /// Helper: parse a duration. + /// + /// # Panics + /// + /// Panics if the time is invalid. + fn d(s: &str) -> Duration { + humantime::parse_duration(s).unwrap() + } + + fn example_lifetime() -> Lifetime { + Lifetime::new( + t("1985-10-25T07:00:00Z"), + t("1985-10-25T08:00:00Z"), + t("1985-10-25T10:00:00Z"), + ) + .unwrap() + } + + const SRV1: [u8; 32] = *b"next saturday night were sending"; + const SRV2: [u8; 32] = *b"you......... back to the future!"; + + fn example_consensus_builder() -> ConsensusBuilder { + let mut bld = MdConsensus::builder(); + + bld.consensus_method(34) + .lifetime(example_lifetime()) + .param("bwweightscale", 1) + .param("hsdir_interval", 1440) + .weights("".parse().unwrap()) + .shared_rand_prev(7, SRV1.into(), None) + .shared_rand_cur(7, SRV2.into(), None); + + bld + } + + #[test] + fn start_of_day() { + assert_eq!( + start_of_day_containing(t("1985-10-25T07:00:00Z")), + t("1985-10-25T00:00:00Z") + ); + assert_eq!( + start_of_day_containing(t("1985-10-25T00:00:00Z")), + t("1985-10-25T00:00:00Z") + ); + assert_eq!( + start_of_day_containing(t("1985-10-25T23:59:59.999Z")), + t("1985-10-25T00:00:00Z") + ); + } + + #[test] + fn vote_period() { + assert_eq!(voting_period(&example_lifetime()).unwrap(), d("1 hour")); + + let lt2 = Lifetime::new( + t("1985-10-25T07:00:00Z"), + t("1985-10-25T07:22:00Z"), + t("1985-10-25T07:59:00Z"), + ) + .unwrap(); + + assert_eq!(voting_period(<2).unwrap(), d("22 min")); + } + + #[test] + fn srv_period() { + // In a basic consensus with no SRV timestamps, we'll assume 24 voting periods. + let consensus = example_consensus_builder().testing_consensus().unwrap(); + assert_eq!(srv_interval(&consensus).unwrap(), d("1 day")); + + // If there are timestamps, we look at the difference between them. + let consensus = example_consensus_builder() + .shared_rand_prev(7, SRV1.into(), Some(t("1985-10-25T00:00:00Z"))) + .shared_rand_cur(7, SRV2.into(), Some(t("1985-10-25T06:00:05Z"))) + .testing_consensus() + .unwrap(); + assert_eq!(srv_interval(&consensus).unwrap(), d("6 hours 5 sec")); + + // Note that if the timestamps are in reversed order, we fall back to 24 hours. + let consensus = example_consensus_builder() + .shared_rand_cur(7, SRV1.into(), Some(t("1985-10-25T00:00:00Z"))) + .shared_rand_prev(7, SRV2.into(), Some(t("1985-10-25T06:00:05Z"))) + .testing_consensus() + .unwrap(); + assert_eq!(srv_interval(&consensus).unwrap(), d("1 day")); + } + + #[test] + fn srvs_extract_and_find() { + let consensus = example_consensus_builder().testing_consensus().unwrap(); + let srvs = extract_srvs(&consensus).unwrap(); + assert_eq!( + srvs, + vec![ + // Since no timestamps are given in the example, the current srv + // is valid from midnight to midnight... + ( + SRV2.into(), + t("1985-10-25T00:00:00Z")..t("1985-10-26T00:00:00Z") + ), + // ...and the previous SRV is valid midnight-to-midnight on the + // previous day. + ( + SRV1.into(), + t("1985-10-24T00:00:00Z")..t("1985-10-25T00:00:00Z") + ) + ] + ); + + // Now try with explicit timestamps on the SRVs. + let consensus = example_consensus_builder() + .shared_rand_prev(7, SRV1.into(), Some(t("1985-10-25T00:00:00Z"))) + .shared_rand_cur(7, SRV2.into(), Some(t("1985-10-25T06:00:05Z"))) + .testing_consensus() + .unwrap(); + let srvs = extract_srvs(&consensus).unwrap(); + assert_eq!( + srvs, + vec![ + ( + SRV2.into(), + t("1985-10-25T06:00:05Z")..t("1985-10-25T12:00:10Z") + ), + ( + SRV1.into(), + t("1985-10-25T00:00:00Z")..t("1985-10-25T06:00:05Z") + ) + ] + ); + + // See if we can look up SRVs in that period. + assert_eq!(None, find_srv_for_time(&srvs, t("1985-10-24T23:59:00Z"))); + assert_eq!( + Some(SRV1.into()), + find_srv_for_time(&srvs, t("1985-10-25T00:00:00Z")) + ); + assert_eq!( + Some(SRV1.into()), + find_srv_for_time(&srvs, t("1985-10-25T03:59:00Z")) + ); + assert_eq!( + Some(SRV1.into()), + find_srv_for_time(&srvs, t("1985-10-25T00:00:00Z")) + ); + assert_eq!( + Some(SRV2.into()), + find_srv_for_time(&srvs, t("1985-10-25T06:00:05Z")) + ); + assert_eq!( + Some(SRV2.into()), + find_srv_for_time(&srvs, t("1985-10-25T12:00:00Z")) + ); + assert_eq!(None, find_srv_for_time(&srvs, t("1985-10-25T12:00:30Z"))); + } + + #[test] + fn disaster() { + use digest::Digest; + use tor_llcrypto::d::Sha3_256; + let period = TimePeriod::new(d("1 day"), t("1970-01-02T17:33:00Z"), d("12 hours")).unwrap(); + assert_eq!(period.length_in_sec(), 86400); + assert_eq!(period.interval_num(), 1); + + let dsrv = disaster_srv(period); + assert_eq!( + dsrv.as_ref(), + &hex!("F8A4948707653837FA44ABB5BBC75A12F6F101E7F8FAF699B9715F4965D3507D") + ); + assert_eq!( + &dsrv.as_ref()[..], + &Sha3_256::digest(b"shared-random-disaster\0\0\0\0\0\0\x05\xA0\0\0\0\0\0\0\0\x01")[..] + ); + } + + #[test] + fn ring_params_simple() { + // Compute ring parameters in a legacy environment, where the time + // period and the SRV lifetime are one day long, and they are offset by + // 12 hours. + let consensus = example_consensus_builder().testing_consensus().unwrap(); + let netparams = NetParameters::from_map(consensus.params()); + let (cur, secondary) = compute_ring_parameters(&consensus, &netparams).unwrap(); + + assert_eq!( + cur.time_period, + TimePeriod::new(d("1 day"), t("1985-10-25T07:00:00Z"), d("12 hours")).unwrap() + ); + // We use the "previous" SRV since the start of this time period was 12:00 on the 24th. + assert_eq!(cur.shared_rand.as_ref(), &SRV1); + + // Our secondary SRV will be the one that starts when we move into the + // next time period. + assert_eq!(secondary.len(), 1); + assert_eq!( + secondary[0].time_period, + TimePeriod::new(d("1 day"), t("1985-10-25T12:00:00Z"), d("12 hours")).unwrap(), + ); + assert_eq!(secondary[0].shared_rand.as_ref(), &SRV2); + } + + #[test] + fn ring_params_tricky() { + // In this case we give the SRVs timestamps and we choose an odd hsdir_interval. + let consensus = example_consensus_builder() + .shared_rand_prev(7, SRV1.into(), Some(t("1985-10-25T00:00:00Z"))) + .shared_rand_cur(7, SRV2.into(), Some(t("1985-10-25T05:00:00Z"))) + .param("hsdir_interval", 120) // 2 hours + .testing_consensus() + .unwrap(); + let netparams = NetParameters::from_map(consensus.params()); + let (cur, secondary) = compute_ring_parameters(&consensus, &netparams).unwrap(); + + assert_eq!( + cur.time_period, + TimePeriod::new(d("2 hours"), t("1985-10-25T07:00:00Z"), d("12 hours")).unwrap() + ); + assert_eq!(cur.shared_rand.as_ref(), &SRV2); + + assert_eq!(secondary.len(), 2); + assert_eq!( + secondary[0].time_period, + TimePeriod::new(d("2 hours"), t("1985-10-25T05:00:00Z"), d("12 hours")).unwrap() + ); + assert_eq!(secondary[0].shared_rand.as_ref(), &SRV1); + assert_eq!( + secondary[1].time_period, + TimePeriod::new(d("2 hours"), t("1985-10-25T09:00:00Z"), d("12 hours")).unwrap() + ); + assert_eq!(secondary[1].shared_rand.as_ref(), &SRV2); + } +} diff --git a/crates/tor-netdir/src/hsdir_ring.rs b/crates/tor-netdir/src/hsdir_ring.rs index f7143b302..be6126571 100644 --- a/crates/tor-netdir/src/hsdir_ring.rs +++ b/crates/tor-netdir/src/hsdir_ring.rs @@ -19,6 +19,8 @@ use tor_hscrypto::{pk::BlindedOnionId, time::TimePeriod}; use tor_llcrypto::pk::ed25519::Ed25519Identity; use tor_netdoc::doc::netstatus::SharedRandVal; +use crate::hsdir_params::HsRingParams; + /// A sort key determining a position in the onion service directory ring. /// /// This is either the sort key of a given relay at a given time period, or the @@ -98,6 +100,15 @@ pub(crate) fn service_index( } impl HsDirRing { + /// Return a new empty HsDirRing from a given set of parameters. + pub(crate) fn empty_from_params(params: &HsRingParams) -> Self { + Self { + period: params.time_period, + shared_rand: params.shared_rand, + ring: Vec::new(), + } + } + /// Find the location or (notional) insertion point for `idx` within `ring`. fn find_pos(&self, idx: HsDirIndex) -> usize { // TODO hs implement this @@ -113,4 +124,9 @@ impl HsDirRing { let idx = self.find_pos(idx); self.ring[idx..].iter().chain(&self.ring[..idx]) } + + /// Return the time period for which this ring applies. + pub(crate) fn time_period(&self) -> TimePeriod { + self.period + } } diff --git a/crates/tor-netdir/src/lib.rs b/crates/tor-netdir/src/lib.rs index 53f6b7277..d0cdc5bd1 100644 --- a/crates/tor-netdir/src/lib.rs +++ b/crates/tor-netdir/src/lib.rs @@ -40,6 +40,8 @@ mod err; #[cfg(feature = "onion-common")] +mod hsdir_params; +#[cfg(feature = "onion-common")] mod hsdir_ring; pub mod params; mod weight; @@ -292,10 +294,10 @@ pub struct NetDir { /// This is the hash ring that we should use whenever we are fetching an /// onion service descriptor. // - // TODO hs: It is ugly to have this be Option. + // TODO hs: It is ugly to have this exist in a partially constructed state + // in a PartialNetDir. #[cfg(feature = "onion-common")] - #[allow(dead_code)] - hsdir_ring: Option, + hsdir_ring: HsDirRing, /// A hash ring describing the onion service directory based on the /// parameters for the previous and next time periods. @@ -304,29 +306,23 @@ pub struct NetDir { /// far into the current time period this directory is, so that /// not-synchronized clients can still find their descriptor. /// - /// Each of these rings is None in a PartialNetDir, and None if this ring - /// should not be used. - /// /// Note that with the current (2023) network parameters, with /// `hsdir_interval = SRV lifetime = 24 hours` at most one of these /// secondary rings will be active at a time. We have two here in order /// to conform with a more flexible regime in proposal 342. // - // TODO hs: It is sort of ugly to have these be Option. + // TODO hs: It is sort of ugly to have these be partially constructed in a + // PartialNetDir. // // TODO hs: hs clients never need this; so I've made it not-present for thm. // But does that risk too much with respect to side channels? // - // TODO hs: Perhaps we should refactor this so that there is just one - // Vec (or SmallVec<>); or so that there are `current`, `next`, - // and `previous`. - // // TODO hs: Perhaps we should refactor this so that it is clear that these // are immutable? On the other hand, the documentation for this type // declares that it is immutable, so we are likely okay. #[cfg(feature = "onion-service")] #[allow(dead_code)] - hsdir_secondary_rings: (Option, Option), + hsdir_secondary_rings: Vec, /// Weight values to apply to a given relay when deciding how frequently /// to choose it for a given role. @@ -556,6 +552,28 @@ impl PartialNetDir { .map(|(rs_idx, rs)| (*rs.rsa_identity(), rs_idx)) .collect(); + #[cfg(feature = "onion-service")] + let hsdir_secondary_rings; + #[cfg(feature = "onion-common")] + let hsdir_ring = { + let (cur_hsparams, secondary_hsparams) = + hsdir_params::compute_ring_parameters(&consensus, ¶ms) + .expect("Invalid consensus!"); + // TODO HS: I dislike using expect above, but this function does not + // return a Result. Perhaps we should change it so that it can? Or as an alternative + // we could let this object exist in a state without any HsDir rings. + + #[cfg(feature = "onion-service")] + { + hsdir_secondary_rings = secondary_hsparams + .iter() + .map(HsDirRing::empty_from_params) + .collect(); + } + + HsDirRing::empty_from_params(&cur_hsparams) + }; + let netdir = NetDir { consensus: Arc::new(consensus), params, @@ -564,9 +582,9 @@ impl PartialNetDir { rs_idx_by_rsa: Arc::new(rs_idx_by_rsa), rs_idx_by_ed: HashMap::with_capacity(n_relays), #[cfg(feature = "onion-common")] - hsdir_ring: None, + hsdir_ring, #[cfg(feature = "onion-service")] - hsdir_secondary_rings: (None, None), + hsdir_secondary_rings, weights, }; @@ -1104,9 +1122,8 @@ impl NetDir { /// of the validity period of this `NetDir`'s consensus. That time period /// is the one we use when acting as an onion service client. #[cfg(feature = "onion-common")] - #[allow(unused, clippy::missing_panics_doc)] // TODO hs: remove. pub fn onion_service_time_period(&self) -> TimePeriod { - todo!() // TODO hs + self.hsdir_ring.time_period() } /// Return the secondary onion service directory "time periods". @@ -1114,9 +1131,11 @@ impl NetDir { /// These are additional time periods that we publish descriptors for when we are /// acting as an onion service. #[cfg(feature = "onion-service")] - #[allow(unused, clippy::missing_panics_doc)] // TODO hs: remove. pub fn onion_service_secondary_time_periods(&self) -> Vec { - todo!() + self.hsdir_secondary_rings + .iter() + .map(HsDirRing::time_period) + .collect() } /// Return the relays in this network directory that will be used to store a