use secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey};
use sha2::{Digest, Sha256};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ElectrumError {
InvalidSeed(String),
DerivationFailed(String),
}
impl std::fmt::Display for ElectrumError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ElectrumError::InvalidSeed(msg) => write!(f, "Invalid seed: {}", msg),
ElectrumError::DerivationFailed(msg) => write!(f, "Derivation failed: {}", msg),
}
}
}
impl std::error::Error for ElectrumError {}
#[derive(Clone)]
pub struct ElectrumDeriver {
master_privkey: SecretKey,
master_pubkey_bytes: [u8; 64],
for_change: bool,
}
impl ElectrumDeriver {
pub fn from_hex_seed(hex_seed: &str) -> Result<Self, ElectrumError> {
if !hex_seed.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(ElectrumError::InvalidSeed("Seed must be valid hex".to_string()));
}
let stretched = stretch_key(hex_seed.as_bytes());
Self::from_stretched_key(stretched)
}
pub fn from_seed_bytes(seed: &[u8]) -> Result<Self, ElectrumError> {
let hex_seed = hex::encode(seed);
Self::from_hex_seed(&hex_seed)
}
fn from_stretched_key(stretched: [u8; 32]) -> Result<Self, ElectrumError> {
let secp = Secp256k1::new();
let master_privkey = SecretKey::from_slice(&stretched)
.map_err(|e| ElectrumError::DerivationFailed(format!("Invalid stretched key: {}", e)))?;
let master_pubkey = PublicKey::from_secret_key(&secp, &master_privkey);
let pubkey_uncompressed = master_pubkey.serialize_uncompressed();
let mut master_pubkey_bytes = [0u8; 64];
master_pubkey_bytes.copy_from_slice(&pubkey_uncompressed[1..65]);
Ok(Self {
master_privkey,
master_pubkey_bytes,
for_change: false,
})
}
pub fn with_change(mut self) -> Self {
self.for_change = true;
self
}
pub fn master_pubkey_hex(&self) -> String {
hex::encode(self.master_pubkey_bytes)
}
pub fn derive_key(&self, index: u32) -> Result<[u8; 32], ElectrumError> {
let for_change = if self.for_change { 1 } else { 0 };
let sequence = get_sequence(&self.master_pubkey_bytes, for_change, index);
let scalar = Scalar::from_be_bytes(sequence)
.map_err(|_| ElectrumError::DerivationFailed("Sequence overflow".to_string()))?;
let child_key = self.master_privkey.add_tweak(&scalar)
.map_err(|e| ElectrumError::DerivationFailed(format!("Key addition failed: {}", e)))?;
Ok(child_key.secret_bytes())
}
pub fn derive_keys(&self, count: u32) -> Result<Vec<[u8; 32]>, ElectrumError> {
(0..count).map(|i| self.derive_key(i)).collect()
}
}
pub fn stretch_key(seed: &[u8]) -> [u8; 32] {
let mut x = [0u8; 32];
let mut hasher = Sha256::new();
hasher.update(seed);
hasher.update(seed);
x.copy_from_slice(&hasher.finalize());
for _ in 1..100_000 {
let mut hasher = Sha256::new();
hasher.update(&x);
hasher.update(seed);
x.copy_from_slice(&hasher.finalize());
}
x
}
pub fn get_sequence(mpk: &[u8; 64], for_change: u8, index: u32) -> [u8; 32] {
let prefix = format!("{}:{}:", index, for_change);
let mut data = prefix.into_bytes();
data.extend_from_slice(mpk);
double_sha256(&data)
}
fn double_sha256(data: &[u8]) -> [u8; 32] {
let first = Sha256::digest(data);
let second = Sha256::digest(&first);
let mut result = [0u8; 32];
result.copy_from_slice(&second);
result
}
pub fn truncate_seed(seed: &str) -> String {
let char_count = seed.chars().count();
if char_count <= 20 {
seed.to_string()
} else {
let first: String = seed.chars().take(8).collect();
let last: String = seed.chars().rev().take(8).collect::<String>().chars().rev().collect();
format!("{}...{}", first, last)
}
}
#[cfg(test)]
mod tests {
use super::*;
const ELECTRUM_TEST_SEED_HEX: &str = "acb740e454c3134901d7c8f16497cc1c";
const ELECTRUM_TEST_MPK: &str = "e9d4b7866dd1e91c862aebf62a49548c7dbf7bcc6e4b7b8c9da820c7737968df9c09d5a3e271dc814a29981f81b3faaf2737b551ef5dcc6189cf0f8252c442b3";
const ELECTRUM_TEST_RECEIVING_0: &str = "1FJEEB8ihPMbzs2SkLmr37dHyRFzakqUmo";
const ELECTRUM_TEST_CHANGE_0: &str = "1KRW8pH6HFHZh889VDq6fEKvmrsmApwNfe";
#[test]
fn test_stretch_key_deterministic() {
let seed = b"test_seed";
let result1 = stretch_key(seed);
let result2 = stretch_key(seed);
assert_eq!(result1, result2, "stretch_key should be deterministic");
}
#[test]
fn test_stretch_key_different_seeds() {
let result1 = stretch_key(b"seed1");
let result2 = stretch_key(b"seed2");
assert_ne!(result1, result2, "Different seeds should produce different results");
}
#[test]
fn test_double_sha256() {
let empty_double = double_sha256(b"");
assert_eq!(
hex::encode(empty_double),
"5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456"
);
}
#[test]
fn test_get_sequence_format() {
let mpk = [0u8; 64];
let seq0 = get_sequence(&mpk, 0, 0);
let seq1 = get_sequence(&mpk, 0, 1);
let seq_change = get_sequence(&mpk, 1, 0);
assert_ne!(seq0, seq1);
assert_ne!(seq0, seq_change);
}
#[test]
fn test_deriver_master_pubkey_electrum() {
let deriver = ElectrumDeriver::from_hex_seed(ELECTRUM_TEST_SEED_HEX).unwrap();
assert_eq!(
deriver.master_pubkey_hex().to_lowercase(),
ELECTRUM_TEST_MPK.to_lowercase(),
);
}
#[test]
fn test_derive_receiving_address_electrum() {
let deriver = ElectrumDeriver::from_hex_seed(ELECTRUM_TEST_SEED_HEX).unwrap();
let key = deriver.derive_key(0).unwrap();
let address = key_to_p2pkh_address_uncompressed(&key);
assert_eq!(address, ELECTRUM_TEST_RECEIVING_0);
}
#[test]
fn test_derive_change_address_electrum() {
let deriver = ElectrumDeriver::from_hex_seed(ELECTRUM_TEST_SEED_HEX)
.unwrap()
.with_change();
let key = deriver.derive_key(0).unwrap();
let address = key_to_p2pkh_address_uncompressed(&key);
assert_eq!(address, ELECTRUM_TEST_CHANGE_0);
}
#[test]
fn test_derive_keys_multiple() {
let deriver = ElectrumDeriver::from_hex_seed(ELECTRUM_TEST_SEED_HEX).unwrap();
let keys = deriver.derive_keys(5).unwrap();
assert_eq!(keys.len(), 5);
for i in 0..keys.len() {
for j in (i + 1)..keys.len() {
assert_ne!(keys[i], keys[j], "Keys at {} and {} should differ", i, j);
}
}
}
#[test]
fn test_invalid_seed() {
let result = ElectrumDeriver::from_hex_seed("not_valid_hex!");
assert!(result.is_err());
}
#[test]
fn test_truncate_seed() {
let short = "abcd1234";
assert_eq!(truncate_seed(short), "abcd1234");
let long = "0bbe2537d7527f2d7376d4bb9de8ac42ca202dbae310471b88f2cbb0492e6e73";
assert_eq!(truncate_seed(long), "0bbe2537...492e6e73");
}
fn key_to_p2pkh_address_uncompressed(key: &[u8; 32]) -> String {
use bitcoin::key::Secp256k1;
use bitcoin::network::Network;
use bitcoin::secp256k1::SecretKey;
use bitcoin::{Address, PrivateKey, PublicKey};
let secp = Secp256k1::new();
let secret = SecretKey::from_slice(key).expect("valid key");
let mut priv_key = PrivateKey::new(secret, Network::Bitcoin);
priv_key.compressed = false;
let pub_key = PublicKey::from_private_key(&secp, &priv_key);
Address::p2pkh(&pub_key, Network::Bitcoin).to_string()
}
}