use crate::coordinator::coordinator::ThresholdSigner;
use crate::coordinator::session::SignerId;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
pub struct LabeledShare {
pub signer_id: SignerId,
pub bytes: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlameReport {
pub session_id: String,
pub bad_signers: Vec<SignerId>,
pub all_individual_valid: bool,
pub error: String,
}
pub fn identify_bad_signer(
session_id: &str,
shares: &[LabeledShare],
threshold: u32,
message: &[u8],
scheme: &str,
signer: &dyn ThresholdSigner,
) -> BlameReport {
if shares.is_empty() {
return BlameReport {
session_id: session_id.into(),
bad_signers: vec![],
all_individual_valid: false,
error: "no shares provided".into(),
};
}
let all_bytes: Vec<Vec<u8>> = shares.iter().map(|s| s.bytes.clone()).collect();
match signer.sign(scheme, &all_bytes, threshold, message) {
Ok(_) => BlameReport {
session_id: session_id.into(),
bad_signers: vec![],
all_individual_valid: true,
error: "aggregation succeeded — no bad signer".into(),
},
Err(e) => {
let error = format!("{e}");
let mut bad_signers = Vec::new();
for (i, _) in shares.iter().enumerate() {
let subset: Vec<Vec<u8>> = shares
.iter()
.enumerate()
.filter(|(j, _)| *j != i)
.map(|(_, s)| s.bytes.clone())
.collect();
if (threshold as usize) > subset.len() {
continue;
}
match signer.sign(scheme, &subset, threshold, message) {
Ok(_) => {
bad_signers.push(shares[i].signer_id.clone());
}
Err(_) => {
}
}
}
BlameReport {
session_id: session_id.into(),
bad_signers,
all_individual_valid: false,
error,
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::coordinator::coordinator::MockSigner;
fn make_share(id: &str, byte: u8) -> LabeledShare {
LabeledShare {
signer_id: id.into(),
bytes: vec![byte; 64],
}
}
#[test]
fn no_shares_returns_empty_report() {
let signer = MockSigner;
let report = identify_bad_signer("s1", &[], 2, b"msg", "CMP20", &signer);
assert!(report.bad_signers.is_empty());
assert!(report.error.contains("no shares"));
}
#[test]
fn all_valid_shares_no_blame() {
let signer = MockSigner;
let shares = vec![
make_share("alice", 0xAA),
make_share("bob", 0xBB),
make_share("carol", 0xCC),
];
let report = identify_bad_signer("s1", &shares, 2, b"msg", "CMP20", &signer);
assert!(report.bad_signers.is_empty());
assert!(report.error.contains("succeeded"));
}
#[test]
fn empty_shares_list_handled() {
let signer = MockSigner;
let report = identify_bad_signer("s1", &[], 0, b"msg", "CMP20", &signer);
assert!(report.bad_signers.is_empty());
}
#[test]
fn blame_report_serializes() {
let report = BlameReport {
session_id: "s1".into(),
bad_signers: vec!["alice".into()],
all_individual_valid: false,
error: "aggregation failed".into(),
};
let json = serde_json::to_string(&report).unwrap();
assert!(json.contains("alice"));
assert!(json.contains("s1"));
}
#[test]
fn blame_report_deserializes() {
let json = r#"{"session_id":"s1","bad_signers":["bob"],"all_individual_valid":false,"error":"test"}"#;
let report: BlameReport = serde_json::from_str(json).unwrap();
assert_eq!(report.session_id, "s1");
assert_eq!(report.bad_signers, vec!["bob"]);
}
#[test]
fn labeled_share_carries_identity() {
let share = make_share("alice", 0x42);
assert_eq!(share.signer_id, "alice");
assert_eq!(share.bytes.len(), 64);
}
#[test]
fn threshold_above_subset_size_skipped() {
let signer = MockSigner;
let shares = vec![make_share("alice", 0xAA), make_share("bob", 0xBB)];
let report = identify_bad_signer("s1", &shares, 2, b"msg", "CMP20", &signer);
assert!(report.bad_signers.is_empty());
}
}