URI:
       tUpdate to support electrum protocol version 1.4 - 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 ecf862c03fa6f99e111e1cf6300d54d8e548970d
   DIR parent c9aa9f068e603562d939493e3be1da18a2ce2c4e
  HTML Author: chris-belcher <chris-belcher@users.noreply.github.com>
       Date:   Mon, 22 Oct 2018 13:53:05 +0100
       
       Update to support electrum protocol version 1.4
       
       EPS now works with the version of Electrum in the master branch
       
       Diffstat:
         M electrumpersonalserver/server/comm… |      73 ++++++++++++++++++++++---------
       
       1 file changed, 53 insertions(+), 20 deletions(-)
       ---
   DIR diff --git a/electrumpersonalserver/server/common.py b/electrumpersonalserver/server/common.py
       t@@ -11,12 +11,12 @@ import electrumpersonalserver.server.merkleproof as merkleproof
        import electrumpersonalserver.server.deterministicwallet as deterministicwallet
        import electrumpersonalserver.server.transactionmonitor as transactionmonitor
        
       -VERSION_NUMBER = "0.1"
       +SERVER_VERSION_NUMBER = "0.1.6"
        
        DONATION_ADDR = "bc1q5d8l0w33h65e2l5x7ty6wgnvkvlqcz0wfaslpz"
        
        BANNER = \
       -"""Welcome to Electrum Personal Server
       +"""Welcome to Electrum Personal Server {serverversion}
        
        Monitoring {detwallets} deterministic wallets, in total {addr} addresses.
        
       t@@ -35,8 +35,12 @@ Donate to help make Electrum Personal Server even better:
        
        """
        
       +SERVER_PROTOCOL_VERSION_MAX = 1.4
       +SERVER_PROTOCOL_VERSION_MIN = 1.1
       +
        ##python has demented rules for variable scope, so these
        ## global variables are actually mutable lists
       +protocol_version = [0]
        subscribed_to_headers = [False]
        are_headers_raw = [False]
        bestblockhash = [None]
       t@@ -57,10 +61,9 @@ def logger_config(logger, fmt=None, filename=None, logfilemode='w'):
        
        def send_response(sock, query, result):
            logger = logging.getLogger('ELECTRUMPERSONALSERVER')
       -    query["result"] = result
       -    query["jsonrpc"] = "2.0"
       -    sock.sendall(json.dumps(query).encode('utf-8') + b'\n')
       -    logger.debug('<= ' + json.dumps(query))
       +    response = {"jsonrpc": "2.0", "result": result, "id": query["id"]}
       +    sock.sendall(json.dumps(response).encode('utf-8') + b'\n')
       +    logger.debug('<= ' + json.dumps(response))
        
        def send_update(sock, update):
            logger = logging.getLogger('ELECTRUMPERSONALSERVER')
       t@@ -126,11 +129,12 @@ def handle_query(sock, line, rpc, txmonitor):
                        electrum_proof["merkle"], txid, electrum_proof["pos"])
                    if implied_merkle_root != electrum_proof["merkleroot"]:
                        raise ValueError
       -            txheader = get_block_header(rpc, tx["blockhash"])
       +            txheader = get_block_header(rpc, tx["blockhash"], False)
                    reply = {"block_height": txheader["block_height"], "pos":
                        electrum_proof["pos"], "merkle": electrum_proof["merkle"]}
                except (ValueError, JsonRpcError) as e:
       -            logger.warning("merkle proof failed for " + txid + " err=" + repr(e))
       +            logger.warning("merkle proof failed for " + txid + " err=" +
       +                repr(e))
                    #so reply with an invalid proof which electrum handles without
                    # disconnecting us
                    #https://github.com/spesmilo/electrum/blob/c8e67e2bd07efe042703bc1368d499c5e555f854/lib/verifier.py#L74
       t@@ -141,7 +145,8 @@ def handle_query(sock, line, rpc, txmonitor):
                if txmonitor.subscribe_address(scrhash):
                    history_hash = txmonitor.get_electrum_history_hash(scrhash)
                else:
       -            logger.warning("address scripthash not known to server: " + scrhash)
       +            logger.warning("address scripthash not known to server: " +
       +                scrhash)
                    history_hash = hashes.get_status_electrum([])
                send_response(sock, query, history_hash)
            elif method == "blockchain.scripthash.get_history":
       t@@ -152,22 +157,40 @@ def handle_query(sock, line, rpc, txmonitor):
                    logger.warning("address scripthash history not known to server: "
                        + scrhash)
                send_response(sock, query, history)
       +    elif method == "server.ping":
       +        send_response(sock, query, None)
            elif method == "blockchain.headers.subscribe":
       +        if protocol_version[0] in (1.2, 1.3):
       +            if len(query["params"]) > 0:
       +                are_headers_raw[0] = query["params"][0]
       +            else:
       +                are_headers_raw[0] = (False if protocol_version[0] == 1.2
       +                    else True)
       +        elif protocol_version[0] == 1.4:
       +            are_headers_raw[0] = True
                subscribed_to_headers[0] = True
       -        if len(query["params"]) > 0:
       -            are_headers_raw[0] = query["params"][0]
                new_bestblockhash, header = get_current_header(rpc, are_headers_raw[0])
                send_response(sock, query, header)
            elif method == "blockchain.block.get_header":
                height = query["params"][0]
                try:
                    blockhash = rpc.call("getblockhash", [height])
       -            header = get_block_header(rpc, blockhash)
       +            header = get_block_header(rpc, blockhash, are_headers_raw[0])
                    send_response(sock, query, header)
                except JsonRpcError:
                    error = {"message": "height " + str(height) + " out of range",
                        "code": -1}
                    send_error(sock, query["id"], error)
       +    elif method == "blockchain.block.header":
       +        height = query["params"][0]
       +        try:
       +            blockhash = rpc.call("getblockhash", [height])
       +            header = get_block_header(rpc, blockhash, True)
       +            send_response(sock, query, header["hex"])
       +        except JsonRpcError:
       +            error = {"message": "height " + str(height) + " out of range",
       +                "code": -1}
       +            send_error(sock, query["id"], error)
            elif method == "blockchain.block.headers":
                MAX_CHUNK_SIZE = 2016
                start_height = query["params"][0]
       t@@ -195,14 +218,12 @@ def handle_query(sock, line, rpc, txmonitor):
                send_response(sock, query, result)
            elif method == "mempool.get_fee_histogram":
                mempool = rpc.call("getrawmempool", [True])
       -
                #algorithm copied from the relevant place in ElectrumX
                #https://github.com/kyuupichan/electrumx/blob/e92c9bd4861c1e35989ad2773d33e01219d33280/server/mempool.py
                fee_hist = defaultdict(int)
                for txid, details in mempool.items():
                    fee_rate = 1e8*details["fee"] // details["size"]
                    fee_hist[fee_rate] += details["size"]
       -
                l = list(reversed(sorted(fee_hist.items())))
                out = []
                size = 0
       t@@ -215,7 +236,6 @@ def handle_query(sock, line, rpc, txmonitor):
                        r += size - binsize
                        size = 0
                        binsize *= 1.1
       -
                result = out
                send_response(sock, query, result)
            elif method == "blockchain.estimatefee":
       t@@ -233,6 +253,7 @@ def handle_query(sock, line, rpc, txmonitor):
                uptime = rpc.call("uptime", [])
                nettotals = rpc.call("getnettotals", [])
                send_response(sock, query, BANNER.format(
       +            serverversion=SERVER_VERSION_NUMBER,
                    detwallets=len(txmonitor.deterministic_wallets),
                    addr=len(txmonitor.address_history),
                    useragent=networkinfo["subversion"],
       t@@ -246,12 +267,24 @@ def handle_query(sock, line, rpc, txmonitor):
            elif method == "server.donation_address":
                send_response(sock, query, DONATION_ADDR)
            elif method == "server.version":
       +        client_protocol_version = query["params"][1]
       +        if isinstance(client_protocol_version, list):
       +            client_min, client_max = float(client_min)
       +        else:
       +            client_min = float(query["params"][1])
       +            client_max = client_min
       +        protocol_version[0] = min(client_max, SERVER_PROTOCOL_VERSION_MAX)
       +        if protocol_version[0] < max(client_min, SERVER_PROTOCOL_VERSION_MIN):
       +            logging.error("*** Client protocol version " + str(
       +                client_protocol_version) + " not supported, update needed")
       +            raise ConnectionRefusedError()
                send_response(sock, query, ["ElectrumPersonalServer "
       -            + VERSION_NUMBER, VERSION_NUMBER])
       +            + SERVER_VERSION_NUMBER, protocol_version[0]])
            elif method == "server.peers.subscribe":
                send_response(sock, query, []) #no peers to report
            else:
       -        logger.error("*** BUG! Not handling method: " + method + " query=" + str(query))
       +        logger.error("*** BUG! Not handling method: " + method + " query=" +
       +            str(query))
                #TODO just send back the same query with result = []
        
        def get_block_header(rpc, blockhash, raw=False):
       t@@ -310,7 +343,7 @@ def get_block_headers_hex(rpc, start_height, count):
                if "nextblockhash" not in header:
                    break
                the_hash = header["nextblockhash"]
       -    return binascii.hexlify(result).decode("utf-8"), int(len(result)/80)
       +    return binascii.hexlify(result).decode("utf-8"), len(result)//80
        
        def create_server_socket(hostport):
            logger = logging.getLogger('ELECTRUMPERSONALSERVER')
       t@@ -367,7 +400,7 @@ def run_electrum_server(rpc, txmonitor, hostport, ip_whitelist,
                        except socket.timeout:
                            on_heartbeat_connected(sock, rpc, txmonitor)
                except (IOError, EOFError) as e:
       -            if isinstance(e, EOFError):
       +            if isinstance(e, (EOFError, ConnectionRefusedError)):
                        logger.info("Electrum wallet disconnected")
                    else:
                        logger.error("IOError: " + repr(e))
       t@@ -481,7 +514,7 @@ def get_certs(config):
                certfile = resource_filename('electrumpersonalserver', __certfile__)
                keyfile = resource_filename('electrumpersonalserver', __keyfile__)
                if os.path.exists(certfile) and os.path.exists(keyfile):
       -            logger.info('using cert: {}, key: {}'.format(certfile, keyfile))
       +            logger.debug('using cert: {}, key: {}'.format(certfile, keyfile))
                    return certfile, keyfile
                else:
                    raise ValueError('invalid cert: {}, key: {}'.format(