Skip to main content

chains_sdk/bls/
mod.rs

1//! BLS12-381 signer for Ethereum Proof-of-Stake (Beacon Chain).
2//!
3//! Uses the `blst` crate for BLS12-381 operations including
4//! single signing, signature aggregation, and aggregated verification.
5
6pub mod eip2333;
7pub mod keystore;
8pub mod threshold;
9
10use crate::error::SignerError;
11use crate::traits;
12use blst::min_pk::{AggregateSignature, PublicKey, SecretKey, Signature as BlstSignature};
13use blst::BLST_ERROR;
14use zeroize::Zeroizing;
15
16/// Domain Separation Tag for Ethereum Beacon Chain BLS.
17pub const ETH2_DST: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_";
18
19/// A BLS12-381 signature (96 bytes, G2 point).
20#[derive(Debug, Clone, PartialEq, Eq)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22#[must_use]
23pub struct BlsSignature {
24    /// The 96-byte compressed G2 signature.
25    #[cfg_attr(feature = "serde", serde(with = "crate::hex_bytes"))]
26    pub bytes: [u8; 96],
27}
28
29impl core::fmt::Display for BlsSignature {
30    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
31        write!(f, "0x")?;
32        for byte in &self.bytes {
33            write!(f, "{byte:02x}")?;
34        }
35        Ok(())
36    }
37}
38
39impl BlsSignature {
40    /// Export the 96-byte signature.
41    pub fn to_bytes(&self) -> [u8; 96] {
42        self.bytes
43    }
44
45    /// Import from 96 bytes.
46    pub fn from_bytes(bytes: &[u8]) -> Result<Self, SignerError> {
47        if bytes.len() != 96 {
48            return Err(SignerError::InvalidSignature(format!(
49                "expected 96 bytes, got {}",
50                bytes.len()
51            )));
52        }
53        let mut out = [0u8; 96];
54        out.copy_from_slice(bytes);
55        Ok(Self { bytes: out })
56    }
57}
58
59/// A BLS12-381 public key (48 bytes, G1 point).
60#[derive(Debug, Clone, PartialEq, Eq)]
61#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
62pub struct BlsPublicKey {
63    /// The 48-byte compressed G1 public key.
64    #[cfg_attr(feature = "serde", serde(with = "crate::hex_bytes"))]
65    pub bytes: [u8; 48],
66}
67
68impl BlsPublicKey {
69    /// Export the 48-byte public key.
70    pub fn to_bytes(&self) -> [u8; 48] {
71        self.bytes
72    }
73
74    /// Import from 48 bytes.
75    pub fn from_bytes(bytes: &[u8]) -> Result<Self, SignerError> {
76        if bytes.len() != 48 {
77            return Err(SignerError::InvalidPublicKey(format!(
78                "expected 48 bytes, got {}",
79                bytes.len()
80            )));
81        }
82        let mut out = [0u8; 48];
83        out.copy_from_slice(bytes);
84        Ok(Self { bytes: out })
85    }
86}
87
88/// BLS12-381 signer for Ethereum PoS.
89pub struct BlsSigner {
90    secret_key: SecretKey,
91}
92
93// Manual Zeroize implementation since blst SecretKey stores raw bytes
94impl Drop for BlsSigner {
95    fn drop(&mut self) {
96        // SecretKey internally stores the scalar — blst handles zeroization
97        // We rely on blst's own cleanup, but mark the type as zeroize-aware
98    }
99}
100
101impl BlsSigner {
102    /// Get the public key.
103    pub fn public_key(&self) -> BlsPublicKey {
104        let pk = self.secret_key.sk_to_pk();
105        let bytes = pk.compress();
106        let mut out = [0u8; 48];
107        out.copy_from_slice(&bytes);
108        BlsPublicKey { bytes: out }
109    }
110}
111
112impl traits::Signer for BlsSigner {
113    type Signature = BlsSignature;
114    type Error = SignerError;
115
116    fn sign(&self, message: &[u8]) -> Result<BlsSignature, SignerError> {
117        let sig = self.secret_key.sign(message, ETH2_DST, &[]);
118        let compressed = sig.compress();
119        let mut bytes = [0u8; 96];
120        bytes.copy_from_slice(&compressed);
121        Ok(BlsSignature { bytes })
122    }
123
124    /// **Note:** BLS uses hash-to-curve (H2C) internally. This method is identical to
125    /// `sign()` — the `digest` parameter is treated as a raw message, not a
126    /// pre-computed hash. For consistency with the `Signer` trait, this is provided as-is.
127    fn sign_prehashed(&self, digest: &[u8]) -> Result<BlsSignature, SignerError> {
128        // BLS hash-to-curve means there's no external pre-hashing.
129        self.sign(digest)
130    }
131
132    fn public_key_bytes(&self) -> Vec<u8> {
133        self.public_key().bytes.to_vec()
134    }
135
136    fn public_key_bytes_uncompressed(&self) -> Vec<u8> {
137        // BLS12-381 G1 only has compressed form
138        self.public_key_bytes()
139    }
140}
141
142impl traits::KeyPair for BlsSigner {
143    fn generate() -> Result<Self, SignerError> {
144        use zeroize::Zeroize;
145        let mut ikm = [0u8; 32];
146        crate::security::secure_random(&mut ikm)?;
147        let secret_key = SecretKey::key_gen(&ikm, &[]).map_err(|_| SignerError::EntropyError)?;
148        ikm.zeroize(); // volatile write barrier — cannot be optimized away
149        Ok(Self { secret_key })
150    }
151
152    fn from_bytes(private_key: &[u8]) -> Result<Self, SignerError> {
153        if private_key.len() != 32 {
154            return Err(SignerError::InvalidPrivateKey(format!(
155                "expected 32 bytes, got {}",
156                private_key.len()
157            )));
158        }
159        let secret_key = SecretKey::from_bytes(private_key)
160            .map_err(|_| SignerError::InvalidPrivateKey("invalid BLS secret key".into()))?;
161        Ok(Self { secret_key })
162    }
163
164    fn private_key_bytes(&self) -> Zeroizing<Vec<u8>> {
165        Zeroizing::new(self.secret_key.to_bytes().to_vec())
166    }
167}
168
169/// BLS12-381 verifier.
170pub struct BlsVerifier {
171    public_key: PublicKey,
172}
173
174impl BlsVerifier {
175    /// Create from 48-byte compressed public key.
176    pub fn from_public_key_bytes(bytes: &[u8]) -> Result<Self, SignerError> {
177        if bytes.len() != 48 {
178            return Err(SignerError::InvalidPublicKey(format!(
179                "expected 48 bytes, got {}",
180                bytes.len()
181            )));
182        }
183        let public_key = PublicKey::from_bytes(bytes)
184            .map_err(|_| SignerError::InvalidPublicKey("invalid BLS public key".into()))?;
185        Ok(Self { public_key })
186    }
187}
188
189impl traits::Verifier for BlsVerifier {
190    type Signature = BlsSignature;
191    type Error = SignerError;
192
193    fn verify(&self, message: &[u8], signature: &BlsSignature) -> Result<bool, SignerError> {
194        let sig = BlstSignature::from_bytes(&signature.bytes)
195            .map_err(|_| SignerError::InvalidSignature("invalid BLS signature".into()))?;
196        let result = sig.verify(true, message, ETH2_DST, &[], &self.public_key, true);
197        Ok(result == BLST_ERROR::BLST_SUCCESS)
198    }
199
200    fn verify_prehashed(
201        &self,
202        digest: &[u8],
203        signature: &BlsSignature,
204    ) -> Result<bool, SignerError> {
205        self.verify(digest, signature)
206    }
207}
208
209/// Aggregate multiple BLS signatures into a single signature.
210pub fn aggregate_signatures(signatures: &[BlsSignature]) -> Result<BlsSignature, SignerError> {
211    if signatures.is_empty() {
212        return Err(SignerError::AggregationError(
213            "no signatures to aggregate".into(),
214        ));
215    }
216
217    let blst_sigs: Vec<BlstSignature> = signatures
218        .iter()
219        .map(|s| {
220            BlstSignature::from_bytes(&s.bytes)
221                .map_err(|_| SignerError::InvalidSignature("invalid BLS signature".into()))
222        })
223        .collect::<Result<Vec<_>, _>>()?;
224
225    let sig_refs: Vec<&BlstSignature> = blst_sigs.iter().collect();
226    let agg = AggregateSignature::aggregate(&sig_refs, true)
227        .map_err(|_| SignerError::AggregationError("aggregation failed".into()))?;
228
229    let compressed = agg.to_signature().compress();
230    let mut bytes = [0u8; 96];
231    bytes.copy_from_slice(&compressed);
232    Ok(BlsSignature { bytes })
233}
234
235/// Verify an aggregated BLS signature against multiple public keys (same message).
236pub fn verify_aggregated(
237    public_keys: &[BlsPublicKey],
238    message: &[u8],
239    agg_signature: &BlsSignature,
240) -> Result<bool, SignerError> {
241    if public_keys.is_empty() {
242        return Err(SignerError::AggregationError("no public keys".into()));
243    }
244
245    let pks: Vec<PublicKey> = public_keys
246        .iter()
247        .map(|pk| {
248            PublicKey::from_bytes(&pk.bytes)
249                .map_err(|_| SignerError::InvalidPublicKey("invalid BLS public key".into()))
250        })
251        .collect::<Result<Vec<_>, _>>()?;
252
253    let pk_refs: Vec<&PublicKey> = pks.iter().collect();
254    let sig = BlstSignature::from_bytes(&agg_signature.bytes)
255        .map_err(|_| SignerError::InvalidSignature("invalid BLS signature".into()))?;
256
257    let msgs: Vec<&[u8]> = vec![message; pk_refs.len()];
258
259    let result = sig.aggregate_verify(true, &msgs, ETH2_DST, &pk_refs, true);
260    Ok(result == BLST_ERROR::BLST_SUCCESS)
261}
262
263/// Verify an aggregated BLS signature where each signer signed a **different message**.
264///
265/// This is the standard ETH2 attestation pattern: N validators each sign their own
266/// message, the signatures are aggregated, and the verifier checks all (pk, msg) pairs
267/// against the single aggregated signature.
268///
269/// `pairs`: slice of `(public_key, message)` tuples.
270pub fn verify_aggregated_multi(
271    pairs: &[(BlsPublicKey, &[u8])],
272    agg_signature: &BlsSignature,
273) -> Result<bool, SignerError> {
274    if pairs.is_empty() {
275        return Err(SignerError::AggregationError("no pairs to verify".into()));
276    }
277
278    let pks: Vec<PublicKey> = pairs
279        .iter()
280        .map(|(pk, _)| {
281            PublicKey::from_bytes(&pk.bytes)
282                .map_err(|_| SignerError::InvalidPublicKey("invalid BLS public key".into()))
283        })
284        .collect::<Result<Vec<_>, _>>()?;
285
286    let pk_refs: Vec<&PublicKey> = pks.iter().collect();
287    let msgs: Vec<&[u8]> = pairs.iter().map(|(_, m)| *m).collect();
288
289    let sig = BlstSignature::from_bytes(&agg_signature.bytes)
290        .map_err(|_| SignerError::InvalidSignature("invalid BLS signature".into()))?;
291
292    let result = sig.aggregate_verify(true, &msgs, ETH2_DST, &pk_refs, true);
293    Ok(result == BLST_ERROR::BLST_SUCCESS)
294}
295
296#[cfg(test)]
297#[allow(clippy::unwrap_used, clippy::expect_used)]
298mod tests {
299    use super::*;
300    use crate::traits::{KeyPair, Signer, Verifier};
301
302    #[test]
303    fn test_generate_keypair() {
304        let signer = BlsSigner::generate().unwrap();
305        assert_eq!(signer.public_key_bytes().len(), 48);
306    }
307
308    #[test]
309    fn test_from_bytes_roundtrip() {
310        let signer = BlsSigner::generate().unwrap();
311        let key_bytes = signer.private_key_bytes();
312        let restored = BlsSigner::from_bytes(&key_bytes).unwrap();
313        assert_eq!(signer.public_key_bytes(), restored.public_key_bytes());
314    }
315
316    #[test]
317    fn test_sign_verify_roundtrip() {
318        let signer = BlsSigner::generate().unwrap();
319        let sig = signer.sign(b"hello bls").unwrap();
320        let verifier = BlsVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
321        assert!(verifier.verify(b"hello bls", &sig).unwrap());
322    }
323
324    #[test]
325    fn test_signature_96_bytes() {
326        let signer = BlsSigner::generate().unwrap();
327        let sig = signer.sign(b"test").unwrap();
328        assert_eq!(sig.bytes.len(), 96);
329    }
330
331    #[test]
332    fn test_aggregate_2_sigs() {
333        let msg = b"aggregate test";
334        let s1 = BlsSigner::generate().unwrap();
335        let s2 = BlsSigner::generate().unwrap();
336        let sig1 = s1.sign(msg).unwrap();
337        let sig2 = s2.sign(msg).unwrap();
338
339        let agg_sig = aggregate_signatures(&[sig1, sig2]).unwrap();
340        let result = verify_aggregated(&[s1.public_key(), s2.public_key()], msg, &agg_sig).unwrap();
341        assert!(result);
342    }
343
344    #[test]
345    fn test_aggregate_10_sigs() {
346        let msg = b"ten signers";
347        let signers: Vec<BlsSigner> = (0..10).map(|_| BlsSigner::generate().unwrap()).collect();
348        let sigs: Vec<BlsSignature> = signers.iter().map(|s| s.sign(msg).unwrap()).collect();
349        let pks: Vec<BlsPublicKey> = signers.iter().map(|s| s.public_key()).collect();
350
351        let agg_sig = aggregate_signatures(&sigs).unwrap();
352        assert!(verify_aggregated(&pks, msg, &agg_sig).unwrap());
353    }
354
355    #[test]
356    fn test_invalid_agg_fails() {
357        let msg = b"bad aggregate";
358        let s1 = BlsSigner::generate().unwrap();
359        let s2 = BlsSigner::generate().unwrap();
360        let sig1 = s1.sign(msg).unwrap();
361        let sig2 = s2.sign(b"different message").unwrap(); // wrong message
362
363        let agg_sig = aggregate_signatures(&[sig1, sig2]).unwrap();
364        let result = verify_aggregated(&[s1.public_key(), s2.public_key()], msg, &agg_sig).unwrap();
365        assert!(!result);
366    }
367
368    #[test]
369    fn test_dst_correctness() {
370        assert_eq!(ETH2_DST, b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_");
371    }
372
373    #[test]
374    fn test_known_vector_eth2() {
375        // Use a deterministic secret key and verify sign → verify round-trip
376        let sk_bytes =
377            hex::decode("263dbd792f5b1be47ed85f8938c0f29586af0d3ac7b977f21c278fe1462040e3")
378                .unwrap();
379        let signer = BlsSigner::from_bytes(&sk_bytes).unwrap();
380        let msg = hex::decode("5656565656565656565656565656565656565656565656565656565656565656")
381            .unwrap();
382        let sig = signer.sign(&msg).unwrap();
383        let verifier = BlsVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
384        assert!(verifier.verify(&msg, &sig).unwrap());
385    }
386
387    #[test]
388    fn test_invalid_key_rejected() {
389        assert!(BlsSigner::from_bytes(&[0u8; 31]).is_err());
390        assert!(BlsSigner::from_bytes(&[0u8; 33]).is_err());
391    }
392
393    #[test]
394    fn test_tampered_sig_fails() {
395        let signer = BlsSigner::generate().unwrap();
396        let sig = signer.sign(b"tamper").unwrap();
397        let verifier = BlsVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
398        let mut tampered = sig.clone();
399        tampered.bytes[0] ^= 0xff;
400        let result = verifier.verify(b"tamper", &tampered);
401        assert!(result.is_err() || !result.unwrap());
402    }
403
404    #[test]
405    fn test_sign_prehashed_roundtrip() {
406        let signer = BlsSigner::generate().unwrap();
407        let msg = b"prehash bls";
408        let sig = signer.sign_prehashed(msg).unwrap();
409        let verifier = BlsVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
410        assert!(verifier.verify(msg, &sig).unwrap());
411    }
412
413    #[test]
414    fn test_zeroize_on_drop() {
415        let signer = BlsSigner::generate().unwrap();
416        let _: Zeroizing<Vec<u8>> = signer.private_key_bytes();
417        drop(signer);
418    }
419
420    #[test]
421    fn test_multi_message_aggregation() {
422        let s1 = BlsSigner::generate().unwrap();
423        let s2 = BlsSigner::generate().unwrap();
424        let s3 = BlsSigner::generate().unwrap();
425
426        let msg1 = b"attestation slot 100";
427        let msg2 = b"attestation slot 101";
428        let msg3 = b"attestation slot 102";
429
430        let sig1 = s1.sign(msg1).unwrap();
431        let sig2 = s2.sign(msg2).unwrap();
432        let sig3 = s3.sign(msg3).unwrap();
433
434        let agg = aggregate_signatures(&[sig1, sig2, sig3]).unwrap();
435
436        let pairs: Vec<(BlsPublicKey, &[u8])> = vec![
437            (s1.public_key(), msg1.as_slice()),
438            (s2.public_key(), msg2.as_slice()),
439            (s3.public_key(), msg3.as_slice()),
440        ];
441        assert!(verify_aggregated_multi(&pairs, &agg).unwrap());
442    }
443
444    #[test]
445    fn test_multi_message_wrong_message_fails() {
446        let s1 = BlsSigner::generate().unwrap();
447        let s2 = BlsSigner::generate().unwrap();
448
449        let sig1 = s1.sign(b"correct 1").unwrap();
450        let sig2 = s2.sign(b"correct 2").unwrap();
451
452        let agg = aggregate_signatures(&[sig1, sig2]).unwrap();
453
454        let pairs: Vec<(BlsPublicKey, &[u8])> = vec![
455            (s1.public_key(), b"correct 1".as_slice()),
456            (s2.public_key(), b"WRONG MESSAGE".as_slice()), // wrong
457        ];
458        assert!(!verify_aggregated_multi(&pairs, &agg).unwrap());
459    }
460}