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]
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() .derive_child(1, false)
1042 .unwrap(); assert_eq!(
1044 &*c.to_xprv(),
1045 "xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs"
1046 );
1047 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(); 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(); 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(); assert_eq!(
1109 &*c.to_xprv(),
1110 "xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76"
1111 );
1112 assert_eq!(c.depth(), 5);
1113 }
1114
1115 #[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(); 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(); assert_eq!(
1150 &*c.to_xprv(),
1151 "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(); assert_eq!(
1174 &*c.to_xprv(),
1175 "xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j"
1176 );
1177 assert_eq!(c.depth(), 5);
1178 }
1179
1180 #[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 "xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L"
1207 );
1208 assert_eq!(c.depth(), 1);
1209 }
1210
1211 #[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); assert!(btc_segwit.steps[0].hardened);
1226
1227 let btc_taproot = DerivationPath::bitcoin_taproot(0);
1228 assert_eq!(btc_taproot.steps[0].index, 86); assert!(btc_taproot.steps[0].hardened);
1230
1231 let xrp = DerivationPath::xrp(0);
1232 assert_eq!(xrp.steps[1].index, 144); let neo = DerivationPath::neo(0);
1235 assert_eq!(neo.steps[1].index, 888); }
1237
1238 #[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); }
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 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 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 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 let child_pub_from_pub = master_pub.derive_child_normal(0).unwrap();
1305
1306 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 let seed = [0xABu8; 64];
1317 let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1318 let pubkey = master.to_extended_public_key().unwrap();
1319
1320 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 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 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 #[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 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 let child_priv = parent_priv.derive_child(idx, false).unwrap();
1449 let expected_pubkey = child_priv.public_key_bytes().unwrap();
1450
1451 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 assert_eq!(
1462 child_priv.chain_code(),
1463 child_pub.chain_code(),
1464 "Normal child {idx}: chain codes must match"
1465 );
1466 }
1467 }
1468}