Skip to main content

tor_netdir/
hsdir_params.rs

1//! Compute which time period and shared random value from a consensus to use at
2//! any given time.
3//!
4//! This is, unfortunately, a bit complex.  It works as follows:
5//!
6//!   * The _current_ time period is the one that contains the valid-after time
7//!     for the consensus...
8//!      * but to compute the time period interval, you need to look at the
9//!        consensus parameters,
10//!      * and to compute the time period offset, you need to know the consensus
11//!        voting interval.
12//!
13//!   * The SRV for any given time period is the one that that was the most
14//!     recent at the _start_ of the time period...
15//!      * but to know when an SRV was most recent, you need to read a timestamp
16//!        from it that won't be there until proposal 342 is implemented...
17//!      * and until then, you have to compute the start of the UTC day when the
18//!        consensus became valid.
19//!
20//! This module could conceivably be part of `tor-netdoc`, but it seems better
21//! to make it part of `tor-netdir`: this is where we put our complexity.
22///
23/// (Here in Arti we use the word "ring" in types and variable names only
24/// to refer to the actual actual reified ring, not to HSDir parameters, or
25/// or other aspects of the HSDir ring structure.)
26use std::time::{Duration, SystemTime};
27
28use crate::{Error, HsDirs, Result, params::NetParameters};
29use tor_hscrypto::time::TimePeriod;
30use tor_netdoc::doc::netstatus::{MdConsensus, SharedRandVal};
31
32#[cfg(feature = "hs-service")]
33use tor_hscrypto::ope::SrvPeriodOffset;
34
35/// Parameters for generating and using an HsDir ring.
36///
37/// These parameters are derived from the shared random values and time
38/// parameters in the consensus, and are used to determine the
39/// position of each HsDir within the ring.
40#[derive(Clone, Debug, Eq, PartialEq)]
41pub struct HsDirParams {
42    /// The time period for this ring.  It's used to ensure that blinded onion
43    /// keys rotate in a _predictable_ way over time.
44    pub(crate) time_period: TimePeriod,
45    /// The SharedRandVal for this ring.  It's used to ensure that the position
46    /// of each HsDir within the ring rotates _unpredictably_ over time.
47    pub(crate) shared_rand: SharedRandVal,
48    /// The range of times over which the srv is most current.
49    pub(crate) srv_lifespan: std::ops::Range<SystemTime>,
50}
51
52/// By how many voting periods do we offset the beginning of our first time
53/// period from the epoch?
54///
55/// We do this so that each of our time periods begins at a time when the SRV is
56/// not rotating.
57const VOTING_PERIODS_IN_OFFSET: u32 = 12;
58
59/// How many voting periods make up an entire round of the shared random value
60/// commit-and-reveal protocol?
61///
62/// We use this to compute an SRV lifetime if one of the SRV values is missing.
63const VOTING_PERIODS_IN_SRV_ROUND: u32 = 24;
64
65impl HsDirParams {
66    /// Return the time period for which these parameters are valid.
67    ///
68    /// The `hs_blind_id` for an onion service changes every time period: when
69    /// uploading, callers should use this time period to determine which
70    /// `hs_blind_id`'s descriptor should be sent to which directory.
71    pub fn time_period(&self) -> TimePeriod {
72        self.time_period
73    }
74
75    /// Return the starting time for the shared-random-value protocol that
76    /// produced the SRV for this time period.
77    pub fn start_of_shard_rand_period(&self) -> SystemTime {
78        self.srv_lifespan.start
79    }
80
81    /// Return an opaque offset for `when` from the start of the shared-random-value protocol
82    /// period corresponding to the SRV for this time period.
83    ///
84    /// When uploading, callers should this offset to determine
85    /// the revision counter for their descriptors.
86    ///
87    /// Returns `None` if when is after the start of the SRV period.
88    #[cfg(feature = "hs-service")]
89    pub fn offset_within_srv_period(&self, when: SystemTime) -> Option<SrvPeriodOffset> {
90        if when >= self.srv_lifespan.start {
91            let d = when
92                .duration_since(self.srv_lifespan.start)
93                .expect("Somehow, range comparison was not reliable!");
94            return Some(SrvPeriodOffset::from(d.as_secs() as u32));
95        }
96
97        None
98    }
99
100    /// Compute the `HsDirParams` for the current time period, according to a given
101    /// consensus.
102    ///
103    /// rend-spec-v3 section 2.2.1 et seq
104    ///
105    /// Return the ring parameters for the current period (which clients use when
106    /// fetching onion service descriptors), along with a Vec of ring
107    /// parameters for any secondary periods that onion services should additionally
108    /// use when publishing their descriptors.
109    ///
110    /// Note that "current" here is always relative to a given consensus, not the
111    /// current wall-clock time.
112    ///
113    /// (This function's return type is a bit cumbersome; these parameters are
114    /// bundled together because it is efficient to compute them all at once.)
115    ///
116    /// Note that this function will only return an error if something is
117    /// _extremely_ wrong with the provided consensus: for other error cases, it
118    /// returns a "disaster fallback".
119    pub(crate) fn compute(
120        consensus: &MdConsensus,
121        params: &NetParameters,
122    ) -> Result<HsDirs<HsDirParams>> {
123        let srvs = extract_srvs(consensus)?;
124        let tp_length: Duration = params.hsdir_timeperiod_length.try_into().map_err(|_| {
125            // Note that this error should be impossible:
126            // The type of hsdir_timeperiod_length() is IntegerMinutes<BoundedInt32<30, 14400>>...
127            // It should be at most 10 days, which _definitely_ fits into a Duration.
128            Error::InvalidConsensus(
129                "Minutes in hsdir timeperiod could not be converted to a Duration",
130            )
131        })?;
132        let offset = consensus.lifetime().voting_period() * VOTING_PERIODS_IN_OFFSET;
133        let cur_period = TimePeriod::new(tp_length, consensus.lifetime().valid_after(), offset)
134            .map_err(|_| {
135                // This error should be nearly impossible too:
136                // - It can occur if the time period length is not an integer
137                //   number of minutes--but we took it from an IntegerMinutes,
138                //   so that's unlikely.
139                // - It can occur if the time period length or the offset is
140                //   greater than can be represented in u32 seconds.
141                // - It can occur if the valid_after time is so far from the
142                //   epoch that we can't represent the distance as a Duration.
143                Error::InvalidConsensus("Consensus valid-after did not fall in a time period")
144            })?;
145
146        let current = find_params_for_time(&srvs[..], cur_period)?.unwrap_or_else(|| {
147            tracing::debug!("No SRV params for {cur_period:?}; falling back to disaster params");
148            disaster_params(cur_period)
149        });
150
151        // When computing secondary rings, we don't try so many fallback operations:
152        // if they aren't available, they aren't available.
153        #[cfg(feature = "hs-service")]
154        let secondary = [cur_period.prev(), cur_period.next()]
155            .iter()
156            .flatten()
157            .flat_map(|period| find_params_for_time(&srvs[..], *period).ok().flatten())
158            .collect();
159
160        Ok(HsDirs {
161            current,
162            #[cfg(feature = "hs-service")]
163            secondary,
164        })
165    }
166}
167
168/// Compute ring parameters using a Disaster SRV for this period.
169fn disaster_params(period: TimePeriod) -> HsDirParams {
170    HsDirParams {
171        time_period: period,
172        shared_rand: disaster_srv(period),
173        srv_lifespan: period
174            .range()
175            .expect("Time period cannot be represented as SystemTime"),
176    }
177}
178
179/// Compute the "Disaster SRV" for a given time period.
180///
181/// This SRV is used if the authorities do not list any shared random value for
182/// that time period, but we need to compute an HsDir ring for it anyway.
183fn disaster_srv(period: TimePeriod) -> SharedRandVal {
184    use digest::Digest;
185    let mut d = tor_llcrypto::d::Sha3_256::new();
186    d.update(b"shared-random-disaster");
187    d.update(u64::from(period.length().as_minutes()).to_be_bytes());
188    d.update(period.interval_num().to_be_bytes());
189
190    let v: [u8; 32] = d.finalize().into();
191    v.into()
192}
193
194/// Helper type: A `SharedRandVal`, and the time range over which it is the most
195/// recent.
196type SrvInfo = (SharedRandVal, std::ops::Range<SystemTime>);
197
198/// Given a list of SrvInfo, return an HsRingParams instance for a given time
199/// period, if possible.
200fn find_params_for_time(info: &[SrvInfo], period: TimePeriod) -> Result<Option<HsDirParams>> {
201    let start = period
202        .range()
203        .map_err(|_| {
204            Error::InvalidConsensus(
205                "HsDir time period in consensus could not be represented as a SystemTime range.",
206            )
207        })?
208        .start;
209
210    Ok(find_srv_for_time(info, start).map(|srv| HsDirParams {
211        time_period: period,
212        shared_rand: srv.0,
213        srv_lifespan: srv.1.clone(),
214    }))
215}
216
217/// Given a list of SrvInfo, return the SrvInfo (if any) that is the most
218/// recent SRV at `when`.
219fn find_srv_for_time(info: &[SrvInfo], when: SystemTime) -> Option<&SrvInfo> {
220    info.iter().find(|(_, range)| range.contains(&when))
221}
222
223/// Return every SRV from a consensus, along with a duration over which it is
224/// most recent SRV.
225fn extract_srvs(consensus: &MdConsensus) -> Result<Vec<SrvInfo>> {
226    let mut v = Vec::new();
227    let srv_interval = srv_interval(consensus);
228
229    if let Some(cur) = consensus.shared_rand_cur() {
230        let ts_begin = cur
231            .timestamp()
232            .map(Ok)
233            .unwrap_or_else(|| start_of_sr_round(consensus))?;
234        let ts_end = ts_begin + srv_interval;
235        v.push((*cur.value(), ts_begin..ts_end));
236    }
237    if let Some(prev) = consensus.shared_rand_prev() {
238        let ts_begin = prev
239            .timestamp()
240            .map(Ok)
241            .unwrap_or_else(|| start_of_sr_round(consensus).map(|t| t - srv_interval))?;
242        let ts_end = ts_begin + srv_interval;
243        v.push((*prev.value(), ts_begin..ts_end));
244    }
245
246    Ok(v)
247}
248
249/// Return the length of time for which a single SRV value is valid.
250fn srv_interval(consensus: &MdConsensus) -> Duration {
251    // What we _want_ to do, ideally, is is to learn the duration from the
252    // difference between the declared time for the previous value and the
253    // declared time for the current one.
254    //
255    // (This assumes that proposal 342 is implemented.)
256    if let (Some(cur), Some(prev)) = (consensus.shared_rand_cur(), consensus.shared_rand_prev()) {
257        if let (Some(cur_ts), Some(prev_ts)) = (cur.timestamp(), prev.timestamp()) {
258            if let Ok(d) = cur_ts.duration_since(prev_ts) {
259                return d;
260            }
261        }
262    }
263
264    // But if one of those values is missing, or if it has no timestamp, we have
265    // to fall back to admitting that we know the schedule for the voting
266    // algorithm.
267    consensus.lifetime().voting_period() * VOTING_PERIODS_IN_SRV_ROUND
268}
269
270/// Return the start time of the current SR protocol run
271/// of the specified `consensus`.
272///
273/// If a full SR protocol run is 24 hours, this function returns
274/// the start of the UTC day containing the `valid-after` timestamp
275/// of the consensus.
276///
277/// Corresponds to C Tor's `sr_state_get_start_time_of_current_protocol_run()`.
278fn start_of_sr_round(consensus: &MdConsensus) -> Result<SystemTime> {
279    let t = consensus.lifetime().valid_after();
280    let beginning_of_curr_round = t
281        .duration_since(SystemTime::UNIX_EPOCH)
282        .map_err(|_| Error::InvalidConsensus("consensus valid-after is before Unix epoch?!"))?
283        .as_secs();
284    let voting_interval = consensus.lifetime().voting_period().as_secs();
285
286    // The voting_interval is always going to be greater than 0,
287    // because the Lifetime's constructor enforces that
288    // valid_after < fresh_until < valid_until.
289    debug_assert!(voting_interval > 0);
290
291    let curr_round_slot =
292        (beginning_of_curr_round / voting_interval) % u64::from(VOTING_PERIODS_IN_SRV_ROUND);
293    let time_elapsed_since_start_of_run = curr_round_slot * voting_interval;
294
295    let offset = Duration::from_secs(beginning_of_curr_round - time_elapsed_since_start_of_run);
296    Ok(SystemTime::UNIX_EPOCH + offset)
297}
298
299#[cfg(test)]
300mod test {
301    // @@ begin test lint list maintained by maint/add_warning @@
302    #![allow(clippy::bool_assert_comparison)]
303    #![allow(clippy::clone_on_copy)]
304    #![allow(clippy::dbg_macro)]
305    #![allow(clippy::mixed_attributes_style)]
306    #![allow(clippy::print_stderr)]
307    #![allow(clippy::print_stdout)]
308    #![allow(clippy::single_char_pattern)]
309    #![allow(clippy::unwrap_used)]
310    #![allow(clippy::unchecked_time_subtraction)]
311    #![allow(clippy::useless_vec)]
312    #![allow(clippy::needless_pass_by_value)]
313    #![allow(clippy::string_slice)] // See arti#2571
314    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
315    use super::*;
316    use hex_literal::hex;
317    use tor_netdoc::doc::netstatus::{Lifetime, MdConsensusBuilder};
318
319    /// Helper: parse an rfc3339 time.
320    ///
321    /// # Panics
322    ///
323    /// Panics if the time is invalid.
324    fn t(s: &str) -> SystemTime {
325        humantime::parse_rfc3339(s).unwrap()
326    }
327    /// Helper: parse a duration.
328    ///
329    /// # Panics
330    ///
331    /// Panics if the time is invalid.
332    fn d(s: &str) -> Duration {
333        humantime::parse_duration(s).unwrap()
334    }
335
336    fn example_lifetime() -> Lifetime {
337        Lifetime::new(
338            t("1985-10-25T07:00:00Z"),
339            t("1985-10-25T08:00:00Z"),
340            t("1985-10-25T10:00:00Z"),
341        )
342        .unwrap()
343    }
344
345    const SRV1: [u8; 32] = *b"next saturday night were sending";
346    const SRV2: [u8; 32] = *b"you......... back to the future!";
347
348    fn example_consensus_builder() -> MdConsensusBuilder {
349        let mut bld = MdConsensus::builder();
350
351        bld.consensus_method(34)
352            .lifetime(example_lifetime())
353            .param("bwweightscale", 1)
354            .param("hsdir_interval", 1440)
355            .weights("".parse().unwrap())
356            .shared_rand_prev(7, SRV1.into(), None)
357            .shared_rand_cur(7, SRV2.into(), None);
358
359        bld
360    }
361
362    #[test]
363    fn invalid_lifetime() {
364        let lifetimes = [
365            // (valid_after, fresh_until, valid_until)
366            //
367            // Invalid because valid_after >= fresh_until
368            (
369                "2015-04-20T00:00:00Z",
370                "2015-04-20T00:00:00Z",
371                "2015-04-22T00:00:00Z",
372            ),
373            (
374                "2015-04-21T00:00:00Z",
375                "2015-04-20T00:00:00Z",
376                "2015-04-22T00:00:00Z",
377            ),
378            // Invalid because fresh_until >= valid_until
379            (
380                "2015-04-20T00:00:00Z",
381                "2015-04-22T00:00:00Z",
382                "2015-04-22T00:00:00Z",
383            ),
384            (
385                "2015-04-20T00:00:00Z",
386                "2015-04-23T00:00:00Z",
387                "2015-04-22T00:00:00Z",
388            ),
389        ];
390
391        for (valid_after, fresh_until, valid_until) in lifetimes {
392            let err = Lifetime::new(t(valid_after), t(fresh_until), t(valid_until))
393                .unwrap_err()
394                .netdoc_error_kind();
395
396            assert_eq!(err, tor_netdoc::NetdocErrorKind::InvalidLifetime);
397        }
398    }
399
400    #[test]
401    fn vote_period() {
402        assert_eq!(example_lifetime().voting_period(), d("1 hour"));
403
404        let lt2 = Lifetime::new(
405            t("1985-10-25T07:00:00Z"),
406            t("1985-10-25T07:22:00Z"),
407            t("1985-10-25T07:59:00Z"),
408        )
409        .unwrap();
410
411        assert_eq!(lt2.voting_period(), d("22 min"));
412    }
413
414    #[test]
415    fn srv_period() {
416        // In a basic consensus with no SRV timestamps, we'll assume 24 voting periods.
417        let consensus = example_consensus_builder().testing_consensus().unwrap();
418        assert_eq!(srv_interval(&consensus), d("1 day"));
419
420        // If there are timestamps, we look at the difference between them.
421        let consensus = example_consensus_builder()
422            .shared_rand_prev(7, SRV1.into(), Some(t("1985-10-25T00:00:00Z")))
423            .shared_rand_cur(7, SRV2.into(), Some(t("1985-10-25T06:00:05Z")))
424            .testing_consensus()
425            .unwrap();
426        assert_eq!(srv_interval(&consensus), d("6 hours 5 sec"));
427
428        // Note that if the timestamps are in reversed order, we fall back to 24 hours.
429        let consensus = example_consensus_builder()
430            .shared_rand_cur(7, SRV1.into(), Some(t("1985-10-25T00:00:00Z")))
431            .shared_rand_prev(7, SRV2.into(), Some(t("1985-10-25T06:00:05Z")))
432            .testing_consensus()
433            .unwrap();
434        assert_eq!(srv_interval(&consensus), d("1 day"));
435    }
436
437    #[test]
438    fn srvs_extract_and_find() {
439        let consensus = example_consensus_builder().testing_consensus().unwrap();
440        let srvs = extract_srvs(&consensus).unwrap();
441        assert_eq!(
442            srvs,
443            vec![
444                // Since no timestamps are given in the example, the current srv
445                // is valid from midnight to midnight...
446                (
447                    SRV2.into(),
448                    t("1985-10-25T00:00:00Z")..t("1985-10-26T00:00:00Z")
449                ),
450                // ...and the previous SRV is valid midnight-to-midnight on the
451                // previous day.
452                (
453                    SRV1.into(),
454                    t("1985-10-24T00:00:00Z")..t("1985-10-25T00:00:00Z")
455                )
456            ]
457        );
458
459        // Now try with explicit timestamps on the SRVs.
460        let consensus = example_consensus_builder()
461            .shared_rand_prev(7, SRV1.into(), Some(t("1985-10-25T00:00:00Z")))
462            .shared_rand_cur(7, SRV2.into(), Some(t("1985-10-25T06:00:05Z")))
463            .testing_consensus()
464            .unwrap();
465        let srvs = extract_srvs(&consensus).unwrap();
466        assert_eq!(
467            srvs,
468            vec![
469                (
470                    SRV2.into(),
471                    t("1985-10-25T06:00:05Z")..t("1985-10-25T12:00:10Z")
472                ),
473                (
474                    SRV1.into(),
475                    t("1985-10-25T00:00:00Z")..t("1985-10-25T06:00:05Z")
476                )
477            ]
478        );
479
480        // See if we can look up SRVs in that period.
481        assert_eq!(None, find_srv_for_time(&srvs, t("1985-10-24T23:59:00Z")));
482        assert_eq!(
483            Some(&srvs[1]),
484            find_srv_for_time(&srvs, t("1985-10-25T00:00:00Z"))
485        );
486        assert_eq!(
487            Some(&srvs[1]),
488            find_srv_for_time(&srvs, t("1985-10-25T03:59:00Z"))
489        );
490        assert_eq!(
491            Some(&srvs[1]),
492            find_srv_for_time(&srvs, t("1985-10-25T00:00:00Z"))
493        );
494        assert_eq!(
495            Some(&srvs[0]),
496            find_srv_for_time(&srvs, t("1985-10-25T06:00:05Z"))
497        );
498        assert_eq!(
499            Some(&srvs[0]),
500            find_srv_for_time(&srvs, t("1985-10-25T12:00:00Z"))
501        );
502        assert_eq!(None, find_srv_for_time(&srvs, t("1985-10-25T12:00:30Z")));
503    }
504
505    #[test]
506    fn disaster() {
507        use digest::Digest;
508        use tor_llcrypto::d::Sha3_256;
509        let period = TimePeriod::new(d("1 day"), t("1970-01-02T17:33:00Z"), d("12 hours")).unwrap();
510        assert_eq!(period.length().as_minutes(), 86400 / 60);
511        assert_eq!(period.interval_num(), 1);
512
513        let dsrv = disaster_srv(period);
514        assert_eq!(
515            dsrv.as_ref(),
516            &hex!("F8A4948707653837FA44ABB5BBC75A12F6F101E7F8FAF699B9715F4965D3507D")
517        );
518        assert_eq!(
519            &dsrv.as_ref()[..],
520            &Sha3_256::digest(b"shared-random-disaster\0\0\0\0\0\0\x05\xA0\0\0\0\0\0\0\0\x01")[..]
521        );
522    }
523
524    #[test]
525    #[cfg(feature = "hs-service")]
526    fn ring_params_simple() {
527        // Compute ring parameters in a legacy environment, where the time
528        // period and the SRV lifetime are one day long, and they are offset by
529        // 12 hours.
530        let consensus = example_consensus_builder().testing_consensus().unwrap();
531        let netparams = NetParameters::from_map(consensus.params());
532        let HsDirs { current, secondary } = HsDirParams::compute(&consensus, &netparams).unwrap();
533
534        assert_eq!(
535            current.time_period,
536            TimePeriod::new(d("1 day"), t("1985-10-25T07:00:00Z"), d("12 hours")).unwrap()
537        );
538        // We use the "previous" SRV since the start of this time period was 12:00 on the 24th.
539        assert_eq!(current.shared_rand.as_ref(), &SRV1);
540
541        // Our secondary SRV will be the one that starts when we move into the
542        // next time period.
543        assert_eq!(secondary.len(), 1);
544        assert_eq!(
545            secondary[0].time_period,
546            TimePeriod::new(d("1 day"), t("1985-10-25T12:00:00Z"), d("12 hours")).unwrap(),
547        );
548        assert_eq!(secondary[0].shared_rand.as_ref(), &SRV2);
549    }
550
551    #[test]
552    #[cfg(feature = "hs-service")]
553    fn ring_params_tricky() {
554        // In this case we give the SRVs timestamps and we choose an odd hsdir_interval.
555        let consensus = example_consensus_builder()
556            .shared_rand_prev(7, SRV1.into(), Some(t("1985-10-25T00:00:00Z")))
557            .shared_rand_cur(7, SRV2.into(), Some(t("1985-10-25T05:00:00Z")))
558            .param("hsdir_interval", 120) // 2 hours
559            .testing_consensus()
560            .unwrap();
561        let netparams = NetParameters::from_map(consensus.params());
562        let HsDirs { current, secondary } = HsDirParams::compute(&consensus, &netparams).unwrap();
563
564        assert_eq!(
565            current.time_period,
566            TimePeriod::new(d("2 hours"), t("1985-10-25T07:00:00Z"), d("12 hours")).unwrap()
567        );
568        assert_eq!(current.shared_rand.as_ref(), &SRV2);
569
570        assert_eq!(secondary.len(), 2);
571        assert_eq!(
572            secondary[0].time_period,
573            TimePeriod::new(d("2 hours"), t("1985-10-25T05:00:00Z"), d("12 hours")).unwrap()
574        );
575        assert_eq!(secondary[0].shared_rand.as_ref(), &SRV1);
576        assert_eq!(
577            secondary[1].time_period,
578            TimePeriod::new(d("2 hours"), t("1985-10-25T09:00:00Z"), d("12 hours")).unwrap()
579        );
580        assert_eq!(secondary[1].shared_rand.as_ref(), &SRV2);
581    }
582
583    #[test]
584    #[cfg(feature = "hs-service")]
585    fn offset_within_srv_period() {
586        // This test doesn't actually use the time_period or shared_rand values, so their value is
587        // arbitrary.
588        let time_period =
589            TimePeriod::new(d("2 hours"), t("1985-10-25T05:00:00Z"), d("12 hours")).unwrap();
590
591        let srv_start = t("1985-10-25T09:00:00Z");
592        let srv_end = t("1985-10-25T20:00:00Z");
593        let srv_lifespan = srv_start..srv_end;
594
595        let params = HsDirParams {
596            time_period,
597            shared_rand: SRV1.into(),
598            srv_lifespan,
599        };
600
601        let before_srv_period = t("1985-10-25T08:59:00Z");
602        let after_srv_period = t("1985-10-26T10:19:00Z");
603        assert!(params.offset_within_srv_period(before_srv_period).is_none());
604        assert_eq!(
605            params.offset_within_srv_period(srv_start).unwrap(),
606            SrvPeriodOffset::from(0)
607        );
608        // The period is 11h long
609        assert_eq!(
610            params.offset_within_srv_period(srv_end).unwrap(),
611            SrvPeriodOffset::from(11 * 60 * 60)
612        );
613        // This timestamp is 1 day 1h 19m from the start of the SRV period
614        assert_eq!(
615            params.offset_within_srv_period(after_srv_period).unwrap(),
616            SrvPeriodOffset::from((25 * 60 + 19) * 60)
617        );
618    }
619
620    #[test]
621    fn start_of_sr_protocol_run() {
622        let test_cases = [
623            // (valid_after, fresh_until, expected_sr_start, expected_srv_interval)
624            //
625            // Voting interval = 1h
626            // The start of the current SR run is always the start
627            // of the day of valid-after of the consensus.
628            // An SRV protocol run takes 1h * 24 rounds = 24h
629            (
630                "2015-04-20T00:00:00Z",
631                "2015-04-20T01:00:00Z",
632                "2015-04-20T00:00:00Z",
633                "24 hours",
634            ),
635            (
636                "2015-04-20T22:00:00Z",
637                "2015-04-20T23:00:00Z",
638                "2015-04-20T00:00:00Z",
639                "24 hours",
640            ),
641            (
642                "2015-04-19T23:00:00Z",
643                "2015-04-20T00:00:00Z",
644                "2015-04-19T00:00:00Z",
645                "24 hours",
646            ),
647            // Voting interval = 10s
648            // An SRV protocol run takes 10s * 24 rounds = 4 mins
649            (
650                "2015-04-20T00:15:30Z",
651                "2015-04-20T00:15:40Z",
652                "2015-04-20T00:12:00Z",
653                "4 minutes",
654            ),
655            // Voting interval = 20s
656            // An SRV protocol run takes 20s * 24 rounds = 8 mins
657            (
658                "2015-04-20T00:15:00Z",
659                "2015-04-20T00:15:20Z",
660                "2015-04-20T00:08:00Z",
661                "8 minutes",
662            ),
663            (
664                "2015-04-20T00:15:30Z",
665                "2015-04-20T00:15:50Z",
666                "2015-04-20T00:08:10Z",
667                "8 minutes",
668            ),
669        ];
670
671        for (valid_after, fresh_until, expected_start, expected_interval) in test_cases {
672            let lifetime = Lifetime::new(
673                t(valid_after),
674                t(fresh_until),
675                // The valid-until doesn't matter for the purposes of this test
676                t("2020-10-25T10:00:00Z"),
677            )
678            .unwrap();
679
680            let consensus = example_consensus_builder()
681                .lifetime(lifetime)
682                .testing_consensus()
683                .unwrap();
684            let sr_start = start_of_sr_round(&consensus).unwrap();
685
686            assert_eq!(
687                t(expected_start),
688                sr_start,
689                "{expected_start} != {} (valid-after = {valid_after}, fresh-until = {fresh_until})",
690                humantime::format_rfc3339(sr_start)
691            );
692
693            assert_eq!(d(expected_interval), srv_interval(&consensus));
694        }
695    }
696}