Skip to main content

dig_keystore/scheme/
l1_wallet_bls.rs

1//! `L1WalletBls` — the DIG / Chia L1 wallet BLS signing key scheme.
2//!
3//! Chia L1 wallets use BLS12-381 signatures. The stored secret is a 32-byte
4//! seed; HD derivation (`m/12381/8444/2/{index}`) happens in the wallet layer
5//! above this crate. For the keystore, we only need to round-trip the master
6//! seed and expose the master key's public key / signature operations.
7//!
8//! On-disk file magic is `DIGLW1`.
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/Chia L1 wallet master BLS key (the root of wallet HD derivation).
18///
19/// Callers typically unlock this, take the derived [`chia_bls::SecretKey`], and
20/// use `chia_bls::DerivableKey::derive_unhardened` / `derive_hardened` at the
21/// wallet layer. The keystore does not itself perform HD derivation.
22#[derive(Debug, Clone, Copy)]
23pub struct L1WalletBls;
24
25impl KeyScheme for L1WalletBls {
26    type PublicKey = PublicKey;
27    type Signature = Signature;
28
29    const MAGIC: [u8; 6] = *b"DIGLW1";
30    const NAME: &'static str = "L1WalletBls";
31    const SCHEME_ID: u16 = 0x0003;
32    const SECRET_LEN: usize = 32;
33
34    fn generate<R: RngCore + CryptoRng>(rng: &mut R) -> Zeroizing<Vec<u8>> {
35        let mut seed = Zeroizing::new(vec![0u8; Self::SECRET_LEN]);
36        rng.fill_bytes(&mut seed);
37        seed
38    }
39
40    fn public_key(secret: &[u8]) -> Result<Self::PublicKey> {
41        let sk = secret_to_secret_key(secret)?;
42        Ok(sk.public_key())
43    }
44
45    fn sign(secret: &[u8], msg: &[u8]) -> Result<Self::Signature> {
46        let sk = secret_to_secret_key(secret)?;
47        Ok(chia_bls::sign(&sk, msg))
48    }
49}
50
51fn secret_to_secret_key(secret: &[u8]) -> Result<SecretKey> {
52    if secret.len() != L1WalletBls::SECRET_LEN {
53        return Err(KeystoreError::InvalidPlaintext {
54            expected: L1WalletBls::SECRET_LEN,
55            got: secret.len(),
56        });
57    }
58    Ok(SecretKey::from_seed(secret))
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    /// **Proves:** `L1WalletBls::MAGIC` and `L1WalletBls::SCHEME_ID` differ
66    /// from those of [`crate::scheme::BlsSigning`].
67    ///
68    /// **Why it matters:** Type confusion between a validator signing key
69    /// and a wallet master seed is the single most dangerous regression this
70    /// crate could ship. If the two schemes ever shared a magic or a scheme
71    /// id, `Keystore::<BlsSigning>::load` would silently accept a wallet
72    /// file (or vice versa) and the two code paths would use each other's
73    /// keys. This test is the tripwire.
74    ///
75    /// **Catches:** copy-paste of the `MAGIC` / `SCHEME_ID` constants from
76    /// `BlsSigning` without editing them for the new scheme.
77    #[test]
78    fn magic_differs_from_bls_signing() {
79        use crate::scheme::BlsSigning;
80        assert_ne!(L1WalletBls::MAGIC, BlsSigning::MAGIC);
81        assert_ne!(L1WalletBls::SCHEME_ID, BlsSigning::SCHEME_ID);
82    }
83
84    /// **Proves:** the full sign→verify round-trip works for `L1WalletBls` —
85    /// a signature produced by `sign` verifies under the pubkey derived
86    /// from the same seed via the public [`chia_bls::verify`].
87    ///
88    /// **Why it matters:** Mirror of
89    /// [`super::bls_signing::tests::sign_verifies_via_chia_bls`] but for
90    /// the wallet scheme. Cheap sanity check that both schemes share the
91    /// same working `chia-bls` integration.
92    ///
93    /// **Catches:** a scheme-specific bug where e.g. `public_key` derives
94    /// via the Chia wallet's HD path but `sign` uses the raw master key
95    /// (or vice versa) — their outputs would mismatch and `verify` would
96    /// return `false`.
97    #[test]
98    fn roundtrip_sign_verify() {
99        let seed = [42u8; 32];
100        let pk = L1WalletBls::public_key(&seed).unwrap();
101        let sig = L1WalletBls::sign(&seed, b"hi").unwrap();
102        assert!(chia_bls::verify(&sig, &pk, b"hi"));
103    }
104}