use std::collections::HashMap;
use auths_keri::{Prefix, Said};
use super::error::{DuplicityEvidence, WitnessReport};
#[cfg(test)]
use super::receipt::ReceiptTag;
use super::receipt::{Receipt, StoredReceipt};
#[derive(Debug, Clone, Default)]
pub struct DuplicityDetector {
first_seen: HashMap<(String, u128), String>,
}
impl DuplicityDetector {
pub fn new() -> Self {
Self::default()
}
pub fn with_state(first_seen: HashMap<(String, u128), String>) -> Self {
Self { first_seen }
}
pub fn check_event(
&mut self,
prefix: &Prefix,
seq: u128,
said: &Said,
) -> Option<DuplicityEvidence> {
let key = (prefix.as_str().to_string(), seq);
match self.first_seen.get(&key) {
None => {
self.first_seen.insert(key, said.as_str().to_string());
None
}
Some(existing_said) => {
if existing_said == said.as_str() {
None
} else {
Some(DuplicityEvidence {
prefix: prefix.clone(),
sequence: seq,
event_a_said: Said::new_unchecked(existing_said.clone()),
event_b_said: said.clone(),
witness_reports: vec![],
})
}
}
}
}
pub fn has_seen(&self, prefix: &Prefix, seq: u128) -> bool {
self.first_seen
.contains_key(&(prefix.as_str().to_string(), seq))
}
pub fn get_said(&self, prefix: &Prefix, seq: u128) -> Option<Said> {
self.first_seen
.get(&(prefix.as_str().to_string(), seq))
.map(|s| Said::new_unchecked(s.clone()))
}
pub fn verify_receipts(&self, receipts: &[Receipt]) -> Result<(), DuplicityEvidence> {
if receipts.is_empty() {
return Ok(());
}
let first = &receipts[0];
let expected_said = &first.d;
for receipt in receipts.iter().skip(1) {
if receipt.d != *expected_said {
return Err(DuplicityEvidence {
prefix: Prefix::default(),
sequence: first.s.value(),
event_a_said: expected_said.clone(),
event_b_said: receipt.d.clone(),
witness_reports: receipts
.iter()
.map(|r| WitnessReport {
witness_id: r.i.as_str().to_string(),
observed_said: r.d.clone(),
observed_at: None,
})
.collect(),
});
}
}
Ok(())
}
pub fn state(&self) -> &HashMap<(String, u128), String> {
&self.first_seen
}
pub fn clear(&mut self) {
self.first_seen.clear();
}
pub fn len(&self) -> usize {
self.first_seen.len()
}
pub fn is_empty(&self) -> bool {
self.first_seen.is_empty()
}
}
pub fn detect_receipt_conflict(receipts: &[StoredReceipt]) -> Option<DuplicityEvidence> {
let first = receipts.first()?;
let expected_said = &first.signed.receipt.d;
let conflict = receipts
.iter()
.find(|r| r.signed.receipt.d != *expected_said)?;
Some(DuplicityEvidence {
prefix: first.signed.receipt.i.clone(),
sequence: first.signed.receipt.s.value(),
event_a_said: expected_said.clone(),
event_b_said: conflict.signed.receipt.d.clone(),
witness_reports: receipts
.iter()
.map(|r| WitnessReport {
witness_id: r.witness.as_str().to_string(),
observed_said: r.signed.receipt.d.clone(),
observed_at: None,
})
.collect(),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn prefix(s: &str) -> Prefix {
Prefix::new_unchecked(s.into())
}
fn said(s: &str) -> Said {
Said::new_unchecked(s.into())
}
fn stored_receipt(witness: &str, event_said: &str) -> StoredReceipt {
use crate::witness::SignedReceipt;
use auths_keri::{KeriSequence, VersionString};
StoredReceipt {
signed: SignedReceipt {
receipt: Receipt {
v: VersionString::placeholder(),
t: ReceiptTag,
d: said(event_said),
i: prefix("EController"),
s: KeriSequence::new(0),
},
signature: vec![],
},
witness: prefix(witness),
}
}
#[test]
fn conflicting_witness_receipts_irreconcilable() {
let receipts = vec![
stored_receipt("BWit1", "ESAID_A"),
stored_receipt("BWit2", "ESAID_B"),
];
let evidence = detect_receipt_conflict(&receipts).expect("conflict must be detected");
assert_eq!(evidence.event_a_said, said("ESAID_A"));
assert_eq!(evidence.event_b_said, said("ESAID_B"));
assert_eq!(evidence.witness_reports.len(), 2);
let ids: Vec<&str> = evidence
.witness_reports
.iter()
.map(|w| w.witness_id.as_str())
.collect();
assert!(ids.contains(&"BWit1") && ids.contains(&"BWit2"));
}
#[test]
fn agreeing_witness_receipts_no_conflict() {
let receipts = vec![
stored_receipt("BWit1", "ESAID_A"),
stored_receipt("BWit2", "ESAID_A"),
];
assert!(detect_receipt_conflict(&receipts).is_none());
}
#[test]
fn first_seen_records_said() {
let mut detector = DuplicityDetector::new();
let p = prefix("EPrefix");
let result = detector.check_event(&p, 0, &said("ESAID_A"));
assert!(result.is_none());
assert!(detector.has_seen(&p, 0));
assert_eq!(detector.get_said(&p, 0), Some(said("ESAID_A")));
}
#[test]
fn same_said_is_idempotent() {
let mut detector = DuplicityDetector::new();
let p = prefix("EPrefix");
detector.check_event(&p, 0, &said("ESAID_A"));
let result = detector.check_event(&p, 0, &said("ESAID_A"));
assert!(result.is_none());
}
#[test]
fn different_said_is_duplicity() {
let mut detector = DuplicityDetector::new();
let p = prefix("EPrefix");
detector.check_event(&p, 0, &said("ESAID_A"));
let result = detector.check_event(&p, 0, &said("ESAID_B"));
assert!(result.is_some());
let evidence = result.unwrap();
assert_eq!(evidence.prefix, "EPrefix");
assert_eq!(evidence.sequence, 0);
assert_eq!(evidence.event_a_said, "ESAID_A");
assert_eq!(evidence.event_b_said, "ESAID_B");
}
#[test]
fn different_seq_is_ok() {
let mut detector = DuplicityDetector::new();
let p = prefix("EPrefix");
assert!(detector.check_event(&p, 0, &said("ESAID_A")).is_none());
assert!(detector.check_event(&p, 1, &said("ESAID_B")).is_none());
assert!(detector.check_event(&p, 2, &said("ESAID_C")).is_none());
}
#[test]
fn different_prefix_is_ok() {
let mut detector = DuplicityDetector::new();
assert!(
detector
.check_event(&prefix("EPrefix1"), 0, &said("ESAID_A"))
.is_none()
);
assert!(
detector
.check_event(&prefix("EPrefix2"), 0, &said("ESAID_B"))
.is_none()
);
}
#[test]
fn verify_receipts_consistent() {
use auths_keri::{KeriSequence, VersionString};
let detector = DuplicityDetector::new();
let receipts = vec![
Receipt {
v: VersionString::placeholder(),
t: ReceiptTag,
d: Said::new_unchecked("EEVENT_SAID".into()),
i: Prefix::new_unchecked("W1".into()),
s: KeriSequence::new(5),
},
Receipt {
v: VersionString::placeholder(),
t: ReceiptTag,
d: Said::new_unchecked("EEVENT_SAID".into()),
i: Prefix::new_unchecked("W2".into()),
s: KeriSequence::new(5),
},
];
assert!(detector.verify_receipts(&receipts).is_ok());
}
#[test]
fn verify_receipts_inconsistent() {
use auths_keri::{KeriSequence, VersionString};
let detector = DuplicityDetector::new();
let receipts = vec![
Receipt {
v: VersionString::placeholder(),
t: ReceiptTag,
d: Said::new_unchecked("ESAID_A".into()),
i: Prefix::new_unchecked("W1".into()),
s: KeriSequence::new(5),
},
Receipt {
v: VersionString::placeholder(),
t: ReceiptTag,
d: Said::new_unchecked("ESAID_B".into()),
i: Prefix::new_unchecked("W2".into()),
s: KeriSequence::new(5),
},
];
let result = detector.verify_receipts(&receipts);
assert!(result.is_err());
let evidence = result.unwrap_err();
assert_eq!(evidence.event_a_said, "ESAID_A");
assert_eq!(evidence.event_b_said, "ESAID_B");
assert_eq!(evidence.witness_reports.len(), 2);
}
#[test]
fn verify_receipts_empty() {
let detector = DuplicityDetector::new();
assert!(detector.verify_receipts(&[]).is_ok());
}
#[test]
fn with_state_restores() {
let mut state = HashMap::new();
state.insert(("EPrefix".to_string(), 0), "ESAID_A".to_string());
state.insert(("EPrefix".to_string(), 1), "ESAID_B".to_string());
let detector = DuplicityDetector::with_state(state);
let p = prefix("EPrefix");
assert!(detector.has_seen(&p, 0));
assert!(detector.has_seen(&p, 1));
assert!(!detector.has_seen(&p, 2));
}
#[test]
fn len_and_is_empty() {
let mut detector = DuplicityDetector::new();
assert!(detector.is_empty());
assert_eq!(detector.len(), 0);
detector.check_event(&prefix("E1"), 0, &said("ES1"));
detector.check_event(&prefix("E2"), 0, &said("ES2"));
assert!(!detector.is_empty());
assert_eq!(detector.len(), 2);
detector.clear();
assert!(detector.is_empty());
}
}