In the docs it describes how to generate a random MainSecretKey, but I am wanting to generate a key based off of a mnemonic phrase. I have this coded up in rust and it works about 25% of the time, but many times it throws and InvalidBytes error. I’ve printed this out and compared a working 32 byte sequence to a non working sequence and I’m at a loss for what the problem is. Is this the recipe to generate a SecretKey from a bip39::Mnemonic??? Here is the snippet of code that describes what I’m doing:
use bip39::{Mnemonic, Language};
use autonomi::SecretKey;
// non working seed phrase
// let seed_phrase: &str = "economy turkey lemon gym tongue there spell height seminar middle twice autumn";
// working seed phrase
// let seed_phrase: &str = "burger stereo merit exit runway chef scale list doll first zero tackle";
// Generate a new mnemonic from the given phrase
let mnemonic = Mnemonic::parse_in_normalized(Language::English, seed_phrase).unwrap();
// Convert the mnemonic to a seed
let seed = mnemonic.to_seed_normalized("");
// Convert the seed to a BLS secret key
let secret_key = SecretKey::from_bytes(seed[..32].try_into().unwrap_or_else(
|error | {
panic!("Problem grabbing the first 32 bytes of the seed: {:?}", error);
}
)).unwrap_or_else(|error| {
//FIXME: throws exception here!! Error::InvalidBytes
panic!("Problem creating the secret key. Try running initialize again: {:?}", error);
}
);
I’ve traced this back into the blsttc crate and got lost in the math. I’m guessing you can’t just feed a random 32 byte sequence, there must be some kind of dependency. Any ideas??
Thanks. Appreciate the feedback. I see, so you’re just baking the root key into the app and then deriving addresses off of your ETH key. Pretty slick. In the end, I’m wanting to have the same convenience I have with the HD crypto wallets so that if I install my application on different computer or want to recreate my environment, I just need to know the 12 word seed phrase and I’m good to go. I guess I would get the same here so any user using the app would be able to recreate it. In the future, if my app disappeared, they would need to know the root key to rebuild their list of derived private keys. Not ideal, but OK.
So I guess there is no way to use BIP39 “as-is” with BLS key generation? Just seems crazy to me that nobody has figured this out yet. I was digging through the blsttc crate and it looks like there is a way to run SecretKey::sample() and define your own random number generator for it and use that to build your secret key. You could build a RNG that would generate a single “random” value, i.e. the list of bytes from the BIP39 seed. Alas, my rust foo is not yet strong enough…
I’ve had a look at this. AFAICT this is because Autonomi uses the BLS12-381 eliptic curve under the hood. Scalar field elements must be < modulus q - which is slightly smaller than 2^255.
So whenever the seed is >= q, it cannot be used as the source of a SecretKey. If you follow the function calls, you’ll end up in Scalar::from_bytes_le from the blstrs crate. This does a sanity check and returns None if it fails.
The good news is, this can be fixed by zeroing a single bit (reducing the entropy every so slightly):
// Take the first 32 bytes for the potential key material
// SecretKey::from_bytes expects a 32-byte big-endian representation
let mut seed: [u8; 32] = mnemonic.to_seed_normalized("")[..32].try_into()?;
// --- SHAVING STEP (Big-Endian) ---
// Context:
// BLS12-381 scalar field elements must be in a 'canonical' range [0, q-1],
// where q is the scalar field modulus.
// The modulus q is a large prime, slightly less than 2^255.
// The 32 bytes derived from the seed represent a 256-bit number (2^256 - 1 max value).
// This number *could* be >= q, which is not allowed by SecretKey::from_bytes
// (it expects the number represented by the bytes to be < q).
// Action:
// To ensure the 256-bit number is overwhelmingly likely to be < q,
// we force its highest possible bit (the 2^255 position) to zero.
// In a 32-byte big-endian representation, the 2^255 bit is the
// most significant bit (MSB) of the first byte (index 0).
// The bitmask 0x7F (binary 01111111) is applied using bitwise AND (&).
// This clears the MSB (bit 7) while leaving bits 0-6 unchanged.
// Why:
// By clearing the 2^255 bit, the resulting 256-bit number is mathematically
// guaranteed to be strictly less than 2^255. Since q is only slightly
// less than 2^255, any number < 2^255 is almost certainly also < q.
// This satisfies the canonical requirement for SecretKey::from_bytes
// with extremely high probability (~1 - 2^-128 failure rate).
seed[0] &= 0x7F;
// Convert the seed to a BLS secret key
let secret_key = SecretKey::from_bytes(seed)?;
This should virtually always work. Give it a try and see if it works for you.
Please note, this is NOT my area of expertise, it would be great if someone with more cryptographic background could comment on this.
@RolandR you are my hero! This worked perfectly on all the tests I threw at it. I’ve been dealing with this issue the last couple days and was completely stuck. Thank you so much! I stole your comment by the way and threw it into the code so I remember why I did this in the future.
The simple approach I suggested above turned out to still lead to a number of invalid values. Unfortunately, zeroing a single bit is not enough to reliably produce safe values. It seems to work well when zeroing two bits instead (&= 0x3F instead of &= 0x7F). With that I wasn’t able to generate invalid values during testing. However, this lowers the entropy by another bit. It may or may not have some other, subtle side-effects I don’t know about. My guess is, its probably fine - but I don’t know for certain.
The better, safer approach IMHO is to follow the advice given above by @mav.
In my own project I am doing this now:
use sn_bls_ckd::derive_master_sk;
use sn_curv::elliptic::curves::ECScalar;
// Derive BLS12-381 master secret key from seed using EIP-2333 standard.
// Guarantees a valid, non-zero scalar represented as 32 Big-Endian bytes.
let key_bytes: [u8; 32] = derive_master_sk(&seed)
.expect("derive_master_sk failed; seed length requirement is >= 32 bytes")
.serialize() // Get the 32-byte Big-Endian representation
.into(); // Convert GenericArray<u8, 32> to [u8; 32]
Two crates need to be added though:
sn_bls_ckd = "0.2.1"
sn_curv = { version = "0.10.1", default-features = false, features = ["num-bigint"] }
For now at least this looks like the way to do it properly.
I’ve tried, but there are crate version conflicts in the dependency tree. Seems blsttc depends on rand 0.8.x and other libs depend on rand 0.9.x, and that creates problems. @chriso perhaps that could be dealt with?