Skip to main content

auths_keri/witness/
error.rs

1//! Error types for witness operations.
2//!
3//! This module defines the error types used by the async witness infrastructure,
4//! including duplicity evidence for split-view detection.
5
6use crate::{Prefix, Said};
7use serde::{Deserialize, Serialize};
8use std::fmt;
9
10/// Evidence of duplicity (split-view attack) detected by witnesses.
11///
12/// When a controller presents different events with the same (prefix, seq)
13/// to different witnesses, this evidence captures the conflicting SAIDs.
14///
15/// # Fields
16///
17/// - `prefix`: The KERI prefix of the identity
18/// - `sequence`: The sequence number where duplicity was detected
19/// - `event_a_said`: SAID of the first event seen
20/// - `event_b_said`: SAID of the conflicting event
21/// - `witness_reports`: Reports from witnesses that observed the conflict
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct DuplicityEvidence {
24    /// The KERI prefix of the identity
25    pub prefix: Prefix,
26    /// The sequence number where duplicity was detected
27    pub sequence: u128,
28    /// SAID of the first event seen (the "canonical" one)
29    pub event_a_said: Said,
30    /// SAID of the conflicting event
31    pub event_b_said: Said,
32    /// Reports from individual witnesses
33    pub witness_reports: Vec<WitnessReport>,
34}
35
36impl fmt::Display for DuplicityEvidence {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        write!(
39            f,
40            "Duplicity detected for {} at seq {}: {} vs {}",
41            self.prefix, self.sequence, self.event_a_said, self.event_b_said
42        )
43    }
44}
45
46/// A report from a single witness about what it observed.
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub struct WitnessReport {
49    /// The witness identifier (DID)
50    pub witness_id: String,
51    /// The SAID this witness observed for the (prefix, seq)
52    pub observed_said: Said,
53    /// When this observation was made (ISO 8601)
54    pub observed_at: Option<String>,
55}
56
57/// Errors that can occur during witness operations.
58///
59/// These errors cover the full range of failure modes for async witness
60/// interactions, from network issues to security violations.
61#[derive(Debug, thiserror::Error)]
62#[non_exhaustive]
63pub enum WitnessError {
64    /// Network error communicating with witness.
65    #[error("network error: {0}")]
66    Network(String),
67
68    /// Duplicity detected - the controller presented different events.
69    ///
70    /// This is a **security violation** indicating a potential split-view attack.
71    #[error("duplicity detected: {0}")]
72    Duplicity(DuplicityEvidence),
73
74    /// The witness rejected the event.
75    ///
76    /// This can happen if the event is malformed, the witness doesn't track
77    /// this identity, or the event fails validation.
78    #[error("event rejected: {reason}")]
79    Rejected {
80        /// Human-readable reason for rejection
81        reason: String,
82    },
83
84    /// Operation timed out.
85    #[error("timeout after {0}ms")]
86    Timeout(u64),
87
88    /// Invalid receipt signature.
89    #[error("invalid receipt signature from witness {witness_id}")]
90    InvalidSignature {
91        /// The witness that provided the invalid signature
92        witness_id: String,
93    },
94
95    /// Insufficient receipts to meet threshold.
96    #[error("insufficient receipts: got {got}, need {required}")]
97    InsufficientReceipts {
98        /// Number of receipts received
99        got: usize,
100        /// Number of receipts required
101        required: usize,
102    },
103
104    /// Receipt is for wrong event.
105    #[error("receipt SAID mismatch: expected {expected}, got {got}")]
106    SaidMismatch {
107        /// Expected event SAID
108        expected: Said,
109        /// Actual SAID in receipt
110        got: Said,
111    },
112
113    /// Storage error.
114    #[error("storage error: {0}")]
115    Storage(String),
116
117    /// Serialization error.
118    #[error("serialization error: {0}")]
119    Serialization(String),
120}
121
122impl auths_crypto::AuthsErrorInfo for WitnessError {
123    fn error_code(&self) -> &'static str {
124        match self {
125            Self::Network(_) => "AUTHS-E3401",
126            Self::Duplicity(_) => "AUTHS-E3402",
127            Self::Rejected { .. } => "AUTHS-E3403",
128            Self::Timeout(_) => "AUTHS-E3404",
129            Self::InvalidSignature { .. } => "AUTHS-E3405",
130            Self::InsufficientReceipts { .. } => "AUTHS-E3406",
131            Self::SaidMismatch { .. } => "AUTHS-E3407",
132            Self::Storage(_) => "AUTHS-E3408",
133            Self::Serialization(_) => "AUTHS-E3409",
134        }
135    }
136
137    fn suggestion(&self) -> Option<&'static str> {
138        match self {
139            Self::Duplicity(_) => {
140                Some("This identity may be compromised — investigate immediately")
141            }
142            Self::Timeout(_) => Some("Check witness endpoint availability and retry"),
143            Self::InsufficientReceipts { .. } => Some("Ensure enough witnesses are online"),
144            Self::Network(_) => Some("Check your internet connection"),
145            _ => None,
146        }
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn duplicity_evidence_display() {
156        let evidence = DuplicityEvidence {
157            prefix: Prefix::new_unchecked("EPrefix123".into()),
158            sequence: 5,
159            event_a_said: Said::new_unchecked("ESAID_A".into()),
160            event_b_said: Said::new_unchecked("ESAID_B".into()),
161            witness_reports: vec![],
162        };
163        let display = format!("{}", evidence);
164        assert!(display.contains("EPrefix123"));
165        assert!(display.contains("5"));
166        assert!(display.contains("ESAID_A"));
167        assert!(display.contains("ESAID_B"));
168    }
169
170    #[test]
171    fn witness_error_variants() {
172        let network_err = WitnessError::Network("connection refused".into());
173        assert!(format!("{}", network_err).contains("network error"));
174
175        let timeout_err = WitnessError::Timeout(5000);
176        assert!(format!("{}", timeout_err).contains("5000ms"));
177
178        let rejected_err = WitnessError::Rejected {
179            reason: "invalid format".into(),
180        };
181        assert!(format!("{}", rejected_err).contains("invalid format"));
182    }
183
184    #[test]
185    fn duplicity_evidence_serialization() {
186        let evidence = DuplicityEvidence {
187            prefix: Prefix::new_unchecked("EPrefix123".into()),
188            sequence: 5,
189            event_a_said: Said::new_unchecked("ESAID_A".into()),
190            event_b_said: Said::new_unchecked("ESAID_B".into()),
191            witness_reports: vec![WitnessReport {
192                witness_id: "did:key:witness1".into(),
193                observed_said: Said::new_unchecked("ESAID_A".into()),
194                observed_at: Some("2024-01-01T00:00:00Z".into()),
195            }],
196        };
197
198        let json = serde_json::to_string(&evidence).unwrap();
199        let parsed: DuplicityEvidence = serde_json::from_str(&json).unwrap();
200        assert_eq!(evidence, parsed);
201    }
202}