Skip to main content

chains_sdk/ethereum/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    /// # ⚠️ Not Actually Pre-Hashed
125    ///
126    /// BLS uses hash-to-curve (H2C) internally. This method is **identical to
127    /// `sign()`** — the `digest` parameter is treated as a raw message, not a
128    /// pre-computed hash. Passing a SHA-256 digest here will sign the digest
129    /// bytes as a message via H2C, NOT use them as a pre-computed hash.
130    ///
131    /// Provided for trait consistency only.
132    fn sign_prehashed(&self, digest: &[u8]) -> Result<BlsSignature, SignerError> {
133        self.sign(digest)
134    }
135
136    fn public_key_bytes(&self) -> Vec<u8> {
137        self.public_key().bytes.to_vec()
138    }
139
140    fn public_key_bytes_uncompressed(&self) -> Vec<u8> {
141        // BLS12-381 G1 only has compressed form
142        self.public_key_bytes()
143    }
144}
145
146impl traits::KeyPair for BlsSigner {
147    fn generate() -> Result<Self, SignerError> {
148        use zeroize::Zeroize;
149        let mut ikm = [0u8; 32];
150        crate::security::secure_random(&mut ikm)?;
151        let secret_key = SecretKey::key_gen(&ikm, &[]).map_err(|_| SignerError::EntropyError)?;
152        ikm.zeroize(); // volatile write barrier — cannot be optimized away
153        Ok(Self { secret_key })
154    }
155
156    fn from_bytes(private_key: &[u8]) -> Result<Self, SignerError> {
157        if private_key.len() != 32 {
158            return Err(SignerError::InvalidPrivateKey(format!(
159                "expected 32 bytes, got {}",
160                private_key.len()
161            )));
162        }
163        let secret_key = SecretKey::from_bytes(private_key)
164            .map_err(|_| SignerError::InvalidPrivateKey("invalid BLS secret key".into()))?;
165        Ok(Self { secret_key })
166    }
167
168    fn private_key_bytes(&self) -> Zeroizing<Vec<u8>> {
169        Zeroizing::new(self.secret_key.to_bytes().to_vec())
170    }
171}
172
173/// BLS12-381 verifier.
174pub struct BlsVerifier {
175    public_key: PublicKey,
176}
177
178impl BlsVerifier {
179    /// Create from 48-byte compressed public key.
180    pub fn from_public_key_bytes(bytes: &[u8]) -> Result<Self, SignerError> {
181        if bytes.len() != 48 {
182            return Err(SignerError::InvalidPublicKey(format!(
183                "expected 48 bytes, got {}",
184                bytes.len()
185            )));
186        }
187        let public_key = PublicKey::from_bytes(bytes)
188            .map_err(|_| SignerError::InvalidPublicKey("invalid BLS public key".into()))?;
189        Ok(Self { public_key })
190    }
191}
192
193impl traits::Verifier for BlsVerifier {
194    type Signature = BlsSignature;
195    type Error = SignerError;
196
197    fn verify(&self, message: &[u8], signature: &BlsSignature) -> Result<bool, SignerError> {
198        let sig = BlstSignature::from_bytes(&signature.bytes)
199            .map_err(|_| SignerError::InvalidSignature("invalid BLS signature".into()))?;
200        let result = sig.verify(true, message, ETH2_DST, &[], &self.public_key, true);
201        Ok(result == BLST_ERROR::BLST_SUCCESS)
202    }
203
204    fn verify_prehashed(
205        &self,
206        digest: &[u8],
207        signature: &BlsSignature,
208    ) -> Result<bool, SignerError> {
209        self.verify(digest, signature)
210    }
211}
212
213/// Aggregate multiple BLS signatures into a single signature.
214pub fn aggregate_signatures(signatures: &[BlsSignature]) -> Result<BlsSignature, SignerError> {
215    if signatures.is_empty() {
216        return Err(SignerError::AggregationError(
217            "no signatures to aggregate".into(),
218        ));
219    }
220
221    let blst_sigs: Vec<BlstSignature> = signatures
222        .iter()
223        .map(|s| {
224            BlstSignature::from_bytes(&s.bytes)
225                .map_err(|_| SignerError::InvalidSignature("invalid BLS signature".into()))
226        })
227        .collect::<Result<Vec<_>, _>>()?;
228
229    let sig_refs: Vec<&BlstSignature> = blst_sigs.iter().collect();
230    let agg = AggregateSignature::aggregate(&sig_refs, true)
231        .map_err(|_| SignerError::AggregationError("aggregation failed".into()))?;
232
233    let compressed = agg.to_signature().compress();
234    let mut bytes = [0u8; 96];
235    bytes.copy_from_slice(&compressed);
236    Ok(BlsSignature { bytes })
237}
238
239/// Verify an aggregated BLS signature against multiple public keys (same message).
240pub fn verify_aggregated(
241    public_keys: &[BlsPublicKey],
242    message: &[u8],
243    agg_signature: &BlsSignature,
244) -> Result<bool, SignerError> {
245    if public_keys.is_empty() {
246        return Err(SignerError::AggregationError("no public keys".into()));
247    }
248
249    let pks: Vec<PublicKey> = public_keys
250        .iter()
251        .map(|pk| {
252            PublicKey::from_bytes(&pk.bytes)
253                .map_err(|_| SignerError::InvalidPublicKey("invalid BLS public key".into()))
254        })
255        .collect::<Result<Vec<_>, _>>()?;
256
257    let pk_refs: Vec<&PublicKey> = pks.iter().collect();
258    let sig = BlstSignature::from_bytes(&agg_signature.bytes)
259        .map_err(|_| SignerError::InvalidSignature("invalid BLS signature".into()))?;
260
261    let msgs: Vec<&[u8]> = vec![message; pk_refs.len()];
262
263    let result = sig.aggregate_verify(true, &msgs, ETH2_DST, &pk_refs, true);
264    Ok(result == BLST_ERROR::BLST_SUCCESS)
265}
266
267/// Verify an aggregated BLS signature where each signer signed a **different message**.
268///
269/// This is the standard ETH2 attestation pattern: N validators each sign their own
270/// message, the signatures are aggregated, and the verifier checks all (pk, msg) pairs
271/// against the single aggregated signature.
272///
273/// `pairs`: slice of `(public_key, message)` tuples.
274pub fn verify_aggregated_multi(
275    pairs: &[(BlsPublicKey, &[u8])],
276    agg_signature: &BlsSignature,
277) -> Result<bool, SignerError> {
278    if pairs.is_empty() {
279        return Err(SignerError::AggregationError("no pairs to verify".into()));
280    }
281
282    let pks: Vec<PublicKey> = pairs
283        .iter()
284        .map(|(pk, _)| {
285            PublicKey::from_bytes(&pk.bytes)
286                .map_err(|_| SignerError::InvalidPublicKey("invalid BLS public key".into()))
287        })
288        .collect::<Result<Vec<_>, _>>()?;
289
290    let pk_refs: Vec<&PublicKey> = pks.iter().collect();
291    let msgs: Vec<&[u8]> = pairs.iter().map(|(_, m)| *m).collect();
292
293    let sig = BlstSignature::from_bytes(&agg_signature.bytes)
294        .map_err(|_| SignerError::InvalidSignature("invalid BLS signature".into()))?;
295
296    let result = sig.aggregate_verify(true, &msgs, ETH2_DST, &pk_refs, true);
297    Ok(result == BLST_ERROR::BLST_SUCCESS)
298}
299
300#[cfg(test)]
301#[allow(clippy::unwrap_used, clippy::expect_used)]
302mod tests {
303    use super::*;
304    use crate::traits::{KeyPair, Signer, Verifier};
305
306    #[test]
307    fn test_generate_keypair() {
308        let signer = BlsSigner::generate().unwrap();
309        assert_eq!(signer.public_key_bytes().len(), 48);
310    }
311
312    #[test]
313    fn test_from_bytes_roundtrip() {
314        let signer = BlsSigner::generate().unwrap();
315        let key_bytes = signer.private_key_bytes();
316        let restored = BlsSigner::from_bytes(&key_bytes).unwrap();
317        assert_eq!(signer.public_key_bytes(), restored.public_key_bytes());
318    }
319
320    #[test]
321    fn test_sign_verify_roundtrip() {
322        let signer = BlsSigner::generate().unwrap();
323        let sig = signer.sign(b"hello bls").unwrap();
324        let verifier = BlsVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
325        assert!(verifier.verify(b"hello bls", &sig).unwrap());
326    }
327
328    #[test]
329    fn test_signature_96_bytes() {
330        let signer = BlsSigner::generate().unwrap();
331        let sig = signer.sign(b"test").unwrap();
332        assert_eq!(sig.bytes.len(), 96);
333    }
334
335    #[test]
336    fn test_aggregate_2_sigs() {
337        let msg = b"aggregate test";
338        let s1 = BlsSigner::generate().unwrap();
339        let s2 = BlsSigner::generate().unwrap();
340        let sig1 = s1.sign(msg).unwrap();
341        let sig2 = s2.sign(msg).unwrap();
342
343        let agg_sig = aggregate_signatures(&[sig1, sig2]).unwrap();
344        let result = verify_aggregated(&[s1.public_key(), s2.public_key()], msg, &agg_sig).unwrap();
345        assert!(result);
346    }
347
348    #[test]
349    fn test_aggregate_10_sigs() {
350        let msg = b"ten signers";
351        let signers: Vec<BlsSigner> = (0..10).map(|_| BlsSigner::generate().unwrap()).collect();
352        let sigs: Vec<BlsSignature> = signers.iter().map(|s| s.sign(msg).unwrap()).collect();
353        let pks: Vec<BlsPublicKey> = signers.iter().map(|s| s.public_key()).collect();
354
355        let agg_sig = aggregate_signatures(&sigs).unwrap();
356        assert!(verify_aggregated(&pks, msg, &agg_sig).unwrap());
357    }
358
359    #[test]
360    fn test_invalid_agg_fails() {
361        let msg = b"bad aggregate";
362        let s1 = BlsSigner::generate().unwrap();
363        let s2 = BlsSigner::generate().unwrap();
364        let sig1 = s1.sign(msg).unwrap();
365        let sig2 = s2.sign(b"different message").unwrap(); // wrong message
366
367        let agg_sig = aggregate_signatures(&[sig1, sig2]).unwrap();
368        let result = verify_aggregated(&[s1.public_key(), s2.public_key()], msg, &agg_sig).unwrap();
369        assert!(!result);
370    }
371
372    #[test]
373    fn test_dst_correctness() {
374        assert_eq!(ETH2_DST, b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_");
375    }
376
377    #[test]
378    fn test_known_vector_eth2() {
379        // Use a deterministic secret key and verify sign → verify round-trip
380        let sk_bytes =
381            hex::decode("263dbd792f5b1be47ed85f8938c0f29586af0d3ac7b977f21c278fe1462040e3")
382                .unwrap();
383        let signer = BlsSigner::from_bytes(&sk_bytes).unwrap();
384        let msg = hex::decode("5656565656565656565656565656565656565656565656565656565656565656")
385            .unwrap();
386        let sig = signer.sign(&msg).unwrap();
387        let verifier = BlsVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
388        assert!(verifier.verify(&msg, &sig).unwrap());
389    }
390
391    #[test]
392    fn test_invalid_key_rejected() {
393        assert!(BlsSigner::from_bytes(&[0u8; 31]).is_err());
394        assert!(BlsSigner::from_bytes(&[0u8; 33]).is_err());
395    }
396
397    #[test]
398    fn test_tampered_sig_fails() {
399        let signer = BlsSigner::generate().unwrap();
400        let sig = signer.sign(b"tamper").unwrap();
401        let verifier = BlsVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
402        let mut tampered = sig.clone();
403        tampered.bytes[0] ^= 0xff;
404        let result = verifier.verify(b"tamper", &tampered);
405        assert!(result.is_err() || !result.unwrap());
406    }
407
408    #[test]
409    fn test_sign_prehashed_roundtrip() {
410        let signer = BlsSigner::generate().unwrap();
411        let msg = b"prehash bls";
412        let sig = signer.sign_prehashed(msg).unwrap();
413        let verifier = BlsVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
414        assert!(verifier.verify(msg, &sig).unwrap());
415    }
416
417    #[test]
418    fn test_zeroize_on_drop() {
419        let signer = BlsSigner::generate().unwrap();
420        let _: Zeroizing<Vec<u8>> = signer.private_key_bytes();
421        drop(signer);
422    }
423
424    #[test]
425    fn test_multi_message_aggregation() {
426        let s1 = BlsSigner::generate().unwrap();
427        let s2 = BlsSigner::generate().unwrap();
428        let s3 = BlsSigner::generate().unwrap();
429
430        let msg1 = b"attestation slot 100";
431        let msg2 = b"attestation slot 101";
432        let msg3 = b"attestation slot 102";
433
434        let sig1 = s1.sign(msg1).unwrap();
435        let sig2 = s2.sign(msg2).unwrap();
436        let sig3 = s3.sign(msg3).unwrap();
437
438        let agg = aggregate_signatures(&[sig1, sig2, sig3]).unwrap();
439
440        let pairs: Vec<(BlsPublicKey, &[u8])> = vec![
441            (s1.public_key(), msg1.as_slice()),
442            (s2.public_key(), msg2.as_slice()),
443            (s3.public_key(), msg3.as_slice()),
444        ];
445        assert!(verify_aggregated_multi(&pairs, &agg).unwrap());
446    }
447
448    #[test]
449    fn test_multi_message_wrong_message_fails() {
450        let s1 = BlsSigner::generate().unwrap();
451        let s2 = BlsSigner::generate().unwrap();
452
453        let sig1 = s1.sign(b"correct 1").unwrap();
454        let sig2 = s2.sign(b"correct 2").unwrap();
455
456        let agg = aggregate_signatures(&[sig1, sig2]).unwrap();
457
458        let pairs: Vec<(BlsPublicKey, &[u8])> = vec![
459            (s1.public_key(), b"correct 1".as_slice()),
460            (s2.public_key(), b"WRONG MESSAGE".as_slice()), // wrong
461        ];
462        assert!(!verify_aggregated_multi(&pairs, &agg).unwrap());
463    }
464}