def _make_multisigs(self): def get_pubkeys(t): desc_pubkeys = [] sorted_pubkeys = [] for i in range(0, 3): path = "/48h/1h/{}h/{}h/0/0".format(i, t) origin = '{}{}'.format(self.fingerprint, path) xpub = self.do_command( self.dev_args + ["--expert", "getxpub", "m{}".format(path)]) desc_pubkeys.append("[{}]{}".format(origin, xpub["pubkey"])) sorted_pubkeys.append(xpub["pubkey"]) sorted_pubkeys.sort() return desc_pubkeys, sorted_pubkeys desc_pubkeys, sorted_pubkeys = get_pubkeys(0) sh_desc = AddChecksum("sh(sortedmulti(2,{},{},{}))".format( desc_pubkeys[0], desc_pubkeys[1], desc_pubkeys[2])) sh_ms_info = self.rpc.createmultisig(2, sorted_pubkeys, "legacy") self.assertEqual( self.rpc.deriveaddresses(sh_desc)[0], sh_ms_info["address"]) # Trezor requires that each address type uses a different derivation path. # Other devices don't have this requirement, and in the tests involving multiple address types, Coldcard will fail. # So for those other devices, stick to the 0 path. desc_pubkeys, sorted_pubkeys = get_pubkeys( 1) if self.full_type == "trezor_t" else get_pubkeys(0) sh_wsh_desc = AddChecksum("sh(wsh(sortedmulti(2,{},{},{})))".format( desc_pubkeys[1], desc_pubkeys[2], desc_pubkeys[0])) sh_wsh_ms_info = self.rpc.createmultisig(2, sorted_pubkeys, "p2sh-segwit") self.assertEqual( self.rpc.deriveaddresses(sh_wsh_desc)[0], sh_wsh_ms_info["address"]) desc_pubkeys, sorted_pubkeys = get_pubkeys( 2) if self.full_type == "trezor_t" else get_pubkeys(0) wsh_desc = AddChecksum("wsh(sortedmulti(2,{},{},{}))".format( desc_pubkeys[2], desc_pubkeys[1], desc_pubkeys[0])) wsh_ms_info = self.rpc.createmultisig(2, sorted_pubkeys, "bech32") self.assertEqual( self.rpc.deriveaddresses(wsh_desc)[0], wsh_ms_info["address"]) return sh_desc, sh_ms_info["address"], sh_wsh_desc, sh_wsh_ms_info[ "address"], wsh_desc, wsh_ms_info["address"]
def parse_old_format(wallet_dict, device_manager): old_format_detected = False new_dict = {} new_dict.update(wallet_dict) if 'key' in wallet_dict: new_dict['keys'] = [wallet_dict['key']] del new_dict['key'] old_format_detected = True if 'device' in wallet_dict: new_dict['devices'] = [wallet_dict['device']] del new_dict['device'] old_format_detected = True devices = [ device_manager.get_by_alias(device) for device in new_dict['devices'] ] if len(new_dict['keys'] ) > 1 and 'sortedmulti' not in new_dict['recv_descriptor']: new_dict['recv_descriptor'] = AddChecksum( new_dict['recv_descriptor'].replace( 'multi', 'sortedmulti').split('#')[0]) old_format_detected = True if len(new_dict['keys'] ) > 1 and 'sortedmulti' not in new_dict['change_descriptor']: new_dict['change_descriptor'] = AddChecksum( new_dict['change_descriptor'].replace( 'multi', 'sortedmulti').split('#')[0]) old_format_detected = True if None in devices: devices = [ ((device['name'] if isinstance(device, dict) else device) if (device['name'] if isinstance(device, dict) else device) in device_manager.devices else None) for device in new_dict['devices'] ] if None in devices: raise Exception( 'A device used by this wallet could not have been found!') else: new_dict['devices'] = [ device_manager.devices[device].alias for device in devices ] old_format_detected = True new_dict['old_format_detected'] = old_format_detected return new_dict
def parse_old_format(wallet_dict, device_manager): old_format_detected = False new_dict = {} new_dict.update(wallet_dict) if "key" in wallet_dict: new_dict["keys"] = [wallet_dict["key"]] del new_dict["key"] old_format_detected = True if "device" in wallet_dict: new_dict["devices"] = [wallet_dict["device"]] del new_dict["device"] old_format_detected = True devices = [ device_manager.get_by_alias(device) for device in new_dict["devices"] ] if (len(new_dict["keys"]) > 1 and "sortedmulti" not in new_dict["recv_descriptor"]): new_dict["recv_descriptor"] = AddChecksum( new_dict["recv_descriptor"].replace( "multi", "sortedmulti").split("#")[0]) old_format_detected = True if (len(new_dict["keys"]) > 1 and "sortedmulti" not in new_dict["change_descriptor"]): new_dict["change_descriptor"] = AddChecksum( new_dict["change_descriptor"].replace( "multi", "sortedmulti").split("#")[0]) old_format_detected = True if None in devices: devices = [ ((device["name"] if isinstance(device, dict) else device) if (device["name"] if isinstance(device, dict) else device) in device_manager.devices else None) for device in new_dict["devices"] ] if None in devices: raise Exception( "A device used by this wallet could not have been found!") else: new_dict["devices"] = [ device_manager.devices[device].alias for device in devices ] old_format_detected = True new_dict["old_format_detected"] = old_format_detected return new_dict
def keypoolrefill(self, start, end=None, change=False): if end is None: end = start + self.GAP_LIMIT desc = self.recv_descriptor if not change else self.change_descriptor args = [{ "desc": desc, "internal": change, "range": [start, end], "timestamp": "now", "keypool": True, "watchonly": True, }] if not self.is_multisig: r = self.rpc.importmulti(args, {"rescan": False}) # bip67 requires sorted public keys for multisig addresses else: # try if sortedmulti is supported r = self.rpc.importmulti(args, {"rescan": False}) # doesn't raise, but instead returns "success": False if not r[0]["success"]: # first import normal multi # remove checksum desc = desc.split("#")[0] # switch to multi desc = desc.replace("sortedmulti", "multi") # add checksum desc = AddChecksum(desc) # update descriptor args[0]["desc"] = desc r = self.rpc.importmulti(args, {"rescan": False}) # make a batch of single addresses to import arg = args[0] # remove range key arg.pop("range") batch = [] for i in range(start, end): sorted_desc = sort_descriptor(self.rpc, desc, index=i, change=change) # create fresh object obj = {} obj.update(arg) obj.update({"desc": sorted_desc}) batch.append(obj) r = self.rpc.importmulti(batch, {"rescan": False}) if change: self.change_keypool = end else: self.keypool = end self.rpc.multi([( "setlabel", self.get_address(i, change=change, check_keypool=False), "{} #{}".format("Change" if change else "Address", i), ) for i in range(start, end)]) self.save_to_file() return end
def _make_multisig(self, addrtype): if addrtype == "legacy": coin_type = 0 desc_prefix = "sh(" desc_suffix = ")" elif addrtype == "p2sh-segwit": coin_type = 1 if self.emulator.strict_bip48 else 0 desc_prefix = "sh(wsh(" desc_suffix = "))" elif addrtype == "bech32": coin_type = 2 if self.emulator.strict_bip48 else 0 desc_prefix = "wsh(" desc_suffix = ")" else: self.fail(f"Unknown address type {addrtype}") desc_pubkeys = [] xpubs: Dict[bytes, KeyOriginInfo] = {} for account in range(0, 3 if self.emulator.supports_device_multiple_multisig else 1): path = f"/48h/1h/{account}h/{coin_type}h" origin = '{}{}'.format(self.emulator.fingerprint, path) xpub = self.do_command(self.dev_args + ["getxpub", "m{}".format(path)]) desc_pubkeys.append("[{}]{}/0/0".format(origin, xpub["xpub"])) if self.emulator.include_xpubs: extkey = ExtendedKey.deserialize(xpub["xpub"]) xpubs[extkey.serialize()] = KeyOriginInfo.from_string(origin) if not self.emulator.supports_device_multiple_multisig: # If the device does not support itself in the multisig more than once, # we need to fetch a key from Core, and use another key that will not be signed with counter_descs = self.wpk_rpc.listdescriptors()["descriptors"] desc = parse_descriptor(counter_descs[0]["desc"]) pubkey_prov = None while pubkey_prov is None: if len(desc.pubkeys) > 0: pubkey_prov = desc.pubkeys[0] else: desc = desc.subdescriptors[0] assert pubkey_prov.extkey is not None assert pubkey_prov.origin is not None pubkey_prov.deriv_path = "/0/0" desc_pubkeys.append(pubkey_prov.to_string()) if self.emulator.include_xpubs: xpubs[pubkey_prov.extkey.serialize()] = pubkey_prov.origin # A fixed key fixed_extkey = ExtendedKey.deserialize("tpubDCBWBScQPGv4Xk3JSbhw6wYYpayMjb2eAYyArpbSqQTbLDpphHGAetB6VQgVeftLML8vDSUEWcC2xDi3qJJ3YCDChJDvqVzpgoYSuT52MhJ") fixed_origin = KeyOriginInfo(b"\xde\xad\xbe\xef", [0x80000000]) desc_pubkeys.append(PubkeyProvider(fixed_origin, fixed_extkey.to_string(), "/0/0").to_string()) if self.emulator.include_xpubs: xpubs[fixed_extkey.serialize()] = fixed_origin desc = AddChecksum(f"{desc_prefix}sortedmulti(2,{desc_pubkeys[0]},{desc_pubkeys[1]},{desc_pubkeys[2]}){desc_suffix}") return desc, self.rpc.deriveaddresses(desc)[0], xpubs
def _make_single_multisig(self, addrtype): desc_pubkeys = [] sorted_pubkeys = [] for i in range(0, 3): path = "/48h/1h/{}h/0h/0/0".format(i) origin = '{}{}'.format(self.fingerprint, path) xpub = self.do_command(self.dev_args + ["--expert", "getxpub", "m{}".format(path)]) desc_pubkeys.append("[{}]{}".format(origin, xpub["pubkey"])) sorted_pubkeys.append((xpub["pubkey"], origin)) sorted_pubkeys.sort(key=lambda tup: tup[0]) if addrtype == "pkh": desc = AddChecksum("sh(sortedmulti(2,{},{},{}))".format( desc_pubkeys[0], desc_pubkeys[1], desc_pubkeys[2])) ms_info = self.rpc.createmultisig(2, [x[0] for x in sorted_pubkeys], "legacy") elif addrtype == "sh_wpkh": desc = AddChecksum("sh(wsh(sortedmulti(2,{},{},{})))".format( desc_pubkeys[1], desc_pubkeys[2], desc_pubkeys[0])) ms_info = self.rpc.createmultisig(2, [x[0] for x in sorted_pubkeys], "p2sh-segwit") elif addrtype == "wpkh": desc = AddChecksum("wsh(sortedmulti(2,{},{},{}))".format( desc_pubkeys[2], desc_pubkeys[1], desc_pubkeys[0])) ms_info = self.rpc.createmultisig(2, [x[0] for x in sorted_pubkeys], "bech32") else: self.fail("Oops the test is broken") self.assertEqual(self.rpc.deriveaddresses(desc)[0], ms_info["address"]) path = "{},{},{}".format(sorted_pubkeys[0][1], sorted_pubkeys[1][1], sorted_pubkeys[2][1]) return ms_info["address"], desc, ms_info["redeemScript"], path
def sort_descriptor(cli, descriptor, index=None, change=False): descriptor = descriptor.replace("sortedmulti", "multi") if index is not None: descriptor = descriptor.replace("*", f"{index}") # remove checksum descriptor = descriptor.split("#")[0] # get address (should be already imported to the wallet) address = cli.deriveaddresses(AddChecksum(descriptor), change=change)[0] # get pubkeys involved address_info = cli.getaddressinfo(address) if 'pubkeys' in address_info: pubkeys = address_info["pubkeys"] elif 'embedded' in address_info and 'pubkeys' in address_info['embedded']: pubkeys = address_info["embedded"]["pubkeys"] else: raise Exception("Could not find 'pubkeys' in address info:\n%s" % json.dumps(address_info, indent=2)) # get xpubs from the descriptor arr = descriptor.split("(multi(")[1].split(")")[0].split(",") # getting [wsh] or [sh, wsh] prefix = descriptor.split("(multi(")[0].split("(") sigs_required = arr[0] keys = arr[1:] # sort them according to sortedmulti z = sorted(zip(pubkeys, keys), key=lambda x: x[0]) keys = [zz[1] for zz in z] inner = f"{sigs_required}," + ",".join(keys) desc = f"multi({inner})" # Write from the inside out prefix.reverse() for p in prefix: desc = f"{p}({desc})" return AddChecksum(desc)
def test_display_address_xpub_multisig(self): if self.full_type not in SUPPORTS_XPUB_MS_DISPLAY: raise unittest.SkipTest("{} does not support multsig display with xpubs".format(self.full_type)) account_xpub = self.do_command(self.dev_args + ['getxpub', 'm/48h/1h/0h/0h'])['xpub'] desc = 'wsh(multi(2,[' + self.fingerprint + '/48h/1h/0h/0h]' + account_xpub + '/0/0,[' + self.fingerprint + '/48h/1h/0h/0h]' + account_xpub + '/1/0))' result = self.do_command(self.dev_args + ['displayaddress', '--desc', desc]) self.assertNotIn('error', result) self.assertNotIn('code', result) self.assertIn('address', result) addr = self.rpc.deriveaddresses(AddChecksum(desc))[0] # removes prefix and checksum since regtest gives # prefix `bcrt` on Bitcoin Core while wallets return testnet `tb` prefix self.assertEqual(addr[4:58], result['address'][2:56])
def get_descriptor(self, index=None, change=False, address=None): """ Returns address descriptor from index, change or from address belonging to the wallet. """ if address is not None: d = self.rpc.getaddressinfo(address)['desc'] path = d.split("[")[1].split("]")[0].split("/") change = bool(int(path[-2])) index = int(path[-1]) if index is None: index = self.change_index if change else self.address_index desc = self.change_descriptor if change else self.recv_descriptor desc = desc.split("#")[0] return AddChecksum(desc.replace("*", f"{index}"))
def create_wallet(self, name, sigs_required, key_type, keys, devices): walletsindir = [ wallet["name"] for wallet in self.cli.listwalletdir()["wallets"] ] wallet_alias = alias(name) i = 2 while os.path.isfile( os.path.join(self.working_folder, "%s.json" % wallet_alias) ) or os.path.join(self.cli_path, wallet_alias) in walletsindir: wallet_alias = alias("%s %d" % (name, i)) i += 1 arr = key_type.split("-") descs = [key.metadata['combined'] for key in keys] recv_descs = ["%s/0/*" % desc for desc in descs] change_descs = ["%s/1/*" % desc for desc in descs] if len(keys) > 1: recv_descriptor = "sortedmulti({},{})" \ .format(sigs_required, ",".join(recv_descs)) change_descriptor = "sortedmulti({},{})" \ .format(sigs_required, ",".join(change_descs)) else: recv_descriptor = recv_descs[0] change_descriptor = change_descs[0] for el in arr[::-1]: recv_descriptor = "%s(%s)" % (el, recv_descriptor) change_descriptor = "%s(%s)" % (el, change_descriptor) recv_descriptor = AddChecksum(recv_descriptor) change_descriptor = AddChecksum(change_descriptor) self.cli.createwallet(os.path.join(self.cli_path, wallet_alias), True) self.wallets[name] = Wallet( name, wallet_alias, "{} of {} {}".format( sigs_required, len(keys), purposes[key_type] ) if len(keys) > 1 else purposes[key_type], addrtypes[key_type], '', -1, '', -1, 0, 0, recv_descriptor, change_descriptor, keys, devices, sigs_required, {}, os.path.join(self.working_folder, "%s.json" % wallet_alias), self.device_manager, self ) # save wallet file to disk if self.working_folder is not None: self.wallets[name].save_to_file() # get Wallet class instance return self.wallets[name]
def setup_device(self, mnemonic, passphrase, wallet_manager, testnet): seed = Mnemonic.to_seed(mnemonic) xprv = seed_to_hd_master_key(seed, testnet=testnet) wallet_name = os.path.join(wallet_manager.cli_path + '_hotstorage', self.alias) wallet_manager.cli.createwallet(wallet_name, False, True) cli = wallet_manager.cli.wallet(wallet_name) # TODO: Maybe more than 1000? Maybe add mechanism to add more later. # NOTE: This will work only on the network the device was added, # so hot devices should be filtered out by network. coin = int(testnet) cli.importmulti([ { 'desc': AddChecksum('sh(wpkh({}/49h/{}h/0h/0/*))'.format(xprv, coin)), 'range': 1000, 'timestamp': 'now', }, { 'desc': AddChecksum('sh(wpkh({}/49h/{}h/0h/1/*))'.format(xprv, coin)), 'range': 1000, 'timestamp': 'now', }, { 'desc': AddChecksum('wpkh({}/84h/{}h/0h/0/*)'.format( xprv, coin)), 'range': 1000, 'timestamp': 'now', }, { 'desc': AddChecksum('wpkh({}/84h/{}h/0h/1/*)'.format( xprv, coin)), 'range': 1000, 'timestamp': 'now', }, { 'desc': AddChecksum('sh(wpkh({}/48h/{}h/0h/1h/0/*))'.format( xprv, coin)), 'range': 1000, 'timestamp': 'now', }, { 'desc': AddChecksum('sh(wpkh({}/48h/{}h/0h/1h/1/*))'.format( xprv, coin)), 'range': 1000, 'timestamp': 'now', }, { 'desc': AddChecksum('wpkh({}/48h/{}h/0h/2h/0/*)'.format(xprv, coin)), 'range': 1000, 'timestamp': 'now', }, { 'desc': AddChecksum('wpkh({}/48h/{}h/0h/2h/1/*)'.format(xprv, coin)), 'range': 1000, 'timestamp': 'now', }, ], {"rescan": False}) if passphrase: cli.encryptwallet(passphrase) xpubs_str = "" paths = [ "m", # to get fingerprint f"m/49h/{coin}h/0h", # nested f"m/84h/{coin}h/0h", # native f"m/48h/{coin}h/0h/1h", # nested multisig f"m/48h/{coin}h/0h/2h", # native multisig ] xpubs = derive_xpubs_from_xprv(xprv, paths, wallet_manager.cli) # it's not parent fingerprint, it's self fingerprint master_fpr = get_xpub_fingerprint(xpubs[0]).hex() if not testnet: # Nested Segwit xpub = xpubs[1] ypub = convert_xpub_prefix(xpub, b'\x04\x9d\x7c\xb2') xpubs_str += "[%s/49'/0'/0']%s\n" % (master_fpr, ypub) # native Segwit xpub = xpubs[2] zpub = convert_xpub_prefix(xpub, b'\x04\xb2\x47\x46') xpubs_str += "[%s/84'/0'/0']%s\n" % (master_fpr, zpub) # Multisig nested Segwit xpub = xpubs[3] Ypub = convert_xpub_prefix(xpub, b'\x02\x95\xb4\x3f') xpubs_str += "[%s/48'/0'/0'/1']%s\n" % (master_fpr, Ypub) # Multisig native Segwit xpub = xpubs[4] Zpub = convert_xpub_prefix(xpub, b'\x02\xaa\x7e\xd3') xpubs_str += "[%s/48'/0'/0'/2']%s\n" % (master_fpr, Zpub) else: # Testnet nested Segwit xpub = xpubs[1] upub = convert_xpub_prefix(xpub, b'\x04\x4a\x52\x62') xpubs_str += "[%s/49'/1'/0']%s\n" % (master_fpr, upub) # Testnet native Segwit xpub = xpubs[2] vpub = convert_xpub_prefix(xpub, b'\x04\x5f\x1c\xf6') xpubs_str += "[%s/84'/1'/0']%s\n" % (master_fpr, vpub) # Testnet multisig nested Segwit xpub = xpubs[3] Upub = convert_xpub_prefix(xpub, b'\x02\x42\x89\xef') xpubs_str += "[%s/48'/1'/0'/1']%s\n" % (master_fpr, Upub) # Testnet multisig native Segwit xpub = xpubs[4] Vpub = convert_xpub_prefix(xpub, b'\x02\x57\x54\x83') xpubs_str += "[%s/48'/1'/0'/2']%s\n" % (master_fpr, Vpub) keys, failed = Key.parse_xpubs(xpubs_str) if len(failed) > 0: # TODO: This should never occur, but just in case, # we must make sure to catch it properly so it # doesn't crash the app no matter what. raise Exception("Failed to parse these xpubs:\n" + "\n".join(failed)) else: self.add_keys(keys)