use std::collections::BTreeSet;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ReplayRegistry {
used: BTreeSet<(String, u64)>,
}
impl ReplayRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn contains(&self, nonce_scope: &str, nonce: u64) -> bool {
self.used.contains(&(nonce_scope.to_string(), nonce))
}
pub fn consume(&mut self, nonce_scope: &str, nonce: u64) -> bool {
self.used.insert((nonce_scope.to_string(), nonce))
}
pub fn len(&self) -> usize {
self.used.len()
}
pub fn is_empty(&self) -> bool {
self.used.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &(String, u64)> {
self.used.iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn consume_is_first_use_wins() {
let mut r = ReplayRegistry::new();
assert!(r.consume("agent:a", 1), "首次消费成功");
assert!(!r.consume("agent:a", 1), "同 scope 同 nonce = 重放");
assert!(r.contains("agent:a", 1));
assert_eq!(r.len(), 1);
}
#[test]
fn same_nonce_different_scope_is_not_replay() {
let mut r = ReplayRegistry::new();
assert!(r.consume("agent:a", 1));
assert!(r.consume("agent:b", 1), "跨作用域不算重放");
assert_eq!(r.len(), 2);
}
#[test]
fn rejected_intent_does_not_consume_nonce() {
let mut r = ReplayRegistry::new();
assert!(!r.contains("agent:a", 7));
assert!(r.consume("agent:a", 7));
assert!(r.contains("agent:a", 7));
}
}