URI: 
       tcommon.py - 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
       ---
       tcommon.py (23598B)
       ---
            1 import socket
            2 import time
            3 from datetime import datetime
            4 import ssl
            5 import os
            6 import os.path
            7 import logging
            8 import tempfile
            9 import platform
           10 import json
           11 import traceback
           12 from json.decoder import JSONDecodeError
           13 from configparser import RawConfigParser, NoSectionError, NoOptionError
           14 from ipaddress import ip_network, ip_address
           15 
           16 from electrumpersonalserver.server.jsonrpc import JsonRpc, JsonRpcError
           17 import electrumpersonalserver.server.hashes as hashes
           18 import electrumpersonalserver.server.deterministicwallet as deterministicwallet
           19 import electrumpersonalserver.server.transactionmonitor as transactionmonitor
           20 from electrumpersonalserver.server.electrumprotocol import (
           21     SERVER_VERSION_NUMBER,
           22     UnknownScripthashError,
           23     ElectrumProtocol,
           24     get_block_header,
           25     get_current_header,
           26     get_block_headers_hex,
           27     DONATION_ADDR,
           28 )
           29 from electrumpersonalserver.server.mempoolhistogram import (
           30     MempoolSync,
           31     PollIntervalChange
           32 )
           33 
           34 ##python has demented rules for variable scope, so these
           35 ## global variables are actually mutable lists
           36 bestblockhash = [None]
           37 
           38 last_heartbeat_listening = [datetime.now()]
           39 last_heartbeat_connected = [datetime.now()]
           40 
           41 def on_heartbeat_listening(poll_interval_listening, txmonitor):
           42     if ((datetime.now() - last_heartbeat_listening[0]).total_seconds()
           43             < poll_interval_listening):
           44         return True
           45     last_heartbeat_listening[0] = datetime.now()
           46     logger = logging.getLogger('ELECTRUMPERSONALSERVER')
           47     try:
           48         txmonitor.check_for_updated_txes()
           49         is_node_reachable = True
           50     except JsonRpcError:
           51         is_node_reachable = False
           52     return is_node_reachable
           53 
           54 def on_heartbeat_connected(poll_interval_connected, rpc, txmonitor, protocol):
           55     if ((datetime.now() - last_heartbeat_connected[0]).total_seconds()
           56             < poll_interval_connected):
           57         return
           58     last_heartbeat_connected[0] = datetime.now()
           59     logger = logging.getLogger('ELECTRUMPERSONALSERVER')
           60     is_tip_updated, header = check_for_new_blockchain_tip(rpc,
           61         protocol.are_headers_raw)
           62     if is_tip_updated:
           63         logger.debug("Blockchain tip updated " + (str(header["height"]) if
           64             "height" in header else ""))
           65         protocol.on_blockchain_tip_updated(header)
           66     updated_scripthashes = txmonitor.check_for_updated_txes()
           67     protocol.on_updated_scripthashes(updated_scripthashes)
           68 
           69 def check_for_new_blockchain_tip(rpc, raw):
           70     new_bestblockhash, header = get_current_header(rpc, raw)
           71     is_tip_new = bestblockhash[0] != new_bestblockhash
           72     bestblockhash[0] = new_bestblockhash
           73     return is_tip_new, header
           74 
           75 def create_server_socket(hostport):
           76     logger = logging.getLogger('ELECTRUMPERSONALSERVER')
           77     server_sock = socket.socket()
           78     server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
           79     server_sock.bind(hostport)
           80     server_sock.listen(1)
           81     logger.info("Listening for Electrum Wallet on " + str(hostport) + "\n\n"
           82         + "If this project is valuable to you please consider donating:\n\t"
           83         + DONATION_ADDR)
           84     return server_sock
           85 
           86 def run_electrum_server(rpc, txmonitor, config):
           87     logger = logging.getLogger('ELECTRUMPERSONALSERVER')
           88     logger.debug("Starting electrum server")
           89 
           90     hostport = (config.get("electrum-server", "host"),
           91             int(config.get("electrum-server", "port")))
           92     ip_whitelist = []
           93     for ip in config.get("electrum-server", "ip_whitelist").split(" "):
           94         if ip == "*":
           95             #matches everything
           96             ip_whitelist.append(ip_network("0.0.0.0/0"))
           97             ip_whitelist.append(ip_network("::0/0"))
           98         else:
           99             ip_whitelist.append(ip_network(ip, strict=False))
          100     poll_interval_listening = int(config.get("bitcoin-rpc",
          101         "poll_interval_listening"))
          102     poll_interval_connected = int(config.get("bitcoin-rpc",
          103         "poll_interval_connected"))
          104     certfile, keyfile = get_certs(config)
          105     logger.debug('using cert: {}, key: {}'.format(certfile, keyfile))
          106     disable_mempool_fee_histogram = config.getboolean("electrum-server",
          107         "disable_mempool_fee_histogram", fallback=False)
          108     mempool_update_interval = int(config.get("bitcoin-rpc",
          109         "mempool_update_interval", fallback=60))
          110     broadcast_method = config.get("electrum-server", "broadcast_method",
          111         fallback="own-node")
          112     tor_host = config.get("electrum-server", "tor_host", fallback="localhost")
          113     tor_port = int(config.get("electrum-server", "tor_port", fallback="9050"))
          114     tor_hostport = (tor_host, tor_port)
          115 
          116     mempool_sync = MempoolSync(rpc,
          117         disable_mempool_fee_histogram, mempool_update_interval)
          118     mempool_sync.initial_sync(logger)
          119 
          120     protocol = ElectrumProtocol(rpc, txmonitor, logger, broadcast_method,
          121         tor_hostport, mempool_sync)
          122 
          123     normal_listening_timeout = min(poll_interval_listening,
          124         mempool_update_interval)
          125     fast_listening_timeout = 0.5
          126     server_sock = create_server_socket(hostport)
          127     server_sock.settimeout(normal_listening_timeout)
          128     accepting_clients = True
          129     while True:
          130         # main server loop, runs forever
          131         sock = None
          132         while sock == None:
          133             # loop waiting for a successful connection from client
          134             try:
          135                 sock, addr = server_sock.accept()
          136                 if not accepting_clients:
          137                     logger.debug("Refusing connection from client because"
          138                         + " Bitcoin node isnt reachable")
          139                     raise ConnectionRefusedError()
          140                 if not any([ip_address(addr[0]) in ipnet
          141                         for ipnet in ip_whitelist]):
          142                     logger.debug(addr[0] + " not in whitelist, closing")
          143                     raise ConnectionRefusedError()
          144                 sock = ssl.wrap_socket(sock, server_side=True,
          145                     certfile=certfile, keyfile=keyfile,
          146                     ssl_version=ssl.PROTOCOL_SSLv23)
          147             except socket.timeout:
          148                 poll_interval_change = mempool_sync.poll_update(1)
          149                 if poll_interval_change == PollIntervalChange.FAST_POLLING:
          150                     server_sock.settimeout(fast_listening_timeout)
          151                 elif poll_interval_change == PollIntervalChange.NORMAL_POLLING:
          152                     server_sock.settimeout(normal_listening_timeout)
          153 
          154                 is_node_reachable = on_heartbeat_listening(
          155                     poll_interval_listening, txmonitor)
          156                 accepting_clients = is_node_reachable
          157             except (ConnectionRefusedError, ssl.SSLError, IOError):
          158                 sock.close()
          159                 sock = None
          160         logger.debug('Electrum connected from ' + str(addr[0]))
          161 
          162         def send_reply_fun(reply):
          163             line = json.dumps(reply)
          164             sock.sendall(line.encode('utf-8') + b'\n')
          165             logger.debug('<= ' + line)
          166         protocol.set_send_reply_fun(send_reply_fun)
          167 
          168         try:
          169             normal_connected_timeout = min(poll_interval_connected,
          170                 mempool_update_interval)
          171             fast_connected_timeout = 0.5
          172             sock.settimeout(normal_connected_timeout)
          173             recv_buffer = bytearray()
          174             while True:
          175                 # loop for replying to client queries
          176                 try:
          177                     recv_data = sock.recv(4096)
          178                     if not recv_data or len(recv_data) == 0:
          179                         raise EOFError()
          180                     recv_buffer.extend(recv_data)
          181                     lb = recv_buffer.find(b'\n')
          182                     if lb == -1:
          183                         continue
          184                     while lb != -1:
          185                         line = recv_buffer[:lb].rstrip()
          186                         recv_buffer = recv_buffer[lb + 1:]
          187                         lb = recv_buffer.find(b'\n')
          188                         try:
          189                             line = line.decode("utf-8")
          190                             query = json.loads(line)
          191                         except (UnicodeDecodeError, JSONDecodeError) as e:
          192                             raise IOError(repr(e))
          193                         logger.debug("=> " + line)
          194                         protocol.handle_query(query)
          195                 except socket.timeout:
          196                     poll_interval_change = mempool_sync.poll_update(1)
          197                     if poll_interval_change == PollIntervalChange.FAST_POLLING:
          198                         sock.settimeout(fast_connected_timeout)
          199                     elif (poll_interval_change
          200                             == PollIntervalChange.NORMAL_POLLING):
          201                         sock.settimeout(normal_connected_timeout)
          202 
          203                     on_heartbeat_connected(poll_interval_connected, rpc,
          204                         txmonitor, protocol)
          205         except JsonRpcError as e:
          206             logger.debug("Error with node connection, e = " + repr(e)
          207                 + "\ntraceback = " + str(traceback.format_exc()))
          208             accepting_clients = False
          209         except UnknownScripthashError as e:
          210             logger.debug("Disconnecting client due to misconfiguration. User"
          211                 + " must correctly configure master public key(s)")
          212         except (IOError, EOFError) as e:
          213             if isinstance(e, (EOFError, ConnectionRefusedError)):
          214                 logger.debug("Electrum wallet disconnected")
          215             else:
          216                 logger.debug("IOError: " + repr(e))
          217         try:
          218             if sock != None:
          219                 sock.close()
          220         except IOError:
          221             pass
          222         protocol.on_disconnect()
          223         time.sleep(0.2)
          224 
          225 def is_address_imported(rpc, address):
          226     return rpc.call("getaddressinfo", [address])["iswatchonly"]
          227 
          228 def get_scriptpubkeys_to_monitor(rpc, config):
          229     logger = logging.getLogger('ELECTRUMPERSONALSERVER')
          230     st = time.time()
          231 
          232     deterministic_wallets = []
          233     for key in config.options("master-public-keys"):
          234         mpk = config.get("master-public-keys", key)
          235         gaplimit = int(config.get("bitcoin-rpc", "gap_limit"))
          236         chain = rpc.call("getblockchaininfo", [])["chain"]
          237         try:
          238             wal = deterministicwallet.parse_electrum_master_public_key(mpk,
          239                 gaplimit, rpc, chain)
          240         except ValueError:
          241             raise ValueError("Bad master public key format. Get it from " +
          242                 "Electrum menu `Wallet` -> `Information`")
          243         deterministic_wallets.append(wal)
          244     #check whether these deterministic wallets have already been imported
          245     import_needed = False
          246     wallets_to_import = []
          247     TEST_ADDR_COUNT = 3
          248     logger.info("Displaying first " + str(TEST_ADDR_COUNT) + " addresses of " +
          249         "each master public key:")
          250     for config_mpk_key, wal in zip(config.options("master-public-keys"),
          251             deterministic_wallets):
          252         first_addrs, first_spk = wal.get_addresses(change=0, from_index=0,
          253             count=TEST_ADDR_COUNT)
          254         logger.info("\n" + config_mpk_key + " =>\n\t" + "\n\t".join(
          255             first_addrs))
          256         last_addr, last_spk = wal.get_addresses(change=0, from_index=int(
          257             config.get("bitcoin-rpc", "initial_import_count")) - 1, count=1)
          258         if not all((is_address_imported(rpc, a) for a in (first_addrs
          259                 + last_addr))):
          260             import_needed = True
          261             wallets_to_import.append(wal)
          262     logger.info("Obtaining bitcoin addresses to monitor . . .")
          263     #check whether watch-only addresses have been imported
          264     watch_only_addresses = []
          265     for key in config.options("watch-only-addresses"):
          266         watch_only_addresses.extend(config.get("watch-only-addresses",
          267             key).split(' '))
          268     watch_only_addresses_to_import = [a for a in watch_only_addresses
          269         if not is_address_imported(rpc, a)]
          270     if len(watch_only_addresses_to_import) > 0:
          271         import_needed = True
          272 
          273     if len(deterministic_wallets) == 0 and len(watch_only_addresses) == 0:
          274         logger.error("No master public keys or watch-only addresses have " +
          275             "been configured at all. Exiting..")
          276         #import = true and none other params means exit
          277         return (True, None, None)
          278 
          279     #if addresses need to be imported then return them
          280     if import_needed:
          281         logger.info("Importing " + str(len(wallets_to_import))
          282             + " wallets and " + str(len(watch_only_addresses_to_import))
          283             + " watch-only addresses into the Bitcoin node")
          284         time.sleep(5)
          285         return True, watch_only_addresses_to_import, wallets_to_import
          286 
          287     #test
          288     # importing one det wallet and no addrs, two det wallets and no addrs
          289     # no det wallets and some addrs, some det wallets and some addrs
          290 
          291     #at this point we know we dont need to import any addresses
          292     #find which index the deterministic wallets are up to
          293     spks_to_monitor = []
          294     for wal in deterministic_wallets:
          295         for change in [0, 1]:
          296             addrs, spks = wal.get_addresses(change, 0,
          297                 int(config.get("bitcoin-rpc", "initial_import_count")))
          298             spks_to_monitor.extend(spks)
          299             #loop until one address found that isnt imported
          300             while True:
          301                 addrs, spks = wal.get_new_addresses(change, count=1)
          302                 if not is_address_imported(rpc, addrs[0]):
          303                     break
          304                 spks_to_monitor.append(spks[0])
          305             wal.rewind_one(change)
          306 
          307     spks_to_monitor.extend([hashes.address_to_script(addr, rpc)
          308         for addr in watch_only_addresses])
          309     et = time.time()
          310     logger.info("Obtained list of addresses to monitor in " + str(et - st)
          311         + "sec")
          312     return False, spks_to_monitor, deterministic_wallets
          313 
          314 def get_certs(config):
          315     from pkg_resources import resource_filename
          316     from electrumpersonalserver import __certfile__, __keyfile__
          317 
          318     logger = logging.getLogger('ELECTRUMPERSONALSERVER')
          319     certfile = config.get('electrum-server', 'certfile', fallback=None)
          320     keyfile = config.get('electrum-server', 'keyfile', fallback=None)
          321     if (certfile and keyfile) and \
          322        (os.path.exists(certfile) and os.path.exists(keyfile)):
          323         return certfile, keyfile
          324     else:
          325         certfile = resource_filename('electrumpersonalserver', __certfile__)
          326         keyfile = resource_filename('electrumpersonalserver', __keyfile__)
          327         if os.path.exists(certfile) and os.path.exists(keyfile):
          328             return certfile, keyfile
          329         else:
          330             raise ValueError('invalid cert: {}, key: {}'.format(
          331                 certfile, keyfile))
          332 
          333 def obtain_cookie_file_path(datadir):
          334     logger = logging.getLogger('ELECTRUMPERSONALSERVER')
          335     if len(datadir.strip()) == 0:
          336         logger.debug("no datadir configuration, checking in default location")
          337         systemname = platform.system()
          338         #paths from https://en.bitcoin.it/wiki/Data_directory
          339         if systemname == "Linux":
          340             datadir = os.path.expanduser("~/.bitcoin")
          341         elif systemname == "Windows":
          342             datadir = os.path.expandvars("%APPDATA%\Bitcoin")
          343         elif systemname == "Darwin": #mac os
          344             datadir = os.path.expanduser(
          345                 "~/Library/Application Support/Bitcoin/")
          346     cookie_path = os.path.join(datadir, ".cookie")
          347     if not os.path.exists(cookie_path):
          348         logger.warning("Unable to find .cookie file, try setting `datadir`" +
          349             " config")
          350         return None
          351     return cookie_path
          352 
          353 def parse_args():
          354     from argparse import ArgumentParser
          355 
          356     parser = ArgumentParser(description='Electrum Personal Server daemon')
          357     parser.add_argument('config_file',
          358                         help='configuration file (mandatory)')
          359     parser.add_argument("--rescan", action="store_true", help="Start the " +
          360         " rescan script instead")
          361     parser.add_argument("--rescan-date", action="store", dest="rescan_date",
          362         default=None, help="Earliest wallet creation date (DD/MM/YYYY) or "
          363         + "block height to rescan from")
          364     parser.add_argument("-v", "--version", action="version", version=
          365         "%(prog)s " + SERVER_VERSION_NUMBER)
          366     return parser.parse_args()
          367 
          368 #log for checking up/seeing your wallet, debug for when something has gone wrong
          369 def logger_config(logger, config):
          370     formatter = logging.Formatter(config.get("logging", "log_format",
          371         fallback="%(levelname)s:%(asctime)s: %(message)s"))
          372     logstream = logging.StreamHandler()
          373     logstream.setFormatter(formatter)
          374     logstream.setLevel(config.get("logging", "log_level_stdout", fallback=
          375         "INFO"))
          376     logger.addHandler(logstream)
          377     filename = config.get("logging", "log_file_location", fallback="")
          378     if len(filename.strip()) == 0:
          379         filename= tempfile.gettempdir() + "/electrumpersonalserver.log"
          380     logfile = logging.FileHandler(filename, mode=('a' if
          381         config.get("logging", "append_log", fallback="false") else 'w'))
          382     logfile.setFormatter(formatter)
          383     logfile.setLevel(logging.DEBUG)
          384     logger.addHandler(logfile)
          385     logger.setLevel(logging.DEBUG)
          386     return logger, filename
          387 
          388 # returns non-zero status code on failure
          389 def main():
          390     opts = parse_args()
          391 
          392     try:
          393         config = RawConfigParser()
          394         config.read(opts.config_file)
          395         config.options("master-public-keys")
          396     except NoSectionError:
          397         print("ERROR: Non-existant configuration file {}".format(
          398             opts.config_file))
          399         return 1
          400     logger = logging.getLogger('ELECTRUMPERSONALSERVER')
          401     logger, logfilename = logger_config(logger, config)
          402     logger.info('Starting Electrum Personal Server ' + str(
          403         SERVER_VERSION_NUMBER))
          404     logger.info('Logging to ' + logfilename)
          405     logger.debug("Process ID (PID) = " + str(os.getpid()))
          406     rpc_u = None
          407     rpc_p = None
          408     cookie_path = None
          409     try:
          410         rpc_u = config.get("bitcoin-rpc", "rpc_user")
          411         rpc_p = config.get("bitcoin-rpc", "rpc_password")
          412         logger.debug("obtaining auth from rpc_user/pass")
          413     except NoOptionError:
          414         cookie_path = obtain_cookie_file_path(config.get(
          415             "bitcoin-rpc", "datadir"))
          416         logger.debug("obtaining auth from .cookie")
          417     if rpc_u == None and cookie_path == None:
          418         return 1
          419     rpc = JsonRpc(host = config.get("bitcoin-rpc", "host"),
          420         port = int(config.get("bitcoin-rpc", "port")),
          421         user = rpc_u, password = rpc_p, cookie_path = cookie_path,
          422         wallet_filename=config.get("bitcoin-rpc", "wallet_filename").strip(),
          423         logger=logger)
          424 
          425     #TODO somewhere here loop until rpc works and fully sync'd, to allow
          426     # people to run this script without waiting for their node to fully
          427     # catch up sync'd when getblockchaininfo blocks == headers, or use
          428     # verificationprogress
          429     printed_error_msg = False
          430     while bestblockhash[0] == None:
          431         try:
          432             bestblockhash[0] = rpc.call("getbestblockhash", [])
          433         except JsonRpcError as e:
          434             if not printed_error_msg:
          435                 logger.error("Error with bitcoin json-rpc: " + repr(e))
          436                 printed_error_msg = True
          437             time.sleep(5)
          438     try:
          439         rpc.call("listunspent", [])
          440     except JsonRpcError as e:
          441         logger.error(repr(e))
          442         logger.error("Wallet related RPC call failed, possibly the " +
          443             "bitcoin node was compiled with the disable wallet flag")
          444         return 1
          445 
          446     test_keydata = (
          447     "2 tpubD6NzVbkrYhZ4YVMVzC7wZeRfz3bhqcHvV8M3UiULCfzFtLtp5nwvi6LnBQegrkx" +
          448     "YGPkSzXUEvcPEHcKdda8W1YShVBkhFBGkLxjSQ1Nx3cJ tpubD6NzVbkrYhZ4WjgNYq2nF" +
          449     "TbiSLW2SZAzs4g5JHLqwQ3AmR3tCWpqsZJJEoZuP5HAEBNxgYQhtWMezszoaeTCg6FWGQB" +
          450     "T74sszGaxaf64o5s")
          451     chain = rpc.call("getblockchaininfo", [])["chain"]
          452     try:
          453         gaplimit = 5
          454         deterministicwallet.parse_electrum_master_public_key(test_keydata,
          455             gaplimit, rpc, chain)
          456     except ValueError as e:
          457         logger.error(repr(e))
          458         logger.error("Descriptor related RPC call failed. Bitcoin Core 0.20.0"
          459             + " or higher required. Exiting..")
          460         return 1
          461     if opts.rescan:
          462         rescan_script(logger, rpc, opts.rescan_date)
          463         return 0
          464     while True:
          465         logger.debug("Checking whether rescan is in progress")
          466         walletinfo = rpc.call("getwalletinfo", [])
          467         if "scanning" in walletinfo and walletinfo["scanning"]:
          468             logger.debug("Waiting for Core wallet rescan to finish")
          469             time.sleep(300)
          470             continue
          471         break
          472     import_needed, relevant_spks_addrs, deterministic_wallets = \
          473         get_scriptpubkeys_to_monitor(rpc, config)
          474     if import_needed:
          475         if not relevant_spks_addrs and not deterministic_wallets:
          476             #import = true and no addresses means exit
          477             return 0
          478         deterministicwallet.import_addresses(rpc, relevant_spks_addrs,
          479             deterministic_wallets, change_param=-1,
          480             count=int(config.get("bitcoin-rpc", "initial_import_count")))
          481         logger.info("Done.\nIf recovering a wallet which already has existing" +
          482             " transactions, then\nrun the rescan script. If you're confident" +
          483             " that the wallets are new\nand empty then there's no need to" +
          484             " rescan, just restart this script")
          485     else:
          486         txmonitor = transactionmonitor.TransactionMonitor(rpc,
          487             deterministic_wallets, logger)
          488         if not txmonitor.build_address_history(relevant_spks_addrs):
          489             return 1
          490         try:
          491             run_electrum_server(rpc, txmonitor, config)
          492         except KeyboardInterrupt:
          493             logger.info('Received KeyboardInterrupt, quitting')
          494             return 1
          495     return 0
          496 
          497 def search_for_block_height_of_date(datestr, rpc):
          498     logger = logging.getLogger('ELECTRUMPERSONALSERVER')
          499     target_time = datetime.strptime(datestr, "%d/%m/%Y")
          500     bestblockhash = rpc.call("getbestblockhash", [])
          501     best_head = rpc.call("getblockheader", [bestblockhash])
          502     if target_time > datetime.fromtimestamp(best_head["time"]):
          503         logger.error("date in the future")
          504         return -1
          505     genesis_block = rpc.call("getblockheader", [rpc.call("getblockhash", [0])])
          506     if target_time < datetime.fromtimestamp(genesis_block["time"]):
          507         logger.warning("date is before the creation of bitcoin")
          508         return 0
          509     first_height = 0
          510     last_height = best_head["height"]
          511     while True:
          512         m = (first_height + last_height) // 2
          513         m_header = rpc.call("getblockheader", [rpc.call("getblockhash", [m])])
          514         m_header_time = datetime.fromtimestamp(m_header["time"])
          515         m_time_diff = (m_header_time - target_time).total_seconds()
          516         if abs(m_time_diff) < 60*60*2: #2 hours
          517             return m_header["height"]
          518         elif m_time_diff < 0:
          519             first_height = m
          520         elif m_time_diff > 0:
          521             last_height = m
          522         else:
          523             return -1
          524 
          525 def rescan_script(logger, rpc, rescan_date):
          526     if rescan_date:
          527         user_input = rescan_date
          528     else:
          529         user_input = input("Enter earliest wallet creation date (DD/MM/YYYY) "
          530             "or block height to rescan from: ")
          531     try:
          532         height = int(user_input)
          533     except ValueError:
          534         height = search_for_block_height_of_date(user_input, rpc)
          535         if height == -1:
          536             return
          537         height -= 2016 #go back two weeks for safety
          538 
          539     if not rescan_date:
          540         if input("Rescan from block height " + str(height) + " ? (y/n):") \
          541                 != 'y':
          542             return
          543     logger.info("Rescanning. . . for progress indicator see the bitcoin node's"
          544         + " debug.log file")
          545     rpc.call("rescanblockchain", [height])
          546     logger.info("end")
          547 
          548 if __name__ == "__main__":
          549     #entry point for pyinstaller executable
          550     try:
          551         res = main()
          552     except:
          553         res = 1
          554 
          555     # only relevant for pyinstaller executables (on Windows):
          556     if os.name == 'nt':
          557         os.system("pause")
          558 
          559     sys.exit(res)
          560