1use std::time::{Duration, SystemTime};
27
28use crate::{Error, HsDirs, Result, params::NetParameters};
29use time::{OffsetDateTime, UtcOffset};
30use tor_hscrypto::time::TimePeriod;
31use tor_netdoc::doc::netstatus::{MdConsensus, SharedRandVal};
32
33#[cfg(feature = "hs-service")]
34use tor_hscrypto::ope::SrvPeriodOffset;
35
36#[derive(Clone, Debug, Eq, PartialEq)]
42pub struct HsDirParams {
43 pub(crate) time_period: TimePeriod,
46 pub(crate) shared_rand: SharedRandVal,
49 pub(crate) srv_lifespan: std::ops::Range<SystemTime>,
51}
52
53const VOTING_PERIODS_IN_OFFSET: u32 = 12;
59
60const VOTING_PERIODS_IN_SRV_ROUND: u32 = 24;
65
66const ONE_DAY: Duration = Duration::new(86400, 0);
68
69impl HsDirParams {
70 pub fn time_period(&self) -> TimePeriod {
76 self.time_period
77 }
78
79 pub fn start_of_shard_rand_period(&self) -> SystemTime {
82 self.srv_lifespan.start
83 }
84
85 #[cfg(feature = "hs-service")]
93 pub fn offset_within_srv_period(&self, when: SystemTime) -> Option<SrvPeriodOffset> {
94 if when >= self.srv_lifespan.start {
95 let d = when
96 .duration_since(self.srv_lifespan.start)
97 .expect("Somehow, range comparison was not reliable!");
98 return Some(SrvPeriodOffset::from(d.as_secs() as u32));
99 }
100
101 None
102 }
103
104 pub(crate) fn compute(
124 consensus: &MdConsensus,
125 params: &NetParameters,
126 ) -> Result<HsDirs<HsDirParams>> {
127 let srvs = extract_srvs(consensus);
128 let tp_length: Duration = params.hsdir_timeperiod_length.try_into().map_err(|_| {
129 Error::InvalidConsensus(
133 "Minutes in hsdir timeperiod could not be converted to a Duration",
134 )
135 })?;
136 let offset = consensus.lifetime().voting_period() * VOTING_PERIODS_IN_OFFSET;
137 let cur_period = TimePeriod::new(tp_length, consensus.lifetime().valid_after(), offset)
138 .map_err(|_| {
139 Error::InvalidConsensus("Consensus valid-after did not fall in a time period")
148 })?;
149
150 let current = find_params_for_time(&srvs[..], cur_period)?.unwrap_or_else(|| {
151 tracing::debug!("No SRV params for {cur_period:?}; falling back to disaster params");
152 disaster_params(cur_period)
153 });
154
155 #[cfg(feature = "hs-service")]
158 let secondary = [cur_period.prev(), cur_period.next()]
159 .iter()
160 .flatten()
161 .flat_map(|period| find_params_for_time(&srvs[..], *period).ok().flatten())
162 .collect();
163
164 Ok(HsDirs {
165 current,
166 #[cfg(feature = "hs-service")]
167 secondary,
168 })
169 }
170}
171
172fn disaster_params(period: TimePeriod) -> HsDirParams {
174 HsDirParams {
175 time_period: period,
176 shared_rand: disaster_srv(period),
177 srv_lifespan: period
178 .range()
179 .expect("Time period cannot be represented as SystemTime"),
180 }
181}
182
183fn disaster_srv(period: TimePeriod) -> SharedRandVal {
188 use digest::Digest;
189 let mut d = tor_llcrypto::d::Sha3_256::new();
190 d.update(b"shared-random-disaster");
191 d.update(u64::from(period.length().as_minutes()).to_be_bytes());
192 d.update(period.interval_num().to_be_bytes());
193
194 let v: [u8; 32] = d.finalize().into();
195 v.into()
196}
197
198type SrvInfo = (SharedRandVal, std::ops::Range<SystemTime>);
201
202fn find_params_for_time(info: &[SrvInfo], period: TimePeriod) -> Result<Option<HsDirParams>> {
205 let start = period
206 .range()
207 .map_err(|_| {
208 Error::InvalidConsensus(
209 "HsDir time period in consensus could not be represented as a SystemTime range.",
210 )
211 })?
212 .start;
213
214 Ok(find_srv_for_time(info, start).map(|srv| HsDirParams {
215 time_period: period,
216 shared_rand: srv.0,
217 srv_lifespan: srv.1.clone(),
218 }))
219}
220
221fn find_srv_for_time(info: &[SrvInfo], when: SystemTime) -> Option<&SrvInfo> {
224 info.iter().find(|(_, range)| range.contains(&when))
225}
226
227fn extract_srvs(consensus: &MdConsensus) -> Vec<SrvInfo> {
230 let mut v = Vec::new();
231 let consensus_ts = consensus.lifetime().valid_after();
232 let srv_interval = srv_interval(consensus);
233
234 if let Some(cur) = consensus.shared_rand_cur() {
235 let ts_begin = cur
236 .timestamp()
237 .unwrap_or_else(|| start_of_day_containing(consensus_ts));
238 let ts_end = ts_begin + srv_interval;
239 v.push((*cur.value(), ts_begin..ts_end));
240 }
241 if let Some(prev) = consensus.shared_rand_prev() {
242 let ts_begin = prev
243 .timestamp()
244 .unwrap_or_else(|| start_of_day_containing(consensus_ts) - ONE_DAY);
245 let ts_end = ts_begin + srv_interval;
246 v.push((*prev.value(), ts_begin..ts_end));
247 }
248
249 v
250}
251
252fn srv_interval(consensus: &MdConsensus) -> Duration {
254 if let (Some(cur), Some(prev)) = (consensus.shared_rand_cur(), consensus.shared_rand_prev()) {
260 if let (Some(cur_ts), Some(prev_ts)) = (cur.timestamp(), prev.timestamp()) {
261 if let Ok(d) = cur_ts.duration_since(prev_ts) {
262 return d;
263 }
264 }
265 }
266
267 consensus.lifetime().voting_period() * VOTING_PERIODS_IN_SRV_ROUND
271}
272
273fn start_of_day_containing(t: SystemTime) -> SystemTime {
279 OffsetDateTime::from(t)
280 .to_offset(UtcOffset::UTC)
281 .replace_time(time::macros::time!(00:00))
282 .into()
283}
284
285#[cfg(test)]
286mod test {
287 #![allow(clippy::bool_assert_comparison)]
289 #![allow(clippy::clone_on_copy)]
290 #![allow(clippy::dbg_macro)]
291 #![allow(clippy::mixed_attributes_style)]
292 #![allow(clippy::print_stderr)]
293 #![allow(clippy::print_stdout)]
294 #![allow(clippy::single_char_pattern)]
295 #![allow(clippy::unwrap_used)]
296 #![allow(clippy::unchecked_time_subtraction)]
297 #![allow(clippy::useless_vec)]
298 #![allow(clippy::needless_pass_by_value)]
299 use super::*;
301 use hex_literal::hex;
302 use tor_netdoc::doc::netstatus::{Lifetime, MdConsensusBuilder};
303
304 fn t(s: &str) -> SystemTime {
310 humantime::parse_rfc3339(s).unwrap()
311 }
312 fn d(s: &str) -> Duration {
318 humantime::parse_duration(s).unwrap()
319 }
320
321 fn example_lifetime() -> Lifetime {
322 Lifetime::new(
323 t("1985-10-25T07:00:00Z"),
324 t("1985-10-25T08:00:00Z"),
325 t("1985-10-25T10:00:00Z"),
326 )
327 .unwrap()
328 }
329
330 const SRV1: [u8; 32] = *b"next saturday night were sending";
331 const SRV2: [u8; 32] = *b"you......... back to the future!";
332
333 fn example_consensus_builder() -> MdConsensusBuilder {
334 let mut bld = MdConsensus::builder();
335
336 bld.consensus_method(34)
337 .lifetime(example_lifetime())
338 .param("bwweightscale", 1)
339 .param("hsdir_interval", 1440)
340 .weights("".parse().unwrap())
341 .shared_rand_prev(7, SRV1.into(), None)
342 .shared_rand_cur(7, SRV2.into(), None);
343
344 bld
345 }
346
347 #[test]
348 fn start_of_day() {
349 assert_eq!(
350 start_of_day_containing(t("1985-10-25T07:00:00Z")),
351 t("1985-10-25T00:00:00Z")
352 );
353 assert_eq!(
354 start_of_day_containing(t("1985-10-25T00:00:00Z")),
355 t("1985-10-25T00:00:00Z")
356 );
357 assert_eq!(
358 start_of_day_containing(t("1985-10-25T23:59:59.999Z")),
359 t("1985-10-25T00:00:00Z")
360 );
361 }
362
363 #[test]
364 fn vote_period() {
365 assert_eq!(example_lifetime().voting_period(), d("1 hour"));
366
367 let lt2 = Lifetime::new(
368 t("1985-10-25T07:00:00Z"),
369 t("1985-10-25T07:22:00Z"),
370 t("1985-10-25T07:59:00Z"),
371 )
372 .unwrap();
373
374 assert_eq!(lt2.voting_period(), d("22 min"));
375 }
376
377 #[test]
378 fn srv_period() {
379 let consensus = example_consensus_builder().testing_consensus().unwrap();
381 assert_eq!(srv_interval(&consensus), d("1 day"));
382
383 let consensus = example_consensus_builder()
385 .shared_rand_prev(7, SRV1.into(), Some(t("1985-10-25T00:00:00Z")))
386 .shared_rand_cur(7, SRV2.into(), Some(t("1985-10-25T06:00:05Z")))
387 .testing_consensus()
388 .unwrap();
389 assert_eq!(srv_interval(&consensus), d("6 hours 5 sec"));
390
391 let consensus = example_consensus_builder()
393 .shared_rand_cur(7, SRV1.into(), Some(t("1985-10-25T00:00:00Z")))
394 .shared_rand_prev(7, SRV2.into(), Some(t("1985-10-25T06:00:05Z")))
395 .testing_consensus()
396 .unwrap();
397 assert_eq!(srv_interval(&consensus), d("1 day"));
398 }
399
400 #[test]
401 fn srvs_extract_and_find() {
402 let consensus = example_consensus_builder().testing_consensus().unwrap();
403 let srvs = extract_srvs(&consensus);
404 assert_eq!(
405 srvs,
406 vec![
407 (
410 SRV2.into(),
411 t("1985-10-25T00:00:00Z")..t("1985-10-26T00:00:00Z")
412 ),
413 (
416 SRV1.into(),
417 t("1985-10-24T00:00:00Z")..t("1985-10-25T00:00:00Z")
418 )
419 ]
420 );
421
422 let consensus = example_consensus_builder()
424 .shared_rand_prev(7, SRV1.into(), Some(t("1985-10-25T00:00:00Z")))
425 .shared_rand_cur(7, SRV2.into(), Some(t("1985-10-25T06:00:05Z")))
426 .testing_consensus()
427 .unwrap();
428 let srvs = extract_srvs(&consensus);
429 assert_eq!(
430 srvs,
431 vec![
432 (
433 SRV2.into(),
434 t("1985-10-25T06:00:05Z")..t("1985-10-25T12:00:10Z")
435 ),
436 (
437 SRV1.into(),
438 t("1985-10-25T00:00:00Z")..t("1985-10-25T06:00:05Z")
439 )
440 ]
441 );
442
443 assert_eq!(None, find_srv_for_time(&srvs, t("1985-10-24T23:59:00Z")));
445 assert_eq!(
446 Some(&srvs[1]),
447 find_srv_for_time(&srvs, t("1985-10-25T00:00:00Z"))
448 );
449 assert_eq!(
450 Some(&srvs[1]),
451 find_srv_for_time(&srvs, t("1985-10-25T03:59:00Z"))
452 );
453 assert_eq!(
454 Some(&srvs[1]),
455 find_srv_for_time(&srvs, t("1985-10-25T00:00:00Z"))
456 );
457 assert_eq!(
458 Some(&srvs[0]),
459 find_srv_for_time(&srvs, t("1985-10-25T06:00:05Z"))
460 );
461 assert_eq!(
462 Some(&srvs[0]),
463 find_srv_for_time(&srvs, t("1985-10-25T12:00:00Z"))
464 );
465 assert_eq!(None, find_srv_for_time(&srvs, t("1985-10-25T12:00:30Z")));
466 }
467
468 #[test]
469 fn disaster() {
470 use digest::Digest;
471 use tor_llcrypto::d::Sha3_256;
472 let period = TimePeriod::new(d("1 day"), t("1970-01-02T17:33:00Z"), d("12 hours")).unwrap();
473 assert_eq!(period.length().as_minutes(), 86400 / 60);
474 assert_eq!(period.interval_num(), 1);
475
476 let dsrv = disaster_srv(period);
477 assert_eq!(
478 dsrv.as_ref(),
479 &hex!("F8A4948707653837FA44ABB5BBC75A12F6F101E7F8FAF699B9715F4965D3507D")
480 );
481 assert_eq!(
482 &dsrv.as_ref()[..],
483 &Sha3_256::digest(b"shared-random-disaster\0\0\0\0\0\0\x05\xA0\0\0\0\0\0\0\0\x01")[..]
484 );
485 }
486
487 #[test]
488 #[cfg(feature = "hs-service")]
489 fn ring_params_simple() {
490 let consensus = example_consensus_builder().testing_consensus().unwrap();
494 let netparams = NetParameters::from_map(consensus.params());
495 let HsDirs { current, secondary } = HsDirParams::compute(&consensus, &netparams).unwrap();
496
497 assert_eq!(
498 current.time_period,
499 TimePeriod::new(d("1 day"), t("1985-10-25T07:00:00Z"), d("12 hours")).unwrap()
500 );
501 assert_eq!(current.shared_rand.as_ref(), &SRV1);
503
504 assert_eq!(secondary.len(), 1);
507 assert_eq!(
508 secondary[0].time_period,
509 TimePeriod::new(d("1 day"), t("1985-10-25T12:00:00Z"), d("12 hours")).unwrap(),
510 );
511 assert_eq!(secondary[0].shared_rand.as_ref(), &SRV2);
512 }
513
514 #[test]
515 #[cfg(feature = "hs-service")]
516 fn ring_params_tricky() {
517 let consensus = example_consensus_builder()
519 .shared_rand_prev(7, SRV1.into(), Some(t("1985-10-25T00:00:00Z")))
520 .shared_rand_cur(7, SRV2.into(), Some(t("1985-10-25T05:00:00Z")))
521 .param("hsdir_interval", 120) .testing_consensus()
523 .unwrap();
524 let netparams = NetParameters::from_map(consensus.params());
525 let HsDirs { current, secondary } = HsDirParams::compute(&consensus, &netparams).unwrap();
526
527 assert_eq!(
528 current.time_period,
529 TimePeriod::new(d("2 hours"), t("1985-10-25T07:00:00Z"), d("12 hours")).unwrap()
530 );
531 assert_eq!(current.shared_rand.as_ref(), &SRV2);
532
533 assert_eq!(secondary.len(), 2);
534 assert_eq!(
535 secondary[0].time_period,
536 TimePeriod::new(d("2 hours"), t("1985-10-25T05:00:00Z"), d("12 hours")).unwrap()
537 );
538 assert_eq!(secondary[0].shared_rand.as_ref(), &SRV1);
539 assert_eq!(
540 secondary[1].time_period,
541 TimePeriod::new(d("2 hours"), t("1985-10-25T09:00:00Z"), d("12 hours")).unwrap()
542 );
543 assert_eq!(secondary[1].shared_rand.as_ref(), &SRV2);
544 }
545
546 #[test]
547 #[cfg(feature = "hs-service")]
548 fn offset_within_srv_period() {
549 let time_period =
552 TimePeriod::new(d("2 hours"), t("1985-10-25T05:00:00Z"), d("12 hours")).unwrap();
553
554 let srv_start = t("1985-10-25T09:00:00Z");
555 let srv_end = t("1985-10-25T20:00:00Z");
556 let srv_lifespan = srv_start..srv_end;
557
558 let params = HsDirParams {
559 time_period,
560 shared_rand: SRV1.into(),
561 srv_lifespan,
562 };
563
564 let before_srv_period = t("1985-10-25T08:59:00Z");
565 let after_srv_period = t("1985-10-26T10:19:00Z");
566 assert!(params.offset_within_srv_period(before_srv_period).is_none());
567 assert_eq!(
568 params.offset_within_srv_period(srv_start).unwrap(),
569 SrvPeriodOffset::from(0)
570 );
571 assert_eq!(
573 params.offset_within_srv_period(srv_end).unwrap(),
574 SrvPeriodOffset::from(11 * 60 * 60)
575 );
576 assert_eq!(
578 params.offset_within_srv_period(after_srv_period).unwrap(),
579 SrvPeriodOffset::from((25 * 60 + 19) * 60)
580 );
581 }
582}