URI: 
       tUse RPC deriveaddresses to generate addresses - electrum-personal-server - Maximally lightweight electrum server for a single user
  HTML git clone https://git.parazyd.org/electrum-personal-server
   DIR Log
   DIR Files
   DIR Refs
   DIR README
       ---
   DIR commit 3e52ed9e11aab176fa2d8e8c619a9cae568424d4
   DIR parent db78fb9e2d819f80b7501c43d56d96b8c22f4237
  HTML Author: chris-belcher <chris-belcher@users.noreply.github.com>
       Date:   Tue,  5 May 2020 21:46:45 +0100
       
       Use RPC deriveaddresses to generate addresses
       
       Output Descriptors and the RPC call deriveaddresses can generate
       addresses much faster than the previously-used pure python routines.
       
       This functionality is only fully supported in Bitcoin Core 0.20.0 so
       tthe code checks for that version.
       
       Diffstat:
         M electrumpersonalserver/bitcoin/det… |       6 +++---
         M electrumpersonalserver/server/comm… |      57 +++++++++++++++++++++-----------
         M electrumpersonalserver/server/dete… |     248 ++++++++++++++-----------------
         M electrumpersonalserver/server/tran… |       5 ++---
       
       4 files changed, 154 insertions(+), 162 deletions(-)
       ---
   DIR diff --git a/electrumpersonalserver/bitcoin/deterministic.py b/electrumpersonalserver/bitcoin/deterministic.py
       t@@ -40,7 +40,7 @@ def raw_bip32_ckd(rawtuple, i):
        
            if i >= 2**31:
                if vbytes in PUBLIC:
       -            raise Exception("Can't do private derivation on public key!")
       +            raise ValueError("Can't do private derivation on public key!")
                I = hmac.new(chaincode, b'\x00' + priv[:32] + encode(i, 256, 4),
                             hashlib.sha512).digest()
            else:
       t@@ -70,7 +70,7 @@ def bip32_serialize(rawtuple):
        def bip32_deserialize(data):
            dbin = changebase(data, 58, 256)
            if bin_dbl_sha256(dbin[:-4])[:4] != dbin[-4:]:
       -        raise Exception("Invalid checksum")
       +        raise ValueError("Invalid checksum")
            vbytes = dbin[0:4]
            depth = from_byte_to_int(dbin[4])
            fingerprint = dbin[5:9]
       t@@ -118,7 +118,7 @@ def raw_crack_bip32_privkey(parent_pub, priv):
            i = int(i)
        
            if i >= 2**31:
       -        raise Exception("Can't crack private derivation!")
       +        raise ValueError("Can't crack private derivation!")
        
            I = hmac.new(pchaincode, pkey + encode(i, 256, 4), hashlib.sha512).digest()
        
   DIR diff --git a/electrumpersonalserver/server/common.py b/electrumpersonalserver/server/common.py
       t@@ -174,34 +174,39 @@ def get_scriptpubkeys_to_monitor(rpc, config):
        
            deterministic_wallets = []
            for key in config.options("master-public-keys"):
       -        wal = deterministicwallet.parse_electrum_master_public_key(
       -            config.get("master-public-keys", key),
       -            int(config.get("bitcoin-rpc", "gap_limit")))
       +        mpk = config.get("master-public-keys", key)
       +        gaplimit = int(config.get("bitcoin-rpc", "gap_limit"))
       +        chain = rpc.call("getblockchaininfo", [])["chain"]
       +        try:
       +            wal = deterministicwallet.parse_electrum_master_public_key(mpk,
       +                gaplimit, rpc, chain)
       +        except ValueError:
       +            raise ValueError("Bad master public key format. Get it from " +
       +                "Electrum menu `Wallet` -> `Information`")
                deterministic_wallets.append(wal)
        
            #check whether these deterministic wallets have already been imported
            import_needed = False
            wallets_imported = 0
       -    spks_to_import = []
       +    addresses_to_import = []
            TEST_ADDR_COUNT = 3
            logger.info("Displaying first " + str(TEST_ADDR_COUNT) + " addresses of " +
                "each master public key:")
            for config_mpk_key, wal in zip(config.options("master-public-keys"),
                    deterministic_wallets):
       -        first_spks = wal.get_scriptpubkeys(change=0, from_index=0,
       +        first_addrs, first_spk = wal.get_addresses(change=0, from_index=0,
                    count=TEST_ADDR_COUNT)
       -        first_addrs = [hashes.script_to_address(s, rpc) for s in first_spks]
                logger.info("\n" + config_mpk_key + " =>\n\t" + "\n\t".join(
                    first_addrs))
       -        last_spk = wal.get_scriptpubkeys(0, int(config.get("bitcoin-rpc",
       -            "initial_import_count")) - 1, 1)
       -        last_addr = [hashes.script_to_address(last_spk[0], rpc)] 
       +        last_addr, last_spk = wal.get_addresses(change=0, from_index=int(
       +            config.get("bitcoin-rpc", "initial_import_count")) - 1, count=1)
                if not set(first_addrs + last_addr).issubset(imported_addresses):
                    import_needed = True
                    wallets_imported += 1
                    for change in [0, 1]:
       -                spks_to_import.extend(wal.get_scriptpubkeys(change, 0,
       -                    int(config.get("bitcoin-rpc", "initial_import_count"))))
       +                addrs, spks = wal.get_addresses(change, 0,
       +                    int(config.get("bitcoin-rpc", "initial_import_count")))
       +                addresses_to_import.extend(addrs)
            logger.info("Obtaining bitcoin addresses to monitor . . .")
            #check whether watch-only addresses have been imported
            watch_only_addresses = []
       t@@ -223,8 +228,6 @@ def get_scriptpubkeys_to_monitor(rpc, config):
        
            #if addresses need to be imported then return them
            if import_needed:
       -        addresses_to_import = [hashes.script_to_address(spk, rpc)
       -            for spk in spks_to_import]
                #TODO minus imported_addresses
                logger.info("Importing " + str(wallets_imported) + " wallets and " +
                    str(len(watch_only_addresses_to_import)) + " watch-only " +
       t@@ -242,15 +245,15 @@ def get_scriptpubkeys_to_monitor(rpc, config):
            spks_to_monitor = []
            for wal in deterministic_wallets:
                for change in [0, 1]:
       -            spks_to_monitor.extend(wal.get_scriptpubkeys(change, 0,
       -                int(config.get("bitcoin-rpc", "initial_import_count"))))
       +            addrs, spks = wal.get_addresses(change, 0,
       +                int(config.get("bitcoin-rpc", "initial_import_count")))
       +            spks_to_monitor.extend(spks)
                    #loop until one address found that isnt imported
                    while True:
       -                spk = wal.get_new_scriptpubkeys(change, count=1)[0]
       -                spks_to_monitor.append(spk)
       -                if hashes.script_to_address(spk, rpc) not in imported_addresses:
       +                addrs, spks = wal.get_new_addresses(change, count=1)
       +                if addrs[0] not in imported_addresses:
                            break
       -            spks_to_monitor.pop()
       +                spks_to_monitor.append(spks[0])
                    wal.rewind_one(change)
        
            spks_to_monitor.extend([hashes.address_to_script(addr, rpc)
       t@@ -389,6 +392,22 @@ def main():
                logger.error("Wallet related RPC call failed, possibly the " +
                    "bitcoin node was compiled with the disable wallet flag")
                return
       +
       +    test_keydata = (
       +    "2 tpubD6NzVbkrYhZ4YVMVzC7wZeRfz3bhqcHvV8M3UiULCfzFtLtp5nwvi6LnBQegrkx" +
       +    "YGPkSzXUEvcPEHcKdda8W1YShVBkhFBGkLxjSQ1Nx3cJ tpubD6NzVbkrYhZ4WjgNYq2nF" +
       +    "TbiSLW2SZAzs4g5JHLqwQ3AmR3tCWpqsZJJEoZuP5HAEBNxgYQhtWMezszoaeTCg6FWGQB" +
       +    "T74sszGaxaf64o5s")
       +    chain = rpc.call("getblockchaininfo", [])["chain"]
       +    try:
       +        gaplimit = 5
       +        deterministicwallet.parse_electrum_master_public_key(test_keydata,
       +            gaplimit, rpc, chain)
       +    except ValueError as e:
       +        logger.error(repr(e))
       +        logger.error("Descriptor related RPC call failed. Bitcoin Core 0.20.0"
       +            + " or higher required. Exiting..")
       +        return
            if opts.rescan:
                rescan_script(logger, rpc, opts.rescan_date)
                return
   DIR diff --git a/electrumpersonalserver/server/deterministicwallet.py b/electrumpersonalserver/server/deterministicwallet.py
       t@@ -1,21 +1,8 @@
        
        import electrumpersonalserver.bitcoin as btc
       -from electrumpersonalserver.server.hashes import bh2u, hash_160, bfh, sha256
       -
       -# the class hierarchy for deterministic wallets in this file:
       -# subclasses are written towards the right
       -# each class knows how to create the scriptPubKeys of that wallet
       -#
       -#                                       |-- SingleSigOldMnemonicWallet
       -#                                       |-- SingleSigP2PKHWallet
       -#                                       |-- SingleSigP2WPKHWallet
       -#                     SingleSigWallet --|
       -#                    /                  |-- SingleSigP2WPKH_P2SHWallet
       -# DeterministicWallet
       -#                    \                 |-- MultisigP2SHWallet
       -#                     MultisigWallet --|
       -#                                      |-- MultisigP2WSHWallet
       -#                                      |-- MultisigP2WSH_P2SHWallet
       +from electrumpersonalserver.server.hashes import bh2u, hash_160, bfh, sha256,\
       +    address_to_script, script_to_address
       +from electrumpersonalserver.server.jsonrpc import JsonRpcError
        
        #the wallet types are here
        #https://github.com/spesmilo/electrum/blob/3.0.6/RELEASE-NOTES
       t@@ -29,13 +16,28 @@ def is_string_parsable_as_hex_int(s):
            except:
                return False
        
       -def parse_electrum_master_public_key(keydata, gaplimit):
       +def parse_electrum_master_public_key(keydata, gaplimit, rpc, chain):
       +    if chain == "main":
       +        xpub_vbytes = b"\x04\x88\xb2\x1e"
       +    elif chain == "test" or chain == "regtest":
       +        xpub_vbytes = b"\x04\x35\x87\xcf"
       +    else:
       +        assert False
       +
       +    #https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md
       +
       +    descriptor_template = None
            if keydata[:4] in ("xpub", "tpub"):
       -        wallet = SingleSigP2PKHWallet(keydata)
       +        descriptor_template = "pkh({xpub}/{change}/*)"
            elif keydata[:4] in ("zpub", "vpub"):
       -        wallet = SingleSigP2WPKHWallet(keydata)
       +        descriptor_template = "wpkh({xpub}/{change}/*)"
            elif keydata[:4] in ("ypub", "upub"):
       -        wallet = SingleSigP2WPKH_P2SHWallet(keydata)
       +        descriptor_template = "sh(wpkh({xpub}/{change}/*))"
       +
       +    if descriptor_template != None:
       +        wallet = SingleSigWallet(rpc, xpub_vbytes, keydata, descriptor_template)
       +    elif is_string_parsable_as_hex_int(keydata) and len(keydata) == 128:
       +        wallet = SingleSigOldMnemonicWallet(rpc, keydata)
            elif keydata.find(" ") != -1: #multiple keys = multisig
                chunks = keydata.split(" ")
                try:
       t@@ -47,32 +49,40 @@ def parse_electrum_master_public_key(keydata, gaplimit):
                if not all([pubkeys[0][:4] == pub[:4] for pub in pubkeys[1:]]):
                    raise ValueError("Inconsistent master public key types")
                if pubkeys[0][:4] in ("xpub", "tpub"):
       -            wallet = MultisigP2SHWallet(m, pubkeys)
       +            descriptor_script = "sh(sortedmulti("
                elif pubkeys[0][:4] in ("Zpub", "Vpub"):
       -            wallet = MultisigP2WSHWallet(m, pubkeys)
       +            descriptor_script = "wsh(sortedmulti("
                elif pubkeys[0][:4] in ("Ypub", "Upub"):
       -            wallet = MultisigP2WSH_P2SHWallet(m, pubkeys)
       -    elif is_string_parsable_as_hex_int(keydata) and len(keydata) == 128:
       -        wallet = SingleSigOldMnemonicWallet(keydata)
       +            descriptor_script = "sh(wsh(sortedmulti("
       +        wallet = MultisigWallet(rpc, xpub_vbytes, m, pubkeys, descriptor_script)
            else:
                raise ValueError("Unrecognized electrum mpk format: " + keydata[:4])
            wallet.gaplimit = gaplimit
            return wallet
        
        class DeterministicWallet(object):
       -    def __init__(self):
       +    def __init__(self, rpc):
                self.gaplimit = 0
                self.next_index = [0, 0]
                self.scriptpubkey_index = {}
       +        self.rpc = rpc
        
       -    def get_new_scriptpubkeys(self, change, count):
       -        """Returns newly-generated addresses from this deterministic wallet"""
       -        return self.get_scriptpubkeys(change, self.next_index[change],
       -            count)
       +    def _derive_addresses(self, change, from_index, count):
       +        raise RuntimeError()
        
       -    def get_scriptpubkeys(self, change, from_index, count):
       +    def get_addresses(self, change, from_index, count):
                """Returns addresses from this deterministic wallet"""
       -        pass
       +        addrs = self._derive_addresses(change, from_index, count)
       +        spks = [address_to_script(a, self.rpc) for a in addrs]
       +        for index, spk in enumerate(spks):
       +            self.scriptpubkey_index[spk] = (change, from_index + index)
       +        self.next_index[change] = max(self.next_index[change], from_index+count)
       +        return addrs, spks
       +
       +    def get_new_addresses(self, change, count):
       +        """Returns newly-generated addresses from this deterministic wallet"""
       +        addrs, spks = self.get_addresses(change, self.next_index[change], count)
       +        return addrs, spks
        
            #called in check_for_new_txes() when a new tx of ours arrives
            #to see if we need to import more addresses
       t@@ -102,120 +112,84 @@ class DeterministicWallet(object):
                """Go back one pubkey in a branch"""
                self.next_index[change] -= 1
        
       -class SingleSigWallet(DeterministicWallet):
       -    def __init__(self, mpk):
       -        super(SingleSigWallet, self).__init__()
       -        try:
       -            self.branches = (btc.bip32_ckd(mpk, 0), btc.bip32_ckd(mpk, 1))
       -        except Exception:
       -            raise ValueError("Bad master public key format. Get it from " +
       -                "Electrum menu `Wallet` -> `Information`")
       -        #m/change/i
       -
       -    def pubkey_to_scriptpubkey(self, pubkey):
       -        raise RuntimeError()
       -
       -    def get_pubkey(self, change, index):
       -        return btc.bip32_extract_key(btc.bip32_ckd(self.branches[change],
       -            index))
       +class DescriptorDeterministicWallet(DeterministicWallet):
       +    def __init__(self, rpc, xpub_vbytes, *args):
       +        super(DescriptorDeterministicWallet, self).__init__(rpc)
       +        self.xpub_vbytes = xpub_vbytes
        
       -    def get_scriptpubkeys(self, change, from_index, count):
       -        result = []
       -        for index in range(from_index, from_index + count):
       -            pubkey = self.get_pubkey(change, index)
       -            scriptpubkey = self.pubkey_to_scriptpubkey(pubkey)
       -            self.scriptpubkey_index[scriptpubkey] = (change, index)
       -            result.append(scriptpubkey)
       -        self.next_index[change] = max(self.next_index[change], from_index+count)
       -        return result
       +        descriptors_without_checksum = \
       +            self.obtain_descriptors_without_checksum(args)
        
       -class SingleSigP2PKHWallet(SingleSigWallet):
       -    def pubkey_to_scriptpubkey(self, pubkey):
       -        pkh = bh2u(hash_160(bfh(pubkey)))
       -        #op_dup op_hash_160 length hash160 op_equalverify op_checksig
       -        return "76a914" + pkh + "88ac"
       +        try:
       +            self.descriptors = []
       +            for desc in descriptors_without_checksum:
       +                self.descriptors.append(self.rpc.call("getdescriptorinfo",
       +                    [desc])["descriptor"])
       +        except JsonRpcError as e:
       +            raise ValueError(repr(e))
       +
       +    def obtain_descriptors_without_checksum(self, *args):
       +        raise RuntimeError()
        
       -class SingleSigP2WPKHWallet(SingleSigWallet):
       -    def pubkey_to_scriptpubkey(self, pubkey):
       -        pkh = bh2u(hash_160(bfh(pubkey)))
       -        #witness-version length hash160
       -        #witness version is always 0, length is always 0x14
       -        return "0014" + pkh
       -
       -class SingleSigP2WPKH_P2SHWallet(SingleSigWallet):
       -    def pubkey_to_scriptpubkey(self, pubkey):
       -        #witness-version length pubkeyhash
       -        #witness version is always 0, length is always 0x14
       -        redeem_script = '0014' + bh2u(hash_160(bfh(pubkey)))
       -        sh = bh2u(hash_160(bfh(redeem_script)))
       -        return "a914" + sh + "87"
       -
       -class SingleSigOldMnemonicWallet(SingleSigWallet):
       -    def __init__(self, mpk):
       -        super(SingleSigWallet, self).__init__()
       +    def _derive_addresses(self, change, from_index, count):
       +        return self.rpc.call("deriveaddresses", [self.descriptors[change], [
       +            from_index, from_index + count - 1]])
       +        ##the minus 1 is because deriveaddresses uses inclusive range
       +        ##e.g. to get just the first address you use [0, 0]
       +
       +    def _convert_to_standard_xpub(self, mpk):
       +        return btc.bip32_serialize((self.xpub_vbytes, *btc.bip32_deserialize(
       +            mpk)[1:]))
       +
       +class SingleSigWallet(DescriptorDeterministicWallet):
       +    def __init__(self, rpc, xpub_vbytes, xpub, descriptor_template):
       +        super(SingleSigWallet, self).__init__(rpc, xpub_vbytes, xpub,
       +            descriptor_template)
       +
       +    def obtain_descriptors_without_checksum(self, args):
       +        ##example descriptor_template:
       +        #"pkh({xpub}/{change}/*)"
       +        xpub, descriptor_template = args
       +
       +        descriptors_without_checksum = []
       +        xpub = self._convert_to_standard_xpub(xpub)
       +        for change in [0, 1]:
       +            descriptors_without_checksum.append(descriptor_template.format(
       +                change=change, xpub=xpub))
       +        return descriptors_without_checksum
       +
       +class MultisigWallet(DescriptorDeterministicWallet):
       +    def __init__(self, rpc, xpub_vbytes, m, xpub_list, descriptor_script):
       +        super(MultisigWallet, self).__init__(rpc, xpub_vbytes, m, xpub_list,
       +            descriptor_script)
       +
       +    def obtain_descriptors_without_checksum(self, args):
       +        ##example descriptor_script:
       +        #"sh(sortedmulti("
       +        m, xpub_list, descriptor_script = args
       +
       +        descriptors_without_checksum = []
       +        xpub_list = [self._convert_to_standard_xpub(xpub) for xpub in xpub_list]
       +        for change in [0, 1]:
       +            descriptors_without_checksum.append(descriptor_script + str(m) +\
       +                "," + ",".join([xpub + "/" + str(change) + "/*"
       +                for xpub in xpub_list]) + ")"*descriptor_script.count("("))
       +        return descriptors_without_checksum
       +
       +class SingleSigOldMnemonicWallet(DeterministicWallet):
       +    def __init__(self, rpc, mpk):
       +        super(SingleSigOldMnemonicWallet, self).__init__(rpc)
                self.mpk = mpk
        
       -    def get_pubkey(self, change, index):
       -        return btc.electrum_pubkey(self.mpk, index, change)
       -
       -    def pubkey_to_scriptpubkey(self, pubkey):
       +    def _pubkey_to_scriptpubkey(self, pubkey):
                pkh = bh2u(hash_160(bfh(pubkey)))
                #op_dup op_hash_160 length hash160 op_equalverify op_checksig
                return "76a914" + pkh + "88ac"
        
       -class MultisigWallet(DeterministicWallet):
       -    def __init__(self, m, mpk_list):
       -        super(MultisigWallet, self).__init__()
       -        self.m = m
       -        try:
       -            self.pubkey_branches = [(btc.bip32_ckd(mpk, 0), btc.bip32_ckd(mpk,
       -                1)) for mpk in mpk_list]
       -        except Exception:
       -            raise ValueError("Bad master public key format. Get it from " +
       -                "Electrum menu `Wallet` -> `Information`")
       -        #derivation path for pubkeys is m/change/index
       -
       -    def redeem_script_to_scriptpubkey(self, redeem_script):
       -        raise RuntimeError()
       -
       -    def get_scriptpubkeys(self, change, from_index, count):
       +    def _derive_addresses(self, change, from_index, count):
                result = []
                for index in range(from_index, from_index + count):
       -            pubkeys = [btc.bip32_extract_key(btc.bip32_ckd(branch[change],
       -                index)) for branch in self.pubkey_branches]
       -            pubkeys = sorted(pubkeys)
       -            redeemScript = ""
       -            redeemScript += "%x"%(0x50 + self.m) #op_m
       -            for p in pubkeys:
       -                redeemScript += "21" #length
       -                redeemScript += p
       -            redeemScript += "%x"%(0x50 + len(pubkeys)) #op_n
       -            redeemScript += "ae" # op_checkmultisig
       -            scriptpubkey = self.redeem_script_to_scriptpubkey(redeemScript)
       -            self.scriptpubkey_index[scriptpubkey] = (change, index)
       -            result.append(scriptpubkey)
       -        self.next_index[change] = max(self.next_index[change], from_index+count)
       +            pubkey = btc.electrum_pubkey(self.mpk, index, change)
       +            scriptpubkey = self._pubkey_to_scriptpubkey(pubkey)
       +            result.append(script_to_address(scriptpubkey, self.rpc))
                return result
       -
       -class MultisigP2SHWallet(MultisigWallet):
       -    def redeem_script_to_scriptpubkey(self, redeem_script):
       -        sh = bh2u(hash_160(bfh(redeem_script)))
       -        #op_hash160 length hash160 op_equal
       -        return "a914" + sh + "87"
       -
       -class MultisigP2WSHWallet(MultisigWallet):
       -    def redeem_script_to_scriptpubkey(self, redeem_script):
       -        sh = bh2u(sha256(bfh(redeem_script)))
       -        #witness-version length sha256
       -        #witness version is always 0, length is always 0x20
       -        return "0020" + sh
       -
       -class MultisigP2WSH_P2SHWallet(MultisigWallet):
       -    def redeem_script_to_scriptpubkey(self, redeem_script):
       -        #witness-version length sha256
       -        #witness version is always 0, length is always 0x20
       -        nested_redeemScript = "0020" + bh2u(sha256(bfh(redeem_script)))
       -        sh = bh2u(hash_160(bfh(nested_redeemScript)))
       -        #op_hash160 length hash160 op_equal
       -        return "a914" + sh + "87"
       -
   DIR diff --git a/electrumpersonalserver/server/transactionmonitor.py b/electrumpersonalserver/server/transactionmonitor.py
       t@@ -500,12 +500,11 @@ class TransactionMonitor(object):
                            output_scriptpubkeys)
                        if overrun_depths != None:
                            for change, import_count in overrun_depths.items():
       -                        spks = wal.get_new_scriptpubkeys(change, import_count)
       +                        new_addrs, spks = wal.get_new_addresses(change,
       +                            import_count)
                                for spk in spks:
                                    self.address_history[script_to_scripthash(
                                        spk)] =  {'history': [], 'subscribed': False}
       -                        new_addrs = [script_to_address(s, self.rpc)
       -                            for s in spks]
                                logger.debug("importing " + str(len(spks)) +
                                    " into change=" + str(change))
                                import_addresses(self.rpc, new_addrs, logger)