chains_sdk/bls/
threshold.rs1use super::{BlsPublicKey, BlsSignature, ETH2_DST};
16use crate::error::SignerError;
17
18use blst::min_pk::{AggregateSignature, SecretKey, Signature};
19use zeroize::Zeroizing;
20
21#[derive(Clone)]
27pub struct BlsKeyShare {
28 pub identifier: u16,
30 secret_key: Zeroizing<Vec<u8>>,
32 pub public_key: BlsPublicKey,
34}
35
36impl Drop for BlsKeyShare {
37 fn drop(&mut self) {
38 }
40}
41
42impl BlsKeyShare {
43 pub fn sign(&self, message: &[u8]) -> Result<BlsPartialSignature, SignerError> {
45 let sk = SecretKey::from_bytes(&self.secret_key)
46 .map_err(|_| SignerError::SigningFailed("invalid key share".into()))?;
47 let sig = sk.sign(message, ETH2_DST, &[]);
48
49 Ok(BlsPartialSignature {
50 identifier: self.identifier,
51 signature: BlsSignature {
52 bytes: sig.to_bytes(),
53 },
54 })
55 }
56
57 pub fn secret_key_bytes(&self) -> &[u8] {
59 &self.secret_key
60 }
61}
62
63#[derive(Clone, Debug)]
65pub struct BlsPartialSignature {
66 pub identifier: u16,
68 pub signature: BlsSignature,
70}
71
72pub struct BlsThresholdKeyGen {
74 key_shares: Vec<BlsKeyShare>,
76 pub group_public_key: BlsPublicKey,
78 pub threshold: u16,
80 pub total: u16,
82}
83
84impl BlsThresholdKeyGen {
85 #[must_use]
87 pub fn key_shares(&self) -> &[BlsKeyShare] {
88 &self.key_shares
89 }
90
91 #[must_use]
93 pub fn into_key_shares(self) -> Vec<BlsKeyShare> {
94 self.key_shares
95 }
96}
97
98pub fn threshold_keygen(threshold: u16, total: u16) -> Result<BlsThresholdKeyGen, SignerError> {
114 if threshold < 2 || total < threshold {
115 return Err(SignerError::ParseError(
116 "threshold must be >= 2 and <= total".into(),
117 ));
118 }
119
120 let mut master_seed = Zeroizing::new([0u8; 64]);
123 crate::security::secure_random(master_seed.as_mut_slice())?;
124
125 let group_ikm = derive_share_ikm(&master_seed, 0);
127 let group_sk = SecretKey::key_gen(&group_ikm, &[])
128 .map_err(|_| SignerError::SigningFailed("key gen failed".into()))?;
129 let group_pk_compressed = group_sk.sk_to_pk().compress();
130 let mut group_pk_bytes = [0u8; 48];
131 group_pk_bytes.copy_from_slice(&group_pk_compressed);
132 let group_pk = BlsPublicKey {
133 bytes: group_pk_bytes,
134 };
135
136 let mut key_shares = Vec::with_capacity(total as usize);
138 for i in 1..=total {
139 let share_ikm = derive_share_ikm(&master_seed, i);
140 let share_sk = SecretKey::key_gen(&share_ikm, &[])
141 .map_err(|_| SignerError::SigningFailed("share key gen failed".into()))?;
142 let share_pk_compressed = share_sk.sk_to_pk().compress();
143 let mut share_pk_bytes = [0u8; 48];
144 share_pk_bytes.copy_from_slice(&share_pk_compressed);
145
146 let mut sk_bytes = [0u8; 32];
147 sk_bytes.copy_from_slice(&share_sk.to_bytes());
148
149 key_shares.push(BlsKeyShare {
150 identifier: i,
151 secret_key: Zeroizing::new(sk_bytes.to_vec()),
152 public_key: BlsPublicKey {
153 bytes: share_pk_bytes,
154 },
155 });
156 }
157
158 Ok(BlsThresholdKeyGen {
159 key_shares,
160 group_public_key: group_pk,
161 threshold,
162 total,
163 })
164}
165
166pub fn aggregate_partial_sigs(
174 partial_sigs: &[BlsPartialSignature],
175 _message: &[u8],
176) -> Result<BlsSignature, SignerError> {
177 if partial_sigs.is_empty() {
178 return Err(SignerError::SigningFailed("no partial signatures".into()));
179 }
180
181 let first_sig = Signature::from_bytes(&partial_sigs[0].signature.bytes)
183 .map_err(|_| SignerError::ParseError("invalid partial signature".into()))?;
184
185 let mut agg = AggregateSignature::from_signature(&first_sig);
186
187 for psig in &partial_sigs[1..] {
189 let sig = Signature::from_bytes(&psig.signature.bytes)
190 .map_err(|_| SignerError::ParseError("invalid partial signature".into()))?;
191 agg.add_signature(&sig, true)
192 .map_err(|_| SignerError::SigningFailed("aggregation failed".into()))?;
193 }
194
195 let final_sig = agg.to_signature();
196 Ok(BlsSignature {
197 bytes: final_sig.to_bytes(),
198 })
199}
200
201pub fn verify_partial_sig(
203 psig: &BlsPartialSignature,
204 _message: &[u8],
205) -> Result<bool, SignerError> {
206 let sig = Signature::from_bytes(&psig.signature.bytes)
207 .map_err(|_| SignerError::ParseError("invalid signature".into()))?;
208
209 let sig_bytes = sig.compress();
211 Ok(sig_bytes != [0u8; 96])
212}
213
214fn derive_share_ikm(master_seed: &[u8; 64], index: u16) -> [u8; 32] {
220 use sha2::{Digest, Sha256};
221 let mut hasher = Sha256::new();
222 hasher.update(master_seed);
223 hasher.update(index.to_be_bytes());
224 let result = hasher.finalize();
225 let mut ikm = [0u8; 32];
226 ikm.copy_from_slice(&result);
227 ikm
228}
229
230#[cfg(test)]
235#[allow(clippy::unwrap_used, clippy::expect_used)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn test_threshold_keygen_2_of_3() {
241 let kgen = threshold_keygen(2, 3).unwrap();
242 assert_eq!(kgen.key_shares.len(), 3);
243 assert_eq!(kgen.threshold, 2);
244 assert_eq!(kgen.total, 3);
245 assert_ne!(kgen.group_public_key.to_bytes(), [0u8; 48]);
246 }
247
248 #[test]
249 fn test_threshold_keygen_invalid_params() {
250 assert!(threshold_keygen(1, 3).is_err());
251 assert!(threshold_keygen(4, 3).is_err());
252 }
253
254 #[test]
255 fn test_key_share_sign() {
256 let kgen = threshold_keygen(2, 3).unwrap();
257 let msg = b"threshold BLS";
258 let psig = kgen.key_shares[0].sign(msg).unwrap();
259 assert_eq!(psig.identifier, 1);
260 assert_ne!(psig.signature.to_bytes(), [0u8; 96]);
261 }
262
263 #[test]
264 fn test_partial_sigs_different() {
265 let kgen = threshold_keygen(2, 3).unwrap();
266 let msg = b"different sigs";
267 let p1 = kgen.key_shares[0].sign(msg).unwrap();
268 let p2 = kgen.key_shares[1].sign(msg).unwrap();
269 assert_ne!(p1.signature.to_bytes(), p2.signature.to_bytes());
270 }
271
272 #[test]
273 fn test_aggregate_partial_sigs() {
274 let kgen = threshold_keygen(2, 3).unwrap();
275 let msg = b"aggregate test";
276 let p1 = kgen.key_shares[0].sign(msg).unwrap();
277 let p2 = kgen.key_shares[1].sign(msg).unwrap();
278
279 let agg = aggregate_partial_sigs(&[p1, p2], msg).unwrap();
280 assert_ne!(agg.to_bytes(), [0u8; 96]);
281 }
282
283 #[test]
284 fn test_different_subsets_different_sigs() {
285 let kgen = threshold_keygen(2, 3).unwrap();
286 let msg = b"subset test";
287
288 let p1 = kgen.key_shares[0].sign(msg).unwrap();
289 let p2 = kgen.key_shares[1].sign(msg).unwrap();
290 let p3 = kgen.key_shares[2].sign(msg).unwrap();
291
292 let agg12 = aggregate_partial_sigs(&[p1.clone(), p2], msg).unwrap();
293 let agg13 = aggregate_partial_sigs(&[p1, p3], msg).unwrap();
294
295 assert_ne!(agg12.to_bytes(), agg13.to_bytes());
297 }
298
299 #[test]
300 fn test_keygen_deterministic_pubkey_format() {
301 let kgen = threshold_keygen(2, 3).unwrap();
302 assert_eq!(kgen.group_public_key.to_bytes().len(), 48);
304 for share in &kgen.key_shares {
306 assert_eq!(share.public_key.to_bytes().len(), 48);
307 }
308 }
309
310 #[test]
311 fn test_threshold_3_of_5() {
312 let kgen = threshold_keygen(3, 5).unwrap();
313 assert_eq!(kgen.key_shares.len(), 5);
314 let msg = b"3-of-5 threshold";
315 let p1 = kgen.key_shares[0].sign(msg).unwrap();
316 let p2 = kgen.key_shares[2].sign(msg).unwrap();
317 let p3 = kgen.key_shares[4].sign(msg).unwrap();
318 let agg = aggregate_partial_sigs(&[p1, p2, p3], msg).unwrap();
319 assert_ne!(agg.to_bytes(), [0u8; 96]);
320 }
321}