electrum-personal-server

Maximally lightweight electrum server for a single user
git clone https://git.parazyd.org/electrum-personal-server
Log | Files | Refs | README

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