use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::{Duration, Instant, SystemTime};
use acdp_types::revocation::{KeyRevocation, RevocationTrustClass};
pub const MAX_MARKERS: usize = 4096;
pub const MAX_FACTS_PER_CLASS: usize = 256;
#[derive(Clone)]
struct StoredFact {
rev: KeyRevocation,
origin: String,
}
#[derive(Default)]
struct FactSet {
facts: Vec<StoredFact>,
producer_signed_full: bool,
attested_full: HashSet<String>,
}
impl FactSet {
fn count(&self, class: RevocationTrustClass, origin: &str) -> usize {
self.facts
.iter()
.filter(|f| {
f.rev.trust_class == class
&& (class == RevocationTrustClass::ProducerSigned || f.origin == origin)
})
.count()
}
fn full(&self, class: RevocationTrustClass, origin: &str) -> bool {
match class {
RevocationTrustClass::ProducerSigned => self.producer_signed_full,
RevocationTrustClass::RegistryAttested => self.attested_full.contains(origin),
}
}
fn mark_full(&mut self, class: RevocationTrustClass, origin: &str) {
match class {
RevocationTrustClass::ProducerSigned => self.producer_signed_full = true,
RevocationTrustClass::RegistryAttested => {
self.attested_full.insert(origin.to_string());
}
}
}
}
type MarkerKey = (String, String, bool);
#[derive(Clone, Copy)]
struct Marker {
minted: Instant,
observed: SystemTime,
}
#[derive(Default)]
struct Inner {
facts: HashMap<String, FactSet>,
markers: HashMap<MarkerKey, Marker>,
}
fn class_key(class: RevocationTrustClass) -> bool {
match class {
RevocationTrustClass::ProducerSigned => false,
RevocationTrustClass::RegistryAttested => true,
}
}
#[derive(Clone)]
pub struct RevocationCache {
inner: Arc<Mutex<Inner>>,
}
impl RevocationCache {
#[must_use]
pub fn new() -> Self {
Self {
inner: Arc::new(Mutex::new(Inner::default())),
}
}
fn lock(&self) -> MutexGuard<'_, Inner> {
self.inner
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub(crate) fn facts_for(
&self,
agent_id: &str,
include_registry_attested: bool,
current_vantage: Option<&str>,
) -> Vec<(KeyRevocation, String)> {
let inner = self.lock();
let Some(set) = inner.facts.get(agent_id) else {
return Vec::new();
};
set.facts
.iter()
.filter(|f| match f.rev.trust_class {
RevocationTrustClass::ProducerSigned => true,
RevocationTrustClass::RegistryAttested => {
include_registry_attested && current_vantage.is_some_and(|v| v == f.origin)
}
})
.map(|f| (f.rev.clone(), f.origin.clone()))
.collect()
}
pub(crate) fn fresh_marker(
&self,
authority: &str,
agent_id: &str,
class: RevocationTrustClass,
freshness: Duration,
) -> Option<SystemTime> {
if freshness.is_zero() {
return None;
}
let inner = self.inner.lock().ok()?;
let key = (
agent_id.to_string(),
authority.to_string(),
class_key(class),
);
inner
.markers
.get(&key)
.filter(|m| m.minted.elapsed() < freshness)
.map(|m| m.observed)
}
#[cfg(test)]
fn marker_fresh(
&self,
authority: &str,
agent_id: &str,
class: RevocationTrustClass,
freshness: Duration,
) -> bool {
self.fresh_marker(authority, agent_id, class, freshness)
.is_some()
}
pub(crate) fn record_success(
&self,
authority: &str,
agent_id: &str,
class: RevocationTrustClass,
facts: &[KeyRevocation],
) {
let mut inner = self.lock();
let set = inner.facts.entry(agent_id.to_string()).or_default();
for f in facts {
if set.facts.iter().any(|existing| existing.rev == *f) {
continue;
}
let fact_origin = authority;
if set.count(f.trust_class, fact_origin) >= MAX_FACTS_PER_CLASS {
set.mark_full(f.trust_class, fact_origin);
continue;
}
set.facts.push(StoredFact {
rev: f.clone(),
origin: fact_origin.to_string(),
});
}
let full = set.full(class, authority);
let key = (
agent_id.to_string(),
authority.to_string(),
class_key(class),
);
if full {
inner.markers.remove(&key);
return;
}
if !inner.markers.contains_key(&key) && inner.markers.len() >= MAX_MARKERS {
if let Some(oldest) = inner
.markers
.iter()
.min_by_key(|(_, m)| m.minted)
.map(|(k, _)| k.clone())
{
inner.markers.remove(&oldest);
}
}
inner.markers.insert(
key,
Marker {
minted: Instant::now(),
observed: SystemTime::now(),
},
);
}
#[doc(hidden)]
#[cfg(feature = "test-transport")]
#[must_use]
pub fn fact_count(&self, agent_id: &str) -> usize {
self.lock().facts.get(agent_id).map_or(0, |s| s.facts.len())
}
}
impl Default for RevocationCache {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for RevocationCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RevocationCache").finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{DateTime, Utc};
fn rev(fp: &str, class: RevocationTrustClass) -> KeyRevocation {
KeyRevocation {
revoked_key_fingerprint: fp.into(),
compromised_since: "2026-05-01T00:00:00.000Z".parse::<DateTime<Utc>>().unwrap(),
reason: None,
revoked_key_id: None,
revoked_key_controller: acdp_types::primitives::AgentDid::new(
"did:web:agents.example.com:p",
),
publisher: acdp_types::primitives::AgentDid::new("did:web:agents.example.com:p"),
trust_class: class,
ctx_id: None,
}
}
#[test]
fn record_success_dedups_identical_facts() {
let cache = RevocationCache::new();
let r = rev("sha256:aaaa", RevocationTrustClass::ProducerSigned);
for _ in 0..5 {
cache.record_success(
"reg.example",
"did:web:p",
RevocationTrustClass::ProducerSigned,
std::slice::from_ref(&r),
);
}
assert_eq!(
cache
.facts_for("did:web:p", true, Some("reg.example"))
.len(),
1
);
}
#[test]
fn record_success_caps_admission_and_drops_markers_at_cap() {
let cache = RevocationCache::new();
for i in 0..(MAX_FACTS_PER_CLASS + 5) {
let fp = format!("sha256:{i:064x}");
let r = rev(&fp, RevocationTrustClass::ProducerSigned);
cache.record_success(
"reg.example",
"did:web:p",
RevocationTrustClass::ProducerSigned,
&[r],
);
}
assert_eq!(
cache
.facts_for("did:web:p", true, Some("reg.example"))
.len(),
MAX_FACTS_PER_CLASS
);
assert!(!cache.marker_fresh(
"reg.example",
"did:web:p",
RevocationTrustClass::ProducerSigned,
Duration::from_secs(3600)
));
}
#[test]
fn attested_cap_is_per_origin() {
let cache = RevocationCache::new();
for i in 0..=MAX_FACTS_PER_CLASS {
let fp = format!("sha256:{i:064x}");
cache.record_success(
"a.example",
"did:web:p",
RevocationTrustClass::RegistryAttested,
&[rev(&fp, RevocationTrustClass::RegistryAttested)],
);
}
let fresh = |auth: &str, class| {
cache.marker_fresh(auth, "did:web:p", class, Duration::from_secs(3600))
};
assert!(!fresh("a.example", RevocationTrustClass::RegistryAttested));
cache.record_success(
"b.example",
"did:web:p",
RevocationTrustClass::RegistryAttested,
&[],
);
cache.record_success(
"a.example",
"did:web:p",
RevocationTrustClass::ProducerSigned,
&[],
);
assert!(fresh("b.example", RevocationTrustClass::RegistryAttested));
assert!(fresh("a.example", RevocationTrustClass::ProducerSigned));
}
#[test]
fn facts_for_filters_by_trust_class() {
let cache = RevocationCache::new();
let attested = rev("sha256:bbbb", RevocationTrustClass::RegistryAttested);
cache.record_success(
"reg.example",
"did:web:p",
RevocationTrustClass::RegistryAttested,
&[attested],
);
assert!(cache
.facts_for("did:web:p", false, Some("reg.example"))
.is_empty());
assert_eq!(
cache
.facts_for("did:web:p", true, Some("reg.example"))
.len(),
1
);
}
#[test]
fn facts_for_filters_registry_attested_facts_by_origin() {
let cache = RevocationCache::new();
let attested = rev("sha256:cccc", RevocationTrustClass::RegistryAttested);
cache.record_success(
"a.example",
"did:web:p",
RevocationTrustClass::RegistryAttested,
&[attested],
);
assert_eq!(
cache.facts_for("did:web:p", true, Some("a.example")).len(),
1,
"reading from the SAME vantage that minted the fact must include it"
);
assert!(
cache
.facts_for("did:web:p", true, Some("b.example"))
.is_empty(),
"reading from a DIFFERENT vantage must exclude a registry-attested fact"
);
assert!(
cache.facts_for("did:web:p", true, None).is_empty(),
"reading with no resolvable current vantage must exclude a registry-attested \
fact, not include it by default"
);
}
#[test]
fn facts_for_never_filters_producer_signed_facts_by_origin() {
let cache = RevocationCache::new();
let signed = rev("sha256:dddd", RevocationTrustClass::ProducerSigned);
cache.record_success(
"a.example",
"did:web:p",
RevocationTrustClass::ProducerSigned,
&[signed],
);
assert_eq!(
cache.facts_for("did:web:p", false, Some("b.example")).len(),
1
);
assert_eq!(cache.facts_for("did:web:p", false, None).len(), 1);
}
#[test]
fn markers_are_per_vantage() {
let cache = RevocationCache::new();
cache.record_success(
"a.example",
"did:web:p",
RevocationTrustClass::ProducerSigned,
&[],
);
assert!(cache.marker_fresh(
"a.example",
"did:web:p",
RevocationTrustClass::ProducerSigned,
Duration::from_secs(60)
));
assert!(!cache.marker_fresh(
"b.example",
"did:web:p",
RevocationTrustClass::ProducerSigned,
Duration::from_secs(60)
));
}
#[test]
fn markers_are_per_trust_class() {
let cache = RevocationCache::new();
cache.record_success(
"a.example",
"did:web:p",
RevocationTrustClass::ProducerSigned,
&[],
);
assert!(!cache.marker_fresh(
"a.example",
"did:web:p",
RevocationTrustClass::RegistryAttested,
Duration::from_secs(60)
));
}
#[test]
fn zero_freshness_never_suppresses() {
let cache = RevocationCache::new();
cache.record_success(
"a.example",
"did:web:p",
RevocationTrustClass::ProducerSigned,
&[],
);
assert!(!cache.marker_fresh(
"a.example",
"did:web:p",
RevocationTrustClass::ProducerSigned,
Duration::ZERO
));
}
#[test]
fn facts_are_never_evicted_by_marker_pressure() {
let cache = RevocationCache::new();
let revoked = rev(
"sha256:aaaa000000000000000000000000000000000000000000000000000000000000",
RevocationTrustClass::ProducerSigned,
);
cache.record_success(
"reg.example",
"did:web:revoked",
RevocationTrustClass::ProducerSigned,
std::slice::from_ref(&revoked),
);
for i in 0..(MAX_MARKERS * 2) {
cache.record_success(
"reg.example",
&format!("did:web:p{i}"),
RevocationTrustClass::ProducerSigned,
&[],
);
}
let facts = cache.facts_for("did:web:revoked", false, None);
assert_eq!(facts.len(), 1, "the verified revocation must survive");
assert_eq!(facts[0].0, revoked);
assert_eq!(facts[0].1, "reg.example");
assert!(!cache.marker_fresh(
"reg.example",
"did:web:revoked",
RevocationTrustClass::ProducerSigned,
Duration::from_secs(3600)
));
assert!(cache.lock().markers.len() <= MAX_MARKERS);
}
#[test]
fn marker_eviction_removes_oldest() {
let cache = RevocationCache::new();
for i in 0..=MAX_MARKERS {
cache.record_success(
"reg.example",
&format!("did:web:p{i}"),
RevocationTrustClass::ProducerSigned,
&[],
);
}
let fresh = |agent: &str| {
cache.marker_fresh(
"reg.example",
agent,
RevocationTrustClass::ProducerSigned,
Duration::from_secs(3600),
)
};
assert!(!fresh("did:web:p0"));
assert!(fresh("did:web:p1"));
assert!(fresh(&format!("did:web:p{MAX_MARKERS}")));
}
#[test]
fn fresh_marker_reports_observed_time() {
let cache = RevocationCache::new();
let before = SystemTime::now();
cache.record_success(
"a.example",
"did:web:p",
RevocationTrustClass::ProducerSigned,
&[],
);
let after = SystemTime::now();
let observed = cache
.fresh_marker(
"a.example",
"did:web:p",
RevocationTrustClass::ProducerSigned,
Duration::from_secs(60),
)
.expect("fresh");
assert!(observed >= before && observed <= after);
}
}