use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum KeyParity {
Even,
Odd,
}
impl KeyParity {
pub fn other(self) -> Self {
match self {
KeyParity::Even => KeyParity::Odd,
KeyParity::Odd => KeyParity::Even,
}
}
pub fn name(&self) -> &'static str {
match self {
KeyParity::Even => "even",
KeyParity::Odd => "odd",
}
}
}
broadcast_common::impl_spec_display!(KeyParity);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KmRefreshThresholds {
pub refresh_period: u64,
pub pre_announcement_period: u64,
}
impl KmRefreshThresholds {
pub const RECOMMENDED: KmRefreshThresholds = KmRefreshThresholds {
refresh_period: 1 << 25,
pre_announcement_period: 4000,
};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum KmRefreshEvent {
PreAnnounce {
next_parity: KeyParity,
},
Switchover {
new_active: KeyParity,
},
Decommission {
retired: KeyParity,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct KmRefreshDriver {
thresholds: KmRefreshThresholds,
active: KeyParity,
epoch_start: u64,
total_sent: u64,
pre_announced: bool,
switched_over: bool,
decommissioned: bool,
}
impl KmRefreshDriver {
pub fn new(thresholds: KmRefreshThresholds, initial_parity: KeyParity) -> Self {
KmRefreshDriver {
thresholds,
active: initial_parity,
epoch_start: 0,
total_sent: 0,
pre_announced: false,
switched_over: false,
decommissioned: false,
}
}
pub fn active_parity(&self) -> KeyParity {
self.active
}
pub fn total_sent(&self) -> u64 {
self.total_sent
}
pub fn is_key_valid(&self, parity: KeyParity) -> bool {
if parity == self.active {
return true;
}
self.switched_over && !self.decommissioned
}
pub fn on_packet_sent(&mut self, n: u64) -> Vec<KmRefreshEvent> {
self.total_sent = self.total_sent.saturating_add(n);
let mut events = Vec::new();
let since_epoch = self.total_sent.saturating_sub(self.epoch_start);
let pre_announce_at = self
.thresholds
.refresh_period
.saturating_sub(self.thresholds.pre_announcement_period);
if !self.pre_announced && since_epoch >= pre_announce_at {
self.pre_announced = true;
events.push(KmRefreshEvent::PreAnnounce {
next_parity: self.active.other(),
});
}
if !self.switched_over && since_epoch >= self.thresholds.refresh_period {
self.switched_over = true;
let new_active = self.active.other();
self.active = new_active;
self.epoch_start += self.thresholds.refresh_period;
events.push(KmRefreshEvent::Switchover { new_active });
}
if self.switched_over && !self.decommissioned {
let since_switchover = self.total_sent.saturating_sub(self.epoch_start);
if since_switchover >= self.thresholds.pre_announcement_period {
self.decommissioned = true;
events.push(KmRefreshEvent::Decommission {
retired: self.active.other(),
});
self.pre_announced = false;
self.switched_over = false;
self.decommissioned = false;
}
}
events
}
pub fn tick(&mut self) -> Vec<KmRefreshEvent> {
self.on_packet_sent(1)
}
}
#[cfg(test)]
mod tests {
use super::*;
const SCALED: KmRefreshThresholds = KmRefreshThresholds {
refresh_period: 100,
pre_announcement_period: 10,
};
#[test]
fn recommended_thresholds_match_spec_values() {
assert_eq!(KmRefreshThresholds::RECOMMENDED.refresh_period, 1 << 25);
assert_eq!(
KmRefreshThresholds::RECOMMENDED.pre_announcement_period,
4000
);
}
#[test]
fn key_parity_alternates_and_labels() {
assert_eq!(KeyParity::Even.other(), KeyParity::Odd);
assert_eq!(KeyParity::Odd.other(), KeyParity::Even);
assert_eq!(KeyParity::Even.to_string(), "even");
assert_eq!(KeyParity::Odd.to_string(), "odd");
}
#[test]
fn fires_pre_announce_switchover_decommission_in_order() {
let mut d = KmRefreshDriver::new(SCALED, KeyParity::Even);
assert_eq!(d.active_parity(), KeyParity::Even);
assert!(d.is_key_valid(KeyParity::Even));
assert!(!d.is_key_valid(KeyParity::Odd));
assert_eq!(d.on_packet_sent(89), Vec::new());
assert_eq!(d.active_parity(), KeyParity::Even);
assert_eq!(
d.on_packet_sent(1),
alloc::vec![KmRefreshEvent::PreAnnounce {
next_parity: KeyParity::Odd
}]
);
assert_eq!(d.active_parity(), KeyParity::Even);
assert!(d.is_key_valid(KeyParity::Even));
assert!(!d.is_key_valid(KeyParity::Odd));
assert_eq!(d.on_packet_sent(1), Vec::new());
assert_eq!(d.on_packet_sent(8), Vec::new()); assert_eq!(
d.on_packet_sent(1), alloc::vec![KmRefreshEvent::Switchover {
new_active: KeyParity::Odd
}]
);
assert_eq!(d.active_parity(), KeyParity::Odd);
assert!(d.is_key_valid(KeyParity::Odd));
assert!(d.is_key_valid(KeyParity::Even));
assert_eq!(d.on_packet_sent(9), Vec::new()); assert_eq!(
d.on_packet_sent(1), alloc::vec![KmRefreshEvent::Decommission {
retired: KeyParity::Even
}]
);
assert_eq!(d.active_parity(), KeyParity::Odd);
assert!(d.is_key_valid(KeyParity::Odd));
assert!(!d.is_key_valid(KeyParity::Even), "old key must be dropped");
}
#[test]
fn large_jump_fires_all_three_events_in_one_call() {
let mut d = KmRefreshDriver::new(SCALED, KeyParity::Even);
let events = d.on_packet_sent(115);
assert_eq!(
events,
alloc::vec![
KmRefreshEvent::PreAnnounce {
next_parity: KeyParity::Odd
},
KmRefreshEvent::Switchover {
new_active: KeyParity::Odd
},
KmRefreshEvent::Decommission {
retired: KeyParity::Even
},
]
);
assert_eq!(d.active_parity(), KeyParity::Odd);
assert!(!d.is_key_valid(KeyParity::Even));
}
#[test]
fn tick_is_a_single_packet_and_rotation_repeats_forever() {
let mut d = KmRefreshDriver::new(SCALED, KeyParity::Even);
let mut all = Vec::new();
for _ in 0..250 {
all.extend(d.tick());
}
assert_eq!(d.total_sent(), 250);
let switchovers: Vec<_> = all
.iter()
.filter(|e| matches!(e, KmRefreshEvent::Switchover { .. }))
.collect();
assert_eq!(switchovers.len(), 2);
assert_eq!(
switchovers[0],
&KmRefreshEvent::Switchover {
new_active: KeyParity::Odd
}
);
assert_eq!(
switchovers[1],
&KmRefreshEvent::Switchover {
new_active: KeyParity::Even
}
);
assert_eq!(d.active_parity(), KeyParity::Even);
}
}