1use std::time::{Duration, SystemTime};
27
28use crate::{params::NetParameters, Error, HsDirs, Result};
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)?
151 .unwrap_or_else(|| disaster_params(cur_period));
152
153 #[cfg(feature = "hs-service")]
156 let secondary = [cur_period.prev(), cur_period.next()]
157 .iter()
158 .flatten()
159 .flat_map(|period| find_params_for_time(&srvs[..], *period).ok().flatten())
160 .collect();
161
162 Ok(HsDirs {
163 current,
164 #[cfg(feature = "hs-service")]
165 secondary,
166 })
167 }
168}
169
170fn disaster_params(period: TimePeriod) -> HsDirParams {
172 HsDirParams {
173 time_period: period,
174 shared_rand: disaster_srv(period),
175 srv_lifespan: period
176 .range()
177 .expect("Time period cannot be represented as SystemTime"),
178 }
179}
180
181fn disaster_srv(period: TimePeriod) -> SharedRandVal {
186 use digest::Digest;
187 let mut d = tor_llcrypto::d::Sha3_256::new();
188 d.update(b"shared-random-disaster");
189 d.update(u64::from(period.length().as_minutes()).to_be_bytes());
190 d.update(period.interval_num().to_be_bytes());
191
192 let v: [u8; 32] = d.finalize().into();
193 v.into()
194}
195
196type SrvInfo = (SharedRandVal, std::ops::Range<SystemTime>);
199
200fn find_params_for_time(info: &[SrvInfo], period: TimePeriod) -> Result<Option<HsDirParams>> {
203 let start = period
204 .range()
205 .map_err(|_| {
206 Error::InvalidConsensus(
207 "HsDir time period in consensus could not be represented as a SystemTime range.",
208 )
209 })?
210 .start;
211
212 Ok(find_srv_for_time(info, start).map(|srv| HsDirParams {
213 time_period: period,
214 shared_rand: srv.0,
215 srv_lifespan: srv.1.clone(),
216 }))
217}
218
219fn find_srv_for_time(info: &[SrvInfo], when: SystemTime) -> Option<&SrvInfo> {
222 info.iter().find(|(_, range)| range.contains(&when))
223}
224
225fn extract_srvs(consensus: &MdConsensus) -> Vec<SrvInfo> {
228 let mut v = Vec::new();
229 let consensus_ts = consensus.lifetime().valid_after();
230 let srv_interval = srv_interval(consensus);
231
232 if let Some(cur) = consensus.shared_rand_cur() {
233 let ts_begin = cur
234 .timestamp()
235 .unwrap_or_else(|| start_of_day_containing(consensus_ts));
236 let ts_end = ts_begin + srv_interval;
237 v.push((*cur.value(), ts_begin..ts_end));
238 }
239 if let Some(prev) = consensus.shared_rand_prev() {
240 let ts_begin = prev
241 .timestamp()
242 .unwrap_or_else(|| start_of_day_containing(consensus_ts) - ONE_DAY);
243 let ts_end = ts_begin + srv_interval;
244 v.push((*prev.value(), ts_begin..ts_end));
245 }
246
247 v
248}
249
250fn srv_interval(consensus: &MdConsensus) -> Duration {
252 if let (Some(cur), Some(prev)) = (consensus.shared_rand_cur(), consensus.shared_rand_prev()) {
258 if let (Some(cur_ts), Some(prev_ts)) = (cur.timestamp(), prev.timestamp()) {
259 if let Ok(d) = cur_ts.duration_since(prev_ts) {
260 return d;
261 }
262 }
263 }
264
265 consensus.lifetime().voting_period() * VOTING_PERIODS_IN_SRV_ROUND
269}
270
271fn start_of_day_containing(t: SystemTime) -> SystemTime {
277 OffsetDateTime::from(t)
278 .to_offset(UtcOffset::UTC)
279 .replace_time(time::macros::time!(00:00))
280 .into()
281}
282
283#[cfg(test)]
284mod test {
285 #![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 use super::*;
299 use hex_literal::hex;
300 use tor_netdoc::doc::netstatus::{ConsensusBuilder, Lifetime, MdConsensusRouterStatus};
301
302 fn t(s: &str) -> SystemTime {
308 humantime::parse_rfc3339(s).unwrap()
309 }
310 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 let consensus = example_consensus_builder().testing_consensus().unwrap();
379 assert_eq!(srv_interval(&consensus), d("1 day"));
380
381 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 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 (
408 SRV2.into(),
409 t("1985-10-25T00:00:00Z")..t("1985-10-26T00:00:00Z")
410 ),
411 (
414 SRV1.into(),
415 t("1985-10-24T00:00:00Z")..t("1985-10-25T00:00:00Z")
416 )
417 ]
418 );
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 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 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 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 assert_eq!(current.shared_rand.as_ref(), &SRV1);
501
502 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 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) .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 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 assert_eq!(
571 params.offset_within_srv_period(srv_end).unwrap(),
572 SrvPeriodOffset::from(11 * 60 * 60)
573 );
574 assert_eq!(
576 params.offset_within_srv_period(after_srv_period).unwrap(),
577 SrvPeriodOffset::from((25 * 60 + 19) * 60)
578 );
579 }
580}