URI: 
       tDefine JSON-RPC errors. - obelisk - Electrum server using libbitcoin as its backend
  HTML git clone https://git.parazyd.org/obelisk
   DIR Log
   DIR Files
   DIR Refs
   DIR README
   DIR LICENSE
       ---
   DIR commit c9c9706597689223dc8f4499efc3216e3deb112b
   DIR parent b65268c196825ce7bb62ec0e163e5eeca88e3612
  HTML Author: parazyd <parazyd@dyne.org>
       Date:   Fri,  9 Apr 2021 10:16:58 +0200
       
       Define JSON-RPC errors.
       
       Diffstat:
         A electrumobelisk/errors.py           |      58 ++++++++++++++++++++++++++++++
         M electrumobelisk/protocol.py         |      94 ++++++++++++++++----------------
       
       2 files changed, 105 insertions(+), 47 deletions(-)
       ---
   DIR diff --git a/electrumobelisk/errors.py b/electrumobelisk/errors.py
       t@@ -0,0 +1,58 @@
       +#!/usr/bin/env python3
       +# Copyright (C) 2020-2021 Ivan J. <parazyd@dyne.org>
       +#
       +# This file is part of obelisk
       +#
       +# This program is free software: you can redistribute it and/or modify
       +# it under the terms of the GNU Affero General Public License version 3
       +# as published by the Free Software Foundation.
       +#
       +# This program is distributed in the hope that it will be useful,
       +# but WITHOUT ANY WARRANTY; without even the implied warranty of
       +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
       +# GNU Affero General Public License for more details.
       +#
       +# You should have received a copy of the GNU Affero General Public License
       +# along with this program.  If not, see <http://www.gnu.org/licenses/>.
       +"""JSON-RPC errors
       +https://www.jsonrpc.org/specification#error_object
       +"""
       +
       +ERRORS = {
       +    "invalidparams": {
       +        "error": {
       +            "code": -32602,
       +            "message": "invalid parameters"
       +        }
       +    },
       +    "internalerror": {
       +        "error": {
       +            "code": -32603,
       +            "message": "internal error"
       +        }
       +    },
       +    "parseerror": {
       +        "error": {
       +            "code": -32700,
       +            "message": "parse error"
       +        }
       +    },
       +    "invalidrequest": {
       +        "error": {
       +            "code": -32600,
       +            "message": "invalid request"
       +        }
       +    },
       +    "nomethod": {
       +        "error": {
       +            "code": -32601,
       +            "message": "method not found"
       +        }
       +    },
       +    "protonotsupported": {
       +        "error": {
       +            "code": -32100,
       +            "message": "client protocol version is not supported",
       +        }
       +    },
       +}
   DIR diff --git a/electrumobelisk/protocol.py b/electrumobelisk/protocol.py
       t@@ -21,6 +21,7 @@ import asyncio
        import json
        from binascii import unhexlify
        
       +from electrumobelisk.errors import ERRORS
        from electrumobelisk.merkle import merkle_branch
        from electrumobelisk.util import (
            block_to_header,
       t@@ -196,7 +197,7 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                func = self.methodmap.get(method)
                if not func:
                    self.log.error("Unhandled method %s, query=%s", method, query)
       -            return
       +            return await self._send_reply(writer, ERRORS["nomethod"], query)
                resp = await func(writer, query)
                return await self._send_reply(writer, resp, query)
        
       t@@ -205,20 +206,20 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                Return the block header at the given height.
                """
                if "params" not in query or len(query["params"]) < 1:
       -            return {"error": "malformed query"}
       +            return ERRORS["invalidparams"]
                # TODO: cp_height
                index = query["params"][0]
                cp_height = query["params"][1] if len(query["params"]) == 2 else 0
        
                if not is_non_negative_integer(index):
       -            return {"error": "invalid block height"}
       +            return ERRORS["invalidparams"]
                if not is_non_negative_integer(cp_height):
       -            return {"error": "invalid cp_height"}
       +            return ERRORS["invalidparams"]
        
                _ec, data = await self.bx.fetch_block_header(index)
                if _ec and _ec != 0:
                    self.log.debug("Got error: %s", repr(_ec))
       -            return {"error": "request corrupted"}
       +            return ERRORS["internalerror"]
                return {"result": safe_hexlify(data)}
        
            async def blockchain_block_headers(self, writer, query):  # pylint: disable=W0613
       t@@ -226,7 +227,7 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                Return a concatenated chunk of block headers from the main chain.
                """
                if "params" not in query or len(query["params"]) < 2:
       -            return {"error": "malformed query"}
       +            return ERRORS["invalidparams"]
                # Electrum doesn't allow max_chunk_size to be less than 2016
                # gopher://bitreich.org/9/memecache/convenience-store.mkv
                # TODO: cp_height
       t@@ -235,9 +236,9 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                count = query["params"][1]
        
                if not is_non_negative_integer(start_height):
       -            return {"error": "invalid start_height"}
       +            return ERRORS["invalidparams"]
                if not is_non_negative_integer(count):
       -            return {"error": "invalid count"}
       +            return ERRORS["invalidparams"]
        
                count = min(count, max_chunk_size)
                headers = bytearray()
       t@@ -245,7 +246,7 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                    _ec, data = await self.bx.fetch_block_header(i)
                    if _ec and _ec != 0:
                        self.log.debug("Got error: %s", repr(_ec))
       -                return {"error": "request corrupted"}
       +                return ERRORS["internalerror"]
                    headers.extend(data)
        
                resp = {
       t@@ -287,11 +288,11 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                _ec, height = await self.bx.fetch_last_height()
                if _ec and _ec != 0:
                    self.log.debug("Got error: %s", repr(_ec))
       -            return {"error": "internal error"}
       +            return ERRORS["internalerror"]
                _ec, tip_header = await self.bx.fetch_block_header(height)
                if _ec and _ec != 0:
                    self.log.debug("Got error: %s", repr(_ec))
       -            return {"error": "internal error"}
       +            return ERRORS["internalerror"]
        
                self.tasks.append(asyncio.create_task(self.header_notifier(writer)))
                ret = {"height": height, "hex": safe_hexlify(tip_header)}
       t@@ -310,15 +311,15 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                Return the confirmed and unconfirmed balances of a script hash.
                """
                if "params" not in query or len(query["params"]) != 1:
       -            return {"error": "malformed query"}
       +            return ERRORS["invalidparams"]
        
                if not is_hash256_str(query["params"][0]):
       -            return {"error": "invalid scripthash"}
       +            return ERRORS["invalidparams"]
        
                _ec, data = await self.bx.fetch_balance(query["params"][0])
                if _ec and _ec != 0:
                    self.log.debug("Got error: %s", repr(_ec))
       -            return {"error": "request corrupted"}
       +            return ERRORS["internalerror"]
        
                # TODO: confirmed/unconfirmed, see what's happening in libbitcoin
                ret = {"confirmed": data, "unconfirmed": 0}
       t@@ -329,15 +330,15 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                Return the confirmed and unconfirmed history of a script hash.
                """
                if "params" not in query or len(query["params"]) != 1:
       -            return {"error": "malformed query"}
       +            return ERRORS["invalidparams"]
        
                if not is_hash256_str(query["params"][0]):
       -            return {"error": "invalid scripthash"}
       +            return ERRORS["invalidparams"]
        
                _ec, data = await self.bx.fetch_history4(query["params"][0])
                if _ec and _ec != 0:
                    self.log.debug("Got error: %s", repr(_ec))
       -            return {"error": "request corrupted"}
       +            return ERRORS["internalerror"]
        
                self.log.debug("hist: %s", data)
                ret = []
       t@@ -362,16 +363,16 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                Return an ordered list of UTXOs sent to a script hash.
                """
                if "params" not in query or len(query["params"]) != 1:
       -            return {"error": "malformed request"}
       +            return ERRORS["invalidparams"]
        
                scripthash = query["params"][0]
                if not is_hash256_str(scripthash):
       -            return {"error": "invalid scripthash"}
       +            return ERRORS["invalidparams"]
        
                _ec, utxo = await self.bx.fetch_utxo(scripthash)
                if _ec and _ec != 0:
                    self.log.debug("Got error: %s", repr(_ec))
       -            return {"error": "internal error"}
       +            return ERRORS["internalerror"]
        
                # TODO: Check mempool
                ret = []
       t@@ -402,15 +403,15 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                Subscribe to a script hash.
                """
                if "params" not in query or len(query["params"]) != 1:
       -            return {"error": "malformed request"}
       +            return ERRORS["invalidparamas"]
        
                scripthash = query["params"][0]
                if not is_hash256_str(scripthash):
       -            return {"error": "invalid scripthash"}
       +            return ERRORS["invalidparams"]
        
                _ec, history = await self.bx.fetch_history4(scripthash)
                if _ec and _ec != 0:
       -            return {"error": "request corrupted"}
       +            return ERRORS["internalerror"]
        
                task = asyncio.create_task(self.scripthash_notifier(
                    writer, scripthash))
       t@@ -441,11 +442,11 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                if its status changes.
                """
                if "params" not in query or len(query["params"]) != 1:
       -            return {"error": "malformed request"}
       +            return ERRORS["invalidparams"]
        
                scripthash = query["params"][0]
                if not is_hash256_str(scripthash):
       -            return {"error": "invalid scripthash"}
       +            return ERRORS["invalidparams"]
        
                if scripthash in self.sh_subscriptions:
                    self.sh_subscriptions[scripthash]["task"].cancel()
       t@@ -461,15 +462,15 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                """
                # Note: Not yet implemented in bs v4
                if "params" not in query or len(query["params"]) != 1:
       -            return {"error": "malformed request"}
       +            return ERRORS["invalidparams"]
        
                hextx = query["params"][0]
                if not is_hex_str(hextx):
       -            return {"error": "tx is not a valid hex string"}
       +            return ERRORS["invalidparams"]
        
                _ec, _ = await self.bx.broadcast_transaction(hextx)
                if _ec and _ec != 0:
       -            return {"error": "request corrupted"}
       +            return ERRORS["internalerror"]
        
                rawtx = unhexlify(hextx)
                txid = double_sha256(rawtx)
       t@@ -480,7 +481,7 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                Return a raw transaction.
                """
                if "params" not in query or len(query["params"]) < 1:
       -            return {"error": "malformed request"}
       +            return ERRORS["invalidparams"]
                tx_hash = query["params"][0]
                verbose = query["params"][1] if len(query["params"]) > 1 else False
        
       t@@ -488,13 +489,15 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                _ec, rawtx = await self.bx.fetch_mempool_transaction(tx_hash)
                if _ec and _ec != 0:
                    self.log.debug("Got error: %s", repr(_ec))
       -            return {"error": "request corrupted"}
       +            return ERRORS["internalerror"]
       +
       +        # Behaviour is undefined in spec
                if not rawtx:
       -            return {"error": f"txid {tx_hash} not found"}
       +            return {"result", None}
        
                if verbose:
                    # TODO: Help needed
       -            return {"error": "not implemented with verbose=true"}
       +            return ERRORS["invalidrequest"]
        
                return {"result", safe_hexlify(rawtx)}
        
       t@@ -504,19 +507,19 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                hash and height.
                """
                if "params" not in query or len(query["params"]) != 2:
       -            return {"error": "malformed request"}
       +            return ERRORS["invalidparams"]
                tx_hash = query["params"][0]
                height = query["params"][1]
        
                if not is_hash256_str(tx_hash):
       -            return {"error": "tx_hash is not a txid"}
       +            return ERRORS["invalidparams"]
                if not is_non_negative_integer(height):
       -            return {"error": "height is not a block height"}
       +            return ERRORS["invalidparams"]
        
                _ec, hashes = await self.bx.fetch_block_transaction_hashes(height)
                if _ec and _ec != 0:
                    self.log.debug("Got error: %s", repr(_ec))
       -            return {"error": "request corrupted"}
       +            return ERRORS["internalerror"]
        
                # Decouple from tuples
                hashes = [i[0] for i in hashes]
       t@@ -536,25 +539,25 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                block height and a position in the block.
                """
                if "params" not in query or len(query["params"]) < 2:
       -            return {"error": "malformed request"}
       +            return ERRORS["invalidparams"]
                height = query["params"][0]
                tx_pos = query["params"][1]
                merkle = query["params"][2] if len(query["params"]) > 2 else False
        
                if not is_non_negative_integer(height):
       -            return {"error": "height is not a non-negative integer"}
       +            return ERRORS["invalidparams"]
                if not is_non_negative_integer(tx_pos):
       -            return {"error": "tx_pos is not a non-negative integer"}
       +            return ERRORS["invalidparams"]
                if not is_boolean(merkle):
       -            return {"error": "merkle is not a boolean value"}
       +            return ERRORS["invalidparams"]
        
                _ec, hashes = await self.bx.fetch_block_transaction_hashes(height)
                if _ec and _ec != 0:
                    self.log.debug("Got error: %s", repr(_ec))
       -            return {"error": "request corrupted"}
       +            return ERRORS["internalerror"]
        
                if len(hashes) - 1 < tx_pos:
       -            return {"error": "index not in block"}
       +            return ERRORS["internalerror"]
        
                # Decouple from tuples
                hashes = [i[0] for i in hashes]
       t@@ -642,7 +645,7 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                    self.log.warning("Got a subsequent %s call", query["method"])
                    return
                if "params" not in query or len(query["params"]) != 2:
       -            return {"error": "malformed request"}
       +            return ERRORS["invalidparams"]
                client_ver = query["params"][1]
                if isinstance(client_ver, list):
                    client_min, client_max = client_ver[0], client_ver[1]
       t@@ -650,9 +653,6 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                    client_min = client_max = client_ver
                version = min(client_max, SERVER_PROTO_MAX)
                if version < max(client_min, SERVER_PROTO_MIN):
       -            return {
       -                "error":
       -                f"client protocol version {client_ver} is not supported"
       -            }
       +            return ERRORS["protonotsupported"]
                self.version_called = True
                return {"response": [f"obelisk {VERSION}", version]}