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.)
26
use std::time::{Duration, SystemTime};
27

            
28
use crate::{Error, HsDirs, Result, params::NetParameters};
29
use tor_hscrypto::time::TimePeriod;
30
use tor_netdoc::doc::netstatus::{MdConsensus, SharedRandVal};
31

            
32
#[cfg(feature = "hs-service")]
33
use 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)]
41
pub 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.
57
const 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.
63
const VOTING_PERIODS_IN_SRV_ROUND: u32 = 24;
64

            
65
impl 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
8850
    pub fn time_period(&self) -> TimePeriod {
72
8850
        self.time_period
73
8850
    }
74

            
75
    /// Return the starting time for the shared-random-value protocol that
76
    /// produced the SRV for this time period.
77
3600
    pub fn start_of_shard_rand_period(&self) -> SystemTime {
78
3600
        self.srv_lifespan.start
79
3600
    }
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
3608
    pub fn offset_within_srv_period(&self, when: SystemTime) -> Option<SrvPeriodOffset> {
90
3608
        if when >= self.srv_lifespan.start {
91
3606
            let d = when
92
3606
                .duration_since(self.srv_lifespan.start)
93
3606
                .expect("Somehow, range comparison was not reliable!");
94
3606
            return Some(SrvPeriodOffset::from(d.as_secs() as u32));
95
2
        }
96

            
97
2
        None
98
3608
    }
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
20770
    pub(crate) fn compute(
120
20770
        consensus: &MdConsensus,
121
20770
        params: &NetParameters,
122
20770
    ) -> Result<HsDirs<HsDirParams>> {
123
20770
        let srvs = extract_srvs(consensus)?;
124
20770
        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
20770
        let offset = consensus.lifetime().voting_period() * VOTING_PERIODS_IN_OFFSET;
133
20770
        let cur_period = TimePeriod::new(tp_length, consensus.lifetime().valid_after(), offset)
134
20770
            .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
21209
        let current = find_params_for_time(&srvs[..], cur_period)?.unwrap_or_else(|| {
147
20366
            tracing::debug!("No SRV params for {cur_period:?}; falling back to disaster params");
148
20366
            disaster_params(cur_period)
149
20366
        });
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
20770
        let secondary = [cur_period.prev(), cur_period.next()]
155
20770
            .iter()
156
20770
            .flatten()
157
41989
            .flat_map(|period| find_params_for_time(&srvs[..], *period).ok().flatten())
158
20770
            .collect();
159

            
160
20770
        Ok(HsDirs {
161
20770
            current,
162
20770
            #[cfg(feature = "hs-service")]
163
20770
            secondary,
164
20770
        })
165
20770
    }
166
}
167

            
168
/// Compute ring parameters using a Disaster SRV for this period.
169
20366
fn disaster_params(period: TimePeriod) -> HsDirParams {
170
20366
    HsDirParams {
171
20366
        time_period: period,
172
20366
        shared_rand: disaster_srv(period),
173
20366
        srv_lifespan: period
174
20366
            .range()
175
20366
            .expect("Time period cannot be represented as SystemTime"),
176
20366
    }
177
20366
}
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.
183
20368
fn disaster_srv(period: TimePeriod) -> SharedRandVal {
184
    use digest::Digest;
185
20368
    let mut d = tor_llcrypto::d::Sha3_256::new();
186
20368
    d.update(b"shared-random-disaster");
187
20368
    d.update(u64::from(period.length().as_minutes()).to_be_bytes());
188
20368
    d.update(period.interval_num().to_be_bytes());
189

            
190
20368
    let v: [u8; 32] = d.finalize().into();
191
20368
    v.into()
192
20368
}
193

            
194
/// Helper type: A `SharedRandVal`, and the time range over which it is the most
195
/// recent.
196
type 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.
200
62310
fn find_params_for_time(info: &[SrvInfo], period: TimePeriod) -> Result<Option<HsDirParams>> {
201
62310
    let start = period
202
62310
        .range()
203
62310
        .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
62310
    Ok(find_srv_for_time(info, start).map(|srv| HsDirParams {
211
1210
        time_period: period,
212
1210
        shared_rand: srv.0,
213
1210
        srv_lifespan: srv.1.clone(),
214
1210
    }))
215
62310
}
216

            
217
/// Given a list of SrvInfo, return the SrvInfo (if any) that is the most
218
/// recent SRV at `when`.
219
62324
fn find_srv_for_time(info: &[SrvInfo], when: SystemTime) -> Option<&SrvInfo> {
220
62369
    info.iter().find(|(_, range)| range.contains(&when))
221
62324
}
222

            
223
/// Return every SRV from a consensus, along with a duration over which it is
224
/// most recent SRV.
225
20774
fn extract_srvs(consensus: &MdConsensus) -> Result<Vec<SrvInfo>> {
226
20774
    let mut v = Vec::new();
227
20774
    let srv_interval = srv_interval(consensus);
228

            
229
20774
    if let Some(cur) = consensus.shared_rand_cur() {
230
408
        let ts_begin = cur
231
408
            .timestamp()
232
408
            .map(Ok)
233
418
            .unwrap_or_else(|| start_of_sr_round(consensus))?;
234
408
        let ts_end = ts_begin + srv_interval;
235
408
        v.push((*cur.value(), ts_begin..ts_end));
236
20366
    }
237
20774
    if let Some(prev) = consensus.shared_rand_prev() {
238
408
        let ts_begin = prev
239
408
            .timestamp()
240
408
            .map(Ok)
241
418
            .unwrap_or_else(|| start_of_sr_round(consensus).map(|t| t - srv_interval))?;
242
408
        let ts_end = ts_begin + srv_interval;
243
408
        v.push((*prev.value(), ts_begin..ts_end));
244
20366
    }
245

            
246
20774
    Ok(v)
247
20774
}
248

            
249
/// Return the length of time for which a single SRV value is valid.
250
20792
fn 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
20792
    if let (Some(cur), Some(prev)) = (consensus.shared_rand_cur(), consensus.shared_rand_prev()) {
257
426
        if let (Some(cur_ts), Some(prev_ts)) = (cur.timestamp(), prev.timestamp()) {
258
8
            if let Ok(d) = cur_ts.duration_since(prev_ts) {
259
6
                return d;
260
2
            }
261
418
        }
262
20366
    }
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
20786
    consensus.lifetime().voting_period() * VOTING_PERIODS_IN_SRV_ROUND
268
20792
}
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()`.
278
820
fn start_of_sr_round(consensus: &MdConsensus) -> Result<SystemTime> {
279
820
    let t = consensus.lifetime().valid_after();
280
820
    let beginning_of_curr_round = t
281
820
        .duration_since(SystemTime::UNIX_EPOCH)
282
820
        .map_err(|_| Error::InvalidConsensus("consensus valid-after is before Unix epoch?!"))?
283
820
        .as_secs();
284
820
    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
820
    debug_assert!(voting_interval > 0);
290

            
291
820
    let curr_round_slot =
292
820
        (beginning_of_curr_round / voting_interval) % u64::from(VOTING_PERIODS_IN_SRV_ROUND);
293
820
    let time_elapsed_since_start_of_run = curr_round_slot * voting_interval;
294

            
295
820
    let offset = Duration::from_secs(beginning_of_curr_round - time_elapsed_since_start_of_run);
296
820
    Ok(SystemTime::UNIX_EPOCH + offset)
297
820
}
298

            
299
#[cfg(test)]
300
mod 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
}