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 [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 [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 if self.logging {
331 println!("Starting automatic derivation amount inference");
332 }
333
334 for addr in self.addrs.iter() {
335 let standard = match DerivationStandard::from_address(addr) {
336 Some(s) => s,
337 None => {
338 unmatched_addresses.push(addr);
339 continue;
340 }
341 };
342
343 standards
344 .iter_mut()
345 .find(|s| s.0 == *standard)
346 .map(|s| s.1 += 1);
347 }
348
349 let total = standards.iter().fold(0, |acc, s| acc + s.1);
350
351 for standard in standards.iter_mut() {
352 standard.1 = (standard.1 as f64 / total as f64 * addr_to_gen_per_mnem as f64)
353 .ceil() as usize;
354 }
355
356 if self.logging {
357 println!(
358 "Found {} addresses whose standard is not supported",
359 unmatched_addresses.len()
360 );
361
362 for standard in standards.iter() {
363 println!("Standard: {}, Amount: {}", standard.0.base_path, standard.1);
364 }
365 }
366
367 standards
368 }
369 };
370
371 let mut found = HashMap::new();
372
373 for (index, mnemonic) in self.mnems.iter().enumerate() {
374 let addresses = Self::generate_addresses(&amount, mnemonic)?;
375
376 for addr in addresses {
377 if self.addrs.contains(&addr) {
378 if self.logging {
379 println!("Found address {} for mnemonic {:?}", addr, mnemonic);
380 }
381 found.entry(mnemonic).or_insert(vec![]).push(addr);
382 }
383 }
384
385 if self.logging && (self.mnems.len() / 10 > 0) && index % (self.mnems.len() / 10) == 0 {
386 println!(
387 "Processed {}% of the mnemonics ({}/{})",
388 index * 100 / self.mnems.len(),
389 index,
390 self.mnems.len()
391 );
392 }
393 }
394
395 Ok(found)
396 }
397
398 pub fn generate_addresses(
436 amount: &[(DerivationStandard, usize)],
437 mnemonic: &Mnemonic,
438 ) -> Result<Vec<String>, MatcherError> {
439 let mut addresses = vec![];
440 let seed = mnemonic.to_seed("");
441 let xpriv = Xpriv::new_master(Network::Bitcoin, &seed)
442 .map_err(|_| MatcherError::PrivKey("Failed to create master key".to_string()))?;
443
444 for (ds, n) in amount {
445 let mut base_path: String = ds.base_path.to_string();
446 base_path.reserve(n.to_string().len() + 10);
447
448 for i in 0..*n {
449 modify_path_index(&mut base_path, i)?;
450
451 let path = DerivationPath::from_str(&base_path)
452 .map_err(|_| MatcherError::from_derivation_path(base_path.as_str()))?;
453
454 let address = ds.into_address(&path, &xpriv)?;
455
456 addresses.push(address);
457 }
458 }
459
460 Ok(addresses)
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467 use bip39::Mnemonic;
468 use std::collections::HashSet;
469
470 #[test]
471 fn test_modify_path_index() {
472 let mut path = "m/44'/0'/0'/0/".to_string();
473 modify_path_index(&mut path, 1).unwrap();
474 assert_eq!(path, "m/44'/0'/0'/0/1");
475 modify_path_index(&mut path, 32).unwrap();
476 assert_eq!(path, "m/44'/0'/0'/0/32");
477
478 let mut path = "m/44'/0'/0'/0/".to_string();
479 modify_path_index(&mut path, 10).unwrap();
480 assert_eq!(path, "m/44'/0'/0'/0/10");
481 modify_path_index(&mut path, 100).unwrap();
482 assert_eq!(path, "m/44'/0'/0'/0/100");
483 modify_path_index(&mut path, 0).unwrap();
484 assert_eq!(path, "m/44'/0'/0'/0/0");
485
486 let mut path = "m/44'/0'/0'/0/".to_string();
487 modify_path_index(&mut path, 100).unwrap();
488 assert_eq!(path, "m/44'/0'/0'/0/100");
489 modify_path_index(&mut path, 1000).unwrap();
490 assert_eq!(path, "m/44'/0'/0'/0/1000");
491 modify_path_index(&mut path, 1).unwrap();
492 assert_eq!(path, "m/44'/0'/0'/0/1");
493 }
494
495 #[test]
496 fn test_derivation_standard_from_address() {
497 let standard = DerivationStandard::from_address("1BvB...");
498 assert_eq!(standard.unwrap().base_path, "m/44'/0'/0'/0/");
499
500 let standard = DerivationStandard::from_address("3J98...");
501 assert_eq!(standard.unwrap().base_path, "m/49'/0'/0'/0/");
502
503 let standard = DerivationStandard::from_address("bc1qar0s...");
504 assert_eq!(standard.unwrap().base_path, "m/84'/0'/0'/0/");
505
506 let standard = DerivationStandard::from_address("bc1par0s...");
507 assert_eq!(standard.unwrap().base_path, "m/86'/0'/0'/0/");
508 }
509
510 #[test]
511 fn test_derivation_standard_from_prefix() {
512 let standard = DerivationStandard::from_prefix("1");
513 assert_eq!(standard.unwrap().base_path, "m/44'/0'/0'/0/");
514
515 let standard = DerivationStandard::from_prefix("3");
516 assert_eq!(standard.unwrap().base_path, "m/49'/0'/0'/0/");
517
518 let standard = DerivationStandard::from_prefix("bc1q");
519 assert_eq!(standard.unwrap().base_path, "m/84'/0'/0'/0/");
520
521 let standard = DerivationStandard::from_prefix("bc1p");
522 assert_eq!(standard.unwrap().base_path, "m/86'/0'/0'/0/");
523 }
524
525 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";
526 const LEGACY_ADDRESSES: [&str; 10] = [
527 "1BGLgRL7EiFxS9H616bfoJPjSugKudECCn",
528 "1Kc48oyfPrTv9UD1Fk61dZkfxgRM83bU46",
529 "1MqmuiTTY8tTgBMFiDZSB9UygSnM5X9sd",
530 "15VRc3icVAJmrHC2CXQgecnRyUWc1oWVQW",
531 "19DosUKX9zQEdAHWaqCvq67pDAVs5AEpck",
532 "1CP296asrQ8hZnFP3GUjrpDcfyc71f6u2r",
533 "14kNFbSL4Tt67LG5H5p4Cje9CXqJvNKqfT",
534 "1E8CV9uRR8GZSuZAZsaFLHwbjKiQGmifqM",
535 "12k1o5tyV1HqimC2FWbN7gzktStByJMYma",
536 "1DsLE3UoAo98Vwmbi6a7pDKRcmkBB4Txzh",
537 ];
538
539 const SEG_WIT_ADDRESSES: [&str; 10] = [
540 "38cArkkfxxL7LtVWAwYNcvZTgf3KtDBcD7",
541 "37xqCGNb1rTokVVtjBkCoWBLxmAZjXjUyQ",
542 "3EXGQZu3GQnFzJLEyx27dxpjuGTenzfDka",
543 "3AyVvwN2SvgwFfQPD423jrg7Yw4umwZcWv",
544 "38VgHXNcp2wJZg5KVvAFuKSyD7fhGWVoz8",
545 "39vHAo5qSVvQj4XWNPJSzbvfhgKZ8PJF7N",
546 "36EHh1dKfXTURcR4N69KAHwAKoZuZp28Qi",
547 "37cEAoExVS2roXeu3QDVERAACMym8wL29d",
548 "32yaS5LmjcavajMcwU7Ebyq3FmXi7o85HT",
549 "32R9GYSTMqWrjW5oyX2oM4xzM52eL8J5jt",
550 ];
551
552 const NATIVE_SEG_WIT_ADDRESSES: [&str; 10] = [
553 "bc1q39ytrq296c6skxvrkd3m64j2fz5keep26q239t",
554 "bc1qe3kvt65klvzmnwehshzfv7w08cng6dqd3vfs2a",
555 "bc1q0c7qe879lh7x6cyufen7q8kmrf4rmr032u2ykx",
556 "bc1qvwn3a4jflzlkrxckjp76jhu5qhl7tjrw09wgc2",
557 "bc1qlmprztxqq4a8l8syfsn48u8v4zejmv4d5l3hle",
558 "bc1qyg60vy2wgp2sfmt825yu60kg7pg8t6mej8cjkh",
559 "bc1qlakvxvjj3raplc4xu678cr8g9kxdj9249gw7tg",
560 "bc1qg8e6kkwzmjek4mt4t6lzu5jm6fq8a2f9y9s9pa",
561 "bc1qncyyemztujnavnpqv0jpe53yayu0a8nrquztcl",
562 "bc1qszrsm6623dz5kf7dc7fyma0zq4enqtm4e8aqlx",
563 ];
564
565 const NOT_MATCHING_LEGACY_ADDRESSES: [&str; 10] = [
566 "15JcFwxJEEektpqjyQpRWPEHF9DQBX6NLy",
567 "1BZkYY2RdM4iLCD7mGuZu5DpGCssst1NuH",
568 "1Ga2CrW3unYeU4esEoNxtLanLXbsu4eR9Z",
569 "1HSXNFBkrHAH87NPXDMd8fDS9iTJaKUtih",
570 "1FqrZNdEs8yk5eV4kTZJzdpHdWbobFk5y4",
571 "17aUocSwJBDdQv8u7S7CN7vMMu7BEAJj1D",
572 "12GPo6Wih1Ps1MsvZ2Yo8gsRuJMfn4dpXu",
573 "14datpqKqjDMoAwsJQWxF7ZeZNgSDMwghC",
574 "1G8bC5cDtE3V6niunvd3B8zG9pdQNM8ePw",
575 "1KnjQiJjsdFhaPcKaxNPY9xubTHXibLKn6",
576 ];
577
578 const NOT_MATCHING_SEGWIT_ADDRESSES: [&str; 10] = [
579 "3BnH3FPZ9CpN4RfxxCJXFLt6tzibvYCi9k",
580 "3NYZQsjn8vYR4oE9xFdueLVY7ofMpSncEK",
581 "39hwATVfL8Nfe9P6ogpFU5xiDoaVPdWZsh",
582 "384QcePn5Z9ZixRXJwaaN5ot2ThPedJiMP",
583 "35MEXexaZyK6BTtikKxVadVeeVw9HhdMm7",
584 "3EScCZRYJjHuPaYvCevV2WCMJaeCoG9H1R",
585 "3JR8Y511MxV2cfP7i7wDXMizbnPZVC8zbo",
586 "3Ft9CkUDQx5ybvahFuSR7hggruGNeZRhuP",
587 "32VWQ66jafP47tE1q7i3aHZo7eYfHyaXHc",
588 "35MVo6Q48NUdtxc2dBxzm1SnEgZqXojPfA",
589 ];
590
591 const NOT_MATCHING_NATIVE_SEGWIT_ADDRESSES: [&str; 10] = [
592 "bc1q6wznd8c7v4pgwaugt5u3u9mfmus7fglpp9ef60",
593 "bc1qz9nx30gtl353gmr5ckmd7wg7jlxkmwl5q9dqr8",
594 "bc1qrwafwp7mq36zsryg98c45yj96c28qx3puahafp",
595 "bc1q560f5kg4me8tp28mpah7gpphhnwe5aa9wxck6e",
596 "bc1qumg52ymsm74y065x6n30uxns8dv5ul73trckgt",
597 "bc1q3f6swqv7r3prj9l5z9nphld9z5tzgmh3snf4zg",
598 "bc1q4z98wwqdjl5drg5esz98lzyf99fzsvyulyr6fy",
599 "bc1qp447e30gatvlww4sz2an9ymaugyeemtksw6fa8",
600 "bc1q0jvyt5hm42ukh7c3t98st0xmeyeu6w4thhadkp",
601 "bc1qlywz7zrwxna4pln0q5xdjpkwxvtnwk8qchz7xd",
602 ];
603
604 #[test]
607 fn test_match_in() {
608 let mut total_addresses = 0;
609
610 let mut addresses = HashSet::new();
611 let mut mnems = vec![];
612
613 for addr in LEGACY_ADDRESSES.iter() {
614 addresses.insert(addr.to_string());
615 total_addresses += 1;
616 }
617
618 for addr in SEG_WIT_ADDRESSES.iter() {
619 addresses.insert(addr.to_string());
620 total_addresses += 1;
621 }
622
623 for addr in NATIVE_SEG_WIT_ADDRESSES.iter() {
624 addresses.insert(addr.to_string());
625 total_addresses += 1;
626 }
627
628 let mnemonic = Mnemonic::from_str(MNEMONIC).unwrap();
629 mnems.push(mnemonic);
630
631 let matcher = Matcher::new(&addresses, &mnems, false);
632
633 let amount = vec![
634 (DerivationStandard::new("m/44'/0'/0'/0/", "1"), 10),
635 (DerivationStandard::new("m/49'/0'/0'/0/", "3"), 10),
636 (DerivationStandard::new("m/84'/0'/0'/0/", "bc1q"), 10),
637 ];
638
639 let found = matcher.match_in(total_addresses, Some(amount)).unwrap();
640
641 let mut mnemonic_with_addresses = 0;
642
643 for (_mn, addresses) in found {
644 assert_eq!(addresses.len(), total_addresses);
645 for addr in addresses {
646 assert!(matcher.addrs.contains(&addr));
647 }
648 mnemonic_with_addresses += 1;
649 }
650
651 assert_eq!(mnemonic_with_addresses, 1);
652 }
653
654 #[test]
655 fn test_match_in_auto() {
656 let mut total_addresses = 0;
657
658 let mut addresses = HashSet::new();
659 let mut mnems = vec![];
660
661 for addr in LEGACY_ADDRESSES.iter() {
662 addresses.insert(addr.to_string());
663 total_addresses += 1;
664 }
665
666 for addr in SEG_WIT_ADDRESSES.iter() {
667 addresses.insert(addr.to_string());
668 total_addresses += 1;
669 }
670
671 for addr in NATIVE_SEG_WIT_ADDRESSES.iter() {
672 addresses.insert(addr.to_string());
673 total_addresses += 1;
674 }
675
676 let mnemonic = Mnemonic::from_str(MNEMONIC).unwrap();
677 mnems.push(mnemonic);
678 let matcher = Matcher::new(&addresses, &mnems, false);
679 let found = matcher.match_in(total_addresses, None).unwrap();
680 let mut mnemonic_with_addresses = 0;
681
682 for (_mn, addresses) in found {
683 assert_eq!(addresses.len(), total_addresses);
684 for addr in addresses {
685 assert!(matcher.addrs.contains(&addr));
686 }
687 mnemonic_with_addresses += 1;
688 }
689
690 assert_eq!(mnemonic_with_addresses, 1);
691 }
692
693 #[test]
694 fn test_match_in_no_more_than_real_matches() {
695 let mut total_addresses = 0;
696
697 let mut addresses = HashSet::new();
698 let mut mnems = vec![];
699
700 for addr in LEGACY_ADDRESSES.iter() {
701 addresses.insert(addr.to_string());
702 total_addresses += 1;
703 }
704
705 for addr in SEG_WIT_ADDRESSES.iter() {
706 addresses.insert(addr.to_string());
707 total_addresses += 1;
708 }
709
710 for addr in NATIVE_SEG_WIT_ADDRESSES.iter() {
711 addresses.insert(addr.to_string());
712 total_addresses += 1;
713 }
714
715 let mut not_matching_addresses = 0;
716
717 for addr in NOT_MATCHING_LEGACY_ADDRESSES.iter() {
718 addresses.insert(addr.to_string());
719 not_matching_addresses += 1;
720 }
721
722 for addr in NOT_MATCHING_SEGWIT_ADDRESSES.iter() {
723 addresses.insert(addr.to_string());
724 not_matching_addresses += 1;
725 }
726
727 for addr in NOT_MATCHING_NATIVE_SEGWIT_ADDRESSES.iter() {
728 addresses.insert(addr.to_string());
729 not_matching_addresses += 1;
730 }
731
732 let mnemonic = Mnemonic::from_str(MNEMONIC).unwrap();
733 mnems.push(mnemonic);
734
735 let matcher = Matcher::new(&addresses, &mnems, false);
736
737 let found = matcher
738 .match_in(total_addresses + not_matching_addresses, None)
739 .unwrap();
740
741 let mut mnemonic_with_addresses = 0;
742
743 for (_mn, addresses) in found {
744 assert_eq!(addresses.len(), total_addresses);
745 for addr in addresses {
746 assert!(matcher.addrs.contains(&addr));
747 }
748 mnemonic_with_addresses += 1;
749 }
750
751 assert_eq!(mnemonic_with_addresses, 1);
752 }
753
754 #[test]
755 fn test_generate_addresses() {
756 let mnemonic = Mnemonic::from_str(MNEMONIC).unwrap();
757
758 let amount = vec![
759 (DerivationStandard::new("m/44'/0'/0'/0/", "1"), 10),
760 (DerivationStandard::new("m/49'/0'/0'/0/", "3"), 10),
761 (DerivationStandard::new("m/84'/0'/0'/0/", "bc1q"), 10),
762 ];
763
764 let addresses = Matcher::generate_addresses(&amount, &mnemonic).unwrap();
765
766 for (addr, legacy) in addresses.iter().zip(LEGACY_ADDRESSES.iter()) {
767 assert_eq!(addr, legacy);
768 }
769
770 for (addr, segwit) in addresses.iter().skip(10).zip(SEG_WIT_ADDRESSES.iter()) {
771 assert_eq!(addr, segwit);
772 }
773
774 for (addr, native_segwit) in addresses
775 .iter()
776 .skip(20)
777 .zip(NATIVE_SEG_WIT_ADDRESSES.iter())
778 {
779 assert_eq!(addr, native_segwit);
780 }
781 }
782}