def handle_query(self, query):
        if "method" not in query:
            raise IOError("Bad client query, no \"method\"")
        method = query["method"]

        if method == "blockchain.transaction.get":
            txid = query["params"][0]
            tx = None
            try:
                tx = self.rpc.call("gettransaction", [txid])["hex"]
            except JsonRpcError:
                if txid in self.txid_blockhash_map:
                    tx = self.rpc.call(
                        "getrawtransaction",
                        [txid, False, self.txid_blockhash_map[txid]])
            if tx is not None:
                self._send_response(query, tx)
            else:
                self._send_error(query["id"], {"message": "txid not found"})
        elif method == "blockchain.transaction.get_merkle":
            txid = query["params"][0]
            try:
                tx = self.rpc.call("gettransaction", [txid])
                txheader = get_block_header(self.rpc, tx["blockhash"], False)
            except JsonRpcError as e:
                self._send_error(query["id"], {"message": "txid not found"})
            else:
                try:
                    core_proof = self.rpc.call("gettxoutproof",
                                               [[txid], tx["blockhash"]])
                    electrum_proof = \
                        convert_core_to_electrum_merkle_proof(core_proof)
                    implied_merkle_root = hash_merkle_root(
                        electrum_proof["merkle"], txid, electrum_proof["pos"])
                    if implied_merkle_root != electrum_proof["merkleroot"]:
                        raise ValueError
                    reply = {
                        "block_height": txheader["block_height"],
                        "pos": electrum_proof["pos"],
                        "merkle": electrum_proof["merkle"]
                    }
                except (ValueError, JsonRpcError) as e:
                    self.logger.info(
                        "merkle proof not found for " + txid +
                        " sending a dummy, Electrum client should be run " +
                        "with --skipmerklecheck")
                    #reply with a proof that the client with accept if
                    # its configured to not check the merkle proof
                    reply = {
                        "block_height": txheader["block_height"],
                        "pos": 0,
                        "merkle": [txid]
                    }
                self._send_response(query, reply)
        elif method == "blockchain.scripthash.subscribe":
            scrhash = query["params"][0]
            if self.txmonitor.subscribe_address(scrhash):
                history_hash = self.txmonitor.get_electrum_history_hash(
                    scrhash)
            else:
                self.logger.warning(
                    "Address not known to server, hash(address)" + " = " +
                    scrhash + ".\nCheck that you've imported the " +
                    "master public key(s) correctly. The first three " +
                    "addresses of each key are printed out on startup,\nso " +
                    "check that they really are addresses you expect. In " +
                    "Electrum go to Wallet -> Information to get the right " +
                    "master public key.")
                history_hash = get_status_electrum([])
            self._send_response(query, history_hash)
        elif method == "blockchain.scripthash.get_history":
            scrhash = query["params"][0]
            history = self.txmonitor.get_electrum_history(scrhash)
            if history == None:
                history = []
                self.logger.warning("Address history not known to server, " +
                                    "hash(address) = " + scrhash)
            self._send_response(query, history)
        elif method == "blockchain.scripthash.get_balance":
            scrhash = query["params"][0]
            balance = self.txmonitor.get_address_balance(scrhash)
            if balance == None:
                self.logger.warning("Address history not known to server, " +
                                    "hash(address) = " + scrhash)
                balance = {"confirmed": 0, "unconfirmed": 0}
            self._send_response(query, balance)
        elif method == "server.ping":
            self._send_response(query, None)
        elif method == "blockchain.headers.subscribe":
            if self.protocol_version in (1.2, 1.3):
                if len(query["params"]) > 0:
                    self.are_headers_raw = query["params"][0]
                else:
                    self.are_headers_raw = (False if self.protocol_version
                                            == 1.2 else True)
            elif self.protocol_version == 1.4:
                self.are_headers_raw = True
            self.logger.debug("are_headers_raw = " + str(self.are_headers_raw))
            self.subscribed_to_headers = True
            new_bestblockhash, header = get_current_header(
                self.rpc, self.are_headers_raw)
            self._send_response(query, header)
        elif method == "blockchain.block.get_header":
            height = query["params"][0]
            try:
                blockhash = self.rpc.call("getblockhash", [height])
                #this deprecated method (as of 1.3) can only
                # return non-raw headers
                header = get_block_header(self.rpc, blockhash, False)
                self._send_response(query, header)
            except JsonRpcError:
                error = {
                    "message": "height " + str(height) + " out of range",
                    "code": -1
                }
                self._send_error(query["id"], error)
        elif method == "blockchain.block.header":
            height = query["params"][0]
            try:
                blockhash = self.rpc.call("getblockhash", [height])
                header = get_block_header(self.rpc, blockhash, True)
                self._send_response(query, header["hex"])
            except JsonRpcError:
                error = {
                    "message": "height " + str(height) + " out of range",
                    "code": -1
                }
                self._send_error(query["id"], error)
        elif method == "blockchain.block.headers":
            MAX_CHUNK_SIZE = 2016
            start_height = query["params"][0]
            count = query["params"][1]
            count = min(count, MAX_CHUNK_SIZE)
            headers_hex, n = get_block_headers_hex(self.rpc, start_height,
                                                   count)
            self._send_response(query, {
                'hex': headers_hex,
                'count': n,
                'max': MAX_CHUNK_SIZE
            })
        elif method == "blockchain.block.get_chunk":
            RETARGET_INTERVAL = 2016
            index = query["params"][0]
            tip_height = self.rpc.call("getblockchaininfo", [])["headers"]
            #logic copied from electrumx get_chunk() in controller.py
            next_height = tip_height + 1
            start_height = min(index * RETARGET_INTERVAL, next_height)
            count = min(next_height - start_height, RETARGET_INTERVAL)
            headers_hex, n = get_block_headers_hex(self.rpc, start_height,
                                                   count)
            self._send_response(query, headers_hex)
        elif method == "blockchain.transaction.broadcast":
            txhex = query["params"][0]
            result = None
            error = None
            txreport = self.rpc.call("testmempoolaccept", [[txhex]])[0]
            if not txreport["allowed"]:
                error = txreport["reject-reason"]
            else:
                result = txreport["txid"]
                broadcast_method = self.broadcast_method
                self.logger.info('Broadcasting tx ' + txreport["txid"] +
                                 " with broadcast method: " + broadcast_method)
                if broadcast_method == "tor-or-own-node":
                    tor_hostport = get_tor_hostport()
                    if tor_hostport is not None:
                        self.logger.info("Tor detected at " +
                                         str(tor_hostport) +
                                         ". Broadcasting through tor.")
                        broadcast_method = "tor"
                    else:
                        self.logger.info(
                            "Could not detect tor. Broadcasting " +
                            "through own node.")
                        broadcast_method = "own-node"
                if broadcast_method == "own-node":
                    if not self.rpc.call("getnetworkinfo", [])["localrelay"]:
                        error = "Broadcast disabled when using blocksonly"
                        result = None
                        self.logger.warning(
                            "Transaction broadcasting disabled" +
                            " when blocksonly")
                    else:
                        try:
                            self.rpc.call("sendrawtransaction", [txhex])
                        except JsonRpcError as e:
                            self.logger.error("Error broadcasting: " + repr(e))
                elif broadcast_method == "tor":
                    network = "mainnet"
                    chaininfo = self.rpc.call("getblockchaininfo", [])
                    if chaininfo["chain"] == "test":
                        network = "testnet"
                    elif chaininfo["chain"] == "regtest":
                        network = "regtest"
                    self.logger.debug("broadcasting to network: " + network)
                    peertopeer.tor_broadcast_tx(txhex, tor_hostport, network,
                                                self.rpc, logger)
                elif broadcast_method.startswith("system "):
                    with tempfile.NamedTemporaryFile() as fd:
                        system_line = broadcast_method[7:].replace(
                            "%s", fd.name)
                        fd.write(txhex.encode())
                        fd.flush()
                        self.logger.debug("running command: " + system_line)
                        os.system(system_line)
                else:
                    self.logger.error("Unrecognized broadcast method = " +
                                      broadcast_method)
                    result = None
                    error = "Unrecognized broadcast method"
            if result != None:
                self._send_response(query, result)
            else:
                self._send_error(query["id"], error)
        elif method == "mempool.get_fee_histogram":
            if self.disable_mempool_fee_histogram:
                result = [[0, 0]]
                self.logger.debug(
                    "fee histogram disabled, sending back empty " + "mempool")
            else:
                st = time.time()
                mempool = self.rpc.call("getrawmempool", [True])
                et = time.time()
                MEMPOOL_WARNING_DURATION = 10  #seconds
                if et - st > MEMPOOL_WARNING_DURATION:
                    self.logger.warning(
                        "Mempool very large resulting in slow " +
                        "response by server. Consider setting " +
                        "`disable_mempool_fee_histogram = 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():
                    size = (details["size"]
                            if "size" in details else details["vsize"])
                    fee_rate = 1e8 * details["fee"] // size
                    fee_hist[fee_rate] += size
                l = list(reversed(sorted(fee_hist.items())))
                out = []
                size = 0
                r = 0
                binsize = 100000
                for fee, s in l:
                    size += s
                    if size + r > binsize:
                        out.append((fee, size))
                        r += size - binsize
                        size = 0
                        binsize *= 1.1
                result = out
            self._send_response(query, result)
        elif method == "blockchain.estimatefee":
            estimate = self.rpc.call("estimatesmartfee", [query["params"][0]])
            feerate = 0.0001
            if "feerate" in estimate:
                feerate = estimate["feerate"]
            self._send_response(query, feerate)
        elif method == "blockchain.relayfee":
            networkinfo = self.rpc.call("getnetworkinfo", [])
            self._send_response(query, networkinfo["relayfee"])
        elif method == "server.banner":
            networkinfo = self.rpc.call("getnetworkinfo", [])
            blockchaininfo = self.rpc.call("getblockchaininfo", [])
            uptime = self.rpc.call("uptime", [])
            nettotals = self.rpc.call("getnettotals", [])
            uptime_days = uptime / 60.0 / 60 / 24
            first_unpruned_block_text = ""
            if blockchaininfo["pruned"]:
                first_unpruned_block_time = self.rpc.call(
                    "getblockheader", [
                        self.rpc.call("getblockhash",
                                      [blockchaininfo["pruneheight"]])
                    ])["time"]
                first_unpruned_block_text = (
                    "First unpruned block: " +
                    str(blockchaininfo["pruneheight"]) + " (" + str(
                        datetime.datetime.fromtimestamp(
                            first_unpruned_block_time)) + ")\n")
            self._send_response(
                query,
                BANNER.format(
                    serverversion=SERVER_VERSION_NUMBER,
                    detwallets=len(self.txmonitor.deterministic_wallets),
                    addr=len(self.txmonitor.address_history),
                    useragent=networkinfo["subversion"],
                    uptime=str(datetime.timedelta(seconds=uptime)),
                    peers=networkinfo["connections"],
                    recvbytes=bytes_fmt(nettotals["totalbytesrecv"]),
                    recvbytesperday=bytes_fmt(nettotals["totalbytesrecv"] /
                                              uptime_days),
                    sentbytes=bytes_fmt(nettotals["totalbytessent"]),
                    sentbytesperday=bytes_fmt(nettotals["totalbytessent"] /
                                              uptime_days),
                    blocksonly=not networkinfo["localrelay"],
                    pruning=blockchaininfo["pruned"],
                    blockchainsizeondisk=bytes_fmt(
                        blockchaininfo["size_on_disk"]),
                    firstunprunedblock=first_unpruned_block_text,
                    donationaddr=DONATION_ADDR))
        elif method == "server.donation_address":
            self._send_response(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
            self.protocol_version = min(client_max,
                                        SERVER_PROTOCOL_VERSION_MAX)
            if self.protocol_version < max(client_min,
                                           SERVER_PROTOCOL_VERSION_MIN):
                logging.error("*** Client protocol version " +
                              str(client_protocol_version) +
                              " not supported, update needed")
                raise ConnectionRefusedError()
            self._send_response(query, [
                "ElectrumPersonalServer " + SERVER_VERSION_NUMBER,
                str(self.protocol_version)
            ])
        elif method == "server.peers.subscribe":
            self._send_response(query, [])  #no peers to report
        elif method == "blockchain.transaction.id_from_pos":
            height = query["params"][0]
            tx_pos = query["params"][1]
            merkle = False
            if len(query["params"]) > 2:
                merkle = query["params"][2]
            try:
                blockhash = self.rpc.call("getblockhash", [height])
                block = self.rpc.call("getblock", [blockhash, 1])
                txid = block["tx"][tx_pos]
                self.txid_blockhash_map[txid] = blockhash
                if not merkle:
                    result = txid
                else:
                    core_proof = self.rpc.call("gettxoutproof",
                                               [[txid], blockhash])
                    electrum_proof =\
                        convert_core_to_electrum_merkle_proof(core_proof)
                    result = {
                        "tx_hash": txid,
                        "merkle": electrum_proof["merkle"]
                    }
                self._send_response(query, result)
            except JsonRpcError as e:
                error = {"message": repr(e)}
                self._send_error(query["id"], error)
        else:
            self.logger.error("*** BUG! Not handling method: " + method +
                              " query=" + str(query))
 def get_electrum_history_hash(self, scrhash):
     return get_status_electrum( [(h["tx_hash"], h["height"])
         for h in self.address_history[scrhash]["history"]] )
Exemple #3
0
def handle_query(sock, line, rpc, txmonitor):
    logger = logging.getLogger('ELECTRUMPERSONALSERVER')
    logger.debug("=> " + line)
    try:
        query = json.loads(line)
    except json.decoder.JSONDecodeError as e:
        raise IOError(e)
    method = query["method"]

    #protocol documentation
    #https://github.com/kyuupichan/electrumx/blob/master/docs/PROTOCOL.rst
    if method == "blockchain.transaction.get":
        txid = query["params"][0]
        tx = None
        try:
            tx = rpc.call("gettransaction", [txid])["hex"]
        except JsonRpcError:
            if txid in txid_blockhash_map:
                tx = rpc.call("getrawtransaction", [txid, False,
                    txid_blockhash_map[txid]])
        if tx is not None:
            send_response(sock, query, tx)
        else:
            send_error(sock, query["id"], {"message": "txid not found"})
    elif method == "blockchain.transaction.get_merkle":
        txid = query["params"][0]
        try:
            tx = rpc.call("gettransaction", [txid])
            core_proof = rpc.call("gettxoutproof", [[txid], tx["blockhash"]])
            electrum_proof = merkleproof.convert_core_to_electrum_merkle_proof(
                core_proof)
            implied_merkle_root = hashes.hash_merkle_root(
                electrum_proof["merkle"], txid, electrum_proof["pos"])
            if implied_merkle_root != electrum_proof["merkleroot"]:
                raise ValueError
            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))
            #so reply with an invalid proof which electrum handles without
            # disconnecting us
            #https://github.com/spesmilo/electrum/blob/c8e67e2bd07efe042703bc1368d499c5e555f854/lib/verifier.py#L74
            reply = {"block_height": 1, "pos": 0, "merkle": [txid]}
        send_response(sock, query, reply)
    elif method == "blockchain.scripthash.subscribe":
        scrhash = query["params"][0]
        if txmonitor.subscribe_address(scrhash):
            history_hash = txmonitor.get_electrum_history_hash(scrhash)
        else:
            logger.warning("Address not known to server, hash(address) = " +
                scrhash + ".\nThis means Electrum is requesting information " +
                "about addresses that are missing from Electrum Personal " +
                "Server's configuration file.")
            history_hash = hashes.get_status_electrum([])
        send_response(sock, query, history_hash)
    elif method == "blockchain.scripthash.get_history":
        scrhash = query["params"][0]
        history = txmonitor.get_electrum_history(scrhash)
        if history == None:
            history = []
            logger.warning("Address history not known to server, " +
                "hash(address) = " + scrhash)
        send_response(sock, query, history)
    elif method == "blockchain.scripthash.get_balance":
        scrhash = query["params"][0]
        balance = txmonitor.get_address_balance(scrhash)
        if balance == None:
            logger.warning("Address history not known to server, " +
                "hash(address) = " + scrhash)
            balance = {"confirmed": 0, "unconfirmed": 0}
        send_response(sock, query, balance)
    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
        logger.debug("are_headers_raw = " + str(are_headers_raw[0]))
        subscribed_to_headers[0] = True
        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])
            #this deprecated method (as of 1.3) can only return non-raw headers
            header = get_block_header(rpc, blockhash, False)
            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]
        count = query["params"][1]
        count = min(count, MAX_CHUNK_SIZE)
        headers_hex, n = get_block_headers_hex(rpc, start_height, count)
        send_response(sock, query, {'hex': headers_hex, 'count': n, 'max':
            MAX_CHUNK_SIZE})
    elif method == "blockchain.block.get_chunk":
        RETARGET_INTERVAL = 2016
        index = query["params"][0]
        tip_height = rpc.call("getblockchaininfo", [])["headers"]
        #logic copied from kyuupichan's electrumx get_chunk() in controller.py
        next_height = tip_height + 1
        start_height = min(index*RETARGET_INTERVAL, next_height)
        count = min(next_height - start_height, RETARGET_INTERVAL)
        headers_hex, n = get_block_headers_hex(rpc, start_height, count)
        send_response(sock, query, headers_hex)
    elif method == "blockchain.transaction.broadcast":
        if not rpc.call("getnetworkinfo", [])["localrelay"]:
            result = "Broadcast disabled when using blocksonly"
        else:
            try:
                result = rpc.call("sendrawtransaction", [query["params"][0]])
            except JsonRpcError as e:
                result = str(e)
        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
        r = 0
        binsize = 100000
        for fee, s in l:
            size += s
            if size + r > binsize:
                out.append((fee, size))
                r += size - binsize
                size = 0
                binsize *= 1.1
        result = out
        send_response(sock, query, result)
    elif method == "blockchain.estimatefee":
        estimate = rpc.call("estimatesmartfee", [query["params"][0]])
        feerate = 0.0001
        if "feerate" in estimate:
            feerate = estimate["feerate"]
        send_response(sock, query, feerate)
    elif method == "blockchain.relayfee":
        networkinfo = rpc.call("getnetworkinfo", [])
        send_response(sock, query, networkinfo["relayfee"])
    elif method == "server.banner":
        networkinfo = rpc.call("getnetworkinfo", [])
        blockchaininfo = rpc.call("getblockchaininfo", [])
        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"],
            peers=networkinfo["connections"],
            uptime=str(datetime.timedelta(seconds=uptime)),
            blocksonly=not networkinfo["localrelay"],
            pruning=blockchaininfo["pruned"],
            recvbytes=hashes.bytes_fmt(nettotals["totalbytesrecv"]),
            sentbytes=hashes.bytes_fmt(nettotals["totalbytessent"]),
            donationaddr=DONATION_ADDR))
    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 "
            + SERVER_VERSION_NUMBER, protocol_version[0]])
    elif method == "server.peers.subscribe":
        send_response(sock, query, []) #no peers to report
    elif method == "blockchain.transaction.id_from_pos":
        height = query["params"][0]
        tx_pos = query["params"][1]
        merkle = False
        if len(query["params"]) > 2:
            merkle = query["params"][2]
        try:
            blockhash = rpc.call("getblockhash", [height])
            block = rpc.call("getblock", [blockhash, 1])
            txid = block["tx"][tx_pos]
            txid_blockhash_map[txid] = blockhash
            if not merkle:
                result = txid
            else:
                core_proof = rpc.call("gettxoutproof", [[txid], blockhash])
                electrum_proof =\
                    merkleproof.convert_core_to_electrum_merkle_proof(
                    core_proof)
                result = {"tx_hash": txid, "merkle": electrum_proof["merkle"]}
            send_response(sock, query, result)
        except JsonRpcError as e:
            error = {"message": repr(e)}
            send_error(sock, query["id"], error)
    else:
        logger.error("*** BUG! Not handling method: " + method + " query=" +
            str(query))