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::{params::NetParameters, Error, HsDirs, Result};
29
use time::{OffsetDateTime, UtcOffset};
30
use tor_hscrypto::time::TimePeriod;
31
use tor_netdoc::doc::netstatus::{MdConsensus, SharedRandVal};
32

            
33
#[cfg(feature = "hs-service")]
34
use tor_hscrypto::ope::SrvPeriodOffset;
35

            
36
/// Parameters for generating and using an HsDir ring.
37
///
38
/// These parameters are derived from the shared random values and time
39
/// parameters in the consensus, and are used to determine the
40
/// position of each HsDir within the ring.
41
#[derive(Clone, Debug, Eq, PartialEq)]
42
pub struct HsDirParams {
43
    /// The time period for this ring.  It's used to ensure that blinded onion
44
    /// keys rotate in a _predictable_ way over time.
45
    pub(crate) time_period: TimePeriod,
46
    /// The SharedRandVal for this ring.  It's used to ensure that the position
47
    /// of each HsDir within the ring rotates _unpredictably_ over time.
48
    pub(crate) shared_rand: SharedRandVal,
49
    /// The range of times over which the srv is most current.
50
    pub(crate) srv_lifespan: std::ops::Range<SystemTime>,
51
}
52

            
53
/// By how many voting periods do we offset the beginning of our first time
54
/// period from the epoch?
55
///
56
/// We do this so that each of our time periods begins at a time when the SRV is
57
/// not rotating.
58
const VOTING_PERIODS_IN_OFFSET: u32 = 12;
59

            
60
/// How many voting periods make up an entire round of the shared random value
61
/// commit-and-reveal protocol?
62
///
63
/// We use this to compute an SRV lifetime if one of the SRV values is missing.
64
const VOTING_PERIODS_IN_SRV_ROUND: u32 = 24;
65

            
66
/// One day.
67
const ONE_DAY: Duration = Duration::new(86400, 0);
68

            
69
impl HsDirParams {
70
    /// Return the time period for which these parameters are valid.
71
    ///
72
    /// The `hs_blind_id` for an onion service changes every time period: when
73
    /// uploading, callers should use this time period to determine which
74
    /// `hs_blind_id`'s descriptor should be sent to which directory.
75
7965
    pub fn time_period(&self) -> TimePeriod {
76
7965
        self.time_period
77
7965
    }
78

            
79
    /// Return the starting time for the shared-random-value protocol that
80
    /// produced the SRV for this time period.
81
3240
    pub fn start_of_shard_rand_period(&self) -> SystemTime {
82
3240
        self.srv_lifespan.start
83
3240
    }
84

            
85
    /// Return an opaque offset for `when` from the start of the shared-random-value protocol
86
    /// period corresponding to the SRV for this time period.
87
    ///
88
    /// When uploading, callers should this offset to determine
89
    /// the revision counter for their descriptors.
90
    ///
91
    /// Returns `None` if when is after the start of the SRV period.
92
    #[cfg(feature = "hs-service")]
93
3248
    pub fn offset_within_srv_period(&self, when: SystemTime) -> Option<SrvPeriodOffset> {
94
3248
        if when >= self.srv_lifespan.start {
95
3246
            let d = when
96
3246
                .duration_since(self.srv_lifespan.start)
97
3246
                .expect("Somehow, range comparison was not reliable!");
98
3246
            return Some(SrvPeriodOffset::from(d.as_secs() as u32));
99
2
        }
100
2

            
101
2
        None
102
3248
    }
103

            
104
    /// Compute the `HsDirParams` for the current time period, according to a given
105
    /// consensus.
106
    ///
107
    /// rend-spec-v3 section 2.2.1 et seq
108
    ///
109
    /// Return the ring parameters for the current period (which clients use when
110
    /// fetching onion service descriptors), along with a Vec of ring
111
    /// parameters for any secondary periods that onion services should additionally
112
    /// use when publishing their descriptors.
113
    ///
114
    /// Note that "current" here is always relative to a given consensus, not the
115
    /// current wall-clock time.
116
    ///
117
    /// (This function's return type is a bit cumbersome; these parameters are
118
    /// bundled together because it is efficient to compute them all at once.)
119
    ///
120
    /// Note that this function will only return an error if something is
121
    /// _extremely_ wrong with the provided consensus: for other error cases, it
122
    /// returns a "disaster fallback".
123
14920
    pub(crate) fn compute(
124
14920
        consensus: &MdConsensus,
125
14920
        params: &NetParameters,
126
14920
    ) -> Result<HsDirs<HsDirParams>> {
127
14920
        let srvs = extract_srvs(consensus);
128
14920
        let tp_length: Duration = params.hsdir_timeperiod_length.try_into().map_err(|_| {
129
            // Note that this error should be impossible:
130
            // The type of hsdir_timeperiod_length() is IntegerMinutes<BoundedInt32<30, 14400>>...
131
            // It should be at most 10 days, which _definitely_ fits into a Duration.
132
            Error::InvalidConsensus(
133
                "Minutes in hsdir timeperiod could not be converted to a Duration",
134
            )
135
14920
        })?;
136
14920
        let offset = consensus.lifetime().voting_period() * VOTING_PERIODS_IN_OFFSET;
137
14920
        let cur_period = TimePeriod::new(tp_length, consensus.lifetime().valid_after(), offset)
138
14920
            .map_err(|_| {
139
                // This error should be nearly impossible too:
140
                // - It can occur if the time period length is not an integer
141
                //   number of minutes--but we took it from an IntegerMinutes,
142
                //   so that's unlikely.
143
                // - It can occur if the time period length or the offset is
144
                //   greater than can be represented in u32 seconds.
145
                // - It can occur if the valid_after time is so far from the
146
                //   epoch that we can't represent the distance as a Duration.
147
                Error::InvalidConsensus("Consensus valid-after did not fall in a time period")
148
14920
            })?;
149

            
150
14920
        let current = find_params_for_time(&srvs[..], cur_period)?
151
15275
            .unwrap_or_else(|| disaster_params(cur_period));
152
14920

            
153
14920
        // When computing secondary rings, we don't try so many fallback operations:
154
14920
        // if they aren't available, they aren't available.
155
14920
        #[cfg(feature = "hs-service")]
156
14920
        let secondary = [cur_period.prev(), cur_period.next()]
157
14920
            .iter()
158
14920
            .flatten()
159
30205
            .flat_map(|period| find_params_for_time(&srvs[..], *period).ok().flatten())
160
14920
            .collect();
161
14920

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

            
170
/// Compute ring parameters using a Disaster SRV for this period.
171
14556
fn disaster_params(period: TimePeriod) -> HsDirParams {
172
14556
    HsDirParams {
173
14556
        time_period: period,
174
14556
        shared_rand: disaster_srv(period),
175
14556
        srv_lifespan: period
176
14556
            .range()
177
14556
            .expect("Time period cannot be represented as SystemTime"),
178
14556
    }
179
14556
}
180

            
181
/// Compute the "Disaster SRV" for a given time period.
182
///
183
/// This SRV is used if the authorities do not list any shared random value for
184
/// that time period, but we need to compute an HsDir ring for it anyway.
185
14558
fn disaster_srv(period: TimePeriod) -> SharedRandVal {
186
    use digest::Digest;
187
14558
    let mut d = tor_llcrypto::d::Sha3_256::new();
188
14558
    d.update(b"shared-random-disaster");
189
14558
    d.update(u64::from(period.length().as_minutes()).to_be_bytes());
190
14558
    d.update(period.interval_num().to_be_bytes());
191
14558

            
192
14558
    let v: [u8; 32] = d.finalize().into();
193
14558
    v.into()
194
14558
}
195

            
196
/// Helper type: A `SharedRandVal`, and the time range over which it is the most
197
/// recent.
198
type SrvInfo = (SharedRandVal, std::ops::Range<SystemTime>);
199

            
200
/// Given a list of SrvInfo, return an HsRingParams instance for a given time
201
/// period, if possible.
202
44760
fn find_params_for_time(info: &[SrvInfo], period: TimePeriod) -> Result<Option<HsDirParams>> {
203
44760
    let start = period
204
44760
        .range()
205
44760
        .map_err(|_| {
206
            Error::InvalidConsensus(
207
                "HsDir time period in consensus could not be represented as a SystemTime range.",
208
            )
209
44760
        })?
210
        .start;
211

            
212
44789
    Ok(find_srv_for_time(info, start).map(|srv| HsDirParams {
213
1090
        time_period: period,
214
1090
        shared_rand: srv.0,
215
1090
        srv_lifespan: srv.1.clone(),
216
44789
    }))
217
44760
}
218

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

            
225
/// Return every SRV from a consensus, along with a duration over which it is
226
/// most recent SRV.
227
14924
fn extract_srvs(consensus: &MdConsensus) -> Vec<SrvInfo> {
228
14924
    let mut v = Vec::new();
229
14924
    let consensus_ts = consensus.lifetime().valid_after();
230
14924
    let srv_interval = srv_interval(consensus);
231

            
232
14924
    if let Some(cur) = consensus.shared_rand_cur() {
233
8
        let ts_begin = cur
234
8
            .timestamp()
235
10
            .unwrap_or_else(|| start_of_day_containing(consensus_ts));
236
8
        let ts_end = ts_begin + srv_interval;
237
8
        v.push((*cur.value(), ts_begin..ts_end));
238
14916
    }
239
14924
    if let Some(prev) = consensus.shared_rand_prev() {
240
368
        let ts_begin = prev
241
368
            .timestamp()
242
378
            .unwrap_or_else(|| start_of_day_containing(consensus_ts) - ONE_DAY);
243
368
        let ts_end = ts_begin + srv_interval;
244
368
        v.push((*prev.value(), ts_begin..ts_end));
245
14556
    }
246

            
247
14924
    v
248
14924
}
249

            
250
/// Return the length of time for which a single SRV value is valid.
251
14930
fn srv_interval(consensus: &MdConsensus) -> Duration {
252
    // What we _want_ to do, ideally, is is to learn the duration from the
253
    // difference between the declared time for the previous value and the
254
    // declared time for the current one.
255
    //
256
    // (This assumes that proposal 342 is implemented.)
257
14930
    if let (Some(cur), Some(prev)) = (consensus.shared_rand_cur(), consensus.shared_rand_prev()) {
258
14
        if let (Some(cur_ts), Some(prev_ts)) = (cur.timestamp(), prev.timestamp()) {
259
8
            if let Ok(d) = cur_ts.duration_since(prev_ts) {
260
6
                return d;
261
2
            }
262
6
        }
263
14916
    }
264

            
265
    // But if one of those values is missing, or if it has no timestamp, we have
266
    // to fall back to admitting that we know the schedule for the voting
267
    // algorithm.
268
14924
    consensus.lifetime().voting_period() * VOTING_PERIODS_IN_SRV_ROUND
269
14930
}
270

            
271
/// Return the length of the voting period in the consensus.
272
///
273
/// (The "voting period" is the length of time between between one consensus and the next.)
274
///
275
/// Return a time at the start of the UTC day containing `t`.
276
374
fn start_of_day_containing(t: SystemTime) -> SystemTime {
277
374
    OffsetDateTime::from(t)
278
374
        .to_offset(UtcOffset::UTC)
279
374
        .replace_time(time::macros::time!(00:00))
280
374
        .into()
281
374
}
282

            
283
#[cfg(test)]
284
mod test {
285
    // @@ begin test lint list maintained by maint/add_warning @@
286
    #![allow(clippy::bool_assert_comparison)]
287
    #![allow(clippy::clone_on_copy)]
288
    #![allow(clippy::dbg_macro)]
289
    #![allow(clippy::mixed_attributes_style)]
290
    #![allow(clippy::print_stderr)]
291
    #![allow(clippy::print_stdout)]
292
    #![allow(clippy::single_char_pattern)]
293
    #![allow(clippy::unwrap_used)]
294
    #![allow(clippy::unchecked_duration_subtraction)]
295
    #![allow(clippy::useless_vec)]
296
    #![allow(clippy::needless_pass_by_value)]
297
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
298
    use super::*;
299
    use hex_literal::hex;
300
    use tor_netdoc::doc::netstatus::{ConsensusBuilder, Lifetime, MdConsensusRouterStatus};
301

            
302
    /// Helper: parse an rfc3339 time.
303
    ///
304
    /// # Panics
305
    ///
306
    /// Panics if the time is invalid.
307
    fn t(s: &str) -> SystemTime {
308
        humantime::parse_rfc3339(s).unwrap()
309
    }
310
    /// Helper: parse a duration.
311
    ///
312
    /// # Panics
313
    ///
314
    /// Panics if the time is invalid.
315
    fn d(s: &str) -> Duration {
316
        humantime::parse_duration(s).unwrap()
317
    }
318

            
319
    fn example_lifetime() -> Lifetime {
320
        Lifetime::new(
321
            t("1985-10-25T07:00:00Z"),
322
            t("1985-10-25T08:00:00Z"),
323
            t("1985-10-25T10:00:00Z"),
324
        )
325
        .unwrap()
326
    }
327

            
328
    const SRV1: [u8; 32] = *b"next saturday night were sending";
329
    const SRV2: [u8; 32] = *b"you......... back to the future!";
330

            
331
    fn example_consensus_builder() -> ConsensusBuilder<MdConsensusRouterStatus> {
332
        let mut bld = MdConsensus::builder();
333

            
334
        bld.consensus_method(34)
335
            .lifetime(example_lifetime())
336
            .param("bwweightscale", 1)
337
            .param("hsdir_interval", 1440)
338
            .weights("".parse().unwrap())
339
            .shared_rand_prev(7, SRV1.into(), None)
340
            .shared_rand_cur(7, SRV2.into(), None);
341

            
342
        bld
343
    }
344

            
345
    #[test]
346
    fn start_of_day() {
347
        assert_eq!(
348
            start_of_day_containing(t("1985-10-25T07:00:00Z")),
349
            t("1985-10-25T00:00:00Z")
350
        );
351
        assert_eq!(
352
            start_of_day_containing(t("1985-10-25T00:00:00Z")),
353
            t("1985-10-25T00:00:00Z")
354
        );
355
        assert_eq!(
356
            start_of_day_containing(t("1985-10-25T23:59:59.999Z")),
357
            t("1985-10-25T00:00:00Z")
358
        );
359
    }
360

            
361
    #[test]
362
    fn vote_period() {
363
        assert_eq!(example_lifetime().voting_period(), d("1 hour"));
364

            
365
        let lt2 = Lifetime::new(
366
            t("1985-10-25T07:00:00Z"),
367
            t("1985-10-25T07:22:00Z"),
368
            t("1985-10-25T07:59:00Z"),
369
        )
370
        .unwrap();
371

            
372
        assert_eq!(lt2.voting_period(), d("22 min"));
373
    }
374

            
375
    #[test]
376
    fn srv_period() {
377
        // In a basic consensus with no SRV timestamps, we'll assume 24 voting periods.
378
        let consensus = example_consensus_builder().testing_consensus().unwrap();
379
        assert_eq!(srv_interval(&consensus), d("1 day"));
380

            
381
        // If there are timestamps, we look at the difference between them.
382
        let consensus = example_consensus_builder()
383
            .shared_rand_prev(7, SRV1.into(), Some(t("1985-10-25T00:00:00Z")))
384
            .shared_rand_cur(7, SRV2.into(), Some(t("1985-10-25T06:00:05Z")))
385
            .testing_consensus()
386
            .unwrap();
387
        assert_eq!(srv_interval(&consensus), d("6 hours 5 sec"));
388

            
389
        // Note that if the timestamps are in reversed order, we fall back to 24 hours.
390
        let consensus = example_consensus_builder()
391
            .shared_rand_cur(7, SRV1.into(), Some(t("1985-10-25T00:00:00Z")))
392
            .shared_rand_prev(7, SRV2.into(), Some(t("1985-10-25T06:00:05Z")))
393
            .testing_consensus()
394
            .unwrap();
395
        assert_eq!(srv_interval(&consensus), d("1 day"));
396
    }
397

            
398
    #[test]
399
    fn srvs_extract_and_find() {
400
        let consensus = example_consensus_builder().testing_consensus().unwrap();
401
        let srvs = extract_srvs(&consensus);
402
        assert_eq!(
403
            srvs,
404
            vec![
405
                // Since no timestamps are given in the example, the current srv
406
                // is valid from midnight to midnight...
407
                (
408
                    SRV2.into(),
409
                    t("1985-10-25T00:00:00Z")..t("1985-10-26T00:00:00Z")
410
                ),
411
                // ...and the previous SRV is valid midnight-to-midnight on the
412
                // previous day.
413
                (
414
                    SRV1.into(),
415
                    t("1985-10-24T00:00:00Z")..t("1985-10-25T00:00:00Z")
416
                )
417
            ]
418
        );
419

            
420
        // Now try with explicit timestamps on the SRVs.
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
        let srvs = extract_srvs(&consensus);
427
        assert_eq!(
428
            srvs,
429
            vec![
430
                (
431
                    SRV2.into(),
432
                    t("1985-10-25T06:00:05Z")..t("1985-10-25T12:00:10Z")
433
                ),
434
                (
435
                    SRV1.into(),
436
                    t("1985-10-25T00:00:00Z")..t("1985-10-25T06:00:05Z")
437
                )
438
            ]
439
        );
440

            
441
        // See if we can look up SRVs in that period.
442
        assert_eq!(None, find_srv_for_time(&srvs, t("1985-10-24T23:59:00Z")));
443
        assert_eq!(
444
            Some(&srvs[1]),
445
            find_srv_for_time(&srvs, t("1985-10-25T00:00:00Z"))
446
        );
447
        assert_eq!(
448
            Some(&srvs[1]),
449
            find_srv_for_time(&srvs, t("1985-10-25T03:59:00Z"))
450
        );
451
        assert_eq!(
452
            Some(&srvs[1]),
453
            find_srv_for_time(&srvs, t("1985-10-25T00:00:00Z"))
454
        );
455
        assert_eq!(
456
            Some(&srvs[0]),
457
            find_srv_for_time(&srvs, t("1985-10-25T06:00:05Z"))
458
        );
459
        assert_eq!(
460
            Some(&srvs[0]),
461
            find_srv_for_time(&srvs, t("1985-10-25T12:00:00Z"))
462
        );
463
        assert_eq!(None, find_srv_for_time(&srvs, t("1985-10-25T12:00:30Z")));
464
    }
465

            
466
    #[test]
467
    fn disaster() {
468
        use digest::Digest;
469
        use tor_llcrypto::d::Sha3_256;
470
        let period = TimePeriod::new(d("1 day"), t("1970-01-02T17:33:00Z"), d("12 hours")).unwrap();
471
        assert_eq!(period.length().as_minutes(), 86400 / 60);
472
        assert_eq!(period.interval_num(), 1);
473

            
474
        let dsrv = disaster_srv(period);
475
        assert_eq!(
476
            dsrv.as_ref(),
477
            &hex!("F8A4948707653837FA44ABB5BBC75A12F6F101E7F8FAF699B9715F4965D3507D")
478
        );
479
        assert_eq!(
480
            &dsrv.as_ref()[..],
481
            &Sha3_256::digest(b"shared-random-disaster\0\0\0\0\0\0\x05\xA0\0\0\0\0\0\0\0\x01")[..]
482
        );
483
    }
484

            
485
    #[test]
486
    #[cfg(feature = "hs-service")]
487
    fn ring_params_simple() {
488
        // Compute ring parameters in a legacy environment, where the time
489
        // period and the SRV lifetime are one day long, and they are offset by
490
        // 12 hours.
491
        let consensus = example_consensus_builder().testing_consensus().unwrap();
492
        let netparams = NetParameters::from_map(consensus.params());
493
        let HsDirs { current, secondary } = HsDirParams::compute(&consensus, &netparams).unwrap();
494

            
495
        assert_eq!(
496
            current.time_period,
497
            TimePeriod::new(d("1 day"), t("1985-10-25T07:00:00Z"), d("12 hours")).unwrap()
498
        );
499
        // We use the "previous" SRV since the start of this time period was 12:00 on the 24th.
500
        assert_eq!(current.shared_rand.as_ref(), &SRV1);
501

            
502
        // Our secondary SRV will be the one that starts when we move into the
503
        // next time period.
504
        assert_eq!(secondary.len(), 1);
505
        assert_eq!(
506
            secondary[0].time_period,
507
            TimePeriod::new(d("1 day"), t("1985-10-25T12:00:00Z"), d("12 hours")).unwrap(),
508
        );
509
        assert_eq!(secondary[0].shared_rand.as_ref(), &SRV2);
510
    }
511

            
512
    #[test]
513
    #[cfg(feature = "hs-service")]
514
    fn ring_params_tricky() {
515
        // In this case we give the SRVs timestamps and we choose an odd hsdir_interval.
516
        let consensus = example_consensus_builder()
517
            .shared_rand_prev(7, SRV1.into(), Some(t("1985-10-25T00:00:00Z")))
518
            .shared_rand_cur(7, SRV2.into(), Some(t("1985-10-25T05:00:00Z")))
519
            .param("hsdir_interval", 120) // 2 hours
520
            .testing_consensus()
521
            .unwrap();
522
        let netparams = NetParameters::from_map(consensus.params());
523
        let HsDirs { current, secondary } = HsDirParams::compute(&consensus, &netparams).unwrap();
524

            
525
        assert_eq!(
526
            current.time_period,
527
            TimePeriod::new(d("2 hours"), t("1985-10-25T07:00:00Z"), d("12 hours")).unwrap()
528
        );
529
        assert_eq!(current.shared_rand.as_ref(), &SRV2);
530

            
531
        assert_eq!(secondary.len(), 2);
532
        assert_eq!(
533
            secondary[0].time_period,
534
            TimePeriod::new(d("2 hours"), t("1985-10-25T05:00:00Z"), d("12 hours")).unwrap()
535
        );
536
        assert_eq!(secondary[0].shared_rand.as_ref(), &SRV1);
537
        assert_eq!(
538
            secondary[1].time_period,
539
            TimePeriod::new(d("2 hours"), t("1985-10-25T09:00:00Z"), d("12 hours")).unwrap()
540
        );
541
        assert_eq!(secondary[1].shared_rand.as_ref(), &SRV2);
542
    }
543

            
544
    #[test]
545
    #[cfg(feature = "hs-service")]
546
    fn offset_within_srv_period() {
547
        // This test doesn't actually use the time_period or shared_rand values, so their value is
548
        // arbitrary.
549
        let time_period =
550
            TimePeriod::new(d("2 hours"), t("1985-10-25T05:00:00Z"), d("12 hours")).unwrap();
551

            
552
        let srv_start = t("1985-10-25T09:00:00Z");
553
        let srv_end = t("1985-10-25T20:00:00Z");
554
        let srv_lifespan = srv_start..srv_end;
555

            
556
        let params = HsDirParams {
557
            time_period,
558
            shared_rand: SRV1.into(),
559
            srv_lifespan,
560
        };
561

            
562
        let before_srv_period = t("1985-10-25T08:59:00Z");
563
        let after_srv_period = t("1985-10-26T10:19:00Z");
564
        assert!(params.offset_within_srv_period(before_srv_period).is_none());
565
        assert_eq!(
566
            params.offset_within_srv_period(srv_start).unwrap(),
567
            SrvPeriodOffset::from(0)
568
        );
569
        // The period is 11h long
570
        assert_eq!(
571
            params.offset_within_srv_period(srv_end).unwrap(),
572
            SrvPeriodOffset::from(11 * 60 * 60)
573
        );
574
        // This timestamp is 1 day 1h 19m from the start of the SRV period
575
        assert_eq!(
576
            params.offset_within_srv_period(after_srv_period).unwrap(),
577
            SrvPeriodOffset::from((25 * 60 + 19) * 60)
578
        );
579
    }
580
}