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 ecf862c03fa6f99e111e1cf6300d54d8e548970d
parent c9aa9f068e603562d939493e3be1da18a2ce2c4e
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:
Melectrumpersonalserver/server/common.py | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
1 file changed, 53 insertions(+), 20 deletions(-)

diff --git a/electrumpersonalserver/server/common.py b/electrumpersonalserver/server/common.py @@ -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. @@ -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] @@ -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') @@ -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 @@ -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": @@ -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] @@ -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 @@ -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": @@ -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"], @@ -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): @@ -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') @@ -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)) @@ -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(