extern crate alloc;
use alloc::collections::BTreeSet;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use arc_swap::ArcSwap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct RevocationViewSubject(String);
impl RevocationViewSubject {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<&str> for RevocationViewSubject {
fn from(value: &str) -> Self {
Self(value.to_string())
}
}
impl From<String> for RevocationViewSubject {
fn from(value: String) -> Self {
Self(value)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RevocationSnapshot {
pub epoch: u64,
pub root_hash: [u8; 32],
pub issued_at_unix_ms: u64,
pub revoked: BTreeSet<RevocationViewSubject>,
}
impl RevocationSnapshot {
pub fn empty() -> Self {
Self {
epoch: 0,
root_hash: [0_u8; 32],
issued_at_unix_ms: 0,
revoked: BTreeSet::new(),
}
}
pub fn is_revoked(&self, subject: &RevocationViewSubject) -> bool {
self.revoked.contains(subject)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RevocationViewError {
NonMonotoneEpoch { candidate: u64, installed: u64 },
}
impl core::fmt::Display for RevocationViewError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::NonMonotoneEpoch {
candidate,
installed,
} => write!(
f,
"candidate snapshot epoch {candidate} is not strictly greater than installed epoch {installed}"
),
}
}
}
impl core::error::Error for RevocationViewError {}
#[derive(Debug)]
pub struct RevocationView {
inner: ArcSwap<RevocationSnapshot>,
}
impl Default for RevocationView {
fn default() -> Self {
Self::new()
}
}
impl RevocationView {
pub fn new() -> Self {
Self {
inner: ArcSwap::from_pointee(RevocationSnapshot::empty()),
}
}
pub fn load(&self) -> Arc<RevocationSnapshot> {
ArcSwap::load_full(&self.inner)
}
pub fn install_if_newer(
&self,
candidate: RevocationSnapshot,
) -> Result<Arc<RevocationSnapshot>, RevocationViewError> {
let candidate_epoch = candidate.epoch;
let candidate = Arc::new(candidate);
loop {
let current = self.load();
if candidate_epoch <= current.epoch {
return Err(RevocationViewError::NonMonotoneEpoch {
candidate: candidate_epoch,
installed: current.epoch,
});
}
let previous = self
.inner
.compare_and_swap(¤t, Arc::clone(&candidate));
if Arc::ptr_eq(¤t, &*previous) {
return Ok(current);
}
}
}
pub fn is_revoked(&self, subject: &RevocationViewSubject) -> bool {
self.load().is_revoked(subject)
}
pub fn current_epoch(&self) -> u64 {
self.load().epoch
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
extern crate std;
use self::std::sync::Barrier;
use self::std::thread;
use super::*;
use alloc::vec;
use alloc::vec::Vec;
fn snapshot(epoch: u64, revoked: &[&str]) -> RevocationSnapshot {
let revoked_set = revoked
.iter()
.copied()
.map(RevocationViewSubject::from)
.collect();
RevocationSnapshot {
epoch,
root_hash: [(epoch as u8); 32],
issued_at_unix_ms: 1_700_000_000_000 + epoch,
revoked: revoked_set,
}
}
#[test]
fn new_view_holds_empty_sentinel() {
let view = RevocationView::new();
let loaded = view.load();
assert_eq!(*loaded, RevocationSnapshot::empty());
assert_eq!(view.current_epoch(), 0);
}
#[test]
fn install_if_newer_advances_monotonically() {
let view = RevocationView::new();
let prev = view.install_if_newer(snapshot(1, &["alice"])).unwrap();
assert_eq!(prev.epoch, 0);
let prev2 = view
.install_if_newer(snapshot(5, &["alice", "bob"]))
.unwrap();
assert_eq!(prev2.epoch, 1);
assert_eq!(view.current_epoch(), 5);
assert!(view.is_revoked(&RevocationViewSubject::from("alice")));
assert!(view.is_revoked(&RevocationViewSubject::from("bob")));
assert!(!view.is_revoked(&RevocationViewSubject::from("carol")));
}
#[test]
fn install_if_newer_rejects_equal_epoch() {
let view = RevocationView::new();
view.install_if_newer(snapshot(1, &["alice"])).unwrap();
let err = view
.install_if_newer(snapshot(1, &["alice", "bob"]))
.expect_err("equal epoch must fail closed");
assert_eq!(
err,
RevocationViewError::NonMonotoneEpoch {
candidate: 1,
installed: 1
}
);
assert!(!view.is_revoked(&RevocationViewSubject::from("bob")));
}
#[test]
fn install_if_newer_rejects_stale_epoch() {
let view = RevocationView::new();
view.install_if_newer(snapshot(5, &["alice"])).unwrap();
let err = view
.install_if_newer(snapshot(3, &["dave"]))
.expect_err("stale epoch must fail closed");
assert_eq!(
err,
RevocationViewError::NonMonotoneEpoch {
candidate: 3,
installed: 5
}
);
assert!(!view.is_revoked(&RevocationViewSubject::from("dave")));
}
#[test]
fn install_if_newer_is_atomic_across_concurrent_writers() {
let view = Arc::new(RevocationView::new());
let barrier = Arc::new(Barrier::new(8));
let mut handles = Vec::new();
for epoch in 1..=8_u64 {
let view = Arc::clone(&view);
let barrier = Arc::clone(&barrier);
handles.push(thread::spawn(move || {
barrier.wait();
let _ = view.install_if_newer(snapshot(epoch, &["alice"]));
}));
}
for handle in handles {
handle.join().unwrap();
}
assert_eq!(view.current_epoch(), 8);
assert!(view.is_revoked(&RevocationViewSubject::from("alice")));
let err = view
.install_if_newer(snapshot(7, &["bob"]))
.expect_err("stale concurrent write must fail closed");
assert_eq!(
err,
RevocationViewError::NonMonotoneEpoch {
candidate: 7,
installed: 8
}
);
assert!(!view.is_revoked(&RevocationViewSubject::from("bob")));
}
#[test]
fn snapshot_round_trips_via_serde() {
let s = snapshot(7, &["alice", "bob"]);
let bytes = serde_json::to_vec(&s).unwrap();
let decoded: RevocationSnapshot = serde_json::from_slice(&bytes).unwrap();
assert_eq!(decoded, s);
}
#[test]
fn snapshot_deny_unknown_fields() {
let s = snapshot(2, &["alice"]);
let mut value: serde_json::Value = serde_json::to_value(&s).unwrap();
value
.as_object_mut()
.unwrap()
.insert("extra".to_string(), serde_json::Value::Bool(true));
let bytes = serde_json::to_vec(&value).unwrap();
let parsed: Result<RevocationSnapshot, _> = serde_json::from_slice(&bytes);
assert!(parsed.is_err(), "unknown fields must be rejected");
}
#[test]
fn empty_sentinel_is_default_install_target() {
let view = RevocationView::default();
assert_eq!(*view.load(), RevocationSnapshot::empty());
}
#[test]
fn snapshot_preserves_revoked_ordering() {
let s = snapshot(1, &["delta", "alpha", "charlie", "bravo"]);
let collected: Vec<String> = s.revoked.iter().map(|k| k.as_str().to_string()).collect();
assert_eq!(
collected,
vec![
String::from("alpha"),
String::from("bravo"),
String::from("charlie"),
String::from("delta"),
]
);
}
}