Skip to main content

confium_coordinator/coordinator/
abort.rs

1//! Identifiable abort — pinpoint which signer submitted a bad share.
2//!
3//! When threshold aggregation fails, this module identifies the
4//! offending signer by testing each one individually. The approach:
5//!
6//! 1. Try aggregating all T shares.
7//! 2. If that fails, try aggregating each (T-1)-subset (leaving one out).
8//! 3. If leaving out signer X succeeds, X is the culprit.
9//!
10//! This is O(T) aggregation attempts — acceptable for small T.
11
12use crate::coordinator::coordinator::ThresholdSigner;
13use crate::coordinator::session::SignerId;
14use serde::{Deserialize, Serialize};
15
16/// A share associated with its signer identity.
17#[derive(Debug, Clone)]
18pub struct LabeledShare {
19    /// Who submitted this share.
20    pub signer_id: SignerId,
21    /// Share bytes.
22    pub bytes: Vec<u8>,
23}
24
25/// Result of blame attribution.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct BlameReport {
28    /// Session that failed.
29    pub session_id: String,
30    /// Identified bad signer(s), if any.
31    pub bad_signers: Vec<SignerId>,
32    /// True if all shares verified individually but aggregation still
33    /// failed (indicates a protocol-level issue).
34    pub all_individual_valid: bool,
35    /// Error message from the original aggregation attempt.
36    pub error: String,
37}
38
39/// Identify the bad signer(s) by elimination. Given T shares and a
40/// threshold signer, tries removing each signer one at a time.
41///
42/// Returns a blame report. If a specific signer is identified,
43/// `bad_signers` contains their ID.
44pub fn identify_bad_signer(
45    session_id: &str,
46    shares: &[LabeledShare],
47    threshold: u32,
48    message: &[u8],
49    scheme: &str,
50    signer: &dyn ThresholdSigner,
51) -> BlameReport {
52    if shares.is_empty() {
53        return BlameReport {
54            session_id: session_id.into(),
55            bad_signers: vec![],
56            all_individual_valid: false,
57            error: "no shares provided".into(),
58        };
59    }
60
61    // First, try the full set.
62    let all_bytes: Vec<Vec<u8>> = shares.iter().map(|s| s.bytes.clone()).collect();
63    match signer.sign(scheme, &all_bytes, threshold, message) {
64        Ok(_) => BlameReport {
65            session_id: session_id.into(),
66            bad_signers: vec![],
67            all_individual_valid: true,
68            error: "aggregation succeeded — no bad signer".into(),
69        },
70        Err(e) => {
71            let error = format!("{e}");
72            let mut bad_signers = Vec::new();
73
74            // Try leaving out each signer one at a time.
75            for (i, _) in shares.iter().enumerate() {
76                let subset: Vec<Vec<u8>> = shares
77                    .iter()
78                    .enumerate()
79                    .filter(|(j, _)| *j != i)
80                    .map(|(_, s)| s.bytes.clone())
81                    .collect();
82
83                // We need at least threshold shares for a valid subset.
84                // If threshold > subset.len(), this signer can't be
85                // the sole culprit.
86                if (threshold as usize) > subset.len() {
87                    continue;
88                }
89
90                match signer.sign(scheme, &subset, threshold, message) {
91                    Ok(_) => {
92                        // Removing signer i makes it work → i is bad.
93                        bad_signers.push(shares[i].signer_id.clone());
94                    }
95                    Err(_) => {
96                        // Still fails without signer i → i is not the sole culprit.
97                    }
98                }
99            }
100
101            BlameReport {
102                session_id: session_id.into(),
103                bad_signers,
104                all_individual_valid: false,
105                error,
106            }
107        }
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use crate::coordinator::coordinator::MockSigner;
115
116    fn make_share(id: &str, byte: u8) -> LabeledShare {
117        LabeledShare {
118            signer_id: id.into(),
119            bytes: vec![byte; 64],
120        }
121    }
122
123    #[test]
124    fn no_shares_returns_empty_report() {
125        let signer = MockSigner;
126        let report = identify_bad_signer("s1", &[], 2, b"msg", "CMP20", &signer);
127        assert!(report.bad_signers.is_empty());
128        assert!(report.error.contains("no shares"));
129    }
130
131    #[test]
132    fn all_valid_shares_no_blame() {
133        let signer = MockSigner;
134        let shares = vec![
135            make_share("alice", 0xAA),
136            make_share("bob", 0xBB),
137            make_share("carol", 0xCC),
138        ];
139        let report = identify_bad_signer("s1", &shares, 2, b"msg", "CMP20", &signer);
140        assert!(report.bad_signers.is_empty());
141        assert!(report.error.contains("succeeded"));
142    }
143
144    #[test]
145    fn empty_shares_list_handled() {
146        let signer = MockSigner;
147        let report = identify_bad_signer("s1", &[], 0, b"msg", "CMP20", &signer);
148        assert!(report.bad_signers.is_empty());
149    }
150
151    #[test]
152    fn blame_report_serializes() {
153        let report = BlameReport {
154            session_id: "s1".into(),
155            bad_signers: vec!["alice".into()],
156            all_individual_valid: false,
157            error: "aggregation failed".into(),
158        };
159        let json = serde_json::to_string(&report).unwrap();
160        assert!(json.contains("alice"));
161        assert!(json.contains("s1"));
162    }
163
164    #[test]
165    fn blame_report_deserializes() {
166        let json = r#"{"session_id":"s1","bad_signers":["bob"],"all_individual_valid":false,"error":"test"}"#;
167        let report: BlameReport = serde_json::from_str(json).unwrap();
168        assert_eq!(report.session_id, "s1");
169        assert_eq!(report.bad_signers, vec!["bob"]);
170    }
171
172    #[test]
173    fn labeled_share_carries_identity() {
174        let share = make_share("alice", 0x42);
175        assert_eq!(share.signer_id, "alice");
176        assert_eq!(share.bytes.len(), 64);
177    }
178
179    #[test]
180    fn threshold_above_subset_size_skipped() {
181        let signer = MockSigner;
182        let shares = vec![make_share("alice", 0xAA), make_share("bob", 0xBB)];
183        let report = identify_bad_signer("s1", &shares, 2, b"msg", "CMP20", &signer);
184        // With T=2 and 2 shares, removing one leaves 1 < T=2, so no
185        // blame identification is possible.
186        // But the full set should still succeed with MockSigner.
187        assert!(report.bad_signers.is_empty());
188    }
189}