obelisk

Electrum server using libbitcoin as its backend
git clone https://git.parazyd.org/obelisk
Log | Files | Refs | README | LICENSE

commit c9c9706597689223dc8f4499efc3216e3deb112b
parent b65268c196825ce7bb62ec0e163e5eeca88e3612
Author: parazyd <parazyd@dyne.org>
Date:   Fri,  9 Apr 2021 10:16:58 +0200

Define JSON-RPC errors.

Diffstat:
Aelectrumobelisk/errors.py | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Melectrumobelisk/protocol.py | 94++++++++++++++++++++++++++++++++++++++++----------------------------------------
2 files changed, 105 insertions(+), 47 deletions(-)

diff --git a/electrumobelisk/errors.py b/electrumobelisk/errors.py @@ -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", + } + }, +} diff --git a/electrumobelisk/protocol.py b/electrumobelisk/protocol.py @@ -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, @@ -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) @@ -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 @@ -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 @@ -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() @@ -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 = { @@ -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)} @@ -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} @@ -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 = [] @@ -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 = [] @@ -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)) @@ -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() @@ -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) @@ -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 @@ -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)} @@ -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] @@ -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] @@ -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] @@ -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]}