Skip to main content

chains_sdk/
hd_key.rs

1//! **BIP-32** Hierarchical Deterministic (HD) key derivation for secp256k1.
2//!
3//! Implements master key generation from seed, hardened & normal child derivation,
4//! and BIP-44 path parsing (`m/44'/60'/0'/0/0`).
5//!
6//! # Example
7//! ```no_run
8//! use chains_sdk::hd_key::{ExtendedPrivateKey, DerivationPath};
9//!
10//! fn main() -> Result<(), Box<dyn std::error::Error>> {
11//!     let seed = [0xab_u8; 64];
12//!     let master = ExtendedPrivateKey::from_seed(&seed)?;
13//!     let path = DerivationPath::parse("m/44'/60'/0'/0/0")?;
14//!     let child = master.derive_path(&path)?;
15//!     Ok(())
16//! }
17//! ```
18
19use crate::crypto;
20use crate::error::SignerError;
21use hmac::{Hmac, Mac};
22use k256::elliptic_curve::sec1::ToEncodedPoint;
23use sha2::Sha512;
24use zeroize::Zeroizing;
25
26type HmacSha512 = Hmac<Sha512>;
27
28/// The BIP-32 master key derivation salt.
29const BIP32_SEED_KEY: &[u8] = b"Bitcoin seed";
30
31/// A BIP-32 extended private key (key + chain code).
32pub struct ExtendedPrivateKey {
33    /// The 32-byte private key scalar.
34    key: Zeroizing<[u8; 32]>,
35    /// The 32-byte chain code used for child derivation.
36    /// Chain code is security-sensitive: knowing it + public key enables
37    /// deriving all non-hardened child keys.
38    chain_code: Zeroizing<[u8; 32]>,
39    /// Derivation depth (0 = master).
40    depth: u8,
41    /// Parent key fingerprint (first 4 bytes of HASH160(parent_pubkey)).
42    parent_fingerprint: [u8; 4],
43    /// Child index used in derivation (includes hardened bit if applicable).
44    child_index: u32,
45}
46
47impl Drop for ExtendedPrivateKey {
48    fn drop(&mut self) {
49        // key and chain_code are both Zeroizing — automatically scrubbed on drop.
50    }
51}
52
53impl ExtendedPrivateKey {
54    /// Derive the master key from a BIP-39 seed (typically 16–64 bytes).
55    ///
56    /// Computes `HMAC-SHA512("Bitcoin seed", seed)`.
57    /// Left 32 bytes = private key, right 32 bytes = chain code.
58    pub fn from_seed(seed: &[u8]) -> Result<Self, SignerError> {
59        if seed.len() < 16 || seed.len() > 64 {
60            return Err(SignerError::InvalidPrivateKey(format!(
61                "BIP-32 seed must be 16–64 bytes, got {}",
62                seed.len()
63            )));
64        }
65
66        let mut mac = HmacSha512::new_from_slice(BIP32_SEED_KEY)
67            .map_err(|_| SignerError::InvalidPrivateKey("HMAC init failed".into()))?;
68        mac.update(seed);
69        let mut result = mac.finalize().into_bytes();
70
71        let mut key = Zeroizing::new([0u8; 32]);
72        key.copy_from_slice(&result[..32]);
73        let mut chain_code = Zeroizing::new([0u8; 32]);
74        chain_code.copy_from_slice(&result[32..]);
75
76        // Zeroize the full HMAC output immediately
77        use zeroize::Zeroize;
78        for b in result.iter_mut() {
79            b.zeroize();
80        }
81
82        // Validate the key is a valid secp256k1 scalar
83        k256::SecretKey::from_bytes((&*key).into())
84            .map_err(|_| SignerError::InvalidPrivateKey("master key is zero or >= n".into()))?;
85
86        Ok(Self {
87            key,
88            chain_code,
89            depth: 0,
90            parent_fingerprint: [0u8; 4],
91            child_index: 0,
92        })
93    }
94
95    /// Derive a child key at the given index.
96    ///
97    /// If `hardened` is true, uses hardened derivation (index + 0x80000000).
98    pub fn derive_child(&self, index: u32, hardened: bool) -> Result<Self, SignerError> {
99        use zeroize::Zeroize;
100
101        let mut mac = HmacSha512::new_from_slice(&*self.chain_code)
102            .map_err(|_| SignerError::InvalidPrivateKey("HMAC init failed".into()))?;
103
104        let effective_index = if hardened {
105            index
106                .checked_add(0x8000_0000)
107                .ok_or_else(|| SignerError::InvalidPrivateKey("index overflow".into()))?
108        } else {
109            index
110        };
111
112        if hardened {
113            // Hardened: HMAC-SHA512(chain_code, 0x00 || key || index)
114            mac.update(&[0x00]);
115            mac.update(&*self.key);
116        } else {
117            // Normal: HMAC-SHA512(chain_code, public_key || index)
118            let sk = k256::SecretKey::from_bytes((&*self.key).into())
119                .map_err(|e| SignerError::InvalidPrivateKey(e.to_string()))?;
120            let pk = sk.public_key().to_encoded_point(true);
121            mac.update(pk.as_bytes());
122        }
123
124        mac.update(&effective_index.to_be_bytes());
125        let mut result = mac.finalize().into_bytes();
126
127        let mut il = [0u8; 32];
128        il.copy_from_slice(&result[..32]);
129        let mut child_chain = Zeroizing::new([0u8; 32]);
130        child_chain.copy_from_slice(&result[32..]);
131
132        // Zeroize the full HMAC output immediately — il and child_chain are copies
133        for b in result.iter_mut() {
134            b.zeroize();
135        }
136
137        // child_key = (il + parent_key) mod n
138        let derive_result = (|| -> Result<Zeroizing<[u8; 32]>, SignerError> {
139            let parent_scalar = k256::NonZeroScalar::try_from(&*self.key as &[u8])
140                .map_err(|_| SignerError::InvalidPrivateKey("parent key invalid".into()))?;
141            let il_scalar = k256::NonZeroScalar::try_from(&il as &[u8])
142                .map_err(|_| SignerError::InvalidPrivateKey("derived key is zero".into()))?;
143
144            // Add scalars: parent + il mod n
145            let child_scalar = parent_scalar.as_ref() + il_scalar.as_ref();
146
147            // Validate the child scalar is non-zero (CtOption -> Option)
148            let child_nz: Option<k256::NonZeroScalar> =
149                k256::NonZeroScalar::new(child_scalar).into();
150            let child_secret = k256::SecretKey::from(
151                child_nz
152                    .ok_or_else(|| SignerError::InvalidPrivateKey("child key is zero".into()))?,
153            );
154
155            let mut child_key = Zeroizing::new([0u8; 32]);
156            child_key.copy_from_slice(&child_secret.to_bytes());
157            Ok(child_key)
158        })();
159
160        // Always zeroize il regardless of success/failure
161        il.zeroize();
162
163        let child_key = derive_result?;
164
165        // Compute parent fingerprint: HASH160(parent_pubkey)[..4]
166        let parent_fp = {
167            let sk = k256::SecretKey::from_bytes((&*self.key).into())
168                .map_err(|e| SignerError::InvalidPrivateKey(e.to_string()))?;
169            let pk_bytes = sk.public_key().to_encoded_point(true);
170            let h160 = crypto::hash160(pk_bytes.as_bytes());
171            let mut fp = [0u8; 4];
172            fp.copy_from_slice(&h160[..4]);
173            fp
174        };
175
176        Ok(Self {
177            key: child_key,
178            chain_code: child_chain,
179            depth: self.depth.saturating_add(1),
180            parent_fingerprint: parent_fp,
181            child_index: effective_index,
182        })
183    }
184
185    /// Derive a child key following a full derivation path.
186    pub fn derive_path(&self, path: &DerivationPath) -> Result<Self, SignerError> {
187        let mut current = Self {
188            key: self.key.clone(),
189            chain_code: self.chain_code.clone(),
190            depth: self.depth,
191            parent_fingerprint: self.parent_fingerprint,
192            child_index: self.child_index,
193        };
194        for step in &path.steps {
195            current = current.derive_child(step.index, step.hardened)?;
196        }
197        Ok(current)
198    }
199
200    /// Get the raw 32-byte private key.
201    #[must_use]
202    pub fn private_key_bytes(&self) -> Zeroizing<Vec<u8>> {
203        Zeroizing::new(self.key.to_vec())
204    }
205
206    /// Get the compressed public key (33 bytes).
207    pub fn public_key_bytes(&self) -> Result<Vec<u8>, SignerError> {
208        let sk = k256::SecretKey::from_bytes((&*self.key).into())
209            .map_err(|e| SignerError::InvalidPrivateKey(e.to_string()))?;
210        Ok(sk.public_key().to_encoded_point(true).as_bytes().to_vec())
211    }
212
213    /// Get the current derivation depth (0 = master).
214    #[must_use]
215    pub fn depth(&self) -> u8 {
216        self.depth
217    }
218
219    /// Get the chain code (useful for extended public key export).
220    #[must_use]
221    pub fn chain_code(&self) -> &[u8; 32] {
222        &self.chain_code
223    }
224
225    /// Get the parent key fingerprint (4 bytes).
226    pub fn parent_fingerprint(&self) -> &[u8; 4] {
227        &self.parent_fingerprint
228    }
229
230    /// Get the child index used in this key's derivation.
231    pub fn child_index(&self) -> u32 {
232        self.child_index
233    }
234
235    /// Serialize as an **xprv** Base58Check string (BIP-32).
236    ///
237    /// Format: `4 bytes version || 1 byte depth || 4 bytes fingerprint || 4 bytes child index || 32 bytes chain code || 1 byte 0x00 || 32 bytes key`
238    ///
239    /// # Security
240    /// The returned String contains the private key — handle with care.
241    #[must_use]
242    pub fn to_xprv(&self) -> Zeroizing<String> {
243        let mut data = Zeroizing::new(Vec::with_capacity(82));
244        data.extend_from_slice(&[0x04, 0x88, 0xAD, 0xE4]); // xprv version
245        data.push(self.depth);
246        data.extend_from_slice(&self.parent_fingerprint);
247        data.extend_from_slice(&self.child_index.to_be_bytes());
248        data.extend_from_slice(&*self.chain_code);
249        data.push(0x00); // private key prefix
250        data.extend_from_slice(&*self.key);
251        // Base58Check: double-SHA256 checksum
252        let checksum = crypto::double_sha256(&data);
253        data.extend_from_slice(&checksum[..4]);
254        Zeroizing::new(bs58::encode(&*data).into_string())
255    }
256
257    /// Serialize the public key as an **xpub** Base58Check string (BIP-32).
258    pub fn to_xpub(&self) -> Result<String, SignerError> {
259        let pubkey = self.public_key_bytes()?;
260        let mut data = Vec::with_capacity(82);
261        data.extend_from_slice(&[0x04, 0x88, 0xB2, 0x1E]); // xpub version
262        data.push(self.depth);
263        data.extend_from_slice(&self.parent_fingerprint);
264        data.extend_from_slice(&self.child_index.to_be_bytes());
265        data.extend_from_slice(&*self.chain_code);
266        data.extend_from_slice(&pubkey);
267        let checksum = crypto::double_sha256(&data);
268        data.extend_from_slice(&checksum[..4]);
269        Ok(bs58::encode(data).into_string())
270    }
271
272    /// Deserialize an **xprv** Base58Check string back into an extended private key.
273    pub fn from_xprv(xprv: &str) -> Result<Self, SignerError> {
274        let data = Zeroizing::new(
275            bs58::decode(xprv)
276                .into_vec()
277                .map_err(|e| SignerError::InvalidPrivateKey(format!("invalid base58: {e}")))?,
278        );
279        if data.len() != 82 {
280            return Err(SignerError::InvalidPrivateKey(format!(
281                "xprv must be 82 bytes, got {}",
282                data.len()
283            )));
284        }
285        // Verify checksum (constant-time)
286        let checksum = crypto::double_sha256(&data[..78]);
287        use subtle::ConstantTimeEq;
288        if data[78..82].ct_eq(&checksum[..4]).unwrap_u8() != 1 {
289            return Err(SignerError::InvalidPrivateKey(
290                "invalid xprv checksum".into(),
291            ));
292        }
293        // Verify version
294        if data[..4] != [0x04, 0x88, 0xAD, 0xE4] {
295            return Err(SignerError::InvalidPrivateKey(
296                "not an xprv (wrong version)".into(),
297            ));
298        }
299        let depth = data[4];
300        let mut parent_fingerprint = [0u8; 4];
301        parent_fingerprint.copy_from_slice(&data[5..9]);
302        let child_index = u32::from_be_bytes([data[9], data[10], data[11], data[12]]);
303        let mut chain_code = Zeroizing::new([0u8; 32]);
304        chain_code.copy_from_slice(&data[13..45]);
305        // data[45] should be 0x00 (private key prefix)
306        if data[45] != 0x00 {
307            return Err(SignerError::InvalidPrivateKey(
308                "invalid private key prefix".into(),
309            ));
310        }
311        let mut key = Zeroizing::new([0u8; 32]);
312        key.copy_from_slice(&data[46..78]);
313        // Validate the key
314        k256::SecretKey::from_bytes((&*key).into())
315            .map_err(|_| SignerError::InvalidPrivateKey("invalid xprv key".into()))?;
316        Ok(Self {
317            key,
318            chain_code,
319            depth,
320            parent_fingerprint,
321            child_index,
322        })
323    }
324
325    /// Convert to an `ExtendedPublicKey` for watch-only derivation.
326    pub fn to_extended_public_key(&self) -> Result<ExtendedPublicKey, SignerError> {
327        let pubkey_bytes = self.public_key_bytes()?;
328        let mut key = [0u8; 33];
329        key.copy_from_slice(&pubkey_bytes);
330        Ok(ExtendedPublicKey {
331            key,
332            chain_code: *self.chain_code,
333            depth: self.depth,
334            parent_fingerprint: self.parent_fingerprint,
335            child_index: self.child_index,
336        })
337    }
338}
339
340// ─── Extended Public Key (BIP-32 Watch-Only) ────────────────────────
341
342/// A BIP-32 extended public key for watch-only wallets.
343///
344/// Supports **normal** (non-hardened) child derivation only.
345/// Hardened derivation requires the private key.
346#[derive(Clone, Debug)]
347pub struct ExtendedPublicKey {
348    /// Compressed SEC1 public key (33 bytes).
349    key: [u8; 33],
350    /// Chain code (32 bytes).
351    chain_code: [u8; 32],
352    /// Derivation depth.
353    depth: u8,
354    /// Parent key fingerprint.
355    parent_fingerprint: [u8; 4],
356    /// Child index.
357    child_index: u32,
358}
359
360impl ExtendedPublicKey {
361    /// Get the compressed public key (33 bytes).
362    #[must_use]
363    pub fn public_key_bytes(&self) -> &[u8; 33] {
364        &self.key
365    }
366
367    /// Get the derivation depth.
368    #[must_use]
369    pub fn depth(&self) -> u8 {
370        self.depth
371    }
372
373    /// Get the chain code.
374    #[must_use]
375    pub fn chain_code(&self) -> &[u8; 32] {
376        &self.chain_code
377    }
378
379    /// Get the parent fingerprint.
380    #[must_use]
381    pub fn parent_fingerprint(&self) -> &[u8; 4] {
382        &self.parent_fingerprint
383    }
384
385    /// Get the child index.
386    #[must_use]
387    pub fn child_index(&self) -> u32 {
388        self.child_index
389    }
390
391    /// Derive a **normal** (non-hardened) child public key.
392    ///
393    /// Only normal derivation (index < 2^31) is supported.
394    /// Hardened derivation requires the private key.
395    pub fn derive_child_normal(&self, index: u32) -> Result<Self, SignerError> {
396        if index >= 0x8000_0000 {
397            return Err(SignerError::InvalidPrivateKey(
398                "hardened derivation requires private key".into(),
399            ));
400        }
401
402        let mut mac = HmacSha512::new_from_slice(&self.chain_code)
403            .map_err(|_| SignerError::InvalidPrivateKey("HMAC init failed".into()))?;
404
405        // For normal child: HMAC-SHA512(chain_code, pubkey || index)
406        mac.update(&self.key);
407        mac.update(&index.to_be_bytes());
408
409        let result = mac.finalize().into_bytes();
410
411        let mut il = [0u8; 32];
412        il.copy_from_slice(&result[..32]);
413        let mut child_chain = [0u8; 32];
414        child_chain.copy_from_slice(&result[32..]);
415
416        // Parse IL as scalar and add to parent point
417        use k256::elliptic_curve::group::GroupEncoding;
418        use k256::elliptic_curve::ops::Reduce;
419        use k256::{ProjectivePoint, Scalar, U256};
420
421        let il_scalar = <Scalar as Reduce<U256>>::reduce(U256::from_be_slice(&il));
422        let parent_point = k256::AffinePoint::from_bytes((&self.key).into());
423        let parent_proj: ProjectivePoint = Option::from(parent_point.map(ProjectivePoint::from))
424            .ok_or_else(|| SignerError::InvalidPublicKey("invalid parent public key".into()))?;
425
426        let child_point = parent_proj + ProjectivePoint::GENERATOR * il_scalar;
427
428        // Serialize child public key
429        use k256::elliptic_curve::sec1::ToEncodedPoint;
430        let child_affine = child_point.to_affine();
431        let encoded = child_affine.to_encoded_point(true);
432        let child_key_bytes = encoded.as_bytes();
433        if child_key_bytes.len() != 33 {
434            return Err(SignerError::InvalidPublicKey(
435                "child key serialization failed".into(),
436            ));
437        }
438        let mut child_key = [0u8; 33];
439        child_key.copy_from_slice(child_key_bytes);
440
441        // Parent fingerprint = first 4 bytes of HASH160(parent_pubkey)
442        let fingerprint = crypto::hash160(&self.key);
443        let mut parent_fp = [0u8; 4];
444        parent_fp.copy_from_slice(&fingerprint[..4]);
445
446        // Zeroize IL
447        use zeroize::Zeroize;
448        il.zeroize();
449
450        Ok(Self {
451            key: child_key,
452            chain_code: child_chain,
453            depth: self.depth.checked_add(1).ok_or_else(|| {
454                SignerError::InvalidPrivateKey("derivation depth overflow".into())
455            })?,
456            parent_fingerprint: parent_fp,
457            child_index: index,
458        })
459    }
460
461    /// Serialize as an **xpub** Base58Check string.
462    #[must_use]
463    pub fn to_xpub(&self) -> String {
464        let mut data = Vec::with_capacity(82);
465        data.extend_from_slice(&[0x04, 0x88, 0xB2, 0x1E]); // xpub version
466        data.push(self.depth);
467        data.extend_from_slice(&self.parent_fingerprint);
468        data.extend_from_slice(&self.child_index.to_be_bytes());
469        data.extend_from_slice(&self.chain_code);
470        data.extend_from_slice(&self.key);
471        let checksum = crypto::double_sha256(&data);
472        data.extend_from_slice(&checksum[..4]);
473        bs58::encode(data).into_string()
474    }
475
476    /// Deserialize an **xpub** Base58Check string.
477    pub fn from_xpub(xpub: &str) -> Result<Self, SignerError> {
478        let data = bs58::decode(xpub)
479            .into_vec()
480            .map_err(|e| SignerError::InvalidPublicKey(format!("invalid base58: {e}")))?;
481        if data.len() != 82 {
482            return Err(SignerError::InvalidPublicKey(format!(
483                "xpub must be 82 bytes, got {}",
484                data.len()
485            )));
486        }
487        let checksum = crypto::double_sha256(&data[..78]);
488        use subtle::ConstantTimeEq;
489        if data[78..82].ct_eq(&checksum[..4]).unwrap_u8() != 1 {
490            return Err(SignerError::InvalidPublicKey(
491                "invalid xpub checksum".into(),
492            ));
493        }
494        if data[..4] != [0x04, 0x88, 0xB2, 0x1E] {
495            return Err(SignerError::InvalidPublicKey(
496                "not an xpub (wrong version)".into(),
497            ));
498        }
499        let depth = data[4];
500        let mut parent_fingerprint = [0u8; 4];
501        parent_fingerprint.copy_from_slice(&data[5..9]);
502        let child_index = u32::from_be_bytes([data[9], data[10], data[11], data[12]]);
503        let mut chain_code = [0u8; 32];
504        chain_code.copy_from_slice(&data[13..45]);
505        let mut key = [0u8; 33];
506        key.copy_from_slice(&data[45..78]);
507        // Validate the public key is on the curve
508        let _pt = k256::AffinePoint::from_bytes((&key).into());
509        use k256::elliptic_curve::group::GroupEncoding;
510        if bool::from(k256::AffinePoint::from_bytes((&key).into()).is_none()) {
511            return Err(SignerError::InvalidPublicKey(
512                "invalid xpub key point".into(),
513            ));
514        }
515        Ok(Self {
516            key,
517            chain_code,
518            depth,
519            parent_fingerprint,
520            child_index,
521        })
522    }
523
524    /// Derive a **P2WPKH** (SegWit) address from this public key.
525    ///
526    /// Uses Bech32 encoding: `bc1q...` for mainnet.
527    ///
528    /// # Arguments
529    /// - `hrp` — Human-readable part: `"bc"` for mainnet, `"tb"` for testnet
530    #[cfg(feature = "bitcoin")]
531    pub fn p2wpkh_address(&self, hrp: &str) -> Result<String, SignerError> {
532        let pubkey_hash = crypto::hash160(&self.key);
533        crate::bitcoin::bech32_encode(hrp, 0, &pubkey_hash)
534    }
535
536    /// Derive a **P2TR** (Taproot) address from this public key.
537    ///
538    /// Extracts the x-only public key (drops the prefix byte) and encodes
539    /// as a Bech32m `bc1p...` address.
540    ///
541    /// # Arguments
542    /// - `hrp` — Human-readable part: `"bc"` for mainnet, `"tb"` for testnet
543    #[cfg(feature = "bitcoin")]
544    pub fn p2tr_address(&self, hrp: &str) -> Result<String, SignerError> {
545        // x-only = drop the 0x02/0x03 prefix from compressed key
546        if self.key.len() != 33 {
547            return Err(SignerError::InvalidPublicKey(
548                "expected 33-byte compressed key".into(),
549            ));
550        }
551        let x_only = &self.key[1..33];
552        crate::bitcoin::bech32_encode(hrp, 1, x_only)
553    }
554}
555
556/// A single step in a BIP-32 derivation path.
557#[derive(Debug, Clone)]
558pub struct DerivationStep {
559    /// Child index (0-based).
560    pub index: u32,
561    /// Whether this is a hardened derivation step.
562    pub hardened: bool,
563}
564
565/// A BIP-32/BIP-44 derivation path (e.g., `m/44'/60'/0'/0/0`).
566#[derive(Debug, Clone)]
567pub struct DerivationPath {
568    /// The steps in this path.
569    pub steps: Vec<DerivationStep>,
570}
571
572impl DerivationPath {
573    /// Parse a BIP-32 path string like `"m/44'/60'/0'/0/0"`.
574    ///
575    /// Supported formats: `44'` or `44h` for hardened.
576    pub fn parse(path: &str) -> Result<Self, SignerError> {
577        let path = path.trim();
578        let segments: Vec<&str> = path.split('/').collect();
579
580        if segments.is_empty() {
581            return Err(SignerError::InvalidPrivateKey(
582                "empty derivation path".into(),
583            ));
584        }
585
586        // First segment must be "m" or "M"
587        if segments[0] != "m" && segments[0] != "M" {
588            return Err(SignerError::InvalidPrivateKey(
589                "derivation path must start with 'm/'".into(),
590            ));
591        }
592
593        let mut steps = Vec::new();
594        for seg in &segments[1..] {
595            if seg.is_empty() {
596                continue;
597            }
598            let (hardened, num_str) =
599                if seg.ends_with('\'') || seg.ends_with('h') || seg.ends_with('H') {
600                    (true, &seg[..seg.len() - 1])
601                } else {
602                    (false, *seg)
603                };
604
605            let index: u32 = num_str.parse().map_err(|_| {
606                SignerError::InvalidPrivateKey(format!("invalid path segment: {seg}"))
607            })?;
608
609            if index >= 0x8000_0000 {
610                return Err(SignerError::InvalidPrivateKey(format!(
611                    "index {index} too large (must be < 2^31)"
612                )));
613            }
614
615            steps.push(DerivationStep { index, hardened });
616        }
617
618        Ok(Self { steps })
619    }
620
621    /// BIP-44 Ethereum path: `m/44'/60'/0'/0/{index}`
622    pub fn ethereum(index: u32) -> Self {
623        Self {
624            steps: vec![
625                DerivationStep {
626                    index: 44,
627                    hardened: true,
628                },
629                DerivationStep {
630                    index: 60,
631                    hardened: true,
632                },
633                DerivationStep {
634                    index: 0,
635                    hardened: true,
636                },
637                DerivationStep {
638                    index: 0,
639                    hardened: false,
640                },
641                DerivationStep {
642                    index,
643                    hardened: false,
644                },
645            ],
646        }
647    }
648
649    /// BIP-44 Bitcoin path: `m/44'/0'/0'/0/{index}`
650    pub fn bitcoin(index: u32) -> Self {
651        Self {
652            steps: vec![
653                DerivationStep {
654                    index: 44,
655                    hardened: true,
656                },
657                DerivationStep {
658                    index: 0,
659                    hardened: true,
660                },
661                DerivationStep {
662                    index: 0,
663                    hardened: true,
664                },
665                DerivationStep {
666                    index: 0,
667                    hardened: false,
668                },
669                DerivationStep {
670                    index,
671                    hardened: false,
672                },
673            ],
674        }
675    }
676
677    /// BIP-84 Bitcoin Segwit path: `m/84'/0'/0'/0/{index}`
678    pub fn bitcoin_segwit(index: u32) -> Self {
679        Self {
680            steps: vec![
681                DerivationStep {
682                    index: 84,
683                    hardened: true,
684                },
685                DerivationStep {
686                    index: 0,
687                    hardened: true,
688                },
689                DerivationStep {
690                    index: 0,
691                    hardened: true,
692                },
693                DerivationStep {
694                    index: 0,
695                    hardened: false,
696                },
697                DerivationStep {
698                    index,
699                    hardened: false,
700                },
701            ],
702        }
703    }
704
705    /// BIP-86 Bitcoin Taproot path: `m/86'/0'/0'/0/{index}`
706    pub fn bitcoin_taproot(index: u32) -> Self {
707        Self {
708            steps: vec![
709                DerivationStep {
710                    index: 86,
711                    hardened: true,
712                },
713                DerivationStep {
714                    index: 0,
715                    hardened: true,
716                },
717                DerivationStep {
718                    index: 0,
719                    hardened: true,
720                },
721                DerivationStep {
722                    index: 0,
723                    hardened: false,
724                },
725                DerivationStep {
726                    index,
727                    hardened: false,
728                },
729            ],
730        }
731    }
732
733    /// BIP-44 Solana path: `m/44'/501'/{index}'/0'`
734    pub fn solana(index: u32) -> Self {
735        Self {
736            steps: vec![
737                DerivationStep {
738                    index: 44,
739                    hardened: true,
740                },
741                DerivationStep {
742                    index: 501,
743                    hardened: true,
744                },
745                DerivationStep {
746                    index,
747                    hardened: true,
748                },
749                DerivationStep {
750                    index: 0,
751                    hardened: true,
752                },
753            ],
754        }
755    }
756
757    /// BIP-44 XRP path: `m/44'/144'/0'/0/{index}`
758    pub fn xrp(index: u32) -> Self {
759        Self {
760            steps: vec![
761                DerivationStep {
762                    index: 44,
763                    hardened: true,
764                },
765                DerivationStep {
766                    index: 144,
767                    hardened: true,
768                },
769                DerivationStep {
770                    index: 0,
771                    hardened: true,
772                },
773                DerivationStep {
774                    index: 0,
775                    hardened: false,
776                },
777                DerivationStep {
778                    index,
779                    hardened: false,
780                },
781            ],
782        }
783    }
784
785    /// BIP-44 NEO path: `m/44'/888'/0'/0/{index}`
786    pub fn neo(index: u32) -> Self {
787        Self {
788            steps: vec![
789                DerivationStep {
790                    index: 44,
791                    hardened: true,
792                },
793                DerivationStep {
794                    index: 888,
795                    hardened: true,
796                },
797                DerivationStep {
798                    index: 0,
799                    hardened: true,
800                },
801                DerivationStep {
802                    index: 0,
803                    hardened: false,
804                },
805                DerivationStep {
806                    index,
807                    hardened: false,
808                },
809            ],
810        }
811    }
812}
813
814#[cfg(test)]
815#[allow(clippy::unwrap_used, clippy::expect_used)]
816mod tests {
817    use super::*;
818
819    // BIP-32 Test Vector 1 (from BIP-32 spec)
820    // Seed: 000102030405060708090a0b0c0d0e0f
821    #[test]
822    fn test_bip32_vector1_master() {
823        let seed = hex::decode("000102030405060708090a0b0c0d0e0f").unwrap();
824        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
825        let pk = master.public_key_bytes().unwrap();
826
827        assert_eq!(
828            hex::encode(&*master.private_key_bytes()),
829            "e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35"
830        );
831        assert_eq!(
832            hex::encode(&pk),
833            "0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2"
834        );
835        assert_eq!(
836            hex::encode(master.chain_code()),
837            "873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508"
838        );
839    }
840
841    #[test]
842    fn test_bip32_vector1_child_0h() {
843        let seed = hex::decode("000102030405060708090a0b0c0d0e0f").unwrap();
844        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
845        let child = master.derive_child(0, true).unwrap();
846
847        assert_eq!(
848            hex::encode(&*child.private_key_bytes()),
849            "edb2e14f9ee77d26dd93b4ecede8d16ed408ce149b6cd80b0715a2d911a0afea"
850        );
851        assert_eq!(child.depth(), 1);
852    }
853
854    #[test]
855    fn test_bip32_vector1_path_m44h_60h_0h_0_0() {
856        let seed = hex::decode("000102030405060708090a0b0c0d0e0f").unwrap();
857        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
858        let path = DerivationPath::parse("m/44'/60'/0'/0/0").unwrap();
859        let child = master.derive_path(&path).unwrap();
860        assert_eq!(child.depth(), 5);
861        assert_eq!(child.private_key_bytes().len(), 32);
862        // Ensure deterministic — derive twice
863        let child2 = master.derive_path(&path).unwrap();
864        assert_eq!(&*child.private_key_bytes(), &*child2.private_key_bytes());
865    }
866
867    #[test]
868    fn test_bip32_vector2_seed() {
869        // BIP-32 Test Vector 2
870        let seed = hex::decode("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542").unwrap();
871        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
872        assert_eq!(
873            hex::encode(&*master.private_key_bytes()),
874            "4b03d6fc340455b363f51020ad3ecca4f0850280cf436c70c727923f6db46c3e"
875        );
876        assert_eq!(
877            hex::encode(master.chain_code()),
878            "60499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd9689"
879        );
880    }
881
882    #[test]
883    fn test_derivation_path_parse() {
884        let path = DerivationPath::parse("m/44'/60'/0'/0/0").unwrap();
885        assert_eq!(path.steps.len(), 5);
886        assert!(path.steps[0].hardened);
887        assert_eq!(path.steps[0].index, 44);
888        assert!(path.steps[1].hardened);
889        assert_eq!(path.steps[1].index, 60);
890        assert!(!path.steps[3].hardened);
891        assert_eq!(path.steps[4].index, 0);
892    }
893
894    #[test]
895    fn test_derivation_path_shortcuts() {
896        let eth = DerivationPath::ethereum(0);
897        assert_eq!(eth.steps.len(), 5);
898        assert_eq!(eth.steps[1].index, 60);
899
900        let btc = DerivationPath::bitcoin(0);
901        assert_eq!(btc.steps[1].index, 0);
902
903        let sol = DerivationPath::solana(0);
904        assert_eq!(sol.steps[1].index, 501);
905        assert_eq!(sol.steps.len(), 4); // Solana uses all-hardened
906    }
907
908    #[test]
909    fn test_invalid_path_rejected() {
910        assert!(DerivationPath::parse("").is_err());
911        assert!(DerivationPath::parse("x/44'/60'").is_err());
912    }
913
914    #[test]
915    fn test_seed_length_validation() {
916        assert!(ExtendedPrivateKey::from_seed(&[0u8; 15]).is_err());
917        assert!(ExtendedPrivateKey::from_seed(&[0u8; 65]).is_err());
918        assert!(ExtendedPrivateKey::from_seed(&[0u8; 16]).is_ok());
919        assert!(ExtendedPrivateKey::from_seed(&[0u8; 64]).is_ok());
920    }
921
922    #[test]
923    fn test_normal_vs_hardened_different_keys() {
924        let seed = hex::decode("000102030405060708090a0b0c0d0e0f").unwrap();
925        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
926        let normal = master.derive_child(0, false).unwrap();
927        let hardened = master.derive_child(0, true).unwrap();
928        assert_ne!(&*normal.private_key_bytes(), &*hardened.private_key_bytes());
929    }
930
931    #[test]
932    fn test_multi_account_derivation() {
933        let seed = hex::decode("000102030405060708090a0b0c0d0e0f").unwrap();
934        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
935
936        let eth0 = master.derive_path(&DerivationPath::ethereum(0)).unwrap();
937        let eth1 = master.derive_path(&DerivationPath::ethereum(1)).unwrap();
938        let btc0 = master.derive_path(&DerivationPath::bitcoin(0)).unwrap();
939
940        // All different keys
941        assert_ne!(&*eth0.private_key_bytes(), &*eth1.private_key_bytes());
942        assert_ne!(&*eth0.private_key_bytes(), &*btc0.private_key_bytes());
943    }
944
945    // ─── BIP-32 Vector 1: xprv/xpub Serialization ───────────────
946
947    #[test]
948    fn test_bip32_vector1_master_xprv() {
949        // BIP-32 Test Vector 1 — Master key xprv
950        let seed = hex::decode("000102030405060708090a0b0c0d0e0f").unwrap();
951        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
952        assert_eq!(
953            &*master.to_xprv(),
954            "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"
955        );
956    }
957
958    #[test]
959    fn test_bip32_vector1_master_xpub() {
960        let seed = hex::decode("000102030405060708090a0b0c0d0e0f").unwrap();
961        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
962        assert_eq!(
963            master.to_xpub().unwrap(),
964            "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8"
965        );
966    }
967
968    #[test]
969    fn test_bip32_vector1_chain_m_0h() {
970        let seed = hex::decode("000102030405060708090a0b0c0d0e0f").unwrap();
971        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
972        let child = master.derive_child(0, true).unwrap();
973        assert_eq!(
974            &*child.to_xprv(),
975            "xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7"
976        );
977        assert_eq!(
978            child.to_xpub().unwrap(),
979            "xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw"
980        );
981    }
982
983    #[test]
984    fn test_bip32_xprv_roundtrip() {
985        let seed = hex::decode("000102030405060708090a0b0c0d0e0f").unwrap();
986        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
987        let xprv_str = master.to_xprv();
988        let restored = ExtendedPrivateKey::from_xprv(&xprv_str).unwrap();
989        assert_eq!(&*master.private_key_bytes(), &*restored.private_key_bytes());
990        assert_eq!(master.chain_code(), restored.chain_code());
991        assert_eq!(master.depth(), restored.depth());
992    }
993
994    #[test]
995    fn test_bip32_from_xprv_invalid_checksum() {
996        // Valid xprv but flip last character to break checksum
997        let xprv = "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHiX";
998        // This should fail because the checksum is invalid (we appended 'X')
999        // But the base58 decoding may also fail since length changes
1000        assert!(ExtendedPrivateKey::from_xprv(xprv).is_err());
1001    }
1002
1003    // ─── BIP-32 Vector 2: Full chain ────────────────────────────
1004
1005    #[test]
1006    fn test_bip32_vector2_master_xprv() {
1007        let seed = hex::decode(
1008            "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"
1009        ).unwrap();
1010        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1011        assert_eq!(
1012            &*master.to_xprv(),
1013            "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U"
1014        );
1015    }
1016
1017    #[test]
1018    fn test_bip32_vector2_chain_m_0() {
1019        let seed = hex::decode(
1020            "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"
1021        ).unwrap();
1022        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1023        let child = master.derive_child(0, false).unwrap();
1024        assert_eq!(
1025            &*child.to_xprv(),
1026            "xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt"
1027        );
1028    }
1029
1030    // ─── BIP-32 Vector 1: Full Chain ────────────────────────────
1031    // Seed: 000102030405060708090a0b0c0d0e0f
1032    // Chain: m → m/0' → m/0'/1 → m/0'/1/2' → m/0'/1/2'/2 → m/0'/1/2'/2/1000000000
1033
1034    #[test]
1035    fn test_bip32_vector1_chain_m_0h_1() {
1036        let seed = hex::decode("000102030405060708090a0b0c0d0e0f").unwrap();
1037        let m = ExtendedPrivateKey::from_seed(&seed).unwrap();
1038        let c = m
1039            .derive_child(0, true)
1040            .unwrap() // m/0'
1041            .derive_child(1, false)
1042            .unwrap(); // m/0'/1
1043        assert_eq!(
1044            &*c.to_xprv(),
1045            "xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs"
1046        );
1047        // Verify depth and key length
1048        assert_eq!(c.depth(), 2);
1049        assert_eq!(c.private_key_bytes().len(), 32);
1050    }
1051
1052    #[test]
1053    fn test_bip32_vector1_chain_m_0h_1_2h() {
1054        let seed = hex::decode("000102030405060708090a0b0c0d0e0f").unwrap();
1055        let m = ExtendedPrivateKey::from_seed(&seed).unwrap();
1056        let c = m
1057            .derive_child(0, true)
1058            .unwrap()
1059            .derive_child(1, false)
1060            .unwrap()
1061            .derive_child(2, true)
1062            .unwrap(); // m/0'/1/2'
1063        assert_eq!(
1064            &*c.to_xprv(),
1065            "xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM"
1066        );
1067        assert_eq!(
1068            c.to_xpub().unwrap(),
1069            "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5"
1070        );
1071    }
1072
1073    #[test]
1074    fn test_bip32_vector1_chain_m_0h_1_2h_2() {
1075        let seed = hex::decode("000102030405060708090a0b0c0d0e0f").unwrap();
1076        let m = ExtendedPrivateKey::from_seed(&seed).unwrap();
1077        let c = m
1078            .derive_child(0, true)
1079            .unwrap()
1080            .derive_child(1, false)
1081            .unwrap()
1082            .derive_child(2, true)
1083            .unwrap()
1084            .derive_child(2, false)
1085            .unwrap(); // m/0'/1/2'/2
1086        assert_eq!(
1087            &*c.to_xprv(),
1088            "xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334"
1089        );
1090        assert_eq!(c.depth(), 4);
1091    }
1092
1093    #[test]
1094    fn test_bip32_vector1_chain_m_0h_1_2h_2_1000000000() {
1095        let seed = hex::decode("000102030405060708090a0b0c0d0e0f").unwrap();
1096        let m = ExtendedPrivateKey::from_seed(&seed).unwrap();
1097        let c = m
1098            .derive_child(0, true)
1099            .unwrap()
1100            .derive_child(1, false)
1101            .unwrap()
1102            .derive_child(2, true)
1103            .unwrap()
1104            .derive_child(2, false)
1105            .unwrap()
1106            .derive_child(1_000_000_000, false)
1107            .unwrap(); // m/0'/1/2'/2/1000000000
1108        assert_eq!(
1109            &*c.to_xprv(),
1110            "xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76"
1111        );
1112        assert_eq!(c.depth(), 5);
1113    }
1114
1115    // ─── BIP-32 Vector 2: Full Chain ────────────────────────────
1116    // Seed: fffcf9f6...484542
1117    // Chain: m → m/0 → m/0/2147483647' → m/0/2147483647'/1 → m/0/2147483647'/1/2147483646' → m/0/2147483647'/1/2147483646'/2
1118
1119    #[test]
1120    fn test_bip32_vector2_chain_m_0_2147483647h() {
1121        let seed = hex::decode(
1122            "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"
1123        ).unwrap();
1124        let m = ExtendedPrivateKey::from_seed(&seed).unwrap();
1125        let c = m
1126            .derive_child(0, false)
1127            .unwrap()
1128            .derive_child(2_147_483_647, true)
1129            .unwrap(); // m/0/2147483647'
1130        assert_eq!(
1131            &*c.to_xprv(),
1132            "xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9"
1133        );
1134    }
1135
1136    #[test]
1137    fn test_bip32_vector2_chain_m_0_2147483647h_1() {
1138        let seed = hex::decode(
1139            "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"
1140        ).unwrap();
1141        let m = ExtendedPrivateKey::from_seed(&seed).unwrap();
1142        let c = m
1143            .derive_child(0, false)
1144            .unwrap()
1145            .derive_child(2_147_483_647, true)
1146            .unwrap()
1147            .derive_child(1, false)
1148            .unwrap(); // m/0/2147483647'/1
1149        assert_eq!(
1150            &*c.to_xprv(),
1151            // spec: xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef
1152            "xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef"
1153        );
1154    }
1155
1156    #[test]
1157    fn test_bip32_vector2_chain_full() {
1158        let seed = hex::decode(
1159            "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"
1160        ).unwrap();
1161        let m = ExtendedPrivateKey::from_seed(&seed).unwrap();
1162        let c = m
1163            .derive_child(0, false)
1164            .unwrap()
1165            .derive_child(2_147_483_647, true)
1166            .unwrap()
1167            .derive_child(1, false)
1168            .unwrap()
1169            .derive_child(2_147_483_646, true)
1170            .unwrap()
1171            .derive_child(2, false)
1172            .unwrap(); // m/0/2147483647'/1/2147483646'/2
1173        assert_eq!(
1174            &*c.to_xprv(),
1175            "xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j"
1176        );
1177        assert_eq!(c.depth(), 5);
1178    }
1179
1180    // ─── BIP-32 Vector 3: Leading zeros edge case ───────────────
1181    // Seed: 4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be
1182
1183    #[test]
1184    fn test_bip32_vector3_master() {
1185        let seed = hex::decode(
1186            "4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be"
1187        ).unwrap();
1188        let m = ExtendedPrivateKey::from_seed(&seed).unwrap();
1189        assert_eq!(
1190            &*m.to_xprv(),
1191            "xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6"
1192        );
1193        assert_eq!(m.depth(), 0);
1194    }
1195
1196    #[test]
1197    fn test_bip32_vector3_chain_m_0h() {
1198        let seed = hex::decode(
1199            "4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be"
1200        ).unwrap();
1201        let m = ExtendedPrivateKey::from_seed(&seed).unwrap();
1202        let c = m.derive_child(0, true).unwrap();
1203        assert_eq!(
1204            &*c.to_xprv(),
1205            // spec: xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L
1206            "xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L"
1207        );
1208        assert_eq!(c.depth(), 1);
1209    }
1210
1211    // ─── Derivation Path Edge Cases ─────────────────────────────
1212
1213    #[test]
1214    fn test_derivation_path_hardened_h_notation() {
1215        let path = DerivationPath::parse("m/44h/60h/0h/0/0").unwrap();
1216        assert_eq!(path.steps.len(), 5);
1217        assert!(path.steps[0].hardened);
1218        assert_eq!(path.steps[0].index, 44);
1219    }
1220
1221    #[test]
1222    fn test_derivation_path_all_chain_presets() {
1223        let btc_segwit = DerivationPath::bitcoin_segwit(0);
1224        assert_eq!(btc_segwit.steps[0].index, 84); // BIP-84
1225        assert!(btc_segwit.steps[0].hardened);
1226
1227        let btc_taproot = DerivationPath::bitcoin_taproot(0);
1228        assert_eq!(btc_taproot.steps[0].index, 86); // BIP-86
1229        assert!(btc_taproot.steps[0].hardened);
1230
1231        let xrp = DerivationPath::xrp(0);
1232        assert_eq!(xrp.steps[1].index, 144); // XRP coin type
1233
1234        let neo = DerivationPath::neo(0);
1235        assert_eq!(neo.steps[1].index, 888); // NEO coin type
1236    }
1237
1238    // ─── ExtendedPublicKey Tests ────────────────────────────────────
1239
1240    #[test]
1241    fn test_extended_public_key_from_private() {
1242        let seed = [0xABu8; 64];
1243        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1244        let pubkey = master.to_extended_public_key().unwrap();
1245        assert_eq!(pubkey.depth(), 0);
1246        assert_eq!(pubkey.public_key_bytes().len(), 33); // compressed
1247    }
1248
1249    #[test]
1250    fn test_xpub_starts_with_xpub() {
1251        let seed = [0xABu8; 64];
1252        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1253        let xpub = master.to_xpub().unwrap();
1254        assert!(xpub.starts_with("xpub"));
1255    }
1256
1257    #[test]
1258    fn test_xpub_roundtrip() {
1259        let seed = [0xABu8; 64];
1260        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1261        let pubkey = master.to_extended_public_key().unwrap();
1262        let xpub_str = pubkey.to_xpub();
1263        let restored = ExtendedPublicKey::from_xpub(&xpub_str).unwrap();
1264        assert_eq!(pubkey.public_key_bytes(), restored.public_key_bytes());
1265        assert_eq!(pubkey.depth(), restored.depth());
1266        assert_eq!(pubkey.chain_code(), restored.chain_code());
1267    }
1268
1269    #[test]
1270    fn test_xpub_deterministic() {
1271        let seed = [0xABu8; 64];
1272        let m1 = ExtendedPrivateKey::from_seed(&seed).unwrap();
1273        let m2 = ExtendedPrivateKey::from_seed(&seed).unwrap();
1274        assert_eq!(
1275            m1.to_extended_public_key().unwrap().to_xpub(),
1276            m2.to_extended_public_key().unwrap().to_xpub(),
1277        );
1278    }
1279
1280    #[test]
1281    fn test_extended_public_key_normal_derivation() {
1282        let seed = [0xABu8; 64];
1283        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1284        let pubkey = master.to_extended_public_key().unwrap();
1285
1286        // Derive normal child 0
1287        let child = pubkey.derive_child_normal(0).unwrap();
1288        assert_eq!(child.depth(), 1);
1289        assert_eq!(child.public_key_bytes().len(), 33);
1290    }
1291
1292    #[test]
1293    fn test_extended_public_key_derivation_consistency() {
1294        // Deriving pub child from pub key must match pub key derived from priv child
1295        let seed = [0x42u8; 64];
1296        let master_priv = ExtendedPrivateKey::from_seed(&seed).unwrap();
1297        let master_pub = master_priv.to_extended_public_key().unwrap();
1298
1299        // Private path: master_priv → child_priv(0) → to_pubkey
1300        let child_priv = master_priv.derive_child(0, false).unwrap();
1301        let child_pub_from_priv = child_priv.to_extended_public_key().unwrap();
1302
1303        // Public path: master_pub → child_pub(0)
1304        let child_pub_from_pub = master_pub.derive_child_normal(0).unwrap();
1305
1306        // Both paths should produce the same public key
1307        assert_eq!(
1308            child_pub_from_priv.public_key_bytes(),
1309            child_pub_from_pub.public_key_bytes(),
1310        );
1311    }
1312
1313    #[test]
1314    fn test_extended_public_key_hardened_rejected() {
1315        // ExtendedPublicKey should not support hardened derivation
1316        let seed = [0xABu8; 64];
1317        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1318        let pubkey = master.to_extended_public_key().unwrap();
1319
1320        // There's no hardened derive method on ExtendedPublicKey,
1321        // so we just verify normal derivation works for multiple indices
1322        for i in 0..5 {
1323            let child = pubkey.derive_child_normal(i).unwrap();
1324            assert_eq!(child.depth(), 1);
1325        }
1326    }
1327
1328    #[test]
1329    fn test_extended_public_key_different_indices() {
1330        let seed = [0xABu8; 64];
1331        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1332        let pubkey = master.to_extended_public_key().unwrap();
1333
1334        let c0 = pubkey.derive_child_normal(0).unwrap();
1335        let c1 = pubkey.derive_child_normal(1).unwrap();
1336        assert_ne!(c0.public_key_bytes(), c1.public_key_bytes());
1337    }
1338
1339    #[test]
1340    fn test_extended_public_key_chain_derivation() {
1341        // Multi-level normal derivation via public path
1342        let seed = [0xABu8; 64];
1343        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1344        let pubkey = master.to_extended_public_key().unwrap();
1345
1346        let child1 = pubkey.derive_child_normal(0).unwrap();
1347        let child2 = child1.derive_child_normal(1).unwrap();
1348        assert_eq!(child2.depth(), 2);
1349        assert_eq!(child2.public_key_bytes().len(), 33);
1350    }
1351
1352    #[test]
1353    fn test_xpub_invalid_prefix_rejected() {
1354        let seed = [0xABu8; 64];
1355        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1356        let xpub = master.to_extended_public_key().unwrap().to_xpub();
1357
1358        // Corrupt the first character
1359        let mut bad = String::from("ypub");
1360        bad.push_str(&xpub[4..]);
1361        assert!(ExtendedPublicKey::from_xpub(&bad).is_err());
1362    }
1363
1364    #[cfg(feature = "bitcoin")]
1365    #[test]
1366    fn test_extended_public_key_p2wpkh_address() {
1367        let seed = [0xABu8; 64];
1368        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1369        let pubkey = master.to_extended_public_key().unwrap();
1370        let addr = pubkey.p2wpkh_address("bc").unwrap();
1371        assert!(
1372            addr.starts_with("bc1q"),
1373            "P2WPKH should start with bc1q: {addr}"
1374        );
1375    }
1376
1377    #[cfg(feature = "bitcoin")]
1378    #[test]
1379    fn test_extended_public_key_p2tr_address() {
1380        let seed = [0xABu8; 64];
1381        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1382        let pubkey = master.to_extended_public_key().unwrap();
1383        let addr = pubkey.p2tr_address("bc").unwrap();
1384        assert!(
1385            addr.starts_with("bc1p"),
1386            "P2TR should start with bc1p: {addr}"
1387        );
1388    }
1389
1390    #[cfg(feature = "bitcoin")]
1391    #[test]
1392    fn test_extended_public_key_derived_addresses_differ() {
1393        let seed = [0xABu8; 64];
1394        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1395        let pubkey = master.to_extended_public_key().unwrap();
1396        let c0 = pubkey.derive_child_normal(0).unwrap();
1397        let c1 = pubkey.derive_child_normal(1).unwrap();
1398        assert_ne!(
1399            c0.p2wpkh_address("bc").unwrap(),
1400            c1.p2wpkh_address("bc").unwrap(),
1401        );
1402    }
1403
1404    #[cfg(feature = "bitcoin")]
1405    #[test]
1406    fn test_parse_unsigned_tx_roundtrip() {
1407        use crate::bitcoin::transaction::*;
1408        let mut tx = Transaction::new(2);
1409        tx.inputs.push(TxIn {
1410            previous_output: OutPoint {
1411                txid: [0xAA; 32],
1412                vout: 0,
1413            },
1414            script_sig: vec![],
1415            sequence: 0xFFFFFFFF,
1416        });
1417        tx.outputs.push(TxOut {
1418            value: 50_000,
1419            script_pubkey: vec![
1420                0x00, 0x14, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
1421                0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
1422            ],
1423        });
1424        let raw = tx.serialize_legacy();
1425        let parsed = parse_unsigned_tx(&raw).unwrap();
1426        assert_eq!(parsed.version, 2);
1427        assert_eq!(parsed.inputs.len(), 1);
1428        assert_eq!(parsed.outputs.len(), 1);
1429        assert_eq!(parsed.outputs[0].value, 50_000);
1430        assert_eq!(parsed.locktime, 0);
1431    }
1432
1433    // ─── Extended Public Key Normal Derivation Consistency ──────
1434    // BIP-32: Public parent key → public child key must match
1435    // private derivation followed by public key extraction.
1436
1437    #[test]
1438    fn test_xpub_normal_derivation_matches_private_path() {
1439        let seed = hex::decode("000102030405060708090a0b0c0d0e0f").unwrap();
1440        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1441
1442        // Derive m/0' (hardened) then derive normal children 0..5
1443        let parent_priv = master.derive_child(0, true).unwrap();
1444        let parent_pub = parent_priv.to_extended_public_key().unwrap();
1445
1446        for idx in 0..5 {
1447            // Private path: derive child then extract pubkey
1448            let child_priv = parent_priv.derive_child(idx, false).unwrap();
1449            let expected_pubkey = child_priv.public_key_bytes().unwrap();
1450
1451            // Public-only path: derive directly from xpub
1452            let child_pub = parent_pub.derive_child_normal(idx).unwrap();
1453            let actual_pubkey = child_pub.public_key_bytes();
1454
1455            assert_eq!(
1456                expected_pubkey, actual_pubkey.as_slice(),
1457                "Normal child {idx}: public-only derivation must match private derivation"
1458            );
1459
1460            // Also verify chain codes match
1461            assert_eq!(
1462                child_priv.chain_code(),
1463                child_pub.chain_code(),
1464                "Normal child {idx}: chain codes must match"
1465            );
1466        }
1467    }
1468}