use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct NormalizedShare {
pub scheme: String,
pub quorum_id: String,
pub party_idx: u32,
pub threshold: u32,
pub party_count: u32,
pub scalar_hex: String,
pub public_key_hex: String,
}
#[derive(Debug, thiserror::Error)]
pub enum AdapterError {
#[error("invalid scalar: {0}")]
InvalidScalar(String),
#[error("invalid public key: {0}")]
InvalidPublicKey(String),
#[error("field mismatch: {0}")]
FieldMismatch(String),
}
impl NormalizedShare {
pub fn new(
scheme: &str,
quorum_id: &str,
party_idx: u32,
threshold: u32,
party_count: u32,
scalar_bytes: &[u8],
public_key_bytes: &[u8],
) -> Result<Self, AdapterError> {
if scalar_bytes.len() != 32 {
return Err(AdapterError::InvalidScalar(format!(
"expected 32 bytes, got {}",
scalar_bytes.len()
)));
}
Ok(Self {
scheme: scheme.into(),
quorum_id: quorum_id.into(),
party_idx,
threshold,
party_count,
scalar_hex: hex::encode(scalar_bytes),
public_key_hex: hex::encode(public_key_bytes),
})
}
pub fn scalar_bytes(&self) -> Result<Vec<u8>, AdapterError> {
hex::decode(&self.scalar_hex).map_err(|e| AdapterError::InvalidScalar(e.to_string()))
}
pub fn public_key_bytes(&self) -> Result<Vec<u8>, AdapterError> {
hex::decode(&self.public_key_hex).map_err(|e| AdapterError::InvalidPublicKey(e.to_string()))
}
pub fn is_compatible_with(&self, other: &NormalizedShare) -> bool {
self.quorum_id == other.quorum_id
&& self.public_key_hex == other.public_key_hex
&& self.threshold == other.threshold
&& self.party_count == other.party_count
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
pub fn reclassify(mut self, new_scheme: &str) -> Self {
self.scheme = new_scheme.into();
self
}
}
pub trait ShareAdapter {
fn to_normalized(&self, quorum_id: &str) -> Result<NormalizedShare, AdapterError>;
fn from_normalized(normalized: &NormalizedShare) -> Result<Self, AdapterError>
where
Self: Sized;
}
#[cfg(test)]
mod tests {
use super::*;
fn make_share(scheme: &str) -> NormalizedShare {
NormalizedShare::new(scheme, "quorum-alpha", 3, 2, 5, &[0xAA; 32], &[0x04; 65]).unwrap()
}
#[test]
fn new_validates_scalar_length() {
assert!(NormalizedShare::new("CMP20", "q", 1, 2, 3, &[0; 31], &[0; 65]).is_err());
assert!(NormalizedShare::new("CMP20", "q", 1, 2, 3, &[0; 32], &[0; 65]).is_ok());
}
#[test]
fn scalar_bytes_round_trips() {
let share = make_share("CMP20");
let bytes = share.scalar_bytes().unwrap();
assert_eq!(bytes, vec![0xAA; 32]);
}
#[test]
fn public_key_bytes_round_trips() {
let share = make_share("CMP20");
let bytes = share.public_key_bytes().unwrap();
assert_eq!(bytes, vec![0x04; 65]);
}
#[test]
fn is_compatible_same_quorum() {
let a = make_share("CMP20");
let b = make_share("CMP20");
assert!(a.is_compatible_with(&b));
}
#[test]
fn is_incompatible_different_quorum() {
let a = make_share("CMP20");
let mut b = make_share("CMP20");
b.quorum_id = "different".into();
assert!(!a.is_compatible_with(&b));
}
#[test]
fn is_incompatible_different_threshold() {
let a = make_share("CMP20");
let mut b = make_share("CMP20");
b.threshold = 3;
assert!(!a.is_compatible_with(&b));
}
#[test]
fn json_round_trip() {
let share = make_share("CMP20");
let json = share.to_json().unwrap();
let recovered = NormalizedShare::from_json(&json).unwrap();
assert_eq!(share, recovered);
}
#[test]
fn reclassify_changes_scheme() {
let share = make_share("CMP20");
let reclassified = share.reclassify("FROST-P256");
assert_eq!(reclassified.scheme, "FROST-P256");
}
#[test]
fn reclassify_preserves_other_fields() {
let share = make_share("CMP20");
let reclassified = share.reclassify("GG18");
assert_eq!(reclassified.quorum_id, "quorum-alpha");
assert_eq!(reclassified.party_idx, 3);
assert_eq!(reclassified.threshold, 2);
}
}