#![allow(dead_code)]
use alloc::vec::Vec;
use core::time::Duration;
use crate::rng::RngCore;
pub(crate) struct PathChallengeState {
outstanding: Vec<([u8; 8], Duration)>,
pending_response: Vec<[u8; 8]>,
}
const PATH_CHALLENGE_CAP: usize = 8;
impl PathChallengeState {
pub(crate) fn new() -> Self {
Self {
outstanding: Vec::new(),
pending_response: Vec::new(),
}
}
pub(crate) fn issue<R: RngCore>(&mut self, rng: &mut R, now: Duration) -> [u8; 8] {
let mut data = [0u8; 8];
rng.fill_bytes(&mut data);
if self.outstanding.len() >= PATH_CHALLENGE_CAP {
self.outstanding.remove(0);
}
self.outstanding.push((data, now));
data
}
pub(crate) fn on_challenge(&mut self, data: [u8; 8]) {
if self.pending_response.len() < PATH_CHALLENGE_CAP {
if !self.pending_response.contains(&data) {
self.pending_response.push(data);
}
}
}
pub(crate) fn on_response(&mut self, data: [u8; 8]) -> bool {
if let Some(idx) = self.outstanding.iter().position(|(d, _)| d == &data) {
self.outstanding.remove(idx);
true
} else {
false
}
}
pub(crate) fn pop_outbound_response(&mut self) -> Option<[u8; 8]> {
if self.pending_response.is_empty() {
None
} else {
Some(self.pending_response.remove(0))
}
}
pub(crate) fn gc(&mut self, now: Duration, max_age: Duration) {
self.outstanding
.retain(|(_, t)| now.saturating_sub(*t) <= max_age);
}
pub(crate) fn has_outstanding(&self) -> bool {
!self.outstanding.is_empty()
}
pub(crate) fn has_pending_response(&self) -> bool {
!self.pending_response.is_empty()
}
}
impl Default for PathChallengeState {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hash::Sha256;
use crate::rng::HmacDrbg;
#[test]
fn path_challenge_response_roundtrip() {
let mut p = PathChallengeState::new();
let mut rng = HmacDrbg::<Sha256>::new(b"path-test", b"nonce", &[]);
let data = p.issue(&mut rng, Duration::from_millis(0));
assert!(p.has_outstanding());
assert!(p.on_response(data));
assert!(!p.has_outstanding());
assert!(!p.on_response(data));
}
#[test]
fn path_challenge_rejects_unsolicited() {
let mut p = PathChallengeState::new();
assert!(!p.on_response([1, 2, 3, 4, 5, 6, 7, 8]));
}
#[test]
fn path_challenge_queues_response() {
let mut p = PathChallengeState::new();
let chal = [0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8];
p.on_challenge(chal);
assert!(p.has_pending_response());
let popped = p.pop_outbound_response().expect("response queued");
assert_eq!(popped, chal);
assert!(!p.has_pending_response());
assert!(p.pop_outbound_response().is_none());
}
#[test]
fn path_challenge_dedups_pending_response() {
let mut p = PathChallengeState::new();
let chal = [0u8, 1, 2, 3, 4, 5, 6, 7];
p.on_challenge(chal);
p.on_challenge(chal); let _ = p.pop_outbound_response().expect("one response");
assert!(p.pop_outbound_response().is_none());
}
#[test]
fn path_challenge_gc_expires_old() {
let mut p = PathChallengeState::new();
let mut rng = HmacDrbg::<Sha256>::new(b"gc", b"n", &[]);
let _ = p.issue(&mut rng, Duration::from_secs(0));
let _ = p.issue(&mut rng, Duration::from_secs(10));
p.gc(Duration::from_secs(12), Duration::from_secs(5));
assert!(p.has_outstanding());
p.gc(Duration::from_secs(12), Duration::from_secs(1));
assert!(!p.has_outstanding());
}
#[test]
fn path_challenge_outstanding_capped() {
let mut p = PathChallengeState::new();
let mut rng = HmacDrbg::<Sha256>::new(b"cap", b"n", &[]);
let mut first = None;
for i in 0..(PATH_CHALLENGE_CAP + 3) {
let d = p.issue(&mut rng, Duration::from_millis(i as u64));
if first.is_none() {
first = Some(d);
}
}
let first = first.unwrap();
assert!(
!p.on_response(first),
"oldest challenge should have dropped"
);
}
}