Skip to main content

auths_core/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 auths_verifier::keri::{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: u64,
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)]
62pub enum WitnessError {
63    /// Network error communicating with witness.
64    #[error("network error: {0}")]
65    Network(String),
66
67    /// Duplicity detected - the controller presented different events.
68    ///
69    /// This is a **security violation** indicating a potential split-view attack.
70    #[error("duplicity detected: {0}")]
71    Duplicity(DuplicityEvidence),
72
73    /// The witness rejected the event.
74    ///
75    /// This can happen if the event is malformed, the witness doesn't track
76    /// this identity, or the event fails validation.
77    #[error("event rejected: {reason}")]
78    Rejected {
79        /// Human-readable reason for rejection
80        reason: String,
81    },
82
83    /// Operation timed out.
84    #[error("timeout after {0}ms")]
85    Timeout(u64),
86
87    /// Invalid receipt signature.
88    #[error("invalid receipt signature from witness {witness_id}")]
89    InvalidSignature {
90        /// The witness that provided the invalid signature
91        witness_id: String,
92    },
93
94    /// Insufficient receipts to meet threshold.
95    #[error("insufficient receipts: got {got}, need {required}")]
96    InsufficientReceipts {
97        /// Number of receipts received
98        got: usize,
99        /// Number of receipts required
100        required: usize,
101    },
102
103    /// Receipt is for wrong event.
104    #[error("receipt SAID mismatch: expected {expected}, got {got}")]
105    SaidMismatch {
106        /// Expected event SAID
107        expected: Said,
108        /// Actual SAID in receipt
109        got: Said,
110    },
111
112    /// Storage error.
113    #[error("storage error: {0}")]
114    Storage(String),
115
116    /// Serialization error.
117    #[error("serialization error: {0}")]
118    Serialization(String),
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn duplicity_evidence_display() {
127        let evidence = DuplicityEvidence {
128            prefix: Prefix::new_unchecked("EPrefix123".into()),
129            sequence: 5,
130            event_a_said: Said::new_unchecked("ESAID_A".into()),
131            event_b_said: Said::new_unchecked("ESAID_B".into()),
132            witness_reports: vec![],
133        };
134        let display = format!("{}", evidence);
135        assert!(display.contains("EPrefix123"));
136        assert!(display.contains("5"));
137        assert!(display.contains("ESAID_A"));
138        assert!(display.contains("ESAID_B"));
139    }
140
141    #[test]
142    fn witness_error_variants() {
143        let network_err = WitnessError::Network("connection refused".into());
144        assert!(format!("{}", network_err).contains("network error"));
145
146        let timeout_err = WitnessError::Timeout(5000);
147        assert!(format!("{}", timeout_err).contains("5000ms"));
148
149        let rejected_err = WitnessError::Rejected {
150            reason: "invalid format".into(),
151        };
152        assert!(format!("{}", rejected_err).contains("invalid format"));
153    }
154
155    #[test]
156    fn duplicity_evidence_serialization() {
157        let evidence = DuplicityEvidence {
158            prefix: Prefix::new_unchecked("EPrefix123".into()),
159            sequence: 5,
160            event_a_said: Said::new_unchecked("ESAID_A".into()),
161            event_b_said: Said::new_unchecked("ESAID_B".into()),
162            witness_reports: vec![WitnessReport {
163                witness_id: "did:key:witness1".into(),
164                observed_said: Said::new_unchecked("ESAID_A".into()),
165                observed_at: Some("2024-01-01T00:00:00Z".into()),
166            }],
167        };
168
169        let json = serde_json::to_string(&evidence).unwrap();
170        let parsed: DuplicityEvidence = serde_json::from_str(&json).unwrap();
171        assert_eq!(evidence, parsed);
172    }
173}