obelisk

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

commit e8ea79bf3393e0a427ece4add23a41728206191a
parent 720e15a7f78eecba1720909c66bcf28a86975672
Author: parazyd <parazyd@dyne.org>
Date:   Thu, 15 Apr 2021 11:59:30 +0200

Reimplement JSON-RPC errors as a class.

Diffstat:
Dobelisk/errors.py | 58----------------------------------------------------------
Aobelisk/errors_jsonrpc.py | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Robelisk/libbitcoin_errors.py -> obelisk/errors_libbitcoin.py | 0
Mobelisk/protocol.py | 103++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Mobelisk/zeromq.py | 5+++--
5 files changed, 112 insertions(+), 106 deletions(-)

diff --git a/obelisk/errors.py b/obelisk/errors.py @@ -1,58 +0,0 @@ -#!/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/obelisk/errors_jsonrpc.py b/obelisk/errors_jsonrpc.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# Copyright (C) 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""" + + +class JsonRPCError: + + def __init__(self): + return + + @staticmethod + def invalidrequest(): + return {"error": {"code": -32600, "message": "invalid request"}} + + @staticmethod + def methodnotfound(): + return {"error": {"code": -32601, "message": "method not found"}} + + @staticmethod + def invalidparams(): + return {"error": {"code": -32602, "message": "invalid parameters"}} + + @staticmethod + def internalerror(): + return {"error": {"code": -32603, "message": "internal error"}} + + @staticmethod + def parseerror(): + return {"error": {"code": -37200, "message": "parse error"}} + + @staticmethod + def protonotsupported(): + return { + "error": { + "code": -32100, + "message": "protocol version unsupported" + } + } diff --git a/obelisk/libbitcoin_errors.py b/obelisk/errors_libbitcoin.py diff --git a/obelisk/protocol.py b/obelisk/protocol.py @@ -21,7 +21,7 @@ import asyncio import json from binascii import unhexlify -from obelisk.errors import ERRORS +from obelisk.errors_jsonrpc import JsonRPCError from obelisk.merkle import merkle_branch, merkle_branch_and_root from obelisk.util import ( bh2u, @@ -213,7 +213,8 @@ 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 await self._send_reply(writer, ERRORS["nomethod"], query) + return await self._send_reply(writer, JsonRPCError.methodnotfound(), + query) resp = await func(writer, query) return await self._send_reply(writer, resp, query) @@ -222,22 +223,22 @@ 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 ERRORS["invalidparams"] + return JsonRPCError.invalidparams() index = query["params"][0] cp_height = query["params"][1] if len(query["params"]) == 2 else 0 if not is_non_negative_integer(index): - return ERRORS["invalidparams"] + return JsonRPCError.invalidparams() if not is_non_negative_integer(cp_height): - return ERRORS["invalidparams"] + return JsonRPCError.invalidparams() if cp_height != 0 and not index <= cp_height: - return ERRORS["invalidparams"] + return JsonRPCError.invalidparams() if cp_height == 0: _ec, header = await self.bx.fetch_block_header(index) if _ec and _ec != 0: self.log.debug("Got error: %s", repr(_ec)) - return ERRORS["internalerror"] + return JsonRPCError.internalerror() return {"result": safe_hexlify(header)} cp_headers = [] @@ -246,7 +247,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 ERRORS["internalerror"] + return JsonRPCError.internalerror() cp_headers.append(data) # TODO: Review @@ -264,7 +265,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 ERRORS["invalidparams"] + return JsonRPCError.invalidparams() # Electrum doesn't allow max_chunk_size to be less than 2016 # gopher://bitreich.org/9/memecache/convenience-store.mkv # TODO: cp_height @@ -273,9 +274,9 @@ class ElectrumProtocol(asyncio.Protocol): # pylint: disable=R0904,R0902 count = query["params"][1] if not is_non_negative_integer(start_height): - return ERRORS["invalidparams"] + return JsonRPCError.invalidparams() if not is_non_negative_integer(count): - return ERRORS["invalidparams"] + return JsonRPCError.invalidparams() count = min(count, max_chunk_size) headers = bytearray() @@ -283,7 +284,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 ERRORS["internalerror"] + return JsonRPCError.internalerror() headers.extend(data) resp = { @@ -325,11 +326,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 ERRORS["internalerror"] + return JsonRPCError.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 ERRORS["internalerror"] + return JsonRPCError.internalerror() self.tasks.append(asyncio.create_task(self.header_notifier(writer))) ret = {"height": height, "hex": safe_hexlify(tip_header)} @@ -348,15 +349,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 ERRORS["invalidparams"] + return JsonRPCError.invalidparams() if not is_hash256_str(query["params"][0]): - return ERRORS["invalidparams"] + return JsonRPCError.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 ERRORS["internalerror"] + return JsonRPCError.internalerror() # TODO: confirmed/unconfirmed, see what's happening in libbitcoin ret = {"confirmed": data, "unconfirmed": 0} @@ -367,15 +368,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 ERRORS["invalidparams"] + return JsonRPCError.invalidparams() if not is_hash256_str(query["params"][0]): - return ERRORS["invalidparams"] + return JsonRPCError.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 ERRORS["internalerror"] + return JsonRPCError.internalerror() self.log.debug("hist: %s", data) ret = [] @@ -398,6 +399,7 @@ class ElectrumProtocol(asyncio.Protocol): # pylint: disable=R0904,R0902 """Method: blockchain.scripthash.get_mempool Return the unconfirmed transactions of a script hash. """ + # TODO: Implement return async def blockchain_scripthash_listunspent(self, writer, query): # pylint: disable=W0613 @@ -405,16 +407,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 ERRORS["invalidparams"] + return JsonRPCError.invalidparams() scripthash = query["params"][0] if not is_hash256_str(scripthash): - return ERRORS["invalidparams"] + return JsonRPCError.invalidparams() _ec, utxo = await self.bx.fetch_utxo(scripthash) if _ec and _ec != 0: self.log.debug("Got error: %s", repr(_ec)) - return ERRORS["internalerror"] + return JsonRPCError.internalerror() # TODO: Check mempool ret = [] @@ -445,15 +447,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 ERRORS["invalidparamas"] + return JsonRPCError.invalidparams() scripthash = query["params"][0] if not is_hash256_str(scripthash): - return ERRORS["invalidparams"] + return JsonRPCError.invalidparams() _ec, history = await self.bx.fetch_history4(scripthash) if _ec and _ec != 0: - return ERRORS["internalerror"] + return JsonRPCError.internalerror() task = asyncio.create_task(self.scripthash_notifier(writer, scripthash)) self.sh_subscriptions[scripthash] = {"task": task} @@ -491,11 +493,11 @@ class ElectrumProtocol(asyncio.Protocol): # pylint: disable=R0904,R0902 if its status changes. """ if "params" not in query or len(query["params"]) != 1: - return ERRORS["invalidparams"] + return JsonRPCError.invalidparams() scripthash = query["params"][0] if not is_hash256_str(scripthash): - return ERRORS["invalidparams"] + return JsonRPCError.invalidparams() if scripthash in self.sh_subscriptions: self.sh_subscriptions[scripthash]["task"].cancel() @@ -511,16 +513,16 @@ 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 ERRORS["invalidparams"] + return JsonRPCError.invalidparams() hextx = query["params"][0] if not is_hex_str(hextx): - return ERRORS["invalidparams"] + return JsonRPCError.invalidparams() _ec, _ = await self.bx.broadcast_transaction(unhexlify(hextx)[::-1]) if _ec and _ec != 0: self.log.debug("Got error: %s", repr(_ec)) - return ERRORS["internalerror"] + return JsonRPCError.internalerror() rawtx = unhexlify(hextx) txid = double_sha256(rawtx) @@ -531,7 +533,8 @@ class ElectrumProtocol(asyncio.Protocol): # pylint: disable=R0904,R0902 Return a raw transaction. """ if "params" not in query or len(query["params"]) < 1: - return ERRORS["invalidparams"] + return JsonRPCError.invalidparams() + tx_hash = query["params"][0] verbose = query["params"][1] if len(query["params"]) > 1 else False @@ -539,7 +542,7 @@ 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 ERRORS["internalerror"] + return JsonRPCError.internalerror() # Behaviour is undefined in spec if not rawtx: @@ -547,7 +550,7 @@ class ElectrumProtocol(asyncio.Protocol): # pylint: disable=R0904,R0902 if verbose: # TODO: Help needed - return ERRORS["invalidrequest"] + return JsonRPCError.invalidrequest() return {"result": bh2u(rawtx)} @@ -557,19 +560,20 @@ class ElectrumProtocol(asyncio.Protocol): # pylint: disable=R0904,R0902 hash and height. """ if "params" not in query or len(query["params"]) != 2: - return ERRORS["invalidparams"] + return JsonRPCError.invalidparams() + tx_hash = query["params"][0] height = query["params"][1] if not is_hash256_str(tx_hash): - return ERRORS["invalidparams"] + return JsonRPCError.invalidparams() if not is_non_negative_integer(height): - return ERRORS["invalidparams"] + return JsonRPCError.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 ERRORS["internalerror"] + return JsonRPCError.internalerror() # Decouple from tuples hashes = [i[0] for i in hashes] @@ -589,25 +593,26 @@ 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 ERRORS["invalidparams"] + return JsonRPCError.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 ERRORS["invalidparams"] + return JsonRPCError.invalidparams() if not is_non_negative_integer(tx_pos): - return ERRORS["invalidparams"] + return JsonRPCError.invalidparams() if not is_boolean(merkle): - return ERRORS["invalidparams"] + return JsonRPCError.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 ERRORS["internalerror"] + return JsonRPCError.internalerror() if len(hashes) - 1 < tx_pos: - return ERRORS["internalerror"] + return JsonRPCError.internalerror() # Decouple from tuples hashes = [i[0] for i in hashes] @@ -615,6 +620,7 @@ class ElectrumProtocol(asyncio.Protocol): # pylint: disable=R0904,R0902 if not merkle: return {"result": txid} + branch = merkle_branch(hashes, tx_pos) return {"result": {"tx_hash": txid, "merkle": branch}} @@ -689,13 +695,18 @@ class ElectrumProtocol(asyncio.Protocol): # pylint: disable=R0904,R0902 Identify the client to the server and negotiate the protocol version. """ if "params" not in query or len(query["params"]) != 2: - return ERRORS["invalidparams"] + return JsonRPCError.invalidparams() + client_ver = query["params"][1] + if isinstance(client_ver, list): client_min, client_max = client_ver[0], client_ver[1] else: client_min = client_max = client_ver + version = min(client_max, SERVER_PROTO_MAX) + if version < max(client_min, SERVER_PROTO_MIN): - return ERRORS["protonotsupported"] + return JsonRPCError.protonotsupported() + return {"result": [f"obelisk {VERSION}", version]} diff --git a/obelisk/zeromq.py b/obelisk/zeromq.py @@ -24,7 +24,8 @@ from random import randint import zmq import zmq.asyncio -from obelisk.libbitcoin_errors import make_error_code, ErrorCode +from obelisk.errors_libbitcoin import make_error_code, ErrorCode +from obelisk.util import hash_to_hex_str def create_random_id(): @@ -372,7 +373,7 @@ class Client: }, height, value, - checksum(tx_hash[::-1].hex(), index), + checksum(hash_to_hex_str(tx_hash), index), ) rows = unpack_table("<BI32sIQ", raw_points)