Beispiel #1
0
    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"]
Beispiel #2
0
 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
Beispiel #3
0
 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
Beispiel #4
0
 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
Beispiel #5
0
    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
Beispiel #6
0
    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
Beispiel #7
0
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)
Beispiel #8
0
    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])
Beispiel #9
0
 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]
Beispiel #11
0
    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)