electrum-personal-server

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

commit c6b4b370e7ea0bb3643bc679dd1a6dc4625da48e
parent a63d0545013b41351fa77d09f1849cecc77db5bb
Author: chris-belcher <chris-belcher@users.noreply.github.com>
Date:   Wed,  6 May 2020 17:41:55 +0100

Use RPC call importmulti to import addresses

Imports of wallets are now much faster because the EC calculations
are done in optimized C code in Bitcoin Core. This does not apply
to the old-style-seed wallets, so they will still be slow.

Diffstat:
Melectrumpersonalserver/server/__init__.py | 5+++--
Melectrumpersonalserver/server/common.py | 34+++++++++++++++-------------------
Melectrumpersonalserver/server/deterministicwallet.py | 44++++++++++++++++++++++++++++++++++++++++++++
Melectrumpersonalserver/server/transactionmonitor.py | 31++-----------------------------
4 files changed, 64 insertions(+), 50 deletions(-)

diff --git a/electrumpersonalserver/server/__init__.py b/electrumpersonalserver/server/__init__.py @@ -21,12 +21,13 @@ from electrumpersonalserver.server.hashes import ( ) from electrumpersonalserver.server.transactionmonitor import ( TransactionMonitor, - import_addresses, - ADDRESSES_LABEL, ) from electrumpersonalserver.server.deterministicwallet import ( parse_electrum_master_public_key, DeterministicWallet, + DescriptorDeterministicWallet, + import_addresses, + ADDRESSES_LABEL, ) from electrumpersonalserver.server.socks import ( socksocket, diff --git a/electrumpersonalserver/server/common.py b/electrumpersonalserver/server/common.py @@ -161,13 +161,13 @@ def get_scriptpubkeys_to_monitor(rpc, config): st = time.time() try: imported_addresses = set(rpc.call("getaddressesbyaccount", - [transactionmonitor.ADDRESSES_LABEL])) + [deterministicwallet.ADDRESSES_LABEL])) logger.debug("using deprecated accounts interface") except JsonRpcError: #bitcoin core 0.17 deprecates accounts, replaced with labels - if transactionmonitor.ADDRESSES_LABEL in rpc.call("listlabels", []): + if deterministicwallet.ADDRESSES_LABEL in rpc.call("listlabels", []): imported_addresses = set(rpc.call("getaddressesbylabel", - [transactionmonitor.ADDRESSES_LABEL]).keys()) + [deterministicwallet.ADDRESSES_LABEL]).keys()) else: #no label, no addresses imported at all imported_addresses = set() @@ -188,8 +188,7 @@ def get_scriptpubkeys_to_monitor(rpc, config): #check whether these deterministic wallets have already been imported import_needed = False - wallets_imported = 0 - addresses_to_import = [] + wallets_to_import = [] TEST_ADDR_COUNT = 3 logger.info("Displaying first " + str(TEST_ADDR_COUNT) + " addresses of " + "each master public key:") @@ -203,11 +202,7 @@ def get_scriptpubkeys_to_monitor(rpc, config): 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]: - addrs, spks = wal.get_addresses(change, 0, - int(config.get("bitcoin-rpc", "initial_import_count"))) - addresses_to_import.extend(addrs) + wallets_to_import.append(wal) logger.info("Obtaining bitcoin addresses to monitor . . .") #check whether watch-only addresses have been imported watch_only_addresses = [] @@ -224,18 +219,17 @@ def get_scriptpubkeys_to_monitor(rpc, config): if len(deterministic_wallets) == 0 and len(watch_only_addresses) == 0: logger.error("No master public keys or watch-only addresses have " + "been configured at all. Exiting..") - #import = true and no addresses means exit - return (True, [], None) + #import = true and none other params means exit + return (True, None, None) #if addresses need to be imported then return them if import_needed: #TODO minus imported_addresses - logger.info("Importing " + str(wallets_imported) + " wallets and " + - str(len(watch_only_addresses_to_import)) + " watch-only " + - "addresses into the Bitcoin node") + logger.info("Importing " + str(len(wallets_to_import)) + + " wallets and " + str(len(watch_only_addresses_to_import)) + + " watch-only addresses into the Bitcoin node") time.sleep(5) - return (True, addresses_to_import + list( - watch_only_addresses_to_import), None) + return True, watch_only_addresses_to_import, wallets_to_import #test # importing one det wallet and no addrs, two det wallets and no addrs @@ -423,10 +417,12 @@ def main(): import_needed, relevant_spks_addrs, deterministic_wallets = \ get_scriptpubkeys_to_monitor(rpc, config) if import_needed: - if len(relevant_spks_addrs) == 0: + if not relevant_spks_addrs and not deterministic_wallets: #import = true and no addresses means exit return - transactionmonitor.import_addresses(rpc, relevant_spks_addrs) + deterministicwallet.import_addresses(rpc, relevant_spks_addrs, + deterministic_wallets, change_param=-1, + count=int(config.get("bitcoin-rpc", "initial_import_count"))) logger.info("Done.\nIf recovering a wallet which already has existing" + " transactions, then\nrun the rescan script. If you're confident" + " that the wallets are new\nand empty then there's no need to" + diff --git a/electrumpersonalserver/server/deterministicwallet.py b/electrumpersonalserver/server/deterministicwallet.py @@ -1,4 +1,6 @@ +import logging + import electrumpersonalserver.bitcoin as btc from electrumpersonalserver.server.hashes import bh2u, hash_160, bfh, sha256,\ address_to_script, script_to_address @@ -9,6 +11,48 @@ from electrumpersonalserver.server.jsonrpc import JsonRpcError #and #https://github.com/spesmilo/electrum-docs/blob/master/xpub_version_bytes.rst +ADDRESSES_LABEL = "electrum-watchonly-addresses" + +def import_addresses(rpc, watchonly_addrs, wallets, change_param, count, + logger=None): + """ + change_param = 0 for receive, 1 for change, -1 for both + """ + logger = logger if logger else logging.getLogger('ELECTRUMPERSONALSERVER') + logger.debug("Importing " + str(len(watchonly_addrs)) + " watch-only " + + "address[es] and " + str(len(wallets)) + " wallet[s] into label \"" + + ADDRESSES_LABEL + "\"") + + watchonly_addr_param = [{"scriptPubKey": {"address": addr}, "label": + ADDRESSES_LABEL, "watchonly": True, "timestamp": "now"} + for addr in watchonly_addrs] + rpc.call("importmulti", [watchonly_addr_param, {"rescan": False}]) + + for i, wal in enumerate(wallets): + logger.info("Importing wallet " + str(i+1) + "/" + str(len(wallets))) + if isinstance(wal, DescriptorDeterministicWallet): + if change_param in (0, -1): + #import receive addrs + rpc.call("importmulti", [[{"desc": wal.descriptors[0], "range": + [0, count-1], "label": ADDRESSES_LABEL, "watchonly": True, + "timestamp": "now"}], {"rescan": False}]) + if change_param in (1, -1): + #import change addrs + rpc.call("importmulti", [[{"desc": wal.descriptors[1], "range": + [0, count-1], "label": ADDRESSES_LABEL, "watchonly": True, + "timestamp": "now"}], {"rescan": False}]) + else: + #old-style-seed wallets + logger.info("importing an old-style-seed wallet, will be slow...") + for change in [0, 1]: + addrs, spks = wal.get_addresses(change, 0, count) + addr_param = [{"scriptPubKey": {"address": a}, "label": + ADDRESSES_LABEL, "watchonly": True, "timestamp": "now"} + for a in addrs] + rpc.call("importmulti", [addr_param, {"rescan": False}]) + logger.debug("Importing done") + + def is_string_parsable_as_hex_int(s): try: int(s, 16) diff --git a/electrumpersonalserver/server/transactionmonitor.py b/electrumpersonalserver/server/transactionmonitor.py @@ -13,6 +13,7 @@ from electrumpersonalserver.server.hashes import ( script_to_scripthash, script_to_address ) +from electrumpersonalserver.server.deterministicwallet import import_addresses #internally this code uses scriptPubKeys, it only converts to bitcoin addresses # when importing to bitcoind or checking whether enough addresses have been @@ -20,40 +21,12 @@ from electrumpersonalserver.server.hashes import ( #the electrum protocol uses sha256(scriptpubkey) as a key for lookups # this code calls them scripthashes -#code will generate the first address from each deterministic wallet -# and check whether they have been imported into the bitcoin node -# if no then initial_import_count addresses will be imported, then exit -# if yes then initial_import_count addresses will be generated and extra -# addresses will be generated one-by-one, each time checking whether they have -# been imported into the bitcoin node -# when an address has been reached that has not been imported, that means -# we've reached the end, then rewind the deterministic wallet index by one - #when a transaction happens paying to an address from a deterministic wallet # lookup the position of that address, if its less than gap_limit then # import more addresses -ADDRESSES_LABEL = "electrum-watchonly-addresses" CONFIRMATIONS_SAFE_FROM_REORG = 100 -def import_addresses(rpc, addrs, logger=None): - logger = logger if logger else logging.getLogger('ELECTRUMPERSONALSERVER') - logger.debug("importing addrs = " + str(addrs)) - logger.debug("into label \"" + ADDRESSES_LABEL + "\"") - logger.info("Importing " + str(len(addrs)) + " addresses in total") - addr_i = iter(addrs) - notifications = 20 - for i in range(notifications): - pc = int(100.0 * i / notifications) - sys.stdout.write("[" + str(pc) + "%]... ") - sys.stdout.flush() - for j in range(int(len(addrs) / notifications)): - rpc.call("importaddress", [next(addr_i), ADDRESSES_LABEL, False]) - for a in addr_i: #import the reminder of addresses - rpc.call("importaddress", [a, ADDRESSES_LABEL, False]) - print("[100%]") - logger.info("Importing done") - class TransactionMonitor(object): """ Class which monitors the bitcoind wallet for new transactions @@ -507,7 +480,7 @@ class TransactionMonitor(object): spk)] = {'history': [], 'subscribed': False} logger.debug("importing " + str(len(spks)) + " into change=" + str(change)) - import_addresses(self.rpc, new_addrs, logger) + import_addresses(self.rpc, new_addrs, [], -1, 0, logger) updated_scripthashes.extend(matching_scripthashes) new_history_element = self.generate_new_history_element(tx, txd)