1use std::{
2 collections::{HashMap, HashSet},
3 str::FromStr,
4};
5
6use bip39::Mnemonic;
7use bitcoin::{
8 Address, Network, PublicKey,
9 bip32::{DerivationPath, Xpriv},
10 key::Secp256k1,
11};
12
13use thiserror::Error;
14
15type PathStr = String;
16
17#[derive(Debug, Error)]
18pub enum MatcherError {
19 #[error("Derivation path is unexpectedly invalid, derivation path: {0}")]
20 Derivation(PathStr),
21
22 #[error("Failed to create extended private key, error: {0}")]
23 PrivKey(String),
24
25 #[error("Failed to inplace-modify path string index: {0}")]
26 PathStrModify(PathStr),
27}
28
29impl MatcherError {
30 pub fn from_derivation_path(path: &str) -> MatcherError {
31 MatcherError::Derivation(path.to_string())
32 }
33}
34
35#[derive(Debug, PartialEq, Clone)]
51pub struct DerivationStandard<'a, 'b> {
52 pub base_path: &'a str,
58 pub starts_with: &'b str,
60}
61
62pub const SUPPORTED_STANDARDS: [DerivationStandard; 4] = [
63 DerivationStandard {
64 base_path: "m/44'/0'/0'/0/", starts_with: "1",
66 },
67 DerivationStandard {
68 base_path: "m/49'/0'/0'/0/", starts_with: "3",
70 },
71 DerivationStandard {
72 base_path: "m/84'/0'/0'/0/", starts_with: "bc1q",
74 },
75 DerivationStandard {
76 base_path: "m/86'/0'/0'/0/", starts_with: "bc1p",
78 },
79];
80
81impl DerivationStandard<'_, '_> {
82 pub fn new<'a, 'b>(base_path: &'a str, starts_with: &'b str) -> DerivationStandard<'a, 'b> {
91 DerivationStandard {
92 base_path,
93 starts_with,
94 }
95 }
96
97 pub fn from_address(address: &str) -> Option<&DerivationStandard> {
107 for standard in SUPPORTED_STANDARDS.iter() {
108 if address.starts_with(standard.starts_with) {
109 return Some(standard);
110 }
111 }
112
113 None
114 }
115
116 pub fn into_address(
126 &self,
127 path: &DerivationPath,
128 xpriv: &Xpriv,
129 ) -> Result<String, MatcherError> {
130 let secp = Secp256k1::new();
131
132 let child_xpriv = xpriv
133 .derive_priv(&secp, path)
134 .map_err(|_| MatcherError::PrivKey("Failed to derive child key".to_string()))?;
135
136 let child_secp_pubkey = child_xpriv.private_key.public_key(&secp);
138 let child_pubkey = PublicKey::new(child_secp_pubkey);
139
140 match self.starts_with {
141 "1" => {
142 Ok(Address::p2pkh(child_pubkey, Network::Bitcoin).to_string())
144 }
145 "3" => {
146 use bitcoin::CompressedPublicKey;
148 let cp =
149 CompressedPublicKey::from_slice(&child_pubkey.to_bytes()).map_err(|_| {
150 MatcherError::PrivKey("Unable to compress public key".to_string())
151 })?;
152 Ok(Address::p2shwpkh(&cp, Network::Bitcoin).to_string())
153 }
154 "bc1q" => {
155 use bitcoin::CompressedPublicKey;
157
158 let cp: bitcoin::CompressedPublicKey =
159 CompressedPublicKey::from_slice(&child_pubkey.to_bytes()).map_err(|_| {
160 MatcherError::PrivKey("Unable to compress public key".to_string())
161 })?;
162 Ok(Address::p2wpkh(&cp, Network::Bitcoin).to_string())
163 }
164 "bc1p" => {
165 Ok(
168 Address::p2tr(&secp, child_pubkey.inner.into(), None, Network::Bitcoin)
169 .to_string(),
170 )
171 }
172 _ => {
173 Ok(Address::p2pkh(child_pubkey, Network::Bitcoin).to_string())
175 }
176 }
177 }
178
179 pub fn from_prefix(prefix: &str) -> Option<&DerivationStandard> {
181 for standard in SUPPORTED_STANDARDS.iter() {
182 if prefix.starts_with(standard.starts_with) {
183 return Some(standard);
184 }
185 }
186
187 None
188 }
189 pub fn get_supported_standards() -> &'static [DerivationStandard<'static, 'static>] {
191 &SUPPORTED_STANDARDS
192 }
193}
194
195fn modify_path_index(path: &mut String, index: usize) -> Result<(), MatcherError> {
197 let index_str = index.to_string();
198 let path_len = path.len();
199
200 let last_slash = match path.rfind('/') {
201 Some(pos) => pos,
202 None => return Err(MatcherError::PathStrModify(path.to_string())),
203 };
204
205 path.replace_range(last_slash + 1..path_len, &index_str);
206
207 Ok(())
208}
209
210pub struct Matcher<'a, 'b> {
221 pub addrs: &'a HashSet<String>,
222 pub mnems: &'b Vec<Mnemonic>,
223 pub logging: bool,
224}
225
226impl<'a, 'b> Matcher<'a, 'b> {
227 pub fn new(
256 addrs: &'a HashSet<String>,
257 mnems: &'b Vec<Mnemonic>,
258 logging: bool,
259 ) -> Matcher<'a, 'b> {
260 Matcher {
261 addrs,
262 mnems,
263 logging,
264 }
265 }
266
267 pub fn match_in(
316 &self,
317 addr_to_gen_per_mnem: usize,
318 amount: Option<Vec<(DerivationStandard, usize)>>,
319 ) -> Result<HashMap<&Mnemonic, Vec<String>>, MatcherError> {
320 let amount = match amount {
321 Some(amount) => amount,
322 None => {
323 let mut unmatched_addresses = vec![];
324
325 let mut standards = DerivationStandard::get_supported_standards()
326 .iter()
327 .map(|s| (s.clone(), 0))
328 .collect::<Vec<(DerivationStandard, usize)>>();
329
330 for addr in self.addrs.iter() {
331 let standard = match DerivationStandard::from_address(addr) {
332 Some(s) => s,
333 None => {
334 unmatched_addresses.push(addr);
335 continue;
336 }
337 };
338
339 standards
340 .iter_mut()
341 .find(|s| s.0 == *standard)
342 .map(|s| s.1 += 1);
343 }
344
345 if self.logging {
346 println!(
347 "Found {} addresses whose standard is not supported",
348 unmatched_addresses.len()
349 );
350 }
351
352 let total = standards.iter().fold(0, |acc, s| acc + s.1);
353
354 for standard in standards.iter_mut() {
355 standard.1 = (standard.1 as f64 / total as f64 * addr_to_gen_per_mnem as f64)
356 .ceil() as usize;
357 }
358
359 standards
360 }
361 };
362
363 let mut found = HashMap::new();
364
365 for mnemonic in self.mnems.iter() {
366 let addresses = Self::generate_addresses(&amount, mnemonic)?;
367
368 for addr in addresses {
369 if self.addrs.contains(&addr) {
370 found.entry(mnemonic).or_insert(vec![]).push(addr);
371 }
372 }
373 }
374
375 Ok(found)
376 }
377
378 pub fn generate_addresses(
416 amount: &[(DerivationStandard, usize)],
417 mnemonic: &Mnemonic,
418 ) -> Result<Vec<String>, MatcherError> {
419 let mut addresses = vec![];
420 let seed = mnemonic.to_seed("");
421 let xpriv = Xpriv::new_master(Network::Bitcoin, &seed)
422 .map_err(|_| MatcherError::PrivKey("Failed to create master key".to_string()))?;
423
424 for (ds, n) in amount {
425 let mut base_path: String = ds.base_path.to_string();
426 base_path.reserve(n.to_string().len() + 10);
427
428 for i in 0..*n {
429 modify_path_index(&mut base_path, i)?;
430
431 let path = DerivationPath::from_str(&base_path)
432 .map_err(|_| MatcherError::from_derivation_path(base_path.as_str()))?;
433
434 let address = ds.into_address(&path, &xpriv)?;
435
436 addresses.push(address);
437 }
438 }
439
440 Ok(addresses)
441 }
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447 use bip39::Mnemonic;
448 use std::collections::HashSet;
449
450 #[test]
451 fn test_modify_path_index() {
452 let mut path = "m/44'/0'/0'/0/".to_string();
453 modify_path_index(&mut path, 1).unwrap();
454 assert_eq!(path, "m/44'/0'/0'/0/1");
455 modify_path_index(&mut path, 32).unwrap();
456 assert_eq!(path, "m/44'/0'/0'/0/32");
457
458 let mut path = "m/44'/0'/0'/0/".to_string();
459 modify_path_index(&mut path, 10).unwrap();
460 assert_eq!(path, "m/44'/0'/0'/0/10");
461 modify_path_index(&mut path, 100).unwrap();
462 assert_eq!(path, "m/44'/0'/0'/0/100");
463 modify_path_index(&mut path, 0).unwrap();
464 assert_eq!(path, "m/44'/0'/0'/0/0");
465
466 let mut path = "m/44'/0'/0'/0/".to_string();
467 modify_path_index(&mut path, 100).unwrap();
468 assert_eq!(path, "m/44'/0'/0'/0/100");
469 modify_path_index(&mut path, 1000).unwrap();
470 assert_eq!(path, "m/44'/0'/0'/0/1000");
471 modify_path_index(&mut path, 1).unwrap();
472 assert_eq!(path, "m/44'/0'/0'/0/1");
473 }
474
475 #[test]
476 fn test_derivation_standard_from_address() {
477 let standard = DerivationStandard::from_address("1BvB...");
478 assert_eq!(standard.unwrap().base_path, "m/44'/0'/0'/0/");
479
480 let standard = DerivationStandard::from_address("3J98...");
481 assert_eq!(standard.unwrap().base_path, "m/49'/0'/0'/0/");
482
483 let standard = DerivationStandard::from_address("bc1qar0s...");
484 assert_eq!(standard.unwrap().base_path, "m/84'/0'/0'/0/");
485
486 let standard = DerivationStandard::from_address("bc1par0s...");
487 assert_eq!(standard.unwrap().base_path, "m/86'/0'/0'/0/");
488 }
489
490 #[test]
491 fn test_derivation_standard_from_prefix() {
492 let standard = DerivationStandard::from_prefix("1");
493 assert_eq!(standard.unwrap().base_path, "m/44'/0'/0'/0/");
494
495 let standard = DerivationStandard::from_prefix("3");
496 assert_eq!(standard.unwrap().base_path, "m/49'/0'/0'/0/");
497
498 let standard = DerivationStandard::from_prefix("bc1q");
499 assert_eq!(standard.unwrap().base_path, "m/84'/0'/0'/0/");
500
501 let standard = DerivationStandard::from_prefix("bc1p");
502 assert_eq!(standard.unwrap().base_path, "m/86'/0'/0'/0/");
503 }
504
505 const MNEMONIC: &str = "method tribe morning flock suit upon salt puppy jar harbor west wealth device tooth bundle expose mansion scrap erupt helmet hurt promote fit hire";
506 const LEGACY_ADDRESSES: [&str; 10] = [
507 "1BGLgRL7EiFxS9H616bfoJPjSugKudECCn",
508 "1Kc48oyfPrTv9UD1Fk61dZkfxgRM83bU46",
509 "1MqmuiTTY8tTgBMFiDZSB9UygSnM5X9sd",
510 "15VRc3icVAJmrHC2CXQgecnRyUWc1oWVQW",
511 "19DosUKX9zQEdAHWaqCvq67pDAVs5AEpck",
512 "1CP296asrQ8hZnFP3GUjrpDcfyc71f6u2r",
513 "14kNFbSL4Tt67LG5H5p4Cje9CXqJvNKqfT",
514 "1E8CV9uRR8GZSuZAZsaFLHwbjKiQGmifqM",
515 "12k1o5tyV1HqimC2FWbN7gzktStByJMYma",
516 "1DsLE3UoAo98Vwmbi6a7pDKRcmkBB4Txzh",
517 ];
518
519 const SEG_WIT_ADDRESSES: [&str; 10] = [
520 "38cArkkfxxL7LtVWAwYNcvZTgf3KtDBcD7",
521 "37xqCGNb1rTokVVtjBkCoWBLxmAZjXjUyQ",
522 "3EXGQZu3GQnFzJLEyx27dxpjuGTenzfDka",
523 "3AyVvwN2SvgwFfQPD423jrg7Yw4umwZcWv",
524 "38VgHXNcp2wJZg5KVvAFuKSyD7fhGWVoz8",
525 "39vHAo5qSVvQj4XWNPJSzbvfhgKZ8PJF7N",
526 "36EHh1dKfXTURcR4N69KAHwAKoZuZp28Qi",
527 "37cEAoExVS2roXeu3QDVERAACMym8wL29d",
528 "32yaS5LmjcavajMcwU7Ebyq3FmXi7o85HT",
529 "32R9GYSTMqWrjW5oyX2oM4xzM52eL8J5jt",
530 ];
531
532 const NATIVE_SEG_WIT_ADDRESSES: [&str; 10] = [
533 "bc1q39ytrq296c6skxvrkd3m64j2fz5keep26q239t",
534 "bc1qe3kvt65klvzmnwehshzfv7w08cng6dqd3vfs2a",
535 "bc1q0c7qe879lh7x6cyufen7q8kmrf4rmr032u2ykx",
536 "bc1qvwn3a4jflzlkrxckjp76jhu5qhl7tjrw09wgc2",
537 "bc1qlmprztxqq4a8l8syfsn48u8v4zejmv4d5l3hle",
538 "bc1qyg60vy2wgp2sfmt825yu60kg7pg8t6mej8cjkh",
539 "bc1qlakvxvjj3raplc4xu678cr8g9kxdj9249gw7tg",
540 "bc1qg8e6kkwzmjek4mt4t6lzu5jm6fq8a2f9y9s9pa",
541 "bc1qncyyemztujnavnpqv0jpe53yayu0a8nrquztcl",
542 "bc1qszrsm6623dz5kf7dc7fyma0zq4enqtm4e8aqlx",
543 ];
544
545 const NOT_MATCHING_LEGACY_ADDRESSES: [&str; 10] = [
546 "15JcFwxJEEektpqjyQpRWPEHF9DQBX6NLy",
547 "1BZkYY2RdM4iLCD7mGuZu5DpGCssst1NuH",
548 "1Ga2CrW3unYeU4esEoNxtLanLXbsu4eR9Z",
549 "1HSXNFBkrHAH87NPXDMd8fDS9iTJaKUtih",
550 "1FqrZNdEs8yk5eV4kTZJzdpHdWbobFk5y4",
551 "17aUocSwJBDdQv8u7S7CN7vMMu7BEAJj1D",
552 "12GPo6Wih1Ps1MsvZ2Yo8gsRuJMfn4dpXu",
553 "14datpqKqjDMoAwsJQWxF7ZeZNgSDMwghC",
554 "1G8bC5cDtE3V6niunvd3B8zG9pdQNM8ePw",
555 "1KnjQiJjsdFhaPcKaxNPY9xubTHXibLKn6",
556 ];
557
558 const NOT_MATCHING_SEGWIT_ADDRESSES: [&str; 10] = [
559 "3BnH3FPZ9CpN4RfxxCJXFLt6tzibvYCi9k",
560 "3NYZQsjn8vYR4oE9xFdueLVY7ofMpSncEK",
561 "39hwATVfL8Nfe9P6ogpFU5xiDoaVPdWZsh",
562 "384QcePn5Z9ZixRXJwaaN5ot2ThPedJiMP",
563 "35MEXexaZyK6BTtikKxVadVeeVw9HhdMm7",
564 "3EScCZRYJjHuPaYvCevV2WCMJaeCoG9H1R",
565 "3JR8Y511MxV2cfP7i7wDXMizbnPZVC8zbo",
566 "3Ft9CkUDQx5ybvahFuSR7hggruGNeZRhuP",
567 "32VWQ66jafP47tE1q7i3aHZo7eYfHyaXHc",
568 "35MVo6Q48NUdtxc2dBxzm1SnEgZqXojPfA",
569 ];
570
571 const NOT_MATCHING_NATIVE_SEGWIT_ADDRESSES: [&str; 10] = [
572 "bc1q6wznd8c7v4pgwaugt5u3u9mfmus7fglpp9ef60",
573 "bc1qz9nx30gtl353gmr5ckmd7wg7jlxkmwl5q9dqr8",
574 "bc1qrwafwp7mq36zsryg98c45yj96c28qx3puahafp",
575 "bc1q560f5kg4me8tp28mpah7gpphhnwe5aa9wxck6e",
576 "bc1qumg52ymsm74y065x6n30uxns8dv5ul73trckgt",
577 "bc1q3f6swqv7r3prj9l5z9nphld9z5tzgmh3snf4zg",
578 "bc1q4z98wwqdjl5drg5esz98lzyf99fzsvyulyr6fy",
579 "bc1qp447e30gatvlww4sz2an9ymaugyeemtksw6fa8",
580 "bc1q0jvyt5hm42ukh7c3t98st0xmeyeu6w4thhadkp",
581 "bc1qlywz7zrwxna4pln0q5xdjpkwxvtnwk8qchz7xd",
582 ];
583
584 #[test]
587 fn test_match_in() {
588 let mut total_addresses = 0;
589
590 let mut addresses = HashSet::new();
591 let mut mnems = vec![];
592
593 for addr in LEGACY_ADDRESSES.iter() {
594 addresses.insert(addr.to_string());
595 total_addresses += 1;
596 }
597
598 for addr in SEG_WIT_ADDRESSES.iter() {
599 addresses.insert(addr.to_string());
600 total_addresses += 1;
601 }
602
603 for addr in NATIVE_SEG_WIT_ADDRESSES.iter() {
604 addresses.insert(addr.to_string());
605 total_addresses += 1;
606 }
607
608 let mnemonic = Mnemonic::from_str(MNEMONIC).unwrap();
609 mnems.push(mnemonic);
610
611 let matcher = Matcher::new(&addresses, &mnems, false);
612
613 let amount = vec![
614 (DerivationStandard::new("m/44'/0'/0'/0/", "1"), 10),
615 (DerivationStandard::new("m/49'/0'/0'/0/", "3"), 10),
616 (DerivationStandard::new("m/84'/0'/0'/0/", "bc1q"), 10),
617 ];
618
619 let found = matcher.match_in(total_addresses, Some(amount)).unwrap();
620
621 let mut mnemonic_with_addresses = 0;
622
623 for (_mn, addresses) in found {
624 assert_eq!(addresses.len(), total_addresses);
625 for addr in addresses {
626 assert!(matcher.addrs.contains(&addr));
627 }
628 mnemonic_with_addresses += 1;
629 }
630
631 assert_eq!(mnemonic_with_addresses, 1);
632 }
633
634 #[test]
635 fn test_match_in_auto() {
636 let mut total_addresses = 0;
637
638 let mut addresses = HashSet::new();
639 let mut mnems = vec![];
640
641 for addr in LEGACY_ADDRESSES.iter() {
642 addresses.insert(addr.to_string());
643 total_addresses += 1;
644 }
645
646 for addr in SEG_WIT_ADDRESSES.iter() {
647 addresses.insert(addr.to_string());
648 total_addresses += 1;
649 }
650
651 for addr in NATIVE_SEG_WIT_ADDRESSES.iter() {
652 addresses.insert(addr.to_string());
653 total_addresses += 1;
654 }
655
656 let mnemonic = Mnemonic::from_str(MNEMONIC).unwrap();
657 mnems.push(mnemonic);
658 let matcher = Matcher::new(&addresses, &mnems, false);
659 let found = matcher.match_in(total_addresses, None).unwrap();
660 let mut mnemonic_with_addresses = 0;
661
662 for (_mn, addresses) in found {
663 assert_eq!(addresses.len(), total_addresses);
664 for addr in addresses {
665 assert!(matcher.addrs.contains(&addr));
666 }
667 mnemonic_with_addresses += 1;
668 }
669
670 assert_eq!(mnemonic_with_addresses, 1);
671 }
672
673 #[test]
674 fn test_match_in_no_more_than_real_matches() {
675 let mut total_addresses = 0;
676
677 let mut addresses = HashSet::new();
678 let mut mnems = vec![];
679
680 for addr in LEGACY_ADDRESSES.iter() {
681 addresses.insert(addr.to_string());
682 total_addresses += 1;
683 }
684
685 for addr in SEG_WIT_ADDRESSES.iter() {
686 addresses.insert(addr.to_string());
687 total_addresses += 1;
688 }
689
690 for addr in NATIVE_SEG_WIT_ADDRESSES.iter() {
691 addresses.insert(addr.to_string());
692 total_addresses += 1;
693 }
694
695 let mut not_matching_addresses = 0;
696
697 for addr in NOT_MATCHING_LEGACY_ADDRESSES.iter() {
698 addresses.insert(addr.to_string());
699 not_matching_addresses += 1;
700 }
701
702 for addr in NOT_MATCHING_SEGWIT_ADDRESSES.iter() {
703 addresses.insert(addr.to_string());
704 not_matching_addresses += 1;
705 }
706
707 for addr in NOT_MATCHING_NATIVE_SEGWIT_ADDRESSES.iter() {
708 addresses.insert(addr.to_string());
709 not_matching_addresses += 1;
710 }
711
712 let mnemonic = Mnemonic::from_str(MNEMONIC).unwrap();
713 mnems.push(mnemonic);
714
715 let matcher = Matcher::new(&addresses, &mnems, false);
716
717 let found = matcher
718 .match_in(total_addresses + not_matching_addresses, None)
719 .unwrap();
720
721 let mut mnemonic_with_addresses = 0;
722
723 for (_mn, addresses) in found {
724 assert_eq!(addresses.len(), total_addresses);
725 for addr in addresses {
726 assert!(matcher.addrs.contains(&addr));
727 }
728 mnemonic_with_addresses += 1;
729 }
730
731 assert_eq!(mnemonic_with_addresses, 1);
732 }
733
734 #[test]
735 fn test_generate_addresses() {
736 let mnemonic = Mnemonic::from_str(MNEMONIC).unwrap();
737
738 let amount = vec![
739 (DerivationStandard::new("m/44'/0'/0'/0/", "1"), 10),
740 (DerivationStandard::new("m/49'/0'/0'/0/", "3"), 10),
741 (DerivationStandard::new("m/84'/0'/0'/0/", "bc1q"), 10),
742 ];
743
744 let addresses = Matcher::generate_addresses(&amount, &mnemonic).unwrap();
745
746 for (addr, legacy) in addresses.iter().zip(LEGACY_ADDRESSES.iter()) {
747 assert_eq!(addr, legacy);
748 }
749
750 for (addr, segwit) in addresses.iter().skip(10).zip(SEG_WIT_ADDRESSES.iter()) {
751 assert_eq!(addr, segwit);
752 }
753
754 for (addr, native_segwit) in addresses
755 .iter()
756 .skip(20)
757 .zip(NATIVE_SEG_WIT_ADDRESSES.iter())
758 {
759 assert_eq!(addr, native_segwit);
760 }
761 }
762}