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 ee671a20350e72a1210f4ad6530677ef563cf2d4
parent 480f603b2c9a988f5407e47105655152f7f58376
Author: chris-belcher <chris-belcher@users.noreply.github.com>
Date:   Sat,  3 Mar 2018 16:15:42 +0000

implemented monitoring of deterministic wallets, only handles p2pkh keys for now

Diffstat:
Mconfig.cfg_sample | 42+++++++++++++++++++++++++++---------------
Adeterministicwallet.py | 125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmerkleproof.py | 2+-
Mserver.py | 250++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mutil.py | 19++++++++++++-------
5 files changed, 316 insertions(+), 122 deletions(-)

diff --git a/config.cfg_sample b/config.cfg_sample @@ -2,17 +2,22 @@ ## Electrum Personal Server configuration file ## Comments start with # -[wallets] -## Add addresses to this section +[electrum-master-public-keys] +## Add electrum master public keys to this section + +#any_key_works = xpub661MyMwAqRbcFseXCwRdRVkhVuzEiskg4QUp5XpUdNf2uGXvQmnD4zcofZ1MN6Fo8PjqQ5cemJQ39f7RTwDVVputHMFjPUn8VRp2pJQMgEF -# These are just random example addresses found on a blockchain explorer +[bip39-master-public-keys] +## Add master public keys in the bip39 standard to this section +## Most hardware wallet keys go here -# A key can be anything -addr = 1DuqpoeTB9zLvVCXQG53VbMxvMkijk494n -# A comma separated list is also accepted -my_test_addresses = 1KKszdQEpXgSY4EvsnGcGEzbQFmdcFwuNS,1EuEVUtQQ8hmuMHNdMdjLwjpkm6Ef7RYVk,bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq -# And space separated -more_test_addresses = 3Hh7QujVLqz11tiQsnUE5CSL16WEHBmiyR 1Arzu6mWZuXGTF9yR2hZhMgBJgu1Xh2bNC 1PXRLo1FQoZyF1Jhnz4qbG5x8Bo3pFpybz + +[watch-only-addresses] +## Add addresses to this section + +#addr = 1DuqpoeTB9zLvVCXQG53VbMxvMkijk494n +# A space or comma separated list is accepted +#my_test_addresses = 3Hh7QujVLqz11tiQsnUE5CSL16WEHBmiyR 1PXRLo1FQoZyF1Jhnz4qbG5x8Bo3pFpybz bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq [bitcoin-rpc] host = localhost @@ -20,18 +25,25 @@ port = 8332 user = bitcoinrpc password = password -#how often in seconds to poll for new transactions when electrum not connected +# how often in seconds to poll for new transactions when electrum not connected poll_interval_listening = 30 -#how often in seconds to poll for new transactions when electrum is connected -poll_interval_connected = 2 +# how often in seconds to poll for new transactions when electrum is connected +poll_interval_connected = 5 + +# Parameters for dealing with deterministic wallets +# how many addresses to import first time, should be big because if you import too little you may have to rescan again +initial_import_count = 200 +# number of unused addresses kept at the head of the wallet +gap_limit = 25 + [electrum-server] -#0.0.0.0 to accept connections from any IP +# 0.0.0.0 to accept connections from any IP #127.0.0.1 to accept from only localhost -#recommended you accept localhost only and connect with a ssh tunnel +# recommended you accept localhost only and for connecting remotely use a ssh tunnel host = 127.0.0.1 port = 50002 [misc] -#not implemented yet +# not implemented yet print_debug = false diff --git a/deterministicwallet.py b/deterministicwallet.py @@ -0,0 +1,125 @@ + +import bitcoin as btc +import util + +def parse_electrum_master_public_key(keydata, gaplimit): + if keydata[:4] == "xpub": + return SingleSigP2PKHWallet(keydata, gaplimit) + else: + raise RuntimeError("Unrecognized electrum mpk format: " + keydata[:4]) + +#the wallet types are here +#https://github.com/spesmilo/electrum/blob/3.0.6/RELEASE-NOTES + +class DeterministicWallet(object): + def __init__(self, gaplimit): + self.gaplimit = gaplimit + + def get_new_scriptpubkeys(self, change, count): + """Returns newly-generated addresses from this deterministic wallet""" + pass + + def get_scriptpubkeys(self, change, from_index, count): + """Returns addresses from this deterministic wallet""" + pass + + #called in check_for_new_txes() when a new tx of ours arrives + #to see if we need to import more addresses + def have_scriptpubkeys_overrun_gaplimit(self, scripts): + """Return None if they havent, or how many addresses to + import if they have""" + pass + + def rewind_one(self, change): + """Go back one pubkey in a branch""" + pass + +class SingleSigP2PKHWallet(DeterministicWallet): + def __init__(self, mpk, gaplimit): + super(SingleSigP2PKHWallet, self).__init__(gaplimit) + self.branches = (btc.bip32_ckd(mpk, 0), btc.bip32_ckd(mpk, 1)) + self.next_index = [0, 0] + self.scriptpubkey_index = {} + + def pubkey_to_scriptpubkey(self, pubkey): + pkh = util.bh2u(util.hash_160(util.bfh(pubkey))) + #op_dup op_hash_160 length hash160 op_equalverify op_checksig + return "76a914" + pkh + "88ac" + #for p2sh its "a9" + hash160 + "87" #op_hash_160 op_equal + + def get_new_scriptpubkeys(self, change, count): + return self.get_scriptpubkeys(change, self.next_index[change], + count) + + def get_scriptpubkeys(self, change, from_index, count): + #m/change/i + result = [] + for index in range(from_index, from_index + count): + pubkey = btc.bip32_extract_key(btc.bip32_ckd(self.branches[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 + + def have_scriptpubkeys_overrun_gaplimit(self, scriptpubkeys): + result = {} + for spk in scriptpubkeys: + if spk not in self.scriptpubkey_index: + continue + change, index = self.scriptpubkey_index[spk] + distance_from_next = self.next_index[change] - index + if distance_from_next > self.gaplimit: + continue + #need to import more + if change in result: + result[change] = max(result[change], self.gaplimit + - distance_from_next + 1) + else: + result[change] = self.gaplimit - distance_from_next + 1 + if len(result) > 0: + return result + else: + return None + + def rewind_one(self, change): + self.next_index[change] -= 1 + +''' +recv +76a914b1847c763c9a9b12631ab42335751c1bf843880c88ac +76a914d8b6b932e892fad5132ea888111adac2171c5af588ac +76a914e44b19ef74814f977ae4e2823dd0a0b33480472a88ac +change +76a914d2c2905ca383a5b8f94818cb7903498061a6286688ac +76a914e7b4ddb7cede132e84ba807defc092cf52e005b888ac +76a91433bdb046a1d373728d7844df89aa24f788443a4588ac +''' + +#need test vectors for each kind of detwallet + + +def test(): + xpub = ("xpub661MyMwAqRbcGVQTLtBFzc3ENvyZHoUEhWRdGwoqLZaf5wXP9VcDY2V" + + "JV7usvsFLZz2RUTVhCVXYXc3S8zpLyAFbDFcfrpUiwLoE9VWH2yz") + wal = parse_electrum_master_public_key(xpub) + initial_count = 15 + gaplimit = 5 + spks = wal.get_scriptpubkeys(0, 0, initial_count) + #for test, generate 15, check that the last 5 lead to gap limit overrun + for i in range(initial_count - gaplimit): + ret = wal.have_scriptpubkeys_overrun_gaplimit([spks[i]], gaplimit) + assert ret == None + for i in range(gaplimit): + index = i + initial_count - gaplimit + ret = wal.have_scriptpubkeys_overrun_gaplimit([spks[index]], gaplimit) + assert ret != None and ret[0] == i+1 + last_index_add = 3 + last_index = initial_count - gaplimit + last_index_add + ret = wal.have_scriptpubkeys_overrun_gaplimit(spks[2:last_index], gaplimit) + assert ret[0] == last_index_add + print("Test passed successfully") + +if __name__ == "__main__": + test() diff --git a/merkleproof.py b/merkleproof.py @@ -259,7 +259,7 @@ def test(): print("All tests passed") ''' -proof = "010000004e24a2880cd72d9bde7502087bd3756819794dc7548f68dd68dc3001000000002793fce9cdf91b4f84760571bf6009d5f0ffaddbfdc9234ef58a036096092117b10f4b4cfd68011c903e350b0200000002ee50562fc6f995eff2df61be0d5f943bac941149aa21aacb32adc130c0f17d6a2077a642b1eabbc5120e31566a11e2689aa4d39b01cce9a1902360baa5e4328e0105" +proof = "" def chunks(d, n): return [d[x:x + n] for x in range(0, len(d), n)] #print(proof) diff --git a/server.py b/server.py @@ -1,62 +1,14 @@ #! /usr/bin/python3 -#add a feature where it prints the first 3 addresses from a deterministic -# wallet, so you can check the addresses are correct before importing them -# into the node - -#or deterministic wallets -#should figure out what do regarding gap limits, when to import more addresses -# and how many addresses to start with -# maybe have a separate list of later addresses and if one of them get -# requested then import more - -#TODO try to support ssl -#doesnt support ssl yet you you must run ./electrum --nossl -#https://github.com/spesmilo/electrum/commit/dc388d4c7c541fadb9869727e359edace4c9f6f0 -#maybe copy from electrumx -#https://github.com/kyuupichan/electrumx/blob/35dd1f61996b02a84691ea71ff50f0900df969bc/server/peers.py#L476 -#https://github.com/kyuupichan/electrumx/blob/2d7403f2efed7e8f33c5cb93e2cd9144415cbb9f/server/controller.py#L259 - -#merkle trees cant be used if bitcoin core has pruning enabled, this will -# probably requires new code to be written for core -#another possible use of merkleproofs in wallet.dat -# https://github.com/JoinMarket-Org/joinmarket/issues/156#issuecomment-231059844 - -#using core's multiple wallet feature might help, should read up on that - -#now that the rescanblockchain rpc call exists in 0.16 which allows specifying -# a starting height, that will cut down the time to rescan as long as the user -# has saved their wallet creation date - -#one day there could be a nice GUI which does everything, including converting -# the wallet creation date to a block height and rescanning -''' -<belcher> now that 0.16 has this rpc called rescanblockchain which takes an optional start_height, i wonder what the most practical way of converting date to block height is -<belcher> thinking about the situation where you have a mnemonic recovery phrase + the date you created it, and want to rescan -<belcher> binary search the timestamps in the block headers i guess, then subtract two weeks just in case -<wumpus> belcher: binary search in something that is not strictly increasing seems faulty -<belcher> yes true, so maybe binary search to roughly get to the right block height then linear search +- a few blocks -<wumpus> belcher: though my gut feeling is that subtracting the two weeks would fix it -<belcher> when people write down the wallet creation date they probably wont be precise, you could get away with writing only the year and month i bet -<wumpus> as the mismatch is at most 2 hours -<Sentineo> wumpus: 2 hours for the clock scew allowed by peers? (when they throw away a block which is older than 2 hours from their actual time)? -<wumpus> Sentineo: that's what I remember, I might be off though -<Sentineo> I am not sure if it s 2 or 4 :D -<Sentineo> lazyness :) -<wumpus> in any case it is a bounded value, which means binary search might work within that precision, too lazy to look for proof though :) -''' - -##### good things - -# well placed to take advantage of dandelion private tx broadcasting -# and broadcasting through tor +#the electrum protocol uses hash(scriptpubkey) as a key for lookups +# as an alternative to address or scriptpubkey import socket, time, json, datetime, struct, binascii, math, pprint from configparser import ConfigParser, NoSectionError from decimal import Decimal from jsonrpc import JsonRpc, JsonRpcError -import util, merkleproof +import util, merkleproof, deterministicwallet ADDRESSES_LABEL = "electrum-watchonly-addresses" @@ -66,7 +18,7 @@ BANNER = \ """Welcome to Electrum Personal Server https://github.com/chris-belcher/electrum-personal-server -Monitoring {addr} addresses +Monitoring {detwallets} deterministic wallets, in total {addr} addresses. Connected bitcoin node: {useragent} Peers: {peers} @@ -103,11 +55,14 @@ def send_update(sock, update): sock.sendall(json.dumps(update).encode('utf-8') + b'\n') debug('<= ' + json.dumps(update)) -def on_heartbeat_listening(rpc, address_history, unconfirmed_txes): +def on_heartbeat_listening(rpc, address_history, unconfirmed_txes, + deterministic_wallets): debug("on heartbeat listening") - check_for_updated_txes(rpc, address_history, unconfirmed_txes) + check_for_updated_txes(rpc, address_history, unconfirmed_txes, + deterministic_wallets) -def on_heartbeat_connected(sock, rpc, address_history, unconfirmed_txes): +def on_heartbeat_connected(sock, rpc, address_history, unconfirmed_txes, + deterministic_wallets): debug("on heartbeat connected") is_tip_updated, header = check_for_new_blockchain_tip(rpc) if is_tip_updated: @@ -117,7 +72,7 @@ def on_heartbeat_connected(sock, rpc, address_history, unconfirmed_txes): "params": [header]} send_update(sock, update) updated_scripthashes = check_for_updated_txes(rpc, address_history, - unconfirmed_txes) + unconfirmed_txes, deterministic_wallets) for scrhash in updated_scripthashes: if not address_history[scrhash]["subscribed"]: continue @@ -132,7 +87,7 @@ def on_disconnect(address_history): for srchash, his in address_history.items(): his["subscribed"] = False -def handle_query(sock, line, rpc, address_history): +def handle_query(sock, line, rpc, address_history, deterministic_wallets): debug("=> " + line) try: query = json.loads(line) @@ -238,6 +193,7 @@ def handle_query(sock, line, rpc, address_history): blockchaininfo = rpc.call("getblockchaininfo", []) uptime = rpc.call("uptime", []) send_response(sock, query, BANNER.format( + detwallets=len(deterministic_wallets), addr=len(address_history), useragent=networkinfo["subversion"], peers=networkinfo["connections"], @@ -286,7 +242,8 @@ def create_server_socket(hostport): return server_sock def run_electrum_server(hostport, rpc, address_history, unconfirmed_txes, - poll_interval_listening, poll_interval_connected): + deterministic_wallets, poll_interval_listening, + poll_interval_connected): log("Starting electrum server") while True: try: @@ -299,7 +256,7 @@ def run_electrum_server(hostport, rpc, address_history, unconfirmed_txes, break except socket.timeout: on_heartbeat_listening(rpc, address_history, - unconfirmed_txes) + unconfirmed_txes, deterministic_wallets) server_sock.close() sock.settimeout(poll_interval_connected) log('Electrum connected from ' + str(addr)) @@ -318,10 +275,10 @@ def run_electrum_server(hostport, rpc, address_history, unconfirmed_txes, recv_buffer = recv_buffer[lb + 1:] lb = recv_buffer.find(b'\n') handle_query(sock, line.decode("utf-8"), rpc, - address_history) + address_history, deterministic_wallets) except socket.timeout: on_heartbeat_connected(sock, rpc, address_history, - unconfirmed_txes) + unconfirmed_txes, deterministic_wallets) except (IOError, EOFError) as e: if isinstance(e, EOFError): log("Electrum wallet disconnected") @@ -392,9 +349,10 @@ def sort_address_history_list(his): his["history"].extend(unconfirm_txes) return unconfirm_txes -def check_for_updated_txes(rpc, address_history, unconfirmed_txes): - updated_srchashes1 = check_for_unconfirmed_txes(rpc, address_history, - unconfirmed_txes) +def check_for_updated_txes(rpc, address_history, unconfirmed_txes, + deterministic_wallets): + updated_srchashes1 = check_for_new_txes(rpc, address_history, + unconfirmed_txes, deterministic_wallets) updated_srchashes2 = check_for_confirmations(rpc, address_history, unconfirmed_txes) updated_srchashes = updated_srchashes1 | updated_srchashes2 @@ -435,7 +393,8 @@ def check_for_confirmations(rpc, address_history, unconfirmed_txes): updated_srchashes.update(set(srchashes)) return updated_srchashes -def check_for_unconfirmed_txes(rpc, address_history, unconfirmed_txes): +def check_for_new_txes(rpc, address_history, unconfirmed_txes, + deterministic_wallets): MAX_TX_REQUEST_COUNT = 256 tx_request_count = 2 max_attempts = int(math.log(MAX_TX_REQUEST_COUNT, 2)) @@ -486,7 +445,6 @@ def check_for_unconfirmed_txes(rpc, address_history, unconfirmed_txes): obtained_txids.add(tx["txid"]) output_scriptpubkeys, input_scriptpubkeys, txd = \ get_input_and_output_scriptpubkeys(rpc, tx["txid"]) - matching_scripthashes = [] for spk in (output_scriptpubkeys + input_scriptpubkeys): scripthash = util.script_to_scripthash(spk) @@ -494,9 +452,21 @@ def check_for_unconfirmed_txes(rpc, address_history, unconfirmed_txes): matching_scripthashes.append(scripthash) if len(matching_scripthashes) == 0: continue + + for wal in deterministic_wallets: + overrun_depths = wal.have_scriptpubkeys_overrun_gaplimit( + output_scriptpubkeys) + if overrun_depths != None: + for change, import_count in overrun_depths.items(): + spks = wal.get_new_scriptpubkeys(change, import_count) + new_addrs = [util.script_to_address(s, rpc) for s in spks] + debug("Importing " + str(len(spks)) + " into change=" + + str(change)) + import_addresses(rpc, new_addrs) + updated_scripthashes.extend(matching_scripthashes) new_history_element = generate_new_history_element(rpc, tx, txd) - log("Found new unconfirmed tx: " + str(new_history_element)) + log("Found new tx: " + str(new_history_element)) for srchash in matching_scripthashes: address_history[srchash]["history"].append(new_history_element) if new_history_element["height"] == 0: @@ -504,16 +474,16 @@ def check_for_unconfirmed_txes(rpc, address_history, unconfirmed_txes): unconfirmed_txes[tx["txid"]].append(srchash) else: unconfirmed_txes[tx["txid"]] = [srchash] + #check whether the gap limits have been overrun and import more addrs return set(updated_scripthashes) -def build_address_history_index(rpc, wallet_addresses): - log("Building history index with " + str(len(wallet_addresses)) + +def build_address_history(rpc, monitored_scriptpubkeys, deterministic_wallets): + log("Building history with " + str(len(monitored_scriptpubkeys)) + " addresses") st = time.time() address_history = {} - for addr in wallet_addresses: - scripthash = util.address_to_scripthash(addr, rpc) - address_history[scripthash] = {'addr': addr, 'history': [], + for spk in monitored_scriptpubkeys: + address_history[util.script_to_scripthash(spk)] = {'history': [], 'subscribed': False} wallet_addr_scripthashes = set(address_history.keys()) #populate history @@ -554,6 +524,18 @@ def build_address_history_index(rpc, wallet_addresses): if len(sh_to_add) == 0: continue + for wal in deterministic_wallets: + overrun_depths = wal.have_scriptpubkeys_overrun_gaplimit( + output_scriptpubkeys) + if overrun_depths != None: + log("ERROR: Not enough addresses imported. Exiting.") + log("Delete wallet.dat and increase the value of " + + "`initial_import_count` in the file `config.cfg` " + + "then reimport and rescan") + #TODO make it so users dont have to delete wallet.dat + # check whether all initial_import_count addresses are + # imported rather than just the first one + return None, None new_history_element = generate_new_history_element(rpc, tx, txd) for scripthash in sh_to_add: address_history[scripthash][ @@ -578,29 +560,92 @@ def build_address_history_index(rpc, wallet_addresses): debug("last_known_recent_txid = " + str(last_known_recent_txid[0])) et = time.time() - log("Found " + str(count) + " txes. Address history index built in " - + str(et - st) + "sec") + log("Found " + str(count) + " txes. History built in " + str(et - st) + + "sec") debug("address_history =\n" + pprint.pformat(address_history)) - return address_history, unconfirmed_txes -def import_watchonly_addresses(rpc, addrs): - log("Importing " + str(len(addrs)) + " watch-only addresses into the" - + " Bitcoin node after 5 seconds . . .") - debug("addrs = " + str(addrs)) - time.sleep(5) +def get_scriptpubkeys_to_monitor(rpc, config): + imported_addresses = set(rpc.call("getaddressesbyaccount", + [ADDRESSES_LABEL])) + + deterministic_wallets = [] + for key in config.options("electrum-master-public-keys"): + wal = deterministicwallet.parse_electrum_master_public_key( + config.get("electrum-master-public-keys", key), + int(config.get("bitcoin-rpc", "gap_limit"))) + deterministic_wallets.append(wal) + #add bip39 wallets here + + #check whether these deterministic wallets have already been imported + import_needed = False + wallets_imported = 0 + spks_to_import = [] + for wal in deterministic_wallets: + first_addr = util.script_to_address(wal.get_scriptpubkeys(change=0, + from_index=0, count=1)[0], rpc) + if first_addr not in 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")))) + #check whether watch-only addresses have been imported + watch_only_addresses = [] + for key in config.options("watch-only-addresses"): + watch_only_addresses.extend(config.get("watch-only-addresses", + key).replace(' ', ',').split(',')) + watch_only_addresses = set(watch_only_addresses) + watch_only_addresses_to_import = [] + if not watch_only_addresses.issubset(imported_addresses): + import_needed = True + watch_only_addresses_to_import = wallet_addresses - imported_addresses + + if import_needed: + addresses_to_import = [util.script_to_address(spk, rpc) + for spk in spks_to_import] + #TODO minus imported_addresses + log("Importing " + str(wallets_imported) + " 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) + + #test + # importing one det wallet and no addrs, two det wallets and no addrs + # no det wallets and some addrs, some det wallets and some addrs + + #at this point we know we dont need to import any addresses + #find which index the deterministic wallets are up to + 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")))) + #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 util.script_to_address(spk, rpc) not in imported_addresses: + break + spks_to_monitor.pop() + wal.rewind_one(change) + + spks_to_monitor.extend([util.address_to_script(addr, rpc) + for addr in watch_only_addresses]) + return False, spks_to_monitor, deterministic_wallets + +def import_addresses(rpc, addrs): + debug("importing addrs = " + str(addrs)) for a in addrs: rpc.call("importaddress", [a, ADDRESSES_LABEL, False]) - #TODO tell people about the `rescanblockchain` call which allows a range - log("Done.\nIf recovering a wallet which already has existing " + - "transactions, then\nrestart Bitcoin with -rescan. If your wallet " + - "is new and empty then just restart this script") def main(): try: config = ConfigParser() config.read(["config.cfg"]) - config.options("wallets") + config.options("electrum-master-public-keys") except NoSectionError: log("Non-existant configuration file `config.cfg`") return @@ -608,6 +653,7 @@ def main(): port = int(config.get("bitcoin-rpc", "port")), user = config.get("bitcoin-rpc", "user"), password = config.get("bitcoin-rpc", "password")) + #TODO somewhere here loop until rpc works and fully sync'd, to allow # people to run this script without waiting for their node to fully # catch up sync'd when getblockchaininfo blocks == headers, or use @@ -618,25 +664,31 @@ def main(): bestblockhash[0] = rpc.call("getbestblockhash", []) except TypeError: if not printed_error_msg: - log("Error with bitcoin rpc, check host/port/username/password") + log("Error with bitcoin rpc, check host/port/user/password") printed_error_msg = True time.sleep(5) - wallet_addresses = [] - for key in config.options("wallets"): - addrs = config.get("wallets", key).replace(' ', ',').split(',') - wallet_addresses.extend(addrs) - wallet_addresses = set(wallet_addresses) - imported_addresses = set(rpc.call("getaddressesbyaccount", - [ADDRESSES_LABEL])) - if not wallet_addresses.issubset(imported_addresses): - import_watchonly_addresses(rpc, wallet_addresses - imported_addresses) + + import_needed, relevant_spks_addrs, deterministic_wallets = \ + get_scriptpubkeys_to_monitor(rpc, config) + if import_needed: + import_addresses(rpc, relevant_spks_addrs) + #TODO tell people about the rescanblockchain call which allows a range + log("Done.\nIf recovering a wallet which already has existing " + + "transactions, then\nrestart Bitcoin with -rescan. If your " + + "wallet is new and empty then just restart this script") else: - address_history, unconfirmed_txes = build_address_history_index( - rpc, wallet_addresses) + address_history, unconfirmed_txes = build_address_history( + rpc, relevant_spks_addrs, deterministic_wallets) + if address_history == None: + return hostport = (config.get("electrum-server", "host"), int(config.get("electrum-server", "port"))) + poll_interval_listening = int(config.get("bitcoin-rpc", + "poll_interval_listening")) + poll_interval_connected = int(config.get("bitcoin-rpc", + "poll_interval_connected")) run_electrum_server(hostport, rpc, address_history, unconfirmed_txes, - int(config.get("bitcoin-rpc", "poll_interval_listening")), - int(config.get("bitcoin-rpc", "poll_interval_connected"))) + deterministic_wallets, poll_interval_listening, + poll_interval_connected) main() diff --git a/util.py b/util.py @@ -1,5 +1,4 @@ -import bitcoin as btc import hashlib, binascii ## stuff copied from electrum's source @@ -57,14 +56,20 @@ def hash_merkle_root(merkle_s, target_hash, pos): h + hash_decode(item)) return hash_encode(h) +def hash_160(public_key): + try: + md = hashlib.new('ripemd160') + md.update(sha256(public_key)) + return md.digest() + except BaseException: + from . import ripemd + md = ripemd.new(sha256(public_key)) + return md.digest() + ## end of electrum copypaste -def script_to_address(script): - #TODO why is this even here? its not used anywhere, maybe old code - #TODO bech32 addresses - #TODO testnet, although everything uses scripthash so the address - # vbyte doesnt matter - return btc.script_to_address(script, 0x00) +def script_to_address(scriptPubKey, rpc): + return rpc.call("decodescript", [scriptPubKey])["addresses"][0] def address_to_script(addr, rpc): return rpc.call("validateaddress", [addr])["scriptPubKey"]