extern crate std;
#[cfg(any(feature = "tokio-client", feature = "blocking-client"))]
use alloc::vec::Vec;
use std::time::Duration;
use crate::verify::VerifiedTime;
#[cfg(feature = "tokio-client")]
mod tokio_client;
#[cfg(feature = "tokio-client")]
pub use tokio_client::TokioClient;
#[cfg(feature = "blocking-client")]
mod blocking_client;
#[cfg(feature = "blocking-client")]
pub use blocking_client::BlockingClient;
#[derive(Debug, Clone, Copy)]
pub struct RoughtimeClientConfig {
pub timeout: Duration,
pub retries: u8,
pub max_spread: Duration,
}
impl Default for RoughtimeClientConfig {
fn default() -> Self {
Self {
timeout: Duration::from_secs(15),
retries: 2,
max_spread: Duration::from_secs(15),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ConsensusResult {
pub time: VerifiedTime,
pub spread_warning: Option<Duration>,
}
#[cfg(any(feature = "tokio-client", feature = "blocking-client"))]
pub(crate) fn consensus(
mut times: Vec<VerifiedTime>,
max_spread: Duration,
) -> Result<ConsensusResult, crate::Error> {
if times.is_empty() {
return Err(crate::Error::NoValidResponses);
}
times.sort_unstable_by_key(|t| t.midpoint_micros);
let median = if times.len() % 2 == 1 {
times[times.len() / 2]
} else {
let mid = times.len() / 2;
let a = times[mid - 1].midpoint_micros;
let b = times[mid].midpoint_micros;
VerifiedTime {
midpoint_micros: u64::midpoint(a, b),
radius_micros: times[mid - 1].radius_micros.max(times[mid].radius_micros),
}
};
let spread_warning = if times.len() > 1 {
let first = times.first().map_or(0, |t| t.midpoint_micros);
let last = times.last().map_or(0, |t| t.midpoint_micros);
let spread = Duration::from_micros(last.saturating_sub(first));
(spread > max_spread).then_some(spread)
} else {
None
};
Ok(ConsensusResult {
time: median,
spread_warning,
})
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use alloc::vec;
use super::{Duration, VerifiedTime, consensus};
use crate::Error;
fn time(midpoint_micros: u64) -> VerifiedTime {
VerifiedTime {
midpoint_micros,
radius_micros: 1_000_000,
}
}
#[test]
fn empty_input_is_no_valid_responses() {
let err = consensus(vec![], Duration::from_secs(10)).unwrap_err();
assert_eq!(err, Error::NoValidResponses);
}
#[test]
fn single_response_is_its_own_median_with_no_spread_warning() {
let result = consensus(vec![time(100)], Duration::from_secs(10)).unwrap();
assert_eq!(result.time.midpoint_micros, 100);
assert_eq!(result.spread_warning, None);
}
#[test]
fn odd_count_takes_the_middle_value() {
let result = consensus(vec![time(30), time(10), time(20)], Duration::from_secs(10)).unwrap();
assert_eq!(result.time.midpoint_micros, 20);
}
#[test]
fn even_count_averages_the_two_middle_values() {
let result = consensus(
vec![time(10), time(20), time(30), time(40)],
Duration::from_secs(10),
)
.unwrap();
assert_eq!(result.time.midpoint_micros, 25);
}
#[test]
fn spread_beyond_max_is_flagged() {
let result = consensus(
vec![time(0), time(5_000_000)],
Duration::from_secs(1),
)
.unwrap();
assert_eq!(result.spread_warning, Some(Duration::from_secs(5)));
}
#[test]
fn spread_within_max_is_not_flagged() {
let result = consensus(
vec![time(0), time(500_000)],
Duration::from_secs(1),
)
.unwrap();
assert_eq!(result.spread_warning, None);
}
}