1use bip39::{Language, Mnemonic};
2use ed25519_dalek::SigningKey;
3use hmac::{Hmac, Mac};
4use k256::{
5 elliptic_curve::{
6 ff::PrimeField,
7 group::Group,
8 sec1::{FromEncodedPoint, ToEncodedPoint},
9 },
10 AffinePoint, EncodedPoint, ProjectivePoint, Scalar,
11};
12use ripemd::Ripemd160;
13use serde::Serialize;
14use sha2::{Digest, Sha256, Sha512};
15use sha3::Keccak256;
16use thiserror::Error;
17use zeroize::{Zeroize, ZeroizeOnDrop};
18
19type HmacSha512 = Hmac<Sha512>;
20pub const HARDENED_OFFSET: u32 = 0x8000_0000;
21pub const API_SCHEMA_VERSION: u8 = 1;
22
23#[derive(Debug, Error)]
24pub enum HdError {
25 #[error("invalid BIP39 English mnemonic")]
26 InvalidMnemonic,
27 #[error("seed must be between 16 and 64 bytes")]
28 InvalidSeed,
29 #[error("invalid derivation path: {0}")]
30 InvalidPath(String),
31 #[error("unsupported chain: {0}")]
32 UnsupportedChain(String),
33 #[error("unsupported script type: {0}")]
34 UnsupportedScript(String),
35 #[error("unsupported extended-key format: {0}")]
36 UnsupportedFormat(String),
37 #[error("extended public keys can derive only non-hardened children")]
38 HardenedPublicDerivation,
39 #[error("invalid BIP32 key material")]
40 InvalidKey,
41 #[error("invalid extended key")]
42 InvalidExtendedKey,
43 #[error("chain is required because extended-key version bytes do not identify every coin")]
44 ChainRequired,
45 #[error("Solana SLIP10 does not define extended public keys")]
46 NoExtendedPublicKey,
47}
48
49pub type Result<T> = std::result::Result<T, HdError>;
50
51#[derive(Clone, Zeroize, ZeroizeOnDrop)]
52pub enum Source {
53 Mnemonic { words: String, passphrase: String },
54 Seed(Vec<u8>),
55}
56
57impl Source {
58 pub fn mnemonic(words: impl Into<String>, passphrase: impl Into<String>) -> Self {
59 Self::Mnemonic {
60 words: words.into(),
61 passphrase: passphrase.into(),
62 }
63 }
64
65 pub fn seed(&self) -> Result<Vec<u8>> {
66 match self {
67 Source::Seed(seed) if (16..=64).contains(&seed.len()) => Ok(seed.clone()),
68 Source::Seed(_) => Err(HdError::InvalidSeed),
69 Source::Mnemonic { words, passphrase } => {
70 let mnemonic = Mnemonic::parse_in_normalized(Language::English, words)
71 .map_err(|_| HdError::InvalidMnemonic)?;
72 Ok(mnemonic.to_seed(passphrase).to_vec())
73 }
74 }
75 }
76}
77
78#[derive(Clone, Copy, Debug)]
79pub struct Format {
80 pub name: &'static str,
81 pub public_version: u32,
82 pub private_version: u32,
83 pub purpose: u32,
84 pub script_type: &'static str,
85}
86
87const XPUB: Format = Format {
88 name: "xpub",
89 public_version: 0x0488_b21e,
90 private_version: 0x0488_ade4,
91 purpose: 44,
92 script_type: "p2pkh",
93};
94const YPUB: Format = Format {
95 name: "ypub",
96 public_version: 0x049d_7cb2,
97 private_version: 0x049d_7878,
98 purpose: 49,
99 script_type: "p2sh-p2wpkh",
100};
101const ZPUB: Format = Format {
102 name: "zpub",
103 public_version: 0x04b2_4746,
104 private_version: 0x04b2_430c,
105 purpose: 84,
106 script_type: "p2wpkh",
107};
108const TPUB: Format = Format {
109 name: "tpub",
110 public_version: 0x0435_87cf,
111 private_version: 0x0435_8394,
112 purpose: 44,
113 script_type: "p2pkh",
114};
115const UPUB: Format = Format {
116 name: "upub",
117 public_version: 0x044a_5262,
118 private_version: 0x044a_4e28,
119 purpose: 49,
120 script_type: "p2sh-p2wpkh",
121};
122const VPUB: Format = Format {
123 name: "vpub",
124 public_version: 0x045f_1cf6,
125 private_version: 0x045f_18bc,
126 purpose: 84,
127 script_type: "p2wpkh",
128};
129const LTUB: Format = Format {
130 name: "Ltub",
131 public_version: 0x019d_a462,
132 private_version: 0x019d_9cfe,
133 purpose: 44,
134 script_type: "p2pkh",
135};
136const MTUB: Format = Format {
137 name: "Mtub",
138 public_version: 0x01b2_6ef6,
139 private_version: 0x01b2_6792,
140 purpose: 49,
141 script_type: "p2sh-p2wpkh",
142};
143
144pub fn format(name: &str) -> Result<Format> {
145 match name {
146 "xpub" => Ok(XPUB),
147 "ypub" => Ok(YPUB),
148 "zpub" => Ok(ZPUB),
149 "tpub" => Ok(TPUB),
150 "upub" => Ok(UPUB),
151 "vpub" => Ok(VPUB),
152 "Ltub" => Ok(LTUB),
153 "Mtub" => Ok(MTUB),
154 _ => Err(HdError::UnsupportedFormat(name.into())),
155 }
156}
157
158#[derive(Clone, Copy, Debug, Serialize)]
159#[serde(rename_all = "camelCase")]
160pub struct Chain {
161 pub id: &'static str,
162 pub name: &'static str,
163 pub symbol: &'static str,
164 pub coin_type: u32,
165 pub curve: &'static str,
166 pub default_format: Option<&'static str>,
167 pub default_script_type: &'static str,
168 #[serde(skip)]
169 pub p2pkh: &'static [u8],
170 #[serde(skip)]
171 pub p2sh: &'static [u8],
172 pub hrp: Option<&'static str>,
173}
174
175pub const CHAINS: &[Chain] = &[
176 Chain {
177 id: "bitcoin",
178 name: "Bitcoin",
179 symbol: "BTC",
180 coin_type: 0,
181 curve: "secp256k1",
182 default_format: Some("zpub"),
183 default_script_type: "p2wpkh",
184 p2pkh: &[0x00],
185 p2sh: &[0x05],
186 hrp: Some("bc"),
187 },
188 Chain {
189 id: "bitcoin-testnet",
190 name: "Bitcoin Testnet",
191 symbol: "TBTC",
192 coin_type: 1,
193 curve: "secp256k1",
194 default_format: Some("vpub"),
195 default_script_type: "p2wpkh",
196 p2pkh: &[0x6f],
197 p2sh: &[0xc4],
198 hrp: Some("tb"),
199 },
200 Chain {
201 id: "litecoin",
202 name: "Litecoin",
203 symbol: "LTC",
204 coin_type: 2,
205 curve: "secp256k1",
206 default_format: Some("Ltub"),
207 default_script_type: "p2pkh",
208 p2pkh: &[0x30],
209 p2sh: &[0x32],
210 hrp: Some("ltc"),
211 },
212 Chain {
213 id: "dogecoin",
214 name: "Dogecoin",
215 symbol: "DOGE",
216 coin_type: 3,
217 curve: "secp256k1",
218 default_format: Some("xpub"),
219 default_script_type: "p2pkh",
220 p2pkh: &[0x1e],
221 p2sh: &[0x16],
222 hrp: None,
223 },
224 Chain {
225 id: "dash",
226 name: "Dash",
227 symbol: "DASH",
228 coin_type: 5,
229 curve: "secp256k1",
230 default_format: Some("xpub"),
231 default_script_type: "p2pkh",
232 p2pkh: &[0x4c],
233 p2sh: &[0x10],
234 hrp: None,
235 },
236 Chain {
237 id: "digibyte",
238 name: "DigiByte",
239 symbol: "DGB",
240 coin_type: 20,
241 curve: "secp256k1",
242 default_format: Some("xpub"),
243 default_script_type: "p2pkh",
244 p2pkh: &[0x1e],
245 p2sh: &[0x3f],
246 hrp: Some("dgb"),
247 },
248 Chain {
249 id: "bitcoin-cash",
250 name: "Bitcoin Cash",
251 symbol: "BCH",
252 coin_type: 145,
253 curve: "secp256k1",
254 default_format: Some("xpub"),
255 default_script_type: "cashaddr",
256 p2pkh: &[0x00],
257 p2sh: &[0x05],
258 hrp: None,
259 },
260 Chain {
261 id: "zcash-transparent",
262 name: "Zcash Transparent",
263 symbol: "ZEC",
264 coin_type: 133,
265 curve: "secp256k1",
266 default_format: Some("xpub"),
267 default_script_type: "p2pkh",
268 p2pkh: &[0x1c, 0xb8],
269 p2sh: &[0x1c, 0xbd],
270 hrp: None,
271 },
272 Chain {
273 id: "ethereum",
274 name: "Ethereum",
275 symbol: "ETH",
276 coin_type: 60,
277 curve: "secp256k1",
278 default_format: None,
279 default_script_type: "evm",
280 p2pkh: &[],
281 p2sh: &[],
282 hrp: None,
283 },
284 Chain {
285 id: "ethereum-classic",
286 name: "Ethereum Classic",
287 symbol: "ETC",
288 coin_type: 61,
289 curve: "secp256k1",
290 default_format: None,
291 default_script_type: "evm",
292 p2pkh: &[],
293 p2sh: &[],
294 hrp: None,
295 },
296 Chain {
297 id: "polygon",
298 name: "Polygon",
299 symbol: "POL",
300 coin_type: 60,
301 curve: "secp256k1",
302 default_format: None,
303 default_script_type: "evm",
304 p2pkh: &[],
305 p2sh: &[],
306 hrp: None,
307 },
308 Chain {
309 id: "bsc",
310 name: "BNB Smart Chain",
311 symbol: "BNB",
312 coin_type: 60,
313 curve: "secp256k1",
314 default_format: None,
315 default_script_type: "evm",
316 p2pkh: &[],
317 p2sh: &[],
318 hrp: None,
319 },
320 Chain {
321 id: "avalanche-c",
322 name: "Avalanche C-Chain",
323 symbol: "AVAX",
324 coin_type: 60,
325 curve: "secp256k1",
326 default_format: None,
327 default_script_type: "evm",
328 p2pkh: &[],
329 p2sh: &[],
330 hrp: None,
331 },
332 Chain {
333 id: "arbitrum",
334 name: "Arbitrum",
335 symbol: "ARB",
336 coin_type: 60,
337 curve: "secp256k1",
338 default_format: None,
339 default_script_type: "evm",
340 p2pkh: &[],
341 p2sh: &[],
342 hrp: None,
343 },
344 Chain {
345 id: "optimism",
346 name: "Optimism",
347 symbol: "OP",
348 coin_type: 60,
349 curve: "secp256k1",
350 default_format: None,
351 default_script_type: "evm",
352 p2pkh: &[],
353 p2sh: &[],
354 hrp: None,
355 },
356 Chain {
357 id: "base",
358 name: "Base",
359 symbol: "ETH",
360 coin_type: 60,
361 curve: "secp256k1",
362 default_format: None,
363 default_script_type: "evm",
364 p2pkh: &[],
365 p2sh: &[],
366 hrp: None,
367 },
368 Chain {
369 id: "tron",
370 name: "TRON",
371 symbol: "TRX",
372 coin_type: 195,
373 curve: "secp256k1",
374 default_format: None,
375 default_script_type: "tron",
376 p2pkh: &[],
377 p2sh: &[],
378 hrp: None,
379 },
380 Chain {
381 id: "solana",
382 name: "Solana",
383 symbol: "SOL",
384 coin_type: 501,
385 curve: "ed25519",
386 default_format: None,
387 default_script_type: "solana",
388 p2pkh: &[],
389 p2sh: &[],
390 hrp: None,
391 },
392];
393
394pub fn supported_chains() -> &'static [Chain] {
395 CHAINS
396}
397
398pub fn chain(id: &str) -> Result<Chain> {
399 CHAINS
400 .iter()
401 .find(|item| item.id == id)
402 .copied()
403 .ok_or_else(|| HdError::UnsupportedChain(id.into()))
404}
405
406#[derive(Clone, Zeroize, ZeroizeOnDrop)]
407pub struct ExtendedPrivateKey {
408 pub private_key: [u8; 32],
409 pub chain_code: [u8; 32],
410 pub depth: u8,
411 pub parent_fingerprint: [u8; 4],
412 pub child_number: u32,
413}
414
415#[derive(Clone, Debug)]
416pub struct ExtendedPublicKey {
417 pub public_key: [u8; 33],
418 pub chain_code: [u8; 32],
419 pub depth: u8,
420 pub parent_fingerprint: [u8; 4],
421 pub child_number: u32,
422}
423
424impl ExtendedPrivateKey {
425 pub fn master(seed: &[u8]) -> Result<Self> {
426 if !(16..=64).contains(&seed.len()) {
427 return Err(HdError::InvalidSeed);
428 }
429 let mut material = seed.to_vec();
430 loop {
431 let digest = hmac_sha512(b"Bitcoin seed", &material);
432 let private_key: [u8; 32] = digest[..32].try_into().unwrap();
433 if scalar(&private_key).is_ok() {
434 return Ok(Self {
435 private_key,
436 chain_code: digest[32..].try_into().unwrap(),
437 depth: 0,
438 parent_fingerprint: [0; 4],
439 child_number: 0,
440 });
441 }
442 material = digest.to_vec();
443 }
444 }
445
446 pub fn public_key(&self) -> [u8; 33] {
447 let scalar = scalar(&self.private_key).expect("validated private key");
448 let point = (ProjectivePoint::GENERATOR * scalar)
449 .to_affine()
450 .to_encoded_point(true);
451 point.as_bytes().try_into().unwrap()
452 }
453
454 pub fn fingerprint(&self) -> [u8; 4] {
455 hash160(&self.public_key())[..4].try_into().unwrap()
456 }
457
458 pub fn derive(&self, index: u32) -> Result<Self> {
459 let mut data = Vec::with_capacity(37);
460 if index >= HARDENED_OFFSET {
461 data.push(0);
462 data.extend(self.private_key);
463 } else {
464 data.extend(self.public_key());
465 }
466 data.extend(index.to_be_bytes());
467 let digest = hmac_sha512(&self.chain_code, &data);
468 let tweak = scalar(&digest[..32].try_into().unwrap())?;
469 let child = tweak + scalar(&self.private_key)?;
470 if bool::from(child.is_zero()) {
471 return Err(HdError::InvalidKey);
472 }
473 Ok(Self {
474 private_key: child.to_bytes().into(),
475 chain_code: digest[32..].try_into().unwrap(),
476 depth: self.depth.checked_add(1).ok_or(HdError::InvalidKey)?,
477 parent_fingerprint: self.fingerprint(),
478 child_number: index,
479 })
480 }
481
482 pub fn derive_path(&self, path: &str) -> Result<Self> {
483 parse_path(path)?
484 .into_iter()
485 .try_fold(self.clone(), |node, index| node.derive(index))
486 }
487
488 pub fn neuter(&self) -> ExtendedPublicKey {
489 ExtendedPublicKey {
490 public_key: self.public_key(),
491 chain_code: self.chain_code,
492 depth: self.depth,
493 parent_fingerprint: self.parent_fingerprint,
494 child_number: self.child_number,
495 }
496 }
497
498 pub fn serialize_private(&self, version: u32) -> String {
499 let mut key = [0u8; 33];
500 key[1..].copy_from_slice(&self.private_key);
501 serialize_key(
502 version,
503 self.depth,
504 &self.parent_fingerprint,
505 self.child_number,
506 &self.chain_code,
507 &key,
508 )
509 }
510
511 pub fn serialize_public(&self, version: u32) -> String {
512 self.neuter().serialize_public(version)
513 }
514}
515
516impl ExtendedPublicKey {
517 pub fn fingerprint(&self) -> [u8; 4] {
518 hash160(&self.public_key)[..4].try_into().unwrap()
519 }
520
521 pub fn derive(&self, index: u32) -> Result<Self> {
522 if index >= HARDENED_OFFSET {
523 return Err(HdError::HardenedPublicDerivation);
524 }
525 let mut data = self.public_key.to_vec();
526 data.extend(index.to_be_bytes());
527 let digest = hmac_sha512(&self.chain_code, &data);
528 let tweak = scalar(&digest[..32].try_into().unwrap())?;
529 let encoded = EncodedPoint::from_bytes(self.public_key).map_err(|_| HdError::InvalidKey)?;
530 let affine = Option::<AffinePoint>::from(AffinePoint::from_encoded_point(&encoded))
531 .ok_or(HdError::InvalidKey)?;
532 let child = ProjectivePoint::GENERATOR * tweak + ProjectivePoint::from(affine);
533 if bool::from(child.is_identity()) {
534 return Err(HdError::InvalidKey);
535 }
536 let point = child.to_affine().to_encoded_point(true);
537 Ok(Self {
538 public_key: point.as_bytes().try_into().unwrap(),
539 chain_code: digest[32..].try_into().unwrap(),
540 depth: self.depth.checked_add(1).ok_or(HdError::InvalidKey)?,
541 parent_fingerprint: self.fingerprint(),
542 child_number: index,
543 })
544 }
545
546 pub fn serialize_public(&self, version: u32) -> String {
547 serialize_key(
548 version,
549 self.depth,
550 &self.parent_fingerprint,
551 self.child_number,
552 &self.chain_code,
553 &self.public_key,
554 )
555 }
556}
557
558pub enum ParsedExtendedKey {
559 Private(ExtendedPrivateKey, u32),
560 Public(ExtendedPublicKey, u32),
561}
562
563pub fn parse_extended_key(value: &str) -> Result<ParsedExtendedKey> {
564 let decoded = bs58::decode(value)
565 .with_check(None)
566 .into_vec()
567 .map_err(|_| HdError::InvalidExtendedKey)?;
568 if decoded.len() != 78 {
569 return Err(HdError::InvalidExtendedKey);
570 }
571 let version = u32::from_be_bytes(decoded[..4].try_into().unwrap());
572 let depth = decoded[4];
573 let parent_fingerprint = decoded[5..9].try_into().unwrap();
574 let child_number = u32::from_be_bytes(decoded[9..13].try_into().unwrap());
575 let chain_code = decoded[13..45].try_into().unwrap();
576 let is_private = decoded[45] == 0;
577 let registered = [XPUB, YPUB, ZPUB, TPUB, UPUB, VPUB, LTUB, MTUB]
578 .iter()
579 .any(|candidate| {
580 version
581 == if is_private {
582 candidate.private_version
583 } else {
584 candidate.public_version
585 }
586 });
587 if !registered || (depth == 0 && (parent_fingerprint != [0; 4] || child_number != 0)) {
588 return Err(HdError::InvalidExtendedKey);
589 }
590 if is_private {
591 let private_key = decoded[46..78].try_into().unwrap();
592 scalar(&private_key)?;
593 Ok(ParsedExtendedKey::Private(
594 ExtendedPrivateKey {
595 private_key,
596 chain_code,
597 depth,
598 parent_fingerprint,
599 child_number,
600 },
601 version,
602 ))
603 } else {
604 let public_key = decoded[45..78].try_into().unwrap();
605 let encoded =
606 EncodedPoint::from_bytes(public_key).map_err(|_| HdError::InvalidExtendedKey)?;
607 Option::<AffinePoint>::from(AffinePoint::from_encoded_point(&encoded))
608 .ok_or(HdError::InvalidExtendedKey)?;
609 Ok(ParsedExtendedKey::Public(
610 ExtendedPublicKey {
611 public_key,
612 chain_code,
613 depth,
614 parent_fingerprint,
615 child_number,
616 },
617 version,
618 ))
619 }
620}
621
622pub fn parse_path(path: &str) -> Result<Vec<u32>> {
623 if path == "m" || path == "M" {
624 return Ok(vec![]);
625 }
626 let mut parts = path.split('/');
627 if !matches!(parts.next(), Some("m" | "M")) {
628 return Err(HdError::InvalidPath(path.into()));
629 }
630 let values: Vec<_> = parts.collect();
631 if values.is_empty() || values.len() > 255 {
632 return Err(HdError::InvalidPath(path.into()));
633 }
634 values
635 .into_iter()
636 .map(|part| {
637 let hardened = part.ends_with(['\'', 'h', 'H']);
638 let raw = if hardened {
639 &part[..part.len() - 1]
640 } else {
641 part
642 };
643 if raw.is_empty() || (raw.len() > 1 && raw.starts_with('0')) {
644 return Err(HdError::InvalidPath(path.into()));
645 }
646 let value: u32 = raw.parse().map_err(|_| HdError::InvalidPath(path.into()))?;
647 if value >= HARDENED_OFFSET {
648 return Err(HdError::InvalidPath(path.into()));
649 }
650 Ok(if hardened {
651 value + HARDENED_OFFSET
652 } else {
653 value
654 })
655 })
656 .collect()
657}
658
659#[derive(Debug, Serialize, Clone)]
660#[serde(rename_all = "camelCase")]
661pub struct DerivedAddress {
662 pub schema_version: u8,
663 pub chain: String,
664 pub curve: String,
665 pub path: String,
666 pub account: u32,
667 pub change: u32,
668 pub index: u32,
669 pub script_type: String,
670 pub address: String,
671 pub public_key_hex: String,
672}
673
674#[derive(Debug, Serialize)]
675#[serde(rename_all = "camelCase")]
676pub struct NodeResult {
677 pub schema_version: u8,
678 pub curve: String,
679 pub path: String,
680 pub public_key_hex: String,
681 pub chain_code_hex: String,
682 pub depth: u8,
683 pub child_number: u32,
684}
685
686#[derive(Debug, Serialize)]
687#[serde(rename_all = "camelCase")]
688pub struct AccountPublicKey {
689 pub schema_version: u8,
690 pub chain: String,
691 pub curve: String,
692 pub path: String,
693 pub format: String,
694 pub extended_public_key: String,
695 pub public_key_hex: String,
696}
697
698#[derive(Serialize)]
699#[serde(rename_all = "camelCase")]
700pub struct AccountPrivateKey {
701 pub schema_version: u8,
702 pub chain: String,
703 pub curve: String,
704 pub path: String,
705 pub format: Option<String>,
706 pub extended_private_key: Option<String>,
707 pub private_key_hex: String,
708 pub public_key_hex: String,
709}
710
711#[derive(Clone, Debug, Default)]
712pub struct DeriveOptions<'a> {
713 pub chain: &'a str,
714 pub format: Option<&'a str>,
715 pub script_type: Option<&'a str>,
716 pub path: Option<&'a str>,
717 pub account: u32,
718 pub change: u32,
719 pub index: u32,
720}
721
722pub fn derive_node(source: &Source, curve: &str, path: &str) -> Result<NodeResult> {
723 let seed = source.seed()?;
724 match curve {
725 "secp256k1" => {
726 let node = ExtendedPrivateKey::master(&seed)?.derive_path(path)?;
727 Ok(NodeResult {
728 schema_version: API_SCHEMA_VERSION,
729 curve: curve.into(),
730 path: path.into(),
731 public_key_hex: hex::encode(node.public_key()),
732 chain_code_hex: hex::encode(node.chain_code),
733 depth: node.depth,
734 child_number: node.child_number,
735 })
736 }
737 "ed25519" => {
738 let node = slip10_ed25519(&seed, path)?;
739 Ok(NodeResult {
740 schema_version: API_SCHEMA_VERSION,
741 curve: curve.into(),
742 path: path.into(),
743 public_key_hex: hex::encode(node.public_key),
744 chain_code_hex: hex::encode(node.chain_code),
745 depth: node.depth,
746 child_number: node.child_number,
747 })
748 }
749 _ => Err(HdError::UnsupportedFormat(curve.into())),
750 }
751}
752
753pub fn serialize_extended_key(key: &ParsedExtendedKey, version: Option<u32>) -> String {
754 match key {
755 ParsedExtendedKey::Private(node, original) => {
756 node.serialize_private(version.unwrap_or(*original))
757 }
758 ParsedExtendedKey::Public(node, original) => {
759 node.serialize_public(version.unwrap_or(*original))
760 }
761 }
762}
763
764pub fn derive_account_public_key(
765 source: &Source,
766 options: DeriveOptions<'_>,
767) -> Result<AccountPublicKey> {
768 let chain = chain(if options.chain.is_empty() {
769 "bitcoin"
770 } else {
771 options.chain
772 })?;
773 if chain.curve == "ed25519" {
774 return Err(HdError::NoExtendedPublicKey);
775 }
776 let fmt = resolve_format(chain, options.format, options.script_type)?;
777 let path = options
778 .path
779 .map(str::to_owned)
780 .unwrap_or_else(|| account_path(chain, options.account, options.script_type, fmt));
781 let node = ExtendedPrivateKey::master(&source.seed()?)?.derive_path(&path)?;
782 Ok(AccountPublicKey {
783 schema_version: API_SCHEMA_VERSION,
784 chain: chain.id.into(),
785 curve: chain.curve.into(),
786 path,
787 format: fmt.name.into(),
788 extended_public_key: node.serialize_public(fmt.public_version),
789 public_key_hex: hex::encode(node.public_key()),
790 })
791}
792
793pub fn derive_account_private_key(
794 source: &Source,
795 options: DeriveOptions<'_>,
796) -> Result<AccountPrivateKey> {
797 let chain = chain(if options.chain.is_empty() {
798 "bitcoin"
799 } else {
800 options.chain
801 })?;
802 let path = options.path.map(str::to_owned).unwrap_or_else(|| {
803 account_path(
804 chain,
805 options.account,
806 options.script_type,
807 resolve_format(chain, options.format, options.script_type).unwrap_or(XPUB),
808 )
809 });
810 let seed = source.seed()?;
811 if chain.curve == "ed25519" {
812 let node = slip10_ed25519(&seed, &path)?;
813 return Ok(AccountPrivateKey {
814 schema_version: API_SCHEMA_VERSION,
815 chain: chain.id.into(),
816 curve: chain.curve.into(),
817 path,
818 format: None,
819 extended_private_key: None,
820 private_key_hex: hex::encode(node.private_key),
821 public_key_hex: hex::encode(node.public_key),
822 });
823 }
824 let fmt = resolve_format(chain, options.format, options.script_type)?;
825 let node = ExtendedPrivateKey::master(&seed)?.derive_path(&path)?;
826 Ok(AccountPrivateKey {
827 schema_version: API_SCHEMA_VERSION,
828 chain: chain.id.into(),
829 curve: chain.curve.into(),
830 path,
831 format: Some(fmt.name.into()),
832 extended_private_key: Some(node.serialize_private(fmt.private_version)),
833 private_key_hex: hex::encode(node.private_key),
834 public_key_hex: hex::encode(node.public_key()),
835 })
836}
837
838pub fn derive_address(source: &Source, options: DeriveOptions<'_>) -> Result<DerivedAddress> {
839 let chain = chain(if options.chain.is_empty() {
840 "bitcoin"
841 } else {
842 options.chain
843 })?;
844 let script_type = options.script_type.unwrap_or(chain.default_script_type);
845 let seed = source.seed()?;
846 if chain.curve == "ed25519" {
847 let path = options.path.map(str::to_owned).unwrap_or_else(|| {
848 format!(
849 "m/44'/{}'/{}'/{}'",
850 chain.coin_type, options.account, options.index
851 )
852 });
853 let node = slip10_ed25519(&seed, &path)?;
854 return Ok(address_result(
855 chain,
856 path,
857 options,
858 script_type,
859 bs58::encode(node.public_key).into_string(),
860 node.public_key.to_vec(),
861 ));
862 }
863 let fmt = resolve_format(chain, options.format, Some(script_type)).unwrap_or(XPUB);
864 let path = options.path.map(str::to_owned).unwrap_or_else(|| {
865 format!(
866 "{}/{}/{}",
867 account_path(chain, options.account, Some(script_type), fmt),
868 options.change,
869 options.index
870 )
871 });
872 let node = ExtendedPrivateKey::master(&seed)?.derive_path(&path)?;
873 let public_key = node.public_key();
874 let address = public_key_to_address(&public_key, chain, script_type)?;
875 Ok(address_result(
876 chain,
877 path,
878 options,
879 script_type,
880 address,
881 public_key.to_vec(),
882 ))
883}
884
885pub fn derive_addresses(
886 source: &Source,
887 mut options: DeriveOptions<'_>,
888 start: u32,
889 count: u32,
890) -> Result<Vec<DerivedAddress>> {
891 if count == 0 || count > 10_000 {
892 return Err(HdError::InvalidPath(
893 "count must be between 1 and 10000".into(),
894 ));
895 }
896 (start..start.checked_add(count).ok_or(HdError::InvalidKey)?)
897 .map(|index| {
898 options.index = index;
899 derive_address(source, options.clone())
900 })
901 .collect()
902}
903
904pub fn derive_address_from_extended_public_key(
905 value: &str,
906 chain_id: Option<&str>,
907 change: u32,
908 index: u32,
909 script_type: Option<&str>,
910) -> Result<DerivedAddress> {
911 let chain = chain(chain_id.ok_or(HdError::ChainRequired)?)?;
912 let (node, version) = match parse_extended_key(value)? {
913 ParsedExtendedKey::Public(node, version) => (node, version),
914 _ => return Err(HdError::InvalidExtendedKey),
915 };
916 let node = node.derive(change)?.derive(index)?;
917 let script = script_type.unwrap_or_else(|| match version {
918 0x049d_7cb2 | 0x044a_5262 | 0x01b2_6ef6 => "p2sh-p2wpkh",
919 0x04b2_4746 | 0x045f_1cf6 => "p2wpkh",
920 _ => chain.default_script_type,
921 });
922 let address = public_key_to_address(&node.public_key, chain, script)?;
923 Ok(address_result(
924 chain,
925 format!("{change}/{index}"),
926 DeriveOptions {
927 chain: chain.id,
928 change,
929 index,
930 ..Default::default()
931 },
932 script,
933 address,
934 node.public_key.to_vec(),
935 ))
936}
937
938fn resolve_format(chain: Chain, requested: Option<&str>, script: Option<&str>) -> Result<Format> {
939 let name = requested
940 .or_else(|| {
941 if script == Some("p2tr") {
942 Some("xpub")
943 } else {
944 chain.default_format
945 }
946 })
947 .unwrap_or("xpub");
948 let fmt = format(name)?;
949 let allowed = match chain.id {
950 "bitcoin" => ["xpub", "ypub", "zpub"].as_slice(),
951 "bitcoin-testnet" => ["tpub", "upub", "vpub"].as_slice(),
952 "litecoin" => ["Ltub", "Mtub"].as_slice(),
953 _ => ["xpub"].as_slice(),
954 };
955 if !allowed.contains(&fmt.name) {
956 return Err(HdError::UnsupportedFormat(name.into()));
957 }
958 Ok(fmt)
959}
960
961fn account_path(chain: Chain, account: u32, script: Option<&str>, fmt: Format) -> String {
962 let purpose = if script == Some("p2tr") {
963 86
964 } else {
965 fmt.purpose
966 };
967 format!("m/{purpose}'/{}'/{account}'", chain.coin_type)
968}
969
970fn address_result(
971 chain: Chain,
972 path: String,
973 options: DeriveOptions<'_>,
974 script: &str,
975 address: String,
976 public_key: Vec<u8>,
977) -> DerivedAddress {
978 DerivedAddress {
979 schema_version: API_SCHEMA_VERSION,
980 chain: chain.id.into(),
981 curve: chain.curve.into(),
982 path,
983 account: options.account,
984 change: options.change,
985 index: options.index,
986 script_type: script.into(),
987 address,
988 public_key_hex: hex::encode(public_key),
989 }
990}
991
992fn public_key_to_address(public_key: &[u8], chain: Chain, script: &str) -> Result<String> {
993 match script {
994 "evm" => Ok(evm_address(public_key)?),
995 "tron" => Ok(tron_address(public_key)?),
996 "cashaddr" => Ok(cash_address("bitcoincash", &hash160(public_key))),
997 "p2pkh" => Ok(base58check(&[chain.p2pkh, &hash160(public_key)].concat())),
998 "p2sh-p2wpkh" => {
999 let redeem = [&[0x00, 0x14][..], &hash160(public_key)].concat();
1000 Ok(base58check(&[chain.p2sh, &hash160(&redeem)].concat()))
1001 }
1002 "p2wpkh" => Ok(segwit_address(
1003 chain
1004 .hrp
1005 .ok_or_else(|| HdError::UnsupportedScript(script.into()))?,
1006 0,
1007 &hash160(public_key),
1008 )),
1009 "p2tr" => Ok(segwit_address(
1010 chain
1011 .hrp
1012 .ok_or_else(|| HdError::UnsupportedScript(script.into()))?,
1013 1,
1014 &taproot_output_key(public_key)?,
1015 )),
1016 _ => Err(HdError::UnsupportedScript(script.into())),
1017 }
1018}
1019
1020fn evm_address(public_key: &[u8]) -> Result<String> {
1021 let encoded = EncodedPoint::from_bytes(public_key).map_err(|_| HdError::InvalidKey)?;
1022 let affine = Option::<AffinePoint>::from(AffinePoint::from_encoded_point(&encoded))
1023 .ok_or(HdError::InvalidKey)?;
1024 let uncompressed = affine.to_encoded_point(false);
1025 let digest = Keccak256::digest(&uncompressed.as_bytes()[1..]);
1026 Ok(eip55(&digest[12..]))
1027}
1028
1029fn tron_address(public_key: &[u8]) -> Result<String> {
1030 let encoded = EncodedPoint::from_bytes(public_key).map_err(|_| HdError::InvalidKey)?;
1031 let affine = Option::<AffinePoint>::from(AffinePoint::from_encoded_point(&encoded))
1032 .ok_or(HdError::InvalidKey)?;
1033 let uncompressed = affine.to_encoded_point(false);
1034 let digest = Keccak256::digest(&uncompressed.as_bytes()[1..]);
1035 Ok(base58check(&[&[0x41], &digest[12..]].concat()))
1036}
1037
1038fn eip55(bytes: &[u8]) -> String {
1039 let lower = hex::encode(bytes);
1040 let hash = hex::encode(Keccak256::digest(lower.as_bytes()));
1041 let mut output = String::from("0x");
1042 for (index, character) in lower.chars().enumerate() {
1043 if character.is_ascii_alphabetic()
1044 && u8::from_str_radix(&hash[index..=index], 16).unwrap() >= 8
1045 {
1046 output.push(character.to_ascii_uppercase());
1047 } else {
1048 output.push(character);
1049 }
1050 }
1051 output
1052}
1053
1054fn taproot_output_key(public_key: &[u8]) -> Result<Vec<u8>> {
1055 let x_only = &public_key[1..];
1056 let even =
1057 EncodedPoint::from_bytes([&[0x02], x_only].concat()).map_err(|_| HdError::InvalidKey)?;
1058 let affine = Option::<AffinePoint>::from(AffinePoint::from_encoded_point(&even))
1059 .ok_or(HdError::InvalidKey)?;
1060 let tweak_bytes: [u8; 32] = tagged_hash(b"TapTweak", x_only).into();
1061 let tweak = scalar(&tweak_bytes)?;
1062 let output = ProjectivePoint::from(affine) + ProjectivePoint::GENERATOR * tweak;
1063 Ok(output.to_affine().to_encoded_point(true).as_bytes()[1..].to_vec())
1064}
1065
1066struct Slip10Node {
1067 private_key: [u8; 32],
1068 public_key: [u8; 32],
1069 chain_code: [u8; 32],
1070 depth: u8,
1071 child_number: u32,
1072}
1073
1074fn slip10_ed25519(seed: &[u8], path: &str) -> Result<Slip10Node> {
1075 if !(16..=64).contains(&seed.len()) {
1076 return Err(HdError::InvalidSeed);
1077 }
1078 let mut digest = hmac_sha512(b"ed25519 seed", seed);
1079 let mut private_key: [u8; 32] = digest[..32].try_into().unwrap();
1080 let mut chain_code: [u8; 32] = digest[32..].try_into().unwrap();
1081 let mut depth = 0u8;
1082 let mut child_number = 0u32;
1083 for index in parse_path(path)? {
1084 if index < HARDENED_OFFSET {
1085 return Err(HdError::HardenedPublicDerivation);
1086 }
1087 let data = [&[0], &private_key[..], &index.to_be_bytes()].concat();
1088 digest = hmac_sha512(&chain_code, &data);
1089 private_key = digest[..32].try_into().unwrap();
1090 chain_code = digest[32..].try_into().unwrap();
1091 depth = depth
1092 .checked_add(1)
1093 .ok_or_else(|| HdError::InvalidPath(path.into()))?;
1094 child_number = index;
1095 }
1096 let public_key = SigningKey::from_bytes(&private_key)
1097 .verifying_key()
1098 .to_bytes();
1099 Ok(Slip10Node {
1100 private_key,
1101 public_key,
1102 chain_code,
1103 depth,
1104 child_number,
1105 })
1106}
1107
1108fn scalar(bytes: &[u8; 32]) -> Result<Scalar> {
1109 let value =
1110 Option::<Scalar>::from(Scalar::from_repr((*bytes).into())).ok_or(HdError::InvalidKey)?;
1111 if bool::from(value.is_zero()) {
1112 Err(HdError::InvalidKey)
1113 } else {
1114 Ok(value)
1115 }
1116}
1117
1118fn hmac_sha512(key: &[u8], data: &[u8]) -> [u8; 64] {
1119 let mut mac = HmacSha512::new_from_slice(key).expect("HMAC accepts arbitrary key sizes");
1120 mac.update(data);
1121 mac.finalize().into_bytes().into()
1122}
1123
1124fn hash160(bytes: &[u8]) -> Vec<u8> {
1125 Ripemd160::digest(Sha256::digest(bytes)).to_vec()
1126}
1127fn base58check(payload: &[u8]) -> String {
1128 bs58::encode(payload).with_check().into_string()
1129}
1130fn serialize_key(
1131 version: u32,
1132 depth: u8,
1133 parent: &[u8; 4],
1134 child: u32,
1135 chain_code: &[u8; 32],
1136 key: &[u8; 33],
1137) -> String {
1138 let payload = [
1139 &version.to_be_bytes()[..],
1140 &[depth],
1141 parent,
1142 &child.to_be_bytes(),
1143 chain_code,
1144 key,
1145 ]
1146 .concat();
1147 base58check(&payload)
1148}
1149
1150const BECH32_ALPHABET: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
1151fn segwit_address(hrp: &str, version: u8, program: &[u8]) -> String {
1152 let mut words = vec![version];
1153 words.extend(convert_bits(program, 8, 5, true).unwrap());
1154 bech32_encode(hrp, &words, if version == 0 { 1 } else { 0x2bc8_30a3 })
1155}
1156fn bech32_encode(hrp: &str, words: &[u8], constant: u32) -> String {
1157 let mut values: Vec<u8> = hrp.bytes().map(|byte| byte >> 5).collect();
1158 values.push(0);
1159 values.extend(hrp.bytes().map(|byte| byte & 31));
1160 values.extend(words);
1161 values.extend([0; 6]);
1162 let polymod = bech32_polymod(&values) ^ constant;
1163 let checksum: Vec<u8> = (0..6)
1164 .map(|index| ((polymod >> (5 * (5 - index))) & 31) as u8)
1165 .collect();
1166 let data: String = words
1167 .iter()
1168 .chain(checksum.iter())
1169 .map(|value| BECH32_ALPHABET[*value as usize] as char)
1170 .collect();
1171 format!("{hrp}1{data}")
1172}
1173fn bech32_polymod(values: &[u8]) -> u32 {
1174 let generators = [
1175 0x3b6a_57b2,
1176 0x2650_8e6d,
1177 0x1ea1_19fa,
1178 0x3d42_33dd,
1179 0x2a14_62b3,
1180 ];
1181 let mut checksum = 1u32;
1182 for value in values {
1183 let top = checksum >> 25;
1184 checksum = ((checksum & 0x1ff_ffff) << 5) ^ *value as u32;
1185 for (index, generator) in generators.iter().enumerate() {
1186 if (top >> index) & 1 == 1 {
1187 checksum ^= generator;
1188 }
1189 }
1190 }
1191 checksum
1192}
1193fn convert_bits(data: &[u8], from: u32, to: u32, pad: bool) -> Result<Vec<u8>> {
1194 let mut acc = 0u32;
1195 let mut bits = 0u32;
1196 let mut output = vec![];
1197 let mask = (1u32 << to) - 1;
1198 for value in data {
1199 if (*value as u32) >> from != 0 {
1200 return Err(HdError::InvalidKey);
1201 }
1202 acc = (acc << from) | *value as u32;
1203 bits += from;
1204 while bits >= to {
1205 bits -= to;
1206 output.push(((acc >> bits) & mask) as u8);
1207 }
1208 }
1209 if pad && bits > 0 {
1210 output.push(((acc << (to - bits)) & mask) as u8);
1211 } else if !pad && (bits >= from || ((acc << (to - bits)) & mask) != 0) {
1212 return Err(HdError::InvalidKey);
1213 }
1214 Ok(output)
1215}
1216
1217fn cash_address(prefix: &str, hash: &[u8]) -> String {
1218 let mut bytes = vec![0];
1219 bytes.extend(hash);
1220 let payload = convert_bits(&bytes, 8, 5, true).unwrap();
1221 let mut values: Vec<u8> = prefix.bytes().map(|byte| byte & 31).collect();
1222 values.push(0);
1223 values.extend(&payload);
1224 values.extend([0; 8]);
1225 let polymod = cashaddr_polymod(&values) ^ 1;
1226 let checksum: Vec<u8> = (0..8)
1227 .map(|index| ((polymod >> (5 * (7 - index))) & 31) as u8)
1228 .collect();
1229 let data: String = payload
1230 .iter()
1231 .chain(checksum.iter())
1232 .map(|value| BECH32_ALPHABET[*value as usize] as char)
1233 .collect();
1234 format!("{prefix}:{data}")
1235}
1236fn cashaddr_polymod(values: &[u8]) -> u64 {
1237 let generators = [
1238 0x98f2_bc8e61,
1239 0x79b7_6d99e2,
1240 0xf33e_5fb3c4,
1241 0xae2e_abe2a8,
1242 0x1e4f_43e470,
1243 ];
1244 let mut checksum = 1u64;
1245 for value in values {
1246 let top = checksum >> 35;
1247 checksum = ((checksum & 0x07_ff_ff_ff_ff) << 5) ^ *value as u64;
1248 for (index, generator) in generators.iter().enumerate() {
1249 if (top >> index) & 1 == 1 {
1250 checksum ^= generator;
1251 }
1252 }
1253 }
1254 checksum
1255}
1256fn tagged_hash(tag: &[u8], message: &[u8]) -> [u8; 32] {
1257 let tag_hash = Sha256::digest(tag);
1258 Sha256::digest([&tag_hash[..], &tag_hash[..], message].concat()).into()
1259}
1260
1261#[cfg(test)]
1262mod tests {
1263 use super::*;
1264 use proptest::prelude::*;
1265 const WORDS: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
1266 fn source() -> Source {
1267 Source::mnemonic(WORDS, "")
1268 }
1269
1270 #[test]
1271 fn slip132_and_bip86_vectors() {
1272 let zpub = derive_account_public_key(
1273 &source(),
1274 DeriveOptions {
1275 chain: "bitcoin",
1276 format: Some("zpub"),
1277 ..Default::default()
1278 },
1279 )
1280 .unwrap();
1281 assert_eq!(zpub.extended_public_key, "zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs");
1282 let address = derive_address(
1283 &source(),
1284 DeriveOptions {
1285 chain: "bitcoin",
1286 ..Default::default()
1287 },
1288 )
1289 .unwrap();
1290 assert_eq!(
1291 address.address,
1292 "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu"
1293 );
1294 let taproot = derive_address(
1295 &source(),
1296 DeriveOptions {
1297 chain: "bitcoin",
1298 script_type: Some("p2tr"),
1299 ..Default::default()
1300 },
1301 )
1302 .unwrap();
1303 assert_eq!(
1304 taproot.address,
1305 "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr"
1306 );
1307 }
1308
1309 #[test]
1310 fn multi_chain_vectors() {
1311 for (id, expected) in [
1312 ("litecoin", "LUWPbpM43E2p7ZSh8cyTBEkvpHmr3cB8Ez"),
1313 ("dogecoin", "DBus3bamQjgJULBJtYXpEzDWQRwF5iwxgC"),
1314 ("ethereum", "0x9858EfFD232B4033E47d90003D41EC34EcaEda94"),
1315 ("tron", "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH"),
1316 ("solana", "HAgk14JpMQLgt6rVgv7cBQFJWFto5Dqxi472uT3DKpqk"),
1317 ] {
1318 assert_eq!(
1319 derive_address(
1320 &source(),
1321 DeriveOptions {
1322 chain: id,
1323 ..Default::default()
1324 }
1325 )
1326 .unwrap()
1327 .address,
1328 expected
1329 );
1330 }
1331 }
1332
1333 #[test]
1334 fn public_and_private_children_match() {
1335 let root = ExtendedPrivateKey::master(&source().seed().unwrap())
1336 .unwrap()
1337 .derive_path("m/84'/0'/0'")
1338 .unwrap();
1339 assert_eq!(
1340 root.derive(0).unwrap().public_key(),
1341 root.neuter().derive(0).unwrap().public_key
1342 );
1343 assert!(root.neuter().derive(HARDENED_OFFSET).is_err());
1344 }
1345
1346 #[test]
1347 fn all_official_bip32_vectors_and_invalid_keys() {
1348 let vectors: serde_json::Value =
1349 serde_json::from_str(include_str!("../../test-vectors/bip32-official.json")).unwrap();
1350 for vector in vectors["vectors"].as_array().unwrap() {
1351 let seed = hex::decode(vector["seedHex"].as_str().unwrap()).unwrap();
1352 let root = ExtendedPrivateKey::master(&seed).unwrap();
1353 for expected in vector["nodes"].as_array().unwrap() {
1354 let path = expected["path"].as_str().unwrap();
1355 let node = root.derive_path(path).unwrap();
1356 assert_eq!(
1357 node.serialize_public(XPUB.public_version),
1358 expected["extendedPublicKey"].as_str().unwrap(),
1359 "{path}"
1360 );
1361 assert_eq!(
1362 node.serialize_private(XPUB.private_version),
1363 expected["extendedPrivateKey"].as_str().unwrap(),
1364 "{path}"
1365 );
1366 }
1367 }
1368 for invalid in vectors["invalidExtendedKeys"].as_array().unwrap() {
1369 assert!(
1370 parse_extended_key(invalid["value"].as_str().unwrap()).is_err(),
1371 "accepted {}",
1372 invalid["reason"].as_str().unwrap()
1373 );
1374 }
1375 }
1376
1377 #[test]
1378 fn official_slip10_ed25519_vector() {
1379 let vectors: serde_json::Value = serde_json::from_str(include_str!(
1380 "../../test-vectors/slip10-ed25519-official.json"
1381 ))
1382 .unwrap();
1383 let seed = hex::decode(vectors["seedHex"].as_str().unwrap()).unwrap();
1384 for expected in vectors["nodes"].as_array().unwrap() {
1385 let path = expected["path"].as_str().unwrap();
1386 let node = slip10_ed25519(&seed, path).unwrap();
1387 assert_eq!(hex::encode(node.chain_code), expected["chainCodeHex"]);
1388 assert_eq!(hex::encode(node.private_key), expected["privateKeyHex"]);
1389 assert_eq!(hex::encode(node.public_key), expected["publicKeyHex"]);
1390 }
1391 }
1392
1393 #[test]
1394 fn public_api_branches_and_failure_boundaries() {
1395 assert_eq!(supported_chains().len(), 18);
1396 assert!(chain("unknown").is_err());
1397 assert!(Source::Seed(vec![0; 15]).seed().is_err());
1398 assert!(Source::mnemonic("abandon abandon", "").seed().is_err());
1399 assert!(ExtendedPrivateKey::master(&[0; 15]).is_err());
1400 for name in [
1401 "xpub", "ypub", "zpub", "tpub", "upub", "vpub", "Ltub", "Mtub",
1402 ] {
1403 assert_eq!(format(name).unwrap().name, name);
1404 }
1405 assert!(format("unknown").is_err());
1406
1407 let secp = derive_node(&source(), "secp256k1", "m/0h/1H/2'").unwrap();
1408 assert_eq!(secp.depth, 3);
1409 let ed = derive_node(&source(), "ed25519", "m/0'").unwrap();
1410 assert_eq!(ed.depth, 1);
1411 assert!(derive_node(&source(), "p256", "m").is_err());
1412 assert!(derive_node(&source(), "ed25519", "m/0").is_err());
1413
1414 let account = derive_account_public_key(&source(), DeriveOptions::default()).unwrap();
1415 let parsed_public = parse_extended_key(&account.extended_public_key).unwrap();
1416 assert_eq!(
1417 serialize_extended_key(&parsed_public, None),
1418 account.extended_public_key
1419 );
1420 assert!(derive_account_public_key(
1421 &source(),
1422 DeriveOptions {
1423 chain: "solana",
1424 ..Default::default()
1425 }
1426 )
1427 .is_err());
1428 assert!(derive_account_public_key(
1429 &source(),
1430 DeriveOptions {
1431 chain: "bitcoin",
1432 format: Some("tpub"),
1433 ..Default::default()
1434 }
1435 )
1436 .is_err());
1437
1438 let secret = derive_account_private_key(
1439 &source(),
1440 DeriveOptions {
1441 chain: "bitcoin",
1442 format: Some("xpub"),
1443 path: Some("m"),
1444 ..Default::default()
1445 },
1446 )
1447 .unwrap();
1448 let parsed_private =
1449 parse_extended_key(secret.extended_private_key.as_deref().unwrap()).unwrap();
1450 assert_eq!(
1451 serialize_extended_key(&parsed_private, Some(XPUB.private_version)),
1452 secret.extended_private_key.unwrap()
1453 );
1454 let solana_secret = derive_account_private_key(
1455 &source(),
1456 DeriveOptions {
1457 chain: "solana",
1458 ..Default::default()
1459 },
1460 )
1461 .unwrap();
1462 assert!(solana_secret.extended_private_key.is_none());
1463
1464 let vectors: serde_json::Value =
1465 serde_json::from_str(include_str!("../../test-vectors/public-vectors.json")).unwrap();
1466 for expected in vectors["addresses"].as_array().unwrap() {
1467 let chain = expected["chain"].as_str().unwrap();
1468 let path = expected["path"].as_str().unwrap();
1469 let script_type = expected["scriptType"].as_str().unwrap();
1470 assert_eq!(
1471 derive_address(
1472 &source(),
1473 DeriveOptions {
1474 chain,
1475 path: Some(path),
1476 script_type: Some(script_type),
1477 ..Default::default()
1478 }
1479 )
1480 .unwrap()
1481 .address,
1482 expected["address"]
1483 );
1484 }
1485 assert_eq!(
1486 derive_addresses(&source(), DeriveOptions::default(), 3, 4)
1487 .unwrap()
1488 .len(),
1489 4
1490 );
1491 assert!(derive_addresses(&source(), DeriveOptions::default(), 0, 0).is_err());
1492 assert!(derive_addresses(&source(), DeriveOptions::default(), 0, 10_001).is_err());
1493 assert!(derive_addresses(&source(), DeriveOptions::default(), u32::MAX, 2).is_err());
1494
1495 let watched = derive_address_from_extended_public_key(
1496 &account.extended_public_key,
1497 Some("bitcoin"),
1498 0,
1499 0,
1500 None,
1501 )
1502 .unwrap();
1503 assert_eq!(
1504 watched.address,
1505 "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu"
1506 );
1507 assert!(derive_address_from_extended_public_key(
1508 &account.extended_public_key,
1509 None,
1510 0,
1511 0,
1512 None
1513 )
1514 .is_err());
1515 let serialized_private = match &parsed_private {
1516 ParsedExtendedKey::Private(node, _) => node.serialize_private(XPUB.private_version),
1517 _ => unreachable!(),
1518 };
1519 assert!(derive_address_from_extended_public_key(
1520 &serialized_private,
1521 Some("bitcoin"),
1522 0,
1523 0,
1524 None
1525 )
1526 .is_err());
1527 assert!(derive_address_from_extended_public_key(
1528 &account.extended_public_key,
1529 Some("bitcoin"),
1530 HARDENED_OFFSET,
1531 0,
1532 None
1533 )
1534 .is_err());
1535
1536 for path in ["relative/0", "m/", "m/00", "m/abc", "m/2147483648"] {
1537 assert!(parse_path(path).is_err(), "accepted {path}");
1538 }
1539 assert_eq!(parse_path("M/0h/1H/2'").unwrap().len(), 3);
1540 assert!(parse_extended_key("not-a-key").is_err());
1541 assert!(public_key_to_address(&[4; 33], chain("ethereum").unwrap(), "evm").is_err());
1542 assert!(public_key_to_address(&[4; 33], chain("tron").unwrap(), "tron").is_err());
1543 assert!(public_key_to_address(
1544 &ExtendedPrivateKey::master(&[0; 16]).unwrap().public_key(),
1545 chain("dogecoin").unwrap(),
1546 "p2wpkh"
1547 )
1548 .is_err());
1549 assert!(public_key_to_address(
1550 &ExtendedPrivateKey::master(&[0; 16]).unwrap().public_key(),
1551 chain("bitcoin").unwrap(),
1552 "unknown"
1553 )
1554 .is_err());
1555 }
1556
1557 proptest! {
1558 #[test]
1559 fn arbitrary_derivation_paths_never_panic(value in ".{0,512}") {
1560 let _ = parse_path(&value);
1561 }
1562
1563 #[test]
1564 fn random_non_hardened_private_and_public_children_agree(index in 0u32..HARDENED_OFFSET) {
1565 let root = ExtendedPrivateKey::master(&[7; 32]).unwrap().derive_path("m/84'/0'/0'").unwrap();
1566 prop_assert_eq!(root.derive(index).unwrap().public_key(), root.neuter().derive(index).unwrap().public_key);
1567 }
1568 }
1569}