use std::collections::HashMap;
use crate::types::IdentityDID;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KelEventRef<'a> {
pub prefix: &'a str,
pub seq: u64,
pub said: &'a str,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type")]
pub enum DuplicityReport {
Clean,
Diverging {
shared_kel_prefix: IdentityDID,
seq: u64,
event_saids: Vec<String>,
},
}
impl DuplicityReport {
pub fn is_diverging(&self) -> bool {
matches!(self, DuplicityReport::Diverging { .. })
}
}
pub fn detect_duplicity(events: &[KelEventRef<'_>]) -> DuplicityReport {
let mut seen: HashMap<(String, u64), Vec<String>> = HashMap::new();
let mut first_conflict: Option<(String, u64)> = None;
for ev in events {
let key = (ev.prefix.to_string(), ev.seq);
let saids = seen.entry(key.clone()).or_default();
if !saids.iter().any(|s| s == ev.said) {
saids.push(ev.said.to_string());
if saids.len() >= 2 && first_conflict.is_none() {
first_conflict = Some(key);
}
}
}
if let Some((prefix, seq)) = first_conflict {
let saids = seen.remove(&(prefix.clone(), seq)).unwrap_or_default();
let shared_kel_prefix = {
#[allow(clippy::disallowed_methods)]
IdentityDID::new_unchecked(prefix)
};
return DuplicityReport::Diverging {
shared_kel_prefix,
seq,
event_saids: saids,
};
}
DuplicityReport::Clean
}
#[cfg(test)]
mod tests {
use super::*;
fn ev<'a>(prefix: &'a str, seq: u64, said: &'a str) -> KelEventRef<'a> {
KelEventRef { prefix, seq, said }
}
#[test]
fn empty_is_clean() {
assert_eq!(detect_duplicity(&[]), DuplicityReport::Clean);
}
#[test]
fn single_event_is_clean() {
let events = vec![ev("did:keri:E1", 0, "EaaaA")];
assert_eq!(detect_duplicity(&events), DuplicityReport::Clean);
}
#[test]
fn sequential_rots_are_clean() {
let events = vec![
ev("did:keri:E1", 0, "EincpA"),
ev("did:keri:E1", 1, "ErotA"),
ev("did:keri:E1", 2, "ErotB"),
];
assert_eq!(detect_duplicity(&events), DuplicityReport::Clean);
}
#[test]
fn identical_said_at_same_seq_is_clean() {
let events = vec![ev("did:keri:E1", 2, "Erot"), ev("did:keri:E1", 2, "Erot")];
assert_eq!(detect_duplicity(&events), DuplicityReport::Clean);
}
#[test]
fn two_different_saids_at_same_seq_is_diverging() {
let events = vec![
ev("did:keri:E1", 0, "Eincp"),
ev("did:keri:E1", 2, "ErotA"),
ev("did:keri:E1", 2, "ErotB"),
];
match detect_duplicity(&events) {
DuplicityReport::Diverging {
shared_kel_prefix,
seq,
event_saids,
} => {
assert_eq!(shared_kel_prefix.as_str(), "did:keri:E1");
assert_eq!(seq, 2);
assert_eq!(event_saids, vec!["ErotA".to_string(), "ErotB".to_string()]);
}
DuplicityReport::Clean => panic!("expected Diverging"),
}
}
#[test]
fn three_way_fork_reports_all_saids() {
let events = vec![
ev("did:keri:E1", 2, "ErotA"),
ev("did:keri:E1", 2, "ErotB"),
ev("did:keri:E1", 2, "ErotC"),
];
match detect_duplicity(&events) {
DuplicityReport::Diverging { event_saids, .. } => {
assert_eq!(event_saids.len(), 3);
}
_ => panic!("expected three-way divergence"),
}
}
#[test]
fn icp_only_stream_is_clean() {
let events = vec![ev("did:keri:E1", 0, "Eincp")];
assert_eq!(detect_duplicity(&events), DuplicityReport::Clean);
}
#[test]
fn diverging_report_is_diverging() {
let report = DuplicityReport::Diverging {
#[allow(clippy::disallowed_methods)]
shared_kel_prefix: IdentityDID::new_unchecked("did:keri:E1".to_string()),
seq: 2,
event_saids: vec!["Ea".into(), "Eb".into()],
};
assert!(report.is_diverging());
assert!(!DuplicityReport::Clean.is_diverging());
}
}