use std::cmp::Ordering;
use crate::error::{Error, Result};
use crate::primitives::ec::{PrivateKey, PublicKey};
use crate::primitives::encoding::{from_base58, to_base58};
use crate::primitives::hash::{hash160, sha256d, sha512_hmac};
use crate::primitives::BigNumber;
pub const HARDENED_KEY_START: u32 = 0x80000000;
pub const MIN_SEED_BYTES: usize = 16;
pub const MAX_SEED_BYTES: usize = 64;
pub const RECOMMENDED_SEED_LEN: usize = 32;
const SERIALIZED_KEY_LEN: usize = 78;
const MASTER_KEY: &[u8] = b"Bitcoin seed";
const MAINNET_PRIVATE: [u8; 4] = [0x04, 0x88, 0xAD, 0xE4]; const MAINNET_PUBLIC: [u8; 4] = [0x04, 0x88, 0xB2, 0x1E]; const TESTNET_PRIVATE: [u8; 4] = [0x04, 0x35, 0x83, 0x94]; const TESTNET_PUBLIC: [u8; 4] = [0x04, 0x35, 0x87, 0xCF];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Network {
Mainnet,
Testnet,
}
#[derive(Clone)]
pub struct ExtendedKey {
version: [u8; 4],
depth: u8,
parent_fingerprint: [u8; 4],
child_number: u32,
chain_code: [u8; 32],
key: [u8; 33], is_private: bool,
}
impl ExtendedKey {
pub fn new_master(seed: &[u8], network: Network) -> Result<Self> {
if seed.len() < MIN_SEED_BYTES || seed.len() > MAX_SEED_BYTES {
return Err(Error::InvalidExtendedKey(format!(
"Seed length must be between {} and {} bytes, got {}",
MIN_SEED_BYTES,
MAX_SEED_BYTES,
seed.len()
)));
}
let hmac = sha512_hmac(MASTER_KEY, seed);
let (il, ir) = hmac.split_at(32);
let secret_key: [u8; 32] = il.try_into().unwrap();
let chain_code: [u8; 32] = ir.try_into().unwrap();
let key_num = BigNumber::from_bytes_be(&secret_key);
let order = BigNumber::secp256k1_order();
if key_num.is_zero() || key_num.compare(&order) != Ordering::Less {
return Err(Error::InvalidExtendedKey(
"Derived master key is invalid".to_string(),
));
}
let mut key = [0u8; 33];
key[0] = 0x00;
key[1..33].copy_from_slice(&secret_key);
let version = match network {
Network::Mainnet => MAINNET_PRIVATE,
Network::Testnet => TESTNET_PRIVATE,
};
Ok(Self {
version,
depth: 0,
parent_fingerprint: [0u8; 4],
child_number: 0,
chain_code,
key,
is_private: true,
})
}
pub fn from_string(s: &str) -> Result<Self> {
let decoded = from_base58(s)?;
if decoded.len() != SERIALIZED_KEY_LEN + 4 {
return Err(Error::InvalidExtendedKey(format!(
"Invalid length: expected {}, got {}",
SERIALIZED_KEY_LEN + 4,
decoded.len()
)));
}
let payload = &decoded[..SERIALIZED_KEY_LEN];
let checksum = &decoded[SERIALIZED_KEY_LEN..];
let expected_checksum = sha256d(payload);
if checksum != &expected_checksum[..4] {
return Err(Error::InvalidChecksum);
}
let version: [u8; 4] = payload[0..4].try_into().unwrap();
let depth = payload[4];
let parent_fingerprint: [u8; 4] = payload[5..9].try_into().unwrap();
let child_number = u32::from_be_bytes(payload[9..13].try_into().unwrap());
let chain_code: [u8; 32] = payload[13..45].try_into().unwrap();
let key_data: [u8; 33] = payload[45..78].try_into().unwrap();
let is_private = key_data[0] == 0x00;
if is_private {
let key_bytes = &key_data[1..33];
let key_num = BigNumber::from_bytes_be(key_bytes);
let order = BigNumber::secp256k1_order();
if key_num.is_zero() || key_num.compare(&order) != Ordering::Less {
return Err(Error::InvalidExtendedKey(
"Invalid private key value".to_string(),
));
}
} else {
if key_data[0] != 0x02 && key_data[0] != 0x03 {
return Err(Error::InvalidExtendedKey(
"Invalid public key prefix".to_string(),
));
}
PublicKey::from_bytes(&key_data)?;
}
Ok(Self {
version,
depth,
parent_fingerprint,
child_number,
chain_code,
key: key_data,
is_private,
})
}
fn serialize(&self) -> String {
let mut data = Vec::with_capacity(SERIALIZED_KEY_LEN + 4);
data.extend_from_slice(&self.version);
data.push(self.depth);
data.extend_from_slice(&self.parent_fingerprint);
data.extend_from_slice(&self.child_number.to_be_bytes());
data.extend_from_slice(&self.chain_code);
if self.is_private {
data.extend_from_slice(&self.key);
} else {
data.extend_from_slice(&self.key);
}
let checksum = sha256d(&data);
data.extend_from_slice(&checksum[..4]);
to_base58(&data)
}
pub fn derive_child(&self, index: u32) -> Result<Self> {
if self.depth == 255 {
return Err(Error::InvalidExtendedKey(
"Cannot derive beyond depth 255".to_string(),
));
}
let is_hardened = index >= HARDENED_KEY_START;
if is_hardened && !self.is_private {
return Err(Error::HardenedFromPublic);
}
let mut data = Vec::with_capacity(37);
if is_hardened {
data.push(0x00);
data.extend_from_slice(&self.key[1..33]); } else {
let pubkey_bytes = self.public_key_bytes()?;
data.extend_from_slice(&pubkey_bytes);
}
data.extend_from_slice(&index.to_be_bytes());
let hmac = sha512_hmac(&self.chain_code, &data);
let (il, ir) = hmac.split_at(32);
let il_num = BigNumber::from_bytes_be(il);
let order = BigNumber::secp256k1_order();
if il_num.compare(&order) != Ordering::Less {
return Err(Error::InvalidExtendedKey(
"Derived key is invalid".to_string(),
));
}
let child_chain_code: [u8; 32] = ir.try_into().unwrap();
let (child_key, child_is_private) = if self.is_private {
let parent_key_num = BigNumber::from_bytes_be(&self.key[1..33]);
let child_key_num = il_num.add(&parent_key_num).modulo(&order);
if child_key_num.is_zero() {
return Err(Error::InvalidExtendedKey(
"Derived key is invalid".to_string(),
));
}
let mut key = [0u8; 33];
key[0] = 0x00;
let key_bytes = child_key_num.to_bytes_be(32);
key[1..33].copy_from_slice(&key_bytes);
(key, true)
} else {
let parent_pubkey = PublicKey::from_bytes(&self.key)?;
let il_bytes: [u8; 32] = il.try_into().unwrap();
let offset_point = PublicKey::from_scalar_mul_generator(&il_bytes)?;
let child_pubkey = parent_pubkey.add(&offset_point)?;
(child_pubkey.to_compressed(), false)
};
let parent_pubkey = self.public_key_bytes()?;
let parent_hash = hash160(&parent_pubkey);
let parent_fp: [u8; 4] = parent_hash[..4].try_into().unwrap();
Ok(Self {
version: self.version,
depth: self.depth + 1,
parent_fingerprint: parent_fp,
child_number: index,
chain_code: child_chain_code,
key: child_key,
is_private: child_is_private,
})
}
pub fn derive_path(&self, path: &str) -> Result<Self> {
let path = path.trim();
if path.is_empty() || path == "m" || path == "M" {
return Ok(self.clone());
}
let path = path
.strip_prefix("m/")
.or_else(|| path.strip_prefix("M/"))
.or_else(|| path.strip_prefix('/'))
.unwrap_or(path);
let mut current = self.clone();
for component in path.split('/') {
let component = component.trim();
if component.is_empty() {
continue;
}
let (index_str, hardened) = if component.ends_with('\'')
|| component.ends_with('h')
|| component.ends_with('H')
{
(&component[..component.len() - 1], true)
} else {
(component, false)
};
let index: u32 = index_str.parse().map_err(|_| {
Error::InvalidDerivationPath(format!("Invalid index: {}", index_str))
})?;
let child_index = if hardened {
index
.checked_add(HARDENED_KEY_START)
.ok_or_else(|| Error::InvalidDerivationPath("Index overflow".to_string()))?
} else {
index
};
current = current.derive_child(child_index)?;
}
Ok(current)
}
pub fn private_key(&self) -> Result<PrivateKey> {
if !self.is_private {
return Err(Error::InvalidExtendedKey(
"Cannot extract private key from public extended key".to_string(),
));
}
PrivateKey::from_bytes(&self.key[1..33])
}
pub fn public_key(&self) -> Result<PublicKey> {
if self.is_private {
let private_key = self.private_key()?;
Ok(private_key.public_key())
} else {
PublicKey::from_bytes(&self.key)
}
}
pub fn neuter(&self) -> Result<Self> {
if !self.is_private {
return Ok(self.clone());
}
let public_key = self.public_key()?;
let pubkey_bytes = public_key.to_compressed();
let version = match self.version {
MAINNET_PRIVATE => MAINNET_PUBLIC,
TESTNET_PRIVATE => TESTNET_PUBLIC,
_ => {
return Err(Error::InvalidExtendedKey(
"Unknown version bytes".to_string(),
))
}
};
Ok(Self {
version,
depth: self.depth,
parent_fingerprint: self.parent_fingerprint,
child_number: self.child_number,
chain_code: self.chain_code,
key: pubkey_bytes,
is_private: false,
})
}
pub fn is_private(&self) -> bool {
self.is_private
}
pub fn depth(&self) -> u8 {
self.depth
}
pub fn child_number(&self) -> u32 {
self.child_number
}
pub fn parent_fingerprint(&self) -> [u8; 4] {
self.parent_fingerprint
}
pub fn chain_code(&self) -> &[u8; 32] {
&self.chain_code
}
pub fn fingerprint(&self) -> Result<[u8; 4]> {
let pubkey_bytes = self.public_key_bytes()?;
let hash = hash160(&pubkey_bytes);
Ok(hash[..4].try_into().unwrap())
}
pub fn network(&self) -> Option<Network> {
match self.version {
MAINNET_PRIVATE | MAINNET_PUBLIC => Some(Network::Mainnet),
TESTNET_PRIVATE | TESTNET_PUBLIC => Some(Network::Testnet),
_ => None,
}
}
pub fn address(&self, mainnet: bool) -> Result<String> {
let pubkey_bytes = self.public_key_bytes()?;
let hash = hash160(&pubkey_bytes);
let version = if mainnet { 0x00 } else { 0x6f };
Ok(crate::primitives::encoding::to_base58_check(
&hash,
&[version],
))
}
fn public_key_bytes(&self) -> Result<[u8; 33]> {
if self.is_private {
let private_key = self.private_key()?;
Ok(private_key.public_key().to_compressed())
} else {
Ok(self.key)
}
}
}
impl std::fmt::Debug for ExtendedKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ExtendedKey")
.field("depth", &self.depth)
.field("child_number", &self.child_number)
.field("is_private", &self.is_private)
.finish_non_exhaustive()
}
}
impl std::fmt::Display for ExtendedKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.serialize())
}
}
pub fn generate_hd_key(seed_length: usize, network: Network) -> Result<ExtendedKey> {
if !(MIN_SEED_BYTES..=MAX_SEED_BYTES).contains(&seed_length) {
return Err(Error::InvalidExtendedKey(format!(
"Seed length must be between {} and {} bytes, got {}",
MIN_SEED_BYTES, MAX_SEED_BYTES, seed_length
)));
}
let mut seed = vec![0u8; seed_length];
getrandom::getrandom(&mut seed)
.map_err(|e| Error::CryptoError(format!("Failed to generate random seed: {}", e)))?;
ExtendedKey::new_master(&seed, network)
}
pub fn generate_hd_key_from_mnemonic(
mnemonic: &super::bip39::Mnemonic,
passphrase: &str,
network: Network,
) -> Result<ExtendedKey> {
let seed = mnemonic.to_seed(passphrase);
ExtendedKey::new_master(&seed, network)
}
pub fn generate_key_pair_strings(seed_length: usize, network: Network) -> Result<(String, String)> {
let master = generate_hd_key(seed_length, network)?;
let xpriv = master.to_string();
let xpub = master.neuter()?.to_string();
Ok((xpriv, xpub))
}
pub fn derive_addresses_for_path(
key: &ExtendedKey,
base_path: &str,
start: u32,
count: u32,
mainnet: bool,
) -> Result<Vec<String>> {
let base_key = key.derive_path(base_path)?;
let mut addresses = Vec::with_capacity(count as usize);
for i in 0..count {
let index = start.checked_add(i).ok_or_else(|| {
Error::InvalidDerivationPath("Index overflow during address derivation".to_string())
})?;
let child = base_key.derive_child(index)?;
addresses.push(child.address(mainnet)?);
}
Ok(addresses)
}
pub fn derive_public_keys_for_path(
key: &ExtendedKey,
base_path: &str,
start: u32,
count: u32,
) -> Result<Vec<PublicKey>> {
let base_key = key.derive_path(base_path)?;
let mut pubkeys = Vec::with_capacity(count as usize);
for i in 0..count {
let index = start.checked_add(i).ok_or_else(|| {
Error::InvalidDerivationPath("Index overflow during public key derivation".to_string())
})?;
let child = base_key.derive_child(index)?;
pubkeys.push(child.public_key()?);
}
Ok(pubkeys)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::encoding::from_hex;
#[test]
fn test_vector_1_master() {
let seed = from_hex("000102030405060708090a0b0c0d0e0f").unwrap();
let master = ExtendedKey::new_master(&seed, Network::Mainnet).unwrap();
assert_eq!(
master.to_string(),
"xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"
);
let xpub = master.neuter().unwrap();
assert_eq!(
xpub.to_string(),
"xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8"
);
}
#[test]
fn test_vector_1_chain_m_0h() {
let seed = from_hex("000102030405060708090a0b0c0d0e0f").unwrap();
let master = ExtendedKey::new_master(&seed, Network::Mainnet).unwrap();
let child = master.derive_child(HARDENED_KEY_START).unwrap();
assert_eq!(
child.to_string(),
"xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7"
);
let xpub = child.neuter().unwrap();
assert_eq!(
xpub.to_string(),
"xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw"
);
}
#[test]
fn test_vector_1_chain_m_0h_1() {
let seed = from_hex("000102030405060708090a0b0c0d0e0f").unwrap();
let master = ExtendedKey::new_master(&seed, Network::Mainnet).unwrap();
let child = master.derive_path("m/0'/1").unwrap();
assert_eq!(
child.to_string(),
"xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs"
);
let xpub = child.neuter().unwrap();
assert_eq!(
xpub.to_string(),
"xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ"
);
}
#[test]
fn test_vector_1_chain_m_0h_1_2h() {
let seed = from_hex("000102030405060708090a0b0c0d0e0f").unwrap();
let master = ExtendedKey::new_master(&seed, Network::Mainnet).unwrap();
let child = master.derive_path("m/0'/1/2'").unwrap();
assert_eq!(
child.to_string(),
"xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM"
);
let xpub = child.neuter().unwrap();
assert_eq!(
xpub.to_string(),
"xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5"
);
}
#[test]
fn test_vector_2_master() {
let seed = from_hex("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542").unwrap();
let master = ExtendedKey::new_master(&seed, Network::Mainnet).unwrap();
assert_eq!(
master.to_string(),
"xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U"
);
let xpub = master.neuter().unwrap();
assert_eq!(
xpub.to_string(),
"xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB"
);
}
#[test]
fn test_vector_2_chain_m_0() {
let seed = from_hex("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542").unwrap();
let master = ExtendedKey::new_master(&seed, Network::Mainnet).unwrap();
let child = master.derive_child(0).unwrap();
assert_eq!(
child.to_string(),
"xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt"
);
let xpub = child.neuter().unwrap();
assert_eq!(
xpub.to_string(),
"xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH"
);
}
#[test]
fn test_vector_1_chain_m_0h_1_2h_2() {
let seed = from_hex("000102030405060708090a0b0c0d0e0f").unwrap();
let master = ExtendedKey::new_master(&seed, Network::Mainnet).unwrap();
let child = master.derive_path("m/0'/1/2'/2").unwrap();
assert_eq!(
child.to_string(),
"xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334"
);
let xpub = child.neuter().unwrap();
assert_eq!(
xpub.to_string(),
"xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV"
);
}
#[test]
fn test_vector_1_chain_m_0h_1_2h_2_1000000000() {
let seed = from_hex("000102030405060708090a0b0c0d0e0f").unwrap();
let master = ExtendedKey::new_master(&seed, Network::Mainnet).unwrap();
let child = master.derive_path("m/0'/1/2'/2/1000000000").unwrap();
assert_eq!(
child.to_string(),
"xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76"
);
let xpub = child.neuter().unwrap();
assert_eq!(
xpub.to_string(),
"xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy"
);
}
#[test]
fn test_vector_1_public_derivation_depth_4() {
let seed = from_hex("000102030405060708090a0b0c0d0e0f").unwrap();
let master = ExtendedKey::new_master(&seed, Network::Mainnet).unwrap();
let child_priv = master.derive_path("m/0'/1/2'").unwrap();
let child_pub = child_priv.neuter().unwrap();
let child2 = child_pub.derive_child(2).unwrap();
assert_eq!(
child2.to_string(),
"xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV"
);
}
#[test]
fn test_vector_1_public_derivation_depth_5() {
let seed = from_hex("000102030405060708090a0b0c0d0e0f").unwrap();
let master = ExtendedKey::new_master(&seed, Network::Mainnet).unwrap();
let child_priv = master.derive_path("m/0'/1/2'/2").unwrap();
let child_pub = child_priv.neuter().unwrap();
let child2 = child_pub.derive_child(1000000000).unwrap();
assert_eq!(
child2.to_string(),
"xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy"
);
}
#[test]
fn test_vector_2_chain_m_0_2147483647h() {
let seed = from_hex("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542").unwrap();
let master = ExtendedKey::new_master(&seed, Network::Mainnet).unwrap();
let child = master.derive_path("m/0/2147483647'").unwrap();
assert_eq!(
child.to_string(),
"xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9"
);
let xpub = child.neuter().unwrap();
assert_eq!(
xpub.to_string(),
"xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a"
);
}
#[test]
fn test_vector_2_chain_m_0_2147483647h_1() {
let seed = from_hex("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542").unwrap();
let master = ExtendedKey::new_master(&seed, Network::Mainnet).unwrap();
let child = master.derive_path("m/0/2147483647'/1").unwrap();
assert_eq!(
child.to_string(),
"xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef"
);
let xpub = child.neuter().unwrap();
assert_eq!(
xpub.to_string(),
"xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon"
);
}
#[test]
fn test_vector_2_public_derivation_depth_3() {
let seed = from_hex("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542").unwrap();
let master = ExtendedKey::new_master(&seed, Network::Mainnet).unwrap();
let child_priv = master.derive_path("m/0/2147483647'").unwrap();
let child_pub = child_priv.neuter().unwrap();
let child2 = child_pub.derive_child(1).unwrap();
assert_eq!(
child2.to_string(),
"xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon"
);
}
#[test]
fn test_vector_2_chain_m_0_2147483647h_1_2147483646h() {
let seed = from_hex("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542").unwrap();
let master = ExtendedKey::new_master(&seed, Network::Mainnet).unwrap();
let child = master.derive_path("m/0/2147483647'/1/2147483646'").unwrap();
assert_eq!(
child.to_string(),
"xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc"
);
let xpub = child.neuter().unwrap();
assert_eq!(
xpub.to_string(),
"xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL"
);
}
#[test]
fn test_vector_2_chain_m_0_2147483647h_1_2147483646h_2() {
let seed = from_hex("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542").unwrap();
let master = ExtendedKey::new_master(&seed, Network::Mainnet).unwrap();
let child = master
.derive_path("m/0/2147483647'/1/2147483646'/2")
.unwrap();
assert_eq!(
child.to_string(),
"xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j"
);
let xpub = child.neuter().unwrap();
assert_eq!(
xpub.to_string(),
"xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt"
);
}
#[test]
fn test_vector_2_public_derivation_depth_5() {
let seed = from_hex("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542").unwrap();
let master = ExtendedKey::new_master(&seed, Network::Mainnet).unwrap();
let child_priv = master.derive_path("m/0/2147483647'/1/2147483646'").unwrap();
let child_pub = child_priv.neuter().unwrap();
let child2 = child_pub.derive_child(2).unwrap();
assert_eq!(
child2.to_string(),
"xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt"
);
}
#[test]
fn test_parse_xprv() {
let xprv = "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi";
let key = ExtendedKey::from_string(xprv).unwrap();
assert!(key.is_private());
assert_eq!(key.depth(), 0);
assert_eq!(key.to_string(), xprv);
}
#[test]
fn test_parse_xpub() {
let xpub = "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8";
let key = ExtendedKey::from_string(xpub).unwrap();
assert!(!key.is_private());
assert_eq!(key.depth(), 0);
assert_eq!(key.to_string(), xpub);
}
#[test]
fn test_hardened_from_public_fails() {
let xpub = "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8";
let key = ExtendedKey::from_string(xpub).unwrap();
let result = key.derive_child(HARDENED_KEY_START);
assert!(result.is_err());
}
#[test]
fn test_derive_path_variants() {
let seed = from_hex("000102030405060708090a0b0c0d0e0f").unwrap();
let master = ExtendedKey::new_master(&seed, Network::Mainnet).unwrap();
let key_1 = master.derive_path("m/0'/1").unwrap();
let key_2 = master.derive_path("m/0h/1").unwrap();
let key_3 = master.derive_path("m/0H/1").unwrap();
let key_4 = master.derive_path("0'/1").unwrap();
let key_5 = master.derive_path("/0'/1").unwrap();
let expected_xprv = "xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs";
assert_eq!(key_1.to_string(), expected_xprv, "m/0'/1 format mismatch");
assert_eq!(key_2.to_string(), expected_xprv, "m/0h/1 format mismatch");
assert_eq!(key_3.to_string(), expected_xprv, "m/0H/1 format mismatch");
assert_eq!(key_4.to_string(), expected_xprv, "0'/1 format mismatch");
assert_eq!(key_5.to_string(), expected_xprv, "/0'/1 format mismatch");
let expected_xpub = "xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ";
assert_eq!(
key_1.neuter().unwrap().to_string(),
expected_xpub,
"m/0'/1 xpub mismatch"
);
assert_eq!(
key_2.neuter().unwrap().to_string(),
expected_xpub,
"m/0h/1 xpub mismatch"
);
assert_eq!(
key_3.neuter().unwrap().to_string(),
expected_xpub,
"m/0H/1 xpub mismatch"
);
assert_eq!(
key_4.neuter().unwrap().to_string(),
expected_xpub,
"0'/1 xpub mismatch"
);
assert_eq!(
key_5.neuter().unwrap().to_string(),
expected_xpub,
"/0'/1 xpub mismatch"
);
}
#[test]
fn test_public_key_derivation() {
let seed = from_hex("000102030405060708090a0b0c0d0e0f").unwrap();
let master = ExtendedKey::new_master(&seed, Network::Mainnet).unwrap();
let priv_child = master.derive_child(0).unwrap();
let pub_master = master.neuter().unwrap();
let pub_child = pub_master.derive_child(0).unwrap();
assert_eq!(
priv_child.neuter().unwrap().to_string(),
pub_child.to_string()
);
}
#[test]
fn test_testnet_keys() {
let seed = from_hex("000102030405060708090a0b0c0d0e0f").unwrap();
let master = ExtendedKey::new_master(&seed, Network::Testnet).unwrap();
assert!(master.to_string().starts_with("tprv"));
let xpub = master.neuter().unwrap();
assert!(xpub.to_string().starts_with("tpub"));
}
#[test]
fn test_invalid_seed_length() {
let result = ExtendedKey::new_master(&[0u8; 15], Network::Mainnet);
assert!(result.is_err());
let result = ExtendedKey::new_master(&[0u8; 65], Network::Mainnet);
assert!(result.is_err());
}
#[test]
fn test_invalid_checksum() {
let mut xprv = "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi".to_string();
xprv.pop();
xprv.push('j');
let result = ExtendedKey::from_string(&xprv);
assert!(result.is_err());
}
#[test]
fn test_address_generation() {
let seed = from_hex("000102030405060708090a0b0c0d0e0f").unwrap();
let master = ExtendedKey::new_master(&seed, Network::Mainnet).unwrap();
let address = master.address(true).unwrap();
assert!(address.starts_with('1'));
let testnet_address = master.address(false).unwrap();
assert!(testnet_address.starts_with('m') || testnet_address.starts_with('n'));
}
#[test]
fn test_generate_hd_key() {
let key = generate_hd_key(32, Network::Mainnet).unwrap();
assert!(key.is_private());
assert!(key.to_string().starts_with("xprv"));
}
#[test]
fn test_generate_from_mnemonic() {
use super::super::bip39::Mnemonic;
let mnemonic = Mnemonic::from_phrase(
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
)
.unwrap();
let master = generate_hd_key_from_mnemonic(&mnemonic, "", Network::Mainnet).unwrap();
assert!(master.is_private());
let master_with_pass =
generate_hd_key_from_mnemonic(&mnemonic, "TREZOR", Network::Mainnet).unwrap();
assert!(master_with_pass.is_private());
assert_ne!(master.to_string(), master_with_pass.to_string());
}
#[test]
fn test_generate_key_pair_strings() {
let (xpriv, xpub) = generate_key_pair_strings(32, Network::Mainnet).unwrap();
assert!(xpriv.starts_with("xprv"));
assert!(xpub.starts_with("xpub"));
let parsed_priv = ExtendedKey::from_string(&xpriv).unwrap();
let parsed_pub = ExtendedKey::from_string(&xpub).unwrap();
assert!(parsed_priv.is_private());
assert!(!parsed_pub.is_private());
assert_eq!(parsed_priv.neuter().unwrap().to_string(), xpub);
let (tpriv, tpub) = generate_key_pair_strings(32, Network::Testnet).unwrap();
assert!(tpriv.starts_with("tprv"));
assert!(tpub.starts_with("tpub"));
}
#[test]
fn test_generate_key_pair_strings_invalid_seed_length() {
let result = generate_key_pair_strings(15, Network::Mainnet);
assert!(result.is_err());
let result = generate_key_pair_strings(65, Network::Mainnet);
assert!(result.is_err());
}
#[test]
fn test_derive_addresses_for_path() {
let seed = from_hex("000102030405060708090a0b0c0d0e0f").unwrap();
let master = ExtendedKey::new_master(&seed, Network::Mainnet).unwrap();
let addresses = derive_addresses_for_path(&master, "m/44'/0'/0'/0", 0, 5, true).unwrap();
assert_eq!(addresses.len(), 5);
for addr in &addresses {
assert!(
addr.starts_with('1'),
"Address should start with '1': {}",
addr
);
}
for i in 0..addresses.len() {
for j in (i + 1)..addresses.len() {
assert_ne!(addresses[i], addresses[j], "Addresses should be unique");
}
}
}
#[test]
fn test_derive_addresses_for_path_testnet() {
let seed = from_hex("000102030405060708090a0b0c0d0e0f").unwrap();
let master = ExtendedKey::new_master(&seed, Network::Mainnet).unwrap();
let addresses = derive_addresses_for_path(&master, "m/44'/0'/0'/0", 0, 3, false).unwrap();
assert_eq!(addresses.len(), 3);
for addr in &addresses {
assert!(
addr.starts_with('m') || addr.starts_with('n'),
"Testnet address should start with 'm' or 'n': {}",
addr
);
}
}
#[test]
fn test_derive_addresses_for_path_with_offset() {
let seed = from_hex("000102030405060708090a0b0c0d0e0f").unwrap();
let master = ExtendedKey::new_master(&seed, Network::Mainnet).unwrap();
let addresses_offset =
derive_addresses_for_path(&master, "m/44'/0'/0'/0", 5, 3, true).unwrap();
let child_5 = master.derive_path("m/44'/0'/0'/0/5").unwrap();
let addr_5 = child_5.address(true).unwrap();
assert_eq!(addresses_offset[0], addr_5);
}
#[test]
fn test_derive_public_keys_for_path() {
let seed = from_hex("000102030405060708090a0b0c0d0e0f").unwrap();
let master = ExtendedKey::new_master(&seed, Network::Mainnet).unwrap();
let pubkeys = derive_public_keys_for_path(&master, "m/44'/0'/0'/0", 0, 5).unwrap();
assert_eq!(pubkeys.len(), 5);
for pk in &pubkeys {
let compressed = pk.to_compressed();
assert_eq!(compressed.len(), 33);
assert!(compressed[0] == 0x02 || compressed[0] == 0x03);
}
for i in 0..pubkeys.len() {
for j in (i + 1)..pubkeys.len() {
assert_ne!(
pubkeys[i].to_compressed(),
pubkeys[j].to_compressed(),
"Public keys should be unique"
);
}
}
}
#[test]
fn test_derive_public_keys_matches_individual_derivation() {
let seed = from_hex("000102030405060708090a0b0c0d0e0f").unwrap();
let master = ExtendedKey::new_master(&seed, Network::Mainnet).unwrap();
let pubkeys = derive_public_keys_for_path(&master, "m/44'/0'/0'/0", 0, 3).unwrap();
for (i, pk) in pubkeys.iter().enumerate() {
let path = format!("m/44'/0'/0'/0/{}", i);
let derived = master.derive_path(&path).unwrap();
let expected_pk = derived.public_key().unwrap();
assert_eq!(
pk.to_compressed(),
expected_pk.to_compressed(),
"Public key at index {} should match",
i
);
}
}
#[test]
fn test_derive_public_keys_from_xpub() {
let seed = from_hex("000102030405060708090a0b0c0d0e0f").unwrap();
let master = ExtendedKey::new_master(&seed, Network::Mainnet).unwrap();
let account_key = master.derive_path("m/44'/0'/0'").unwrap();
let xpub = account_key.neuter().unwrap();
let pubkeys_from_xpub = derive_public_keys_for_path(&xpub, "0", 0, 3).unwrap();
let pubkeys_from_xprv = derive_public_keys_for_path(&account_key, "0", 0, 3).unwrap();
for i in 0..3 {
assert_eq!(
pubkeys_from_xpub[i].to_compressed(),
pubkeys_from_xprv[i].to_compressed(),
"Public key at index {} should match between xpub and xprv derivation",
i
);
}
}
#[test]
fn test_derive_addresses_empty_count() {
let seed = from_hex("000102030405060708090a0b0c0d0e0f").unwrap();
let master = ExtendedKey::new_master(&seed, Network::Mainnet).unwrap();
let addresses = derive_addresses_for_path(&master, "m/44'/0'/0'/0", 0, 0, true).unwrap();
assert!(addresses.is_empty());
}
#[test]
fn test_derive_public_keys_empty_count() {
let seed = from_hex("000102030405060708090a0b0c0d0e0f").unwrap();
let master = ExtendedKey::new_master(&seed, Network::Mainnet).unwrap();
let pubkeys = derive_public_keys_for_path(&master, "m/44'/0'/0'/0", 0, 0).unwrap();
assert!(pubkeys.is_empty());
}
}