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}