use std::time::{Duration, SystemTime};
use crate::{Error, HsDirs, Result, params::NetParameters};
use time::{OffsetDateTime, UtcOffset};
use tor_hscrypto::time::TimePeriod;
use tor_netdoc::doc::netstatus::{MdConsensus, SharedRandVal};
#[cfg(feature = "hs-service")]
use tor_hscrypto::ope::SrvPeriodOffset;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HsDirParams {
pub(crate) time_period: TimePeriod,
pub(crate) shared_rand: SharedRandVal,
pub(crate) srv_lifespan: std::ops::Range<SystemTime>,
}
const VOTING_PERIODS_IN_OFFSET: u32 = 12;
const VOTING_PERIODS_IN_SRV_ROUND: u32 = 24;
const ONE_DAY: Duration = Duration::new(86400, 0);
impl HsDirParams {
pub fn time_period(&self) -> TimePeriod {
self.time_period
}
pub fn start_of_shard_rand_period(&self) -> SystemTime {
self.srv_lifespan.start
}
#[cfg(feature = "hs-service")]
pub fn offset_within_srv_period(&self, when: SystemTime) -> Option<SrvPeriodOffset> {
if when >= self.srv_lifespan.start {
let d = when
.duration_since(self.srv_lifespan.start)
.expect("Somehow, range comparison was not reliable!");
return Some(SrvPeriodOffset::from(d.as_secs() as u32));
}
None
}
pub(crate) fn compute(
consensus: &MdConsensus,
params: &NetParameters,
) -> Result<HsDirs<HsDirParams>> {
let srvs = extract_srvs(consensus);
let tp_length: Duration = params.hsdir_timeperiod_length.try_into().map_err(|_| {
Error::InvalidConsensus(
"Minutes in hsdir timeperiod could not be converted to a Duration",
)
})?;
let offset = consensus.lifetime().voting_period() * VOTING_PERIODS_IN_OFFSET;
let cur_period = TimePeriod::new(tp_length, consensus.lifetime().valid_after(), offset)
.map_err(|_| {
Error::InvalidConsensus("Consensus valid-after did not fall in a time period")
})?;
let current = find_params_for_time(&srvs[..], cur_period)?
.unwrap_or_else(|| disaster_params(cur_period));
#[cfg(feature = "hs-service")]
let secondary = [cur_period.prev(), cur_period.next()]
.iter()
.flatten()
.flat_map(|period| find_params_for_time(&srvs[..], *period).ok().flatten())
.collect();
Ok(HsDirs {
current,
#[cfg(feature = "hs-service")]
secondary,
})
}
}
fn disaster_params(period: TimePeriod) -> HsDirParams {
HsDirParams {
time_period: period,
shared_rand: disaster_srv(period),
srv_lifespan: period
.range()
.expect("Time period cannot be represented as SystemTime"),
}
}
fn disaster_srv(period: TimePeriod) -> SharedRandVal {
use digest::Digest;
let mut d = tor_llcrypto::d::Sha3_256::new();
d.update(b"shared-random-disaster");
d.update(u64::from(period.length().as_minutes()).to_be_bytes());
d.update(period.interval_num().to_be_bytes());
let v: [u8; 32] = d.finalize().into();
v.into()
}
type SrvInfo = (SharedRandVal, std::ops::Range<SystemTime>);
fn find_params_for_time(info: &[SrvInfo], period: TimePeriod) -> Result<Option<HsDirParams>> {
let start = period
.range()
.map_err(|_| {
Error::InvalidConsensus(
"HsDir time period in consensus could not be represented as a SystemTime range.",
)
})?
.start;
Ok(find_srv_for_time(info, start).map(|srv| HsDirParams {
time_period: period,
shared_rand: srv.0,
srv_lifespan: srv.1.clone(),
}))
}
fn find_srv_for_time(info: &[SrvInfo], when: SystemTime) -> Option<&SrvInfo> {
info.iter().find(|(_, range)| range.contains(&when))
}
fn extract_srvs(consensus: &MdConsensus) -> Vec<SrvInfo> {
let mut v = Vec::new();
let consensus_ts = consensus.lifetime().valid_after();
let srv_interval = srv_interval(consensus);
if let Some(cur) = consensus.shared_rand_cur() {
let ts_begin = cur
.timestamp()
.unwrap_or_else(|| start_of_day_containing(consensus_ts));
let ts_end = ts_begin + srv_interval;
v.push((*cur.value(), ts_begin..ts_end));
}
if let Some(prev) = consensus.shared_rand_prev() {
let ts_begin = prev
.timestamp()
.unwrap_or_else(|| start_of_day_containing(consensus_ts) - ONE_DAY);
let ts_end = ts_begin + srv_interval;
v.push((*prev.value(), ts_begin..ts_end));
}
v
}
fn srv_interval(consensus: &MdConsensus) -> Duration {
if let (Some(cur), Some(prev)) = (consensus.shared_rand_cur(), consensus.shared_rand_prev()) {
if let (Some(cur_ts), Some(prev_ts)) = (cur.timestamp(), prev.timestamp()) {
if let Ok(d) = cur_ts.duration_since(prev_ts) {
return d;
}
}
}
consensus.lifetime().voting_period() * VOTING_PERIODS_IN_SRV_ROUND
}
fn start_of_day_containing(t: SystemTime) -> SystemTime {
OffsetDateTime::from(t)
.to_offset(UtcOffset::UTC)
.replace_time(time::macros::time!(00:00))
.into()
}
#[cfg(test)]
mod test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_time_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use super::*;
use hex_literal::hex;
use tor_netdoc::doc::netstatus::{Lifetime, MdConsensusBuilder};
fn t(s: &str) -> SystemTime {
humantime::parse_rfc3339(s).unwrap()
}
fn d(s: &str) -> Duration {
humantime::parse_duration(s).unwrap()
}
fn example_lifetime() -> Lifetime {
Lifetime::new(
t("1985-10-25T07:00:00Z"),
t("1985-10-25T08:00:00Z"),
t("1985-10-25T10:00:00Z"),
)
.unwrap()
}
const SRV1: [u8; 32] = *b"next saturday night were sending";
const SRV2: [u8; 32] = *b"you......... back to the future!";
fn example_consensus_builder() -> MdConsensusBuilder {
let mut bld = MdConsensus::builder();
bld.consensus_method(34)
.lifetime(example_lifetime())
.param("bwweightscale", 1)
.param("hsdir_interval", 1440)
.weights("".parse().unwrap())
.shared_rand_prev(7, SRV1.into(), None)
.shared_rand_cur(7, SRV2.into(), None);
bld
}
#[test]
fn start_of_day() {
assert_eq!(
start_of_day_containing(t("1985-10-25T07:00:00Z")),
t("1985-10-25T00:00:00Z")
);
assert_eq!(
start_of_day_containing(t("1985-10-25T00:00:00Z")),
t("1985-10-25T00:00:00Z")
);
assert_eq!(
start_of_day_containing(t("1985-10-25T23:59:59.999Z")),
t("1985-10-25T00:00:00Z")
);
}
#[test]
fn vote_period() {
assert_eq!(example_lifetime().voting_period(), d("1 hour"));
let lt2 = Lifetime::new(
t("1985-10-25T07:00:00Z"),
t("1985-10-25T07:22:00Z"),
t("1985-10-25T07:59:00Z"),
)
.unwrap();
assert_eq!(lt2.voting_period(), d("22 min"));
}
#[test]
fn srv_period() {
let consensus = example_consensus_builder().testing_consensus().unwrap();
assert_eq!(srv_interval(&consensus), d("1 day"));
let consensus = example_consensus_builder()
.shared_rand_prev(7, SRV1.into(), Some(t("1985-10-25T00:00:00Z")))
.shared_rand_cur(7, SRV2.into(), Some(t("1985-10-25T06:00:05Z")))
.testing_consensus()
.unwrap();
assert_eq!(srv_interval(&consensus), d("6 hours 5 sec"));
let consensus = example_consensus_builder()
.shared_rand_cur(7, SRV1.into(), Some(t("1985-10-25T00:00:00Z")))
.shared_rand_prev(7, SRV2.into(), Some(t("1985-10-25T06:00:05Z")))
.testing_consensus()
.unwrap();
assert_eq!(srv_interval(&consensus), d("1 day"));
}
#[test]
fn srvs_extract_and_find() {
let consensus = example_consensus_builder().testing_consensus().unwrap();
let srvs = extract_srvs(&consensus);
assert_eq!(
srvs,
vec![
(
SRV2.into(),
t("1985-10-25T00:00:00Z")..t("1985-10-26T00:00:00Z")
),
(
SRV1.into(),
t("1985-10-24T00:00:00Z")..t("1985-10-25T00:00:00Z")
)
]
);
let consensus = example_consensus_builder()
.shared_rand_prev(7, SRV1.into(), Some(t("1985-10-25T00:00:00Z")))
.shared_rand_cur(7, SRV2.into(), Some(t("1985-10-25T06:00:05Z")))
.testing_consensus()
.unwrap();
let srvs = extract_srvs(&consensus);
assert_eq!(
srvs,
vec![
(
SRV2.into(),
t("1985-10-25T06:00:05Z")..t("1985-10-25T12:00:10Z")
),
(
SRV1.into(),
t("1985-10-25T00:00:00Z")..t("1985-10-25T06:00:05Z")
)
]
);
assert_eq!(None, find_srv_for_time(&srvs, t("1985-10-24T23:59:00Z")));
assert_eq!(
Some(&srvs[1]),
find_srv_for_time(&srvs, t("1985-10-25T00:00:00Z"))
);
assert_eq!(
Some(&srvs[1]),
find_srv_for_time(&srvs, t("1985-10-25T03:59:00Z"))
);
assert_eq!(
Some(&srvs[1]),
find_srv_for_time(&srvs, t("1985-10-25T00:00:00Z"))
);
assert_eq!(
Some(&srvs[0]),
find_srv_for_time(&srvs, t("1985-10-25T06:00:05Z"))
);
assert_eq!(
Some(&srvs[0]),
find_srv_for_time(&srvs, t("1985-10-25T12:00:00Z"))
);
assert_eq!(None, find_srv_for_time(&srvs, t("1985-10-25T12:00:30Z")));
}
#[test]
fn disaster() {
use digest::Digest;
use tor_llcrypto::d::Sha3_256;
let period = TimePeriod::new(d("1 day"), t("1970-01-02T17:33:00Z"), d("12 hours")).unwrap();
assert_eq!(period.length().as_minutes(), 86400 / 60);
assert_eq!(period.interval_num(), 1);
let dsrv = disaster_srv(period);
assert_eq!(
dsrv.as_ref(),
&hex!("F8A4948707653837FA44ABB5BBC75A12F6F101E7F8FAF699B9715F4965D3507D")
);
assert_eq!(
&dsrv.as_ref()[..],
&Sha3_256::digest(b"shared-random-disaster\0\0\0\0\0\0\x05\xA0\0\0\0\0\0\0\0\x01")[..]
);
}
#[test]
#[cfg(feature = "hs-service")]
fn ring_params_simple() {
let consensus = example_consensus_builder().testing_consensus().unwrap();
let netparams = NetParameters::from_map(consensus.params());
let HsDirs { current, secondary } = HsDirParams::compute(&consensus, &netparams).unwrap();
assert_eq!(
current.time_period,
TimePeriod::new(d("1 day"), t("1985-10-25T07:00:00Z"), d("12 hours")).unwrap()
);
assert_eq!(current.shared_rand.as_ref(), &SRV1);
assert_eq!(secondary.len(), 1);
assert_eq!(
secondary[0].time_period,
TimePeriod::new(d("1 day"), t("1985-10-25T12:00:00Z"), d("12 hours")).unwrap(),
);
assert_eq!(secondary[0].shared_rand.as_ref(), &SRV2);
}
#[test]
#[cfg(feature = "hs-service")]
fn ring_params_tricky() {
let consensus = example_consensus_builder()
.shared_rand_prev(7, SRV1.into(), Some(t("1985-10-25T00:00:00Z")))
.shared_rand_cur(7, SRV2.into(), Some(t("1985-10-25T05:00:00Z")))
.param("hsdir_interval", 120) .testing_consensus()
.unwrap();
let netparams = NetParameters::from_map(consensus.params());
let HsDirs { current, secondary } = HsDirParams::compute(&consensus, &netparams).unwrap();
assert_eq!(
current.time_period,
TimePeriod::new(d("2 hours"), t("1985-10-25T07:00:00Z"), d("12 hours")).unwrap()
);
assert_eq!(current.shared_rand.as_ref(), &SRV2);
assert_eq!(secondary.len(), 2);
assert_eq!(
secondary[0].time_period,
TimePeriod::new(d("2 hours"), t("1985-10-25T05:00:00Z"), d("12 hours")).unwrap()
);
assert_eq!(secondary[0].shared_rand.as_ref(), &SRV1);
assert_eq!(
secondary[1].time_period,
TimePeriod::new(d("2 hours"), t("1985-10-25T09:00:00Z"), d("12 hours")).unwrap()
);
assert_eq!(secondary[1].shared_rand.as_ref(), &SRV2);
}
#[test]
#[cfg(feature = "hs-service")]
fn offset_within_srv_period() {
let time_period =
TimePeriod::new(d("2 hours"), t("1985-10-25T05:00:00Z"), d("12 hours")).unwrap();
let srv_start = t("1985-10-25T09:00:00Z");
let srv_end = t("1985-10-25T20:00:00Z");
let srv_lifespan = srv_start..srv_end;
let params = HsDirParams {
time_period,
shared_rand: SRV1.into(),
srv_lifespan,
};
let before_srv_period = t("1985-10-25T08:59:00Z");
let after_srv_period = t("1985-10-26T10:19:00Z");
assert!(params.offset_within_srv_period(before_srv_period).is_none());
assert_eq!(
params.offset_within_srv_period(srv_start).unwrap(),
SrvPeriodOffset::from(0)
);
assert_eq!(
params.offset_within_srv_period(srv_end).unwrap(),
SrvPeriodOffset::from(11 * 60 * 60)
);
assert_eq!(
params.offset_within_srv_period(after_srv_period).unwrap(),
SrvPeriodOffset::from((25 * 60 + 19) * 60)
);
}
}