1use 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
28const BIP32_SEED_KEY: &[u8] = b"Bitcoin seed";
30
31pub struct ExtendedPrivateKey {
33 key: Zeroizing<[u8; 32]>,
35 chain_code: Zeroizing<[u8; 32]>,
39 depth: u8,
41 parent_fingerprint: [u8; 4],
43 child_index: u32,
45}
46
47impl Drop for ExtendedPrivateKey {
48 fn drop(&mut self) {
49 }
51}
52
53impl ExtendedPrivateKey {
54 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 use zeroize::Zeroize;
78 for b in result.iter_mut() {
79 b.zeroize();
80 }
81
82 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 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 mac.update(&[0x00]);
115 mac.update(&*self.key);
116 } else {
117 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 for b in result.iter_mut() {
134 b.zeroize();
135 }
136
137 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 let child_scalar = parent_scalar.as_ref() + il_scalar.as_ref();
146
147 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 il.zeroize();
162
163 let child_key = derive_result?;
164
165 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 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 #[must_use]
202 pub fn private_key_bytes(&self) -> Zeroizing<Vec<u8>> {
203 Zeroizing::new(self.key.to_vec())
204 }
205
206 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 #[must_use]
215 pub fn depth(&self) -> u8 {
216 self.depth
217 }
218
219 #[must_use]
221 pub fn chain_code(&self) -> &[u8; 32] {
222 &self.chain_code
223 }
224
225 pub fn parent_fingerprint(&self) -> &[u8; 4] {
227 &self.parent_fingerprint
228 }
229
230 pub fn child_index(&self) -> u32 {
232 self.child_index
233 }
234
235 #[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]); 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); data.extend_from_slice(&*self.key);
251 let checksum = crypto::double_sha256(&data);
253 data.extend_from_slice(&checksum[..4]);
254 Zeroizing::new(bs58::encode(&*data).into_string())
255 }
256
257 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]); 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 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 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 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 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 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 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#[derive(Clone, Debug)]
347pub struct ExtendedPublicKey {
348 key: [u8; 33],
350 chain_code: [u8; 32],
352 depth: u8,
354 parent_fingerprint: [u8; 4],
356 child_index: u32,
358}
359
360impl ExtendedPublicKey {
361 #[must_use]
363 pub fn public_key_bytes(&self) -> &[u8; 33] {
364 &self.key
365 }
366
367 #[must_use]
369 pub fn depth(&self) -> u8 {
370 self.depth
371 }
372
373 #[must_use]
375 pub fn chain_code(&self) -> &[u8; 32] {
376 &self.chain_code
377 }
378
379 #[must_use]
381 pub fn parent_fingerprint(&self) -> &[u8; 4] {
382 &self.parent_fingerprint
383 }
384
385 #[must_use]
387 pub fn child_index(&self) -> u32 {
388 self.child_index
389 }
390
391 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 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 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 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 let fingerprint = crypto::hash160(&self.key);
443 let mut parent_fp = [0u8; 4];
444 parent_fp.copy_from_slice(&fingerprint[..4]);
445
446 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 #[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]); 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 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 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 #[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 #[cfg(feature = "bitcoin")]
544 pub fn p2tr_address(&self, hrp: &str) -> Result<String, SignerError> {
545 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#[derive(Debug, Clone)]
558pub struct DerivationStep {
559 pub index: u32,
561 pub hardened: bool,
563}
564
565#[derive(Debug, Clone)]
567pub struct DerivationPath {
568 pub steps: Vec<DerivationStep>,
570}
571
572impl DerivationPath {
573 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 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 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 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 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 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 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 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 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 #[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 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 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); }
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 assert_ne!(&*eth0.private_key_bytes(), &*eth1.private_key_bytes());
942 assert_ne!(&*eth0.private_key_bytes(), &*btc0.private_key_bytes());
943 }
944
945 #[test]
948 fn test_bip32_vector1_master_xprv() {
949 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 let xprv = "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHiX";
998 assert!(ExtendedPrivateKey::from_xprv(xprv).is_err());
1001 }
1002
1003 #[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 #[test]
1033 fn test_derivation_path_hardened_h_notation() {
1034 let path = DerivationPath::parse("m/44h/60h/0h/0/0").unwrap();
1035 assert_eq!(path.steps.len(), 5);
1036 assert!(path.steps[0].hardened);
1037 assert_eq!(path.steps[0].index, 44);
1038 }
1039
1040 #[test]
1041 fn test_derivation_path_all_chain_presets() {
1042 let btc_segwit = DerivationPath::bitcoin_segwit(0);
1043 assert_eq!(btc_segwit.steps[0].index, 84); assert!(btc_segwit.steps[0].hardened);
1045
1046 let btc_taproot = DerivationPath::bitcoin_taproot(0);
1047 assert_eq!(btc_taproot.steps[0].index, 86); assert!(btc_taproot.steps[0].hardened);
1049
1050 let xrp = DerivationPath::xrp(0);
1051 assert_eq!(xrp.steps[1].index, 144); let neo = DerivationPath::neo(0);
1054 assert_eq!(neo.steps[1].index, 888); }
1056
1057 #[test]
1060 fn test_extended_public_key_from_private() {
1061 let seed = [0xABu8; 64];
1062 let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1063 let pubkey = master.to_extended_public_key().unwrap();
1064 assert_eq!(pubkey.depth(), 0);
1065 assert_eq!(pubkey.public_key_bytes().len(), 33); }
1067
1068 #[test]
1069 fn test_xpub_starts_with_xpub() {
1070 let seed = [0xABu8; 64];
1071 let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1072 let xpub = master.to_xpub().unwrap();
1073 assert!(xpub.starts_with("xpub"));
1074 }
1075
1076 #[test]
1077 fn test_xpub_roundtrip() {
1078 let seed = [0xABu8; 64];
1079 let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1080 let pubkey = master.to_extended_public_key().unwrap();
1081 let xpub_str = pubkey.to_xpub();
1082 let restored = ExtendedPublicKey::from_xpub(&xpub_str).unwrap();
1083 assert_eq!(pubkey.public_key_bytes(), restored.public_key_bytes());
1084 assert_eq!(pubkey.depth(), restored.depth());
1085 assert_eq!(pubkey.chain_code(), restored.chain_code());
1086 }
1087
1088 #[test]
1089 fn test_xpub_deterministic() {
1090 let seed = [0xABu8; 64];
1091 let m1 = ExtendedPrivateKey::from_seed(&seed).unwrap();
1092 let m2 = ExtendedPrivateKey::from_seed(&seed).unwrap();
1093 assert_eq!(
1094 m1.to_extended_public_key().unwrap().to_xpub(),
1095 m2.to_extended_public_key().unwrap().to_xpub(),
1096 );
1097 }
1098
1099 #[test]
1100 fn test_extended_public_key_normal_derivation() {
1101 let seed = [0xABu8; 64];
1102 let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1103 let pubkey = master.to_extended_public_key().unwrap();
1104
1105 let child = pubkey.derive_child_normal(0).unwrap();
1107 assert_eq!(child.depth(), 1);
1108 assert_eq!(child.public_key_bytes().len(), 33);
1109 }
1110
1111 #[test]
1112 fn test_extended_public_key_derivation_consistency() {
1113 let seed = [0x42u8; 64];
1115 let master_priv = ExtendedPrivateKey::from_seed(&seed).unwrap();
1116 let master_pub = master_priv.to_extended_public_key().unwrap();
1117
1118 let child_priv = master_priv.derive_child(0, false).unwrap();
1120 let child_pub_from_priv = child_priv.to_extended_public_key().unwrap();
1121
1122 let child_pub_from_pub = master_pub.derive_child_normal(0).unwrap();
1124
1125 assert_eq!(
1127 child_pub_from_priv.public_key_bytes(),
1128 child_pub_from_pub.public_key_bytes(),
1129 );
1130 }
1131
1132 #[test]
1133 fn test_extended_public_key_hardened_rejected() {
1134 let seed = [0xABu8; 64];
1136 let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1137 let pubkey = master.to_extended_public_key().unwrap();
1138
1139 for i in 0..5 {
1142 let child = pubkey.derive_child_normal(i).unwrap();
1143 assert_eq!(child.depth(), 1);
1144 }
1145 }
1146
1147 #[test]
1148 fn test_extended_public_key_different_indices() {
1149 let seed = [0xABu8; 64];
1150 let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1151 let pubkey = master.to_extended_public_key().unwrap();
1152
1153 let c0 = pubkey.derive_child_normal(0).unwrap();
1154 let c1 = pubkey.derive_child_normal(1).unwrap();
1155 assert_ne!(c0.public_key_bytes(), c1.public_key_bytes());
1156 }
1157
1158 #[test]
1159 fn test_extended_public_key_chain_derivation() {
1160 let seed = [0xABu8; 64];
1162 let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1163 let pubkey = master.to_extended_public_key().unwrap();
1164
1165 let child1 = pubkey.derive_child_normal(0).unwrap();
1166 let child2 = child1.derive_child_normal(1).unwrap();
1167 assert_eq!(child2.depth(), 2);
1168 assert_eq!(child2.public_key_bytes().len(), 33);
1169 }
1170
1171 #[test]
1172 fn test_xpub_invalid_prefix_rejected() {
1173 let seed = [0xABu8; 64];
1174 let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1175 let xpub = master.to_extended_public_key().unwrap().to_xpub();
1176
1177 let mut bad = String::from("ypub");
1179 bad.push_str(&xpub[4..]);
1180 assert!(ExtendedPublicKey::from_xpub(&bad).is_err());
1181 }
1182
1183 #[cfg(feature = "bitcoin")]
1184 #[test]
1185 fn test_extended_public_key_p2wpkh_address() {
1186 let seed = [0xABu8; 64];
1187 let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1188 let pubkey = master.to_extended_public_key().unwrap();
1189 let addr = pubkey.p2wpkh_address("bc").unwrap();
1190 assert!(
1191 addr.starts_with("bc1q"),
1192 "P2WPKH should start with bc1q: {addr}"
1193 );
1194 }
1195
1196 #[cfg(feature = "bitcoin")]
1197 #[test]
1198 fn test_extended_public_key_p2tr_address() {
1199 let seed = [0xABu8; 64];
1200 let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1201 let pubkey = master.to_extended_public_key().unwrap();
1202 let addr = pubkey.p2tr_address("bc").unwrap();
1203 assert!(
1204 addr.starts_with("bc1p"),
1205 "P2TR should start with bc1p: {addr}"
1206 );
1207 }
1208
1209 #[cfg(feature = "bitcoin")]
1210 #[test]
1211 fn test_extended_public_key_derived_addresses_differ() {
1212 let seed = [0xABu8; 64];
1213 let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1214 let pubkey = master.to_extended_public_key().unwrap();
1215 let c0 = pubkey.derive_child_normal(0).unwrap();
1216 let c1 = pubkey.derive_child_normal(1).unwrap();
1217 assert_ne!(
1218 c0.p2wpkh_address("bc").unwrap(),
1219 c1.p2wpkh_address("bc").unwrap(),
1220 );
1221 }
1222
1223 #[cfg(feature = "bitcoin")]
1224 #[test]
1225 fn test_parse_unsigned_tx_roundtrip() {
1226 use crate::bitcoin::transaction::*;
1227 let mut tx = Transaction::new(2);
1228 tx.inputs.push(TxIn {
1229 previous_output: OutPoint {
1230 txid: [0xAA; 32],
1231 vout: 0,
1232 },
1233 script_sig: vec![],
1234 sequence: 0xFFFFFFFF,
1235 });
1236 tx.outputs.push(TxOut {
1237 value: 50_000,
1238 script_pubkey: vec![
1239 0x00, 0x14, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
1240 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
1241 ],
1242 });
1243 let raw = tx.serialize_legacy();
1244 let parsed = parse_unsigned_tx(&raw).unwrap();
1245 assert_eq!(parsed.version, 2);
1246 assert_eq!(parsed.inputs.len(), 1);
1247 assert_eq!(parsed.outputs.len(), 1);
1248 assert_eq!(parsed.outputs[0].value, 50_000);
1249 assert_eq!(parsed.locktime, 0);
1250 }
1251}