deterministicwallet.py (9899B)
1 2 import logging 3 4 import electrumpersonalserver.bitcoin as btc 5 from electrumpersonalserver.server.hashes import bh2u, hash_160, bfh, sha256,\ 6 address_to_script, script_to_address 7 from electrumpersonalserver.server.jsonrpc import JsonRpcError 8 9 #the wallet types are here 10 #https://github.com/spesmilo/electrum/blob/3.0.6/RELEASE-NOTES 11 #and 12 #https://github.com/spesmilo/electrum-docs/blob/master/xpub_version_bytes.rst 13 14 ADDRESSES_LABEL = "electrum-watchonly-addresses" 15 16 def import_addresses(rpc, watchonly_addrs, wallets, change_param, count, 17 logger=None): 18 """ 19 change_param = 0 for receive, 1 for change, -1 for both 20 """ 21 logger = logger if logger else logging.getLogger('ELECTRUMPERSONALSERVER') 22 logger.debug("Importing " + str(len(watchonly_addrs)) + " watch-only " 23 + "address[es] and " + str(len(wallets)) + " wallet[s] into label \"" 24 + ADDRESSES_LABEL + "\"") 25 26 watchonly_addr_param = [{"scriptPubKey": {"address": addr}, "label": 27 ADDRESSES_LABEL, "watchonly": True, "timestamp": "now"} 28 for addr in watchonly_addrs] 29 rpc.call("importmulti", [watchonly_addr_param, {"rescan": False}]) 30 31 for i, wal in enumerate(wallets): 32 logger.info("Importing wallet " + str(i+1) + "/" + str(len(wallets))) 33 if isinstance(wal, DescriptorDeterministicWallet): 34 if change_param in (0, -1): 35 #import receive addrs 36 rpc.call("importmulti", [[{"desc": wal.descriptors[0], "range": 37 [0, count-1], "label": ADDRESSES_LABEL, "watchonly": True, 38 "timestamp": "now"}], {"rescan": False}]) 39 if change_param in (1, -1): 40 #import change addrs 41 rpc.call("importmulti", [[{"desc": wal.descriptors[1], "range": 42 [0, count-1], "label": ADDRESSES_LABEL, "watchonly": True, 43 "timestamp": "now"}], {"rescan": False}]) 44 else: 45 #old-style-seed wallets 46 logger.info("importing an old-style-seed wallet, will be slow...") 47 for change in [0, 1]: 48 addrs, spks = wal.get_addresses(change, 0, count) 49 addr_param = [{"scriptPubKey": {"address": a}, "label": 50 ADDRESSES_LABEL, "watchonly": True, "timestamp": "now"} 51 for a in addrs] 52 rpc.call("importmulti", [addr_param, {"rescan": False}]) 53 logger.debug("Importing done") 54 55 56 def is_string_parsable_as_hex_int(s): 57 try: 58 int(s, 16) 59 return True 60 except: 61 return False 62 63 def parse_electrum_master_public_key(keydata, gaplimit, rpc, chain): 64 if chain == "main": 65 xpub_vbytes = b"\x04\x88\xb2\x1e" 66 elif chain == "test" or chain == "regtest": 67 xpub_vbytes = b"\x04\x35\x87\xcf" 68 else: 69 assert False 70 71 #https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md 72 73 descriptor_template = None 74 if keydata[:4] in ("xpub", "tpub"): 75 descriptor_template = "pkh({xpub}/{change}/*)" 76 elif keydata[:4] in ("zpub", "vpub"): 77 descriptor_template = "wpkh({xpub}/{change}/*)" 78 elif keydata[:4] in ("ypub", "upub"): 79 descriptor_template = "sh(wpkh({xpub}/{change}/*))" 80 81 if descriptor_template != None: 82 wallet = SingleSigWallet(rpc, xpub_vbytes, keydata, descriptor_template) 83 elif is_string_parsable_as_hex_int(keydata) and len(keydata) == 128: 84 wallet = SingleSigOldMnemonicWallet(rpc, keydata) 85 elif keydata.find(" ") != -1: #multiple keys = multisig 86 chunks = keydata.split(" ") 87 try: 88 m = int(chunks[0]) 89 except ValueError: 90 raise ValueError("Unable to parse m in multisig key data: " 91 + chunks[0]) 92 pubkeys = chunks[1:] 93 if not all([pubkeys[0][:4] == pub[:4] for pub in pubkeys[1:]]): 94 raise ValueError("Inconsistent master public key types") 95 if pubkeys[0][:4] in ("xpub", "tpub"): 96 descriptor_script = "sh(sortedmulti(" 97 elif pubkeys[0][:4] in ("Zpub", "Vpub"): 98 descriptor_script = "wsh(sortedmulti(" 99 elif pubkeys[0][:4] in ("Ypub", "Upub"): 100 descriptor_script = "sh(wsh(sortedmulti(" 101 wallet = MultisigWallet(rpc, xpub_vbytes, m, pubkeys, descriptor_script) 102 else: 103 raise ValueError("Unrecognized electrum mpk format: " + keydata[:4]) 104 wallet.gaplimit = gaplimit 105 return wallet 106 107 class DeterministicWallet(object): 108 def __init__(self, rpc): 109 self.gaplimit = 0 110 self.next_index = [0, 0] 111 self.scriptpubkey_index = {} 112 self.rpc = rpc 113 114 def _derive_addresses(self, change, from_index, count): 115 raise RuntimeError() 116 117 def get_addresses(self, change, from_index, count): 118 """Returns addresses from this deterministic wallet""" 119 addrs = self._derive_addresses(change, from_index, count) 120 spks = [address_to_script(a, self.rpc) for a in addrs] 121 for index, spk in enumerate(spks): 122 self.scriptpubkey_index[spk] = (change, from_index + index) 123 self.next_index[change] = max(self.next_index[change], from_index+count) 124 return addrs, spks 125 126 def get_new_addresses(self, change, count): 127 """Returns newly-generated addresses from this deterministic wallet""" 128 addrs, spks = self.get_addresses(change, self.next_index[change], count) 129 return addrs, spks 130 131 #called in check_for_new_txes() when a new tx of ours arrives 132 #to see if we need to import more addresses 133 def have_scriptpubkeys_overrun_gaplimit(self, scriptpubkeys): 134 """Return None if they havent, or how many addresses to 135 import if they have""" 136 result = {} 137 for spk in scriptpubkeys: 138 if spk not in self.scriptpubkey_index: 139 continue 140 change, index = self.scriptpubkey_index[spk] 141 distance_from_next = self.next_index[change] - index 142 if distance_from_next > self.gaplimit: 143 continue 144 #need to import more 145 if change in result: 146 result[change] = max(result[change], self.gaplimit 147 - distance_from_next + 1) 148 else: 149 result[change] = self.gaplimit - distance_from_next + 1 150 if len(result) > 0: 151 return result 152 else: 153 return None 154 155 def rewind_one(self, change): 156 """Go back one pubkey in a branch""" 157 self.next_index[change] -= 1 158 159 class DescriptorDeterministicWallet(DeterministicWallet): 160 def __init__(self, rpc, xpub_vbytes, *args): 161 super(DescriptorDeterministicWallet, self).__init__(rpc) 162 self.xpub_vbytes = xpub_vbytes 163 164 descriptors_without_checksum = \ 165 self.obtain_descriptors_without_checksum(args) 166 167 try: 168 self.descriptors = [] 169 for desc in descriptors_without_checksum: 170 self.descriptors.append(self.rpc.call("getdescriptorinfo", 171 [desc])["descriptor"]) 172 except JsonRpcError as e: 173 raise ValueError(repr(e)) 174 175 def obtain_descriptors_without_checksum(self, *args): 176 raise RuntimeError() 177 178 def _derive_addresses(self, change, from_index, count): 179 return self.rpc.call("deriveaddresses", [self.descriptors[change], [ 180 from_index, from_index + count - 1]]) 181 ##the minus 1 is because deriveaddresses uses inclusive range 182 ##e.g. to get just the first address you use [0, 0] 183 184 def _convert_to_standard_xpub(self, mpk): 185 return btc.bip32_serialize((self.xpub_vbytes, *btc.bip32_deserialize( 186 mpk)[1:])) 187 188 class SingleSigWallet(DescriptorDeterministicWallet): 189 def __init__(self, rpc, xpub_vbytes, xpub, descriptor_template): 190 super(SingleSigWallet, self).__init__(rpc, xpub_vbytes, xpub, 191 descriptor_template) 192 193 def obtain_descriptors_without_checksum(self, args): 194 ##example descriptor_template: 195 #"pkh({xpub}/{change}/*)" 196 xpub, descriptor_template = args 197 198 descriptors_without_checksum = [] 199 xpub = self._convert_to_standard_xpub(xpub) 200 for change in [0, 1]: 201 descriptors_without_checksum.append(descriptor_template.format( 202 change=change, xpub=xpub)) 203 return descriptors_without_checksum 204 205 class MultisigWallet(DescriptorDeterministicWallet): 206 def __init__(self, rpc, xpub_vbytes, m, xpub_list, descriptor_script): 207 super(MultisigWallet, self).__init__(rpc, xpub_vbytes, m, xpub_list, 208 descriptor_script) 209 210 def obtain_descriptors_without_checksum(self, args): 211 ##example descriptor_script: 212 #"sh(sortedmulti(" 213 m, xpub_list, descriptor_script = args 214 215 descriptors_without_checksum = [] 216 xpub_list = [self._convert_to_standard_xpub(xpub) for xpub in xpub_list] 217 for change in [0, 1]: 218 descriptors_without_checksum.append(descriptor_script + str(m) +\ 219 "," + ",".join([xpub + "/" + str(change) + "/*" 220 for xpub in xpub_list]) + ")"*descriptor_script.count("(")) 221 return descriptors_without_checksum 222 223 class SingleSigOldMnemonicWallet(DeterministicWallet): 224 def __init__(self, rpc, mpk): 225 super(SingleSigOldMnemonicWallet, self).__init__(rpc) 226 self.mpk = mpk 227 228 def _pubkey_to_scriptpubkey(self, pubkey): 229 pkh = bh2u(hash_160(bfh(pubkey))) 230 #op_dup op_hash_160 length hash160 op_equalverify op_checksig 231 return "76a914" + pkh + "88ac" 232 233 def _derive_addresses(self, change, from_index, count): 234 result = [] 235 for index in range(from_index, from_index + count): 236 pubkey = btc.electrum_pubkey(self.mpk, index, change) 237 scriptpubkey = self._pubkey_to_scriptpubkey(pubkey) 238 result.append(script_to_address(scriptpubkey, self.rpc)) 239 return result