confium_tc_core/
share_adapter.rs1use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
15pub struct NormalizedShare {
16 pub scheme: String,
18 pub quorum_id: String,
20 pub party_idx: u32,
22 pub threshold: u32,
24 pub party_count: u32,
26 pub scalar_hex: String,
28 pub public_key_hex: String,
30}
31
32#[derive(Debug, thiserror::Error)]
34pub enum AdapterError {
35 #[error("invalid scalar: {0}")]
37 InvalidScalar(String),
38 #[error("invalid public key: {0}")]
40 InvalidPublicKey(String),
41 #[error("field mismatch: {0}")]
43 FieldMismatch(String),
44}
45
46impl NormalizedShare {
47 pub fn new(
49 scheme: &str,
50 quorum_id: &str,
51 party_idx: u32,
52 threshold: u32,
53 party_count: u32,
54 scalar_bytes: &[u8],
55 public_key_bytes: &[u8],
56 ) -> Result<Self, AdapterError> {
57 if scalar_bytes.len() != 32 {
58 return Err(AdapterError::InvalidScalar(format!(
59 "expected 32 bytes, got {}",
60 scalar_bytes.len()
61 )));
62 }
63 Ok(Self {
64 scheme: scheme.into(),
65 quorum_id: quorum_id.into(),
66 party_idx,
67 threshold,
68 party_count,
69 scalar_hex: hex::encode(scalar_bytes),
70 public_key_hex: hex::encode(public_key_bytes),
71 })
72 }
73
74 pub fn scalar_bytes(&self) -> Result<Vec<u8>, AdapterError> {
76 hex::decode(&self.scalar_hex).map_err(|e| AdapterError::InvalidScalar(e.to_string()))
77 }
78
79 pub fn public_key_bytes(&self) -> Result<Vec<u8>, AdapterError> {
81 hex::decode(&self.public_key_hex).map_err(|e| AdapterError::InvalidPublicKey(e.to_string()))
82 }
83
84 pub fn is_compatible_with(&self, other: &NormalizedShare) -> bool {
87 self.quorum_id == other.quorum_id
88 && self.public_key_hex == other.public_key_hex
89 && self.threshold == other.threshold
90 && self.party_count == other.party_count
91 }
92
93 pub fn to_json(&self) -> Result<String, serde_json::Error> {
95 serde_json::to_string_pretty(self)
96 }
97
98 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
100 serde_json::from_str(json)
101 }
102
103 pub fn reclassify(mut self, new_scheme: &str) -> Self {
106 self.scheme = new_scheme.into();
107 self
108 }
109}
110
111pub trait ShareAdapter {
114 fn to_normalized(&self, quorum_id: &str) -> Result<NormalizedShare, AdapterError>;
116
117 fn from_normalized(normalized: &NormalizedShare) -> Result<Self, AdapterError>
120 where
121 Self: Sized;
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 fn make_share(scheme: &str) -> NormalizedShare {
129 NormalizedShare::new(scheme, "quorum-alpha", 3, 2, 5, &[0xAA; 32], &[0x04; 65]).unwrap()
130 }
131
132 #[test]
133 fn new_validates_scalar_length() {
134 assert!(NormalizedShare::new("CMP20", "q", 1, 2, 3, &[0; 31], &[0; 65]).is_err());
135 assert!(NormalizedShare::new("CMP20", "q", 1, 2, 3, &[0; 32], &[0; 65]).is_ok());
136 }
137
138 #[test]
139 fn scalar_bytes_round_trips() {
140 let share = make_share("CMP20");
141 let bytes = share.scalar_bytes().unwrap();
142 assert_eq!(bytes, vec![0xAA; 32]);
143 }
144
145 #[test]
146 fn public_key_bytes_round_trips() {
147 let share = make_share("CMP20");
148 let bytes = share.public_key_bytes().unwrap();
149 assert_eq!(bytes, vec![0x04; 65]);
150 }
151
152 #[test]
153 fn is_compatible_same_quorum() {
154 let a = make_share("CMP20");
155 let b = make_share("CMP20");
156 assert!(a.is_compatible_with(&b));
157 }
158
159 #[test]
160 fn is_incompatible_different_quorum() {
161 let a = make_share("CMP20");
162 let mut b = make_share("CMP20");
163 b.quorum_id = "different".into();
164 assert!(!a.is_compatible_with(&b));
165 }
166
167 #[test]
168 fn is_incompatible_different_threshold() {
169 let a = make_share("CMP20");
170 let mut b = make_share("CMP20");
171 b.threshold = 3;
172 assert!(!a.is_compatible_with(&b));
173 }
174
175 #[test]
176 fn json_round_trip() {
177 let share = make_share("CMP20");
178 let json = share.to_json().unwrap();
179 let recovered = NormalizedShare::from_json(&json).unwrap();
180 assert_eq!(share, recovered);
181 }
182
183 #[test]
184 fn reclassify_changes_scheme() {
185 let share = make_share("CMP20");
186 let reclassified = share.reclassify("FROST-P256");
187 assert_eq!(reclassified.scheme, "FROST-P256");
188 }
189
190 #[test]
191 fn reclassify_preserves_other_fields() {
192 let share = make_share("CMP20");
193 let reclassified = share.reclassify("GG18");
194 assert_eq!(reclassified.quorum_id, "quorum-alpha");
195 assert_eq!(reclassified.party_idx, 3);
196 assert_eq!(reclassified.threshold, 2);
197 }
198}