#[derive(Debug, thiserror::Error)]
pub enum KeyError {
#[error("mnemonic: {0}")]
Mnemonic(String),
#[error("derivation: {0}")]
Derivation(String),
#[error("entropy: {0}")]
Entropy(String),
}
impl From<mudra::Error> for KeyError {
fn from(e: mudra::Error) -> Self {
KeyError::Derivation(e.to_string())
}
}
pub struct Seed {
entropy: [u8; 32],
}
impl std::fmt::Debug for Seed {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Seed(..)")
}
}
pub struct Neuron {
key: mudra::domain::DomainKey,
pub pubkey: [u8; 33],
pub bech32: String,
pub native: [u8; 32],
}
impl std::fmt::Debug for Neuron {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Neuron").field("bech32", &self.bech32).finish()
}
}
impl Seed {
pub fn generate() -> Self {
let mut entropy = [0u8; 32];
getrandom::getrandom(&mut entropy).expect("os entropy");
Self { entropy }
}
pub fn from_entropy(entropy: [u8; 32]) -> Self {
Self { entropy }
}
pub fn entropy(&self) -> [u8; 32] {
self.entropy
}
#[cfg(feature = "mnemonic")]
pub fn from_mnemonic(phrase: &str) -> Result<Self, KeyError> {
let mnemonic = bip39::Mnemonic::parse_in_normalized(bip39::Language::English, phrase)
.map_err(|e| KeyError::Mnemonic(e.to_string()))?;
let (entropy, len) = mnemonic.to_entropy_array();
if len != 32 {
return Err(KeyError::Entropy(format!("expected 24 words (32 B), got {len} B")));
}
let mut e = [0u8; 32];
e.copy_from_slice(&entropy[..32]);
Ok(Self { entropy: e })
}
#[cfg(feature = "mnemonic")]
pub fn to_mnemonic(&self) -> String {
bip39::Mnemonic::from_entropy(&self.entropy).expect("32 B entropy").to_string()
}
pub fn neuron(&self, domain: &str, hrp: &str) -> Result<Neuron, KeyError> {
let key = mudra::domain::DomainKey::derive(&self.entropy, domain, hrp)?;
let pubkey = key.pubkey;
let bech32 = key.bech32.clone();
let native = key.native;
Ok(Neuron { key, pubkey, bech32, native })
}
}
impl Neuron {
pub fn signing_key(&self) -> &k256::ecdsa::SigningKey {
self.key.signing_key()
}
}
#[cfg(test)]
mod tests {
use super::*;
const PHRASE: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art";
#[test]
fn same_domain_same_neuron() {
let seed = Seed::from_mnemonic(PHRASE).unwrap();
let a = seed.neuron("example.com", "lytics").unwrap();
let b = seed.neuron("example.com", "lytics").unwrap();
assert_eq!(a.bech32, b.bech32);
assert_eq!(a.native, b.native);
}
#[test]
fn different_domains_different_neurons() {
let seed = Seed::from_mnemonic(PHRASE).unwrap();
let a = seed.neuron("example.com", "lytics").unwrap();
let b = seed.neuron("cyber.page", "lytics").unwrap();
assert_ne!(a.bech32, b.bech32);
}
#[test]
fn generated_entropy_roundtrips_through_mnemonic() {
let seed = Seed::generate();
let phrase = seed.to_mnemonic();
assert_eq!(phrase.split_whitespace().count(), 24);
let restored = Seed::from_mnemonic(&phrase).unwrap();
assert_eq!(seed.entropy(), restored.entropy());
let a = seed.neuron("example.com", "lytics").unwrap();
let b = restored.neuron("example.com", "lytics").unwrap();
assert_eq!(a.bech32, b.bech32);
}
#[test]
fn mnemonic_encoding_matches_the_standard() {
let seed = Seed::from_entropy([0u8; 32]);
assert_eq!(seed.to_mnemonic(), PHRASE);
}
#[test]
fn entropy_is_the_seed() {
let e = [7u8; 32];
let a = Seed::from_entropy(e).neuron("example.com", "lytics").unwrap();
let b = Seed::from_entropy(e).neuron("example.com", "lytics").unwrap();
assert_eq!(a.bech32, b.bech32);
}
#[test]
fn bech32_uses_hrp() {
let seed = Seed::from_mnemonic(PHRASE).unwrap();
let n = seed.neuron("example.com", "lytics").unwrap();
assert!(n.bech32.starts_with("lytics1"));
}
}