1use 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#[derive(Clone, Debug, Eq, PartialEq)]
41pub struct HsDirParams {
42 pub(crate) time_period: TimePeriod,
45 pub(crate) shared_rand: SharedRandVal,
48 pub(crate) srv_lifespan: std::ops::Range<SystemTime>,
50}
51
52const VOTING_PERIODS_IN_OFFSET: u32 = 12;
58
59const VOTING_PERIODS_IN_SRV_ROUND: u32 = 24;
64
65impl HsDirParams {
66 pub fn time_period(&self) -> TimePeriod {
72 self.time_period
73 }
74
75 pub fn start_of_shard_rand_period(&self) -> SystemTime {
78 self.srv_lifespan.start
79 }
80
81 #[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 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 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 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 #[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
168fn 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
179fn 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
194type SrvInfo = (SharedRandVal, std::ops::Range<SystemTime>);
197
198fn 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
217fn find_srv_for_time(info: &[SrvInfo], when: SystemTime) -> Option<&SrvInfo> {
220 info.iter().find(|(_, range)| range.contains(&when))
221}
222
223fn 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
249fn srv_interval(consensus: &MdConsensus) -> Duration {
251 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 consensus.lifetime().voting_period() * VOTING_PERIODS_IN_SRV_ROUND
268}
269
270fn 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 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 #![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)] use super::*;
316 use hex_literal::hex;
317 use tor_netdoc::doc::netstatus::{Lifetime, MdConsensusBuilder};
318
319 fn t(s: &str) -> SystemTime {
325 humantime::parse_rfc3339(s).unwrap()
326 }
327 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 (
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 (
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 let consensus = example_consensus_builder().testing_consensus().unwrap();
418 assert_eq!(srv_interval(&consensus), d("1 day"));
419
420 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 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 (
447 SRV2.into(),
448 t("1985-10-25T00:00:00Z")..t("1985-10-26T00:00:00Z")
449 ),
450 (
453 SRV1.into(),
454 t("1985-10-24T00:00:00Z")..t("1985-10-25T00:00:00Z")
455 )
456 ]
457 );
458
459 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 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 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 assert_eq!(current.shared_rand.as_ref(), &SRV1);
540
541 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 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) .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 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 assert_eq!(
610 params.offset_within_srv_period(srv_end).unwrap(),
611 SrvPeriodOffset::from(11 * 60 * 60)
612 );
613 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 (
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 (
650 "2015-04-20T00:15:30Z",
651 "2015-04-20T00:15:40Z",
652 "2015-04-20T00:12:00Z",
653 "4 minutes",
654 ),
655 (
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 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}