use chia_bls::{PublicKey, SecretKey, Signature};
use rand_core::{CryptoRng, RngCore};
use zeroize::Zeroizing;
use crate::error::{KeystoreError, Result};
use crate::scheme::KeyScheme;
#[derive(Debug, Clone, Copy)]
pub struct L1WalletBls;
impl KeyScheme for L1WalletBls {
type PublicKey = PublicKey;
type Signature = Signature;
const MAGIC: [u8; 6] = *b"DIGLW1";
const NAME: &'static str = "L1WalletBls";
const SCHEME_ID: u16 = 0x0003;
const SECRET_LEN: usize = 32;
fn generate<R: RngCore + CryptoRng>(rng: &mut R) -> Zeroizing<Vec<u8>> {
let mut entropy = Zeroizing::new(vec![0u8; Self::SECRET_LEN]);
rng.fill_bytes(&mut entropy);
let master_sk = SecretKey::from_seed(&entropy);
Zeroizing::new(master_sk.to_bytes().to_vec())
}
fn public_key(secret: &[u8]) -> Result<Self::PublicKey> {
let sk = secret_to_secret_key(secret)?;
Ok(sk.public_key())
}
fn sign(secret: &[u8], msg: &[u8]) -> Result<Self::Signature> {
let sk = secret_to_secret_key(secret)?;
Ok(chia_bls::sign(&sk, msg))
}
}
fn secret_to_secret_key(secret: &[u8]) -> Result<SecretKey> {
if secret.len() != L1WalletBls::SECRET_LEN {
return Err(KeystoreError::InvalidPlaintext {
expected: L1WalletBls::SECRET_LEN,
got: secret.len(),
});
}
let bytes: [u8; 32] = secret
.try_into()
.expect("length checked above to equal SECRET_LEN (32)");
SecretKey::from_bytes(&bytes).map_err(|e| KeystoreError::InvalidSeed(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn magic_differs_from_bls_signing() {
use crate::scheme::BlsSigning;
assert_ne!(L1WalletBls::MAGIC, BlsSigning::MAGIC);
assert_ne!(L1WalletBls::SCHEME_ID, BlsSigning::SCHEME_ID);
}
#[test]
fn roundtrip_sign_verify() {
let seed = [42u8; 32];
let pk = L1WalletBls::public_key(&seed).unwrap();
let sig = L1WalletBls::sign(&seed, b"hi").unwrap();
assert!(chia_bls::verify(&sig, &pk, b"hi"));
}
#[test]
fn wrong_length_secret_rejected() {
let short = [0u8; 16];
let pk_err = L1WalletBls::public_key(&short).unwrap_err();
match pk_err {
KeystoreError::InvalidPlaintext { expected, got } => {
assert_eq!(expected, L1WalletBls::SECRET_LEN);
assert_eq!(got, 16);
}
other => panic!("expected InvalidPlaintext, got {other:?}"),
}
let sign_err = L1WalletBls::sign(&short, b"x").unwrap_err();
assert!(matches!(
sign_err,
KeystoreError::InvalidPlaintext { got: 16, .. }
));
}
#[test]
fn public_key_matches_chia_standard_master_key_for_all_zero_mnemonic() {
use bip39::{Language, Mnemonic};
let mnemonic = Mnemonic::parse_in_normalized(
Language::English,
"abandon abandon abandon abandon abandon abandon abandon abandon \
abandon abandon abandon about",
)
.expect("well-known valid BIP-39 test vector");
let bip39_seed = mnemonic.to_seed("");
let expected_master_sk = SecretKey::from_seed(&bip39_seed);
let expected_pk = expected_master_sk.public_key();
let stored_secret = expected_master_sk.to_bytes();
let got_pk = L1WalletBls::public_key(&stored_secret).unwrap();
assert_eq!(
got_pk, expected_pk,
"L1WalletBls::public_key must reconstruct the SAME master key \
derived via `from_seed` once — not re-derive by hashing its raw \
bytes through `from_seed` again"
);
assert_eq!(
hex::encode(got_pk.to_bytes()),
"82ae65efe846b15a92c51b7ad6c32589fd79d38263d3cbefbeeba08be8e90d8bc335a\
1e2fcc66a10b8c817c06232285a"
);
let msg = b"dig-keystore l1_wallet_bls golden vector";
let sig = L1WalletBls::sign(&stored_secret, msg).unwrap();
assert!(
chia_bls::verify(&sig, &expected_pk, msg),
"signature produced from the stored master-key bytes must verify \
under the SAME (non-re-derived) master key's pubkey"
);
}
}