Skip to main content

dig_keystore/scheme/
bls_signing.rs

1//! `BlsSigning` — the DIG L2 validator BLS signing key scheme.
2//!
3//! Stored as a 32-byte seed. On unlock, derives a [`chia_bls::SecretKey`] via
4//! [`chia_bls::SecretKey::from_seed`], which is the standard Chia BLS12-381
5//! EIP-2333-compatible derivation.
6//!
7//! This is the scheme every DIG validator uses for its L2 signing key. The
8//! on-disk file magic is `DIGVK1`.
9
10use chia_bls::{PublicKey, SecretKey, Signature};
11use rand_core::{CryptoRng, RngCore};
12use zeroize::Zeroizing;
13
14use crate::error::{KeystoreError, Result};
15use crate::scheme::KeyScheme;
16
17/// DIG validator BLS signing key (G1 pubkey / G2 signature on BLS12-381).
18///
19/// See [`crate::scheme`] for how this plugs into [`crate::Keystore`].
20#[derive(Debug, Clone, Copy)]
21pub struct BlsSigning;
22
23impl KeyScheme for BlsSigning {
24    type PublicKey = PublicKey;
25    type Signature = Signature;
26
27    const MAGIC: [u8; 6] = *b"DIGVK1";
28    const NAME: &'static str = "BlsSigning";
29    const SCHEME_ID: u16 = 0x0001;
30    const SECRET_LEN: usize = 32;
31
32    fn generate<R: RngCore + CryptoRng>(rng: &mut R) -> Zeroizing<Vec<u8>> {
33        let mut seed = Zeroizing::new(vec![0u8; Self::SECRET_LEN]);
34        rng.fill_bytes(&mut seed);
35        seed
36    }
37
38    fn public_key(secret: &[u8]) -> Result<Self::PublicKey> {
39        let sk = secret_to_bls_secret_key(secret)?;
40        let pk = sk.public_key();
41        Ok(pk)
42    }
43
44    fn sign(secret: &[u8], msg: &[u8]) -> Result<Self::Signature> {
45        let sk = secret_to_bls_secret_key(secret)?;
46        Ok(chia_bls::sign(&sk, msg))
47    }
48}
49
50/// Derive a `chia_bls::SecretKey` from exactly-32 bytes of seed material.
51pub(crate) fn secret_to_bls_secret_key(secret: &[u8]) -> Result<SecretKey> {
52    if secret.len() != BlsSigning::SECRET_LEN {
53        return Err(KeystoreError::InvalidPlaintext {
54            expected: BlsSigning::SECRET_LEN,
55            got: secret.len(),
56        });
57    }
58    // `SecretKey::from_seed` accepts any byte length, but for interop with other
59    // Chia tooling we fix at 32 bytes.
60    Ok(SecretKey::from_seed(secret))
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use rand_chacha::rand_core::SeedableRng;
67
68    #[test]
69    fn generate_is_32_bytes() {
70        let mut rng = rand_chacha::ChaCha20Rng::seed_from_u64(42);
71        let seed = BlsSigning::generate(&mut rng);
72        assert_eq!(seed.len(), 32);
73    }
74
75    #[test]
76    fn public_key_deterministic() {
77        let seed = [7u8; 32];
78        let pk1 = BlsSigning::public_key(&seed).unwrap();
79        let pk2 = BlsSigning::public_key(&seed).unwrap();
80        assert_eq!(pk1.to_bytes(), pk2.to_bytes());
81    }
82
83    #[test]
84    fn sign_verifies_via_chia_bls() {
85        let seed = [11u8; 32];
86        let pk = BlsSigning::public_key(&seed).unwrap();
87        let msg = b"hello";
88        let sig = BlsSigning::sign(&seed, msg).unwrap();
89        assert!(chia_bls::verify(&sig, &pk, msg));
90    }
91
92    #[test]
93    fn sign_different_messages_produce_different_signatures() {
94        let seed = [11u8; 32];
95        let s1 = BlsSigning::sign(&seed, b"msg1").unwrap();
96        let s2 = BlsSigning::sign(&seed, b"msg2").unwrap();
97        assert_ne!(s1.to_bytes(), s2.to_bytes());
98    }
99
100    #[test]
101    fn different_seeds_produce_different_keys() {
102        let pk1 = BlsSigning::public_key(&[1u8; 32]).unwrap();
103        let pk2 = BlsSigning::public_key(&[2u8; 32]).unwrap();
104        assert_ne!(pk1.to_bytes(), pk2.to_bytes());
105    }
106
107    #[test]
108    fn wrong_seed_length_errors() {
109        let err = BlsSigning::public_key(&[0u8; 16]).unwrap_err();
110        assert!(matches!(
111            err,
112            KeystoreError::InvalidPlaintext {
113                expected: 32,
114                got: 16
115            }
116        ));
117    }
118}