use crate::{Prefix, Said};
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DuplicityEvidence {
pub prefix: Prefix,
pub sequence: u128,
pub event_a_said: Said,
pub event_b_said: Said,
pub witness_reports: Vec<WitnessReport>,
}
impl fmt::Display for DuplicityEvidence {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Duplicity detected for {} at seq {}: {} vs {}",
self.prefix, self.sequence, self.event_a_said, self.event_b_said
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WitnessReport {
pub witness_id: String,
pub observed_said: Said,
pub observed_at: Option<String>,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum WitnessError {
#[error("network error: {0}")]
Network(String),
#[error("duplicity detected: {0}")]
Duplicity(DuplicityEvidence),
#[error("event rejected: {reason}")]
Rejected {
reason: String,
},
#[error("timeout after {0}ms")]
Timeout(u64),
#[error("invalid receipt signature from witness {witness_id}")]
InvalidSignature {
witness_id: String,
},
#[error("insufficient receipts: got {got}, need {required}")]
InsufficientReceipts {
got: usize,
required: usize,
},
#[error("receipt SAID mismatch: expected {expected}, got {got}")]
SaidMismatch {
expected: Said,
got: Said,
},
#[error("storage error: {0}")]
Storage(String),
#[error("serialization error: {0}")]
Serialization(String),
}
impl auths_crypto::AuthsErrorInfo for WitnessError {
fn error_code(&self) -> &'static str {
match self {
Self::Network(_) => "AUTHS-E3401",
Self::Duplicity(_) => "AUTHS-E3402",
Self::Rejected { .. } => "AUTHS-E3403",
Self::Timeout(_) => "AUTHS-E3404",
Self::InvalidSignature { .. } => "AUTHS-E3405",
Self::InsufficientReceipts { .. } => "AUTHS-E3406",
Self::SaidMismatch { .. } => "AUTHS-E3407",
Self::Storage(_) => "AUTHS-E3408",
Self::Serialization(_) => "AUTHS-E3409",
}
}
fn suggestion(&self) -> Option<&'static str> {
match self {
Self::Duplicity(_) => {
Some("This identity may be compromised — investigate immediately")
}
Self::Timeout(_) => Some("Check witness endpoint availability and retry"),
Self::InsufficientReceipts { .. } => Some("Ensure enough witnesses are online"),
Self::Network(_) => Some("Check your internet connection"),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn duplicity_evidence_display() {
let evidence = DuplicityEvidence {
prefix: Prefix::new_unchecked("EPrefix123".into()),
sequence: 5,
event_a_said: Said::new_unchecked("ESAID_A".into()),
event_b_said: Said::new_unchecked("ESAID_B".into()),
witness_reports: vec![],
};
let display = format!("{}", evidence);
assert!(display.contains("EPrefix123"));
assert!(display.contains("5"));
assert!(display.contains("ESAID_A"));
assert!(display.contains("ESAID_B"));
}
#[test]
fn witness_error_variants() {
let network_err = WitnessError::Network("connection refused".into());
assert!(format!("{}", network_err).contains("network error"));
let timeout_err = WitnessError::Timeout(5000);
assert!(format!("{}", timeout_err).contains("5000ms"));
let rejected_err = WitnessError::Rejected {
reason: "invalid format".into(),
};
assert!(format!("{}", rejected_err).contains("invalid format"));
}
#[test]
fn duplicity_evidence_serialization() {
let evidence = DuplicityEvidence {
prefix: Prefix::new_unchecked("EPrefix123".into()),
sequence: 5,
event_a_said: Said::new_unchecked("ESAID_A".into()),
event_b_said: Said::new_unchecked("ESAID_B".into()),
witness_reports: vec![WitnessReport {
witness_id: "did:key:witness1".into(),
observed_said: Said::new_unchecked("ESAID_A".into()),
observed_at: Some("2024-01-01T00:00:00Z".into()),
}],
};
let json = serde_json::to_string(&evidence).unwrap();
let parsed: DuplicityEvidence = serde_json::from_str(&json).unwrap();
assert_eq!(evidence, parsed);
}
}