obelisk

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

commit f707ca4997dfa23685946434974ddb3c9db3552d
parent 24a277654df91c955e2edc9ec8dc9a20bcf112b4
Author: parazyd <parazyd@dyne.org>
Date:   Wed,  7 Apr 2021 17:37:03 +0200

Implement blockchain.block.header

Diffstat:
Melectrumobelisk/merkle.py | 2+-
Melectrumobelisk/protocol.py | 28++++++++++++++++++++++------
Melectrumobelisk/util.py | 8++++++++
Melectrumobelisk/zeromq.py | 24++++++++++++++++++++++--
4 files changed, 53 insertions(+), 9 deletions(-)

diff --git a/electrumobelisk/merkle.py b/electrumobelisk/merkle.py @@ -17,7 +17,7 @@ """Module for calculating merkle branches""" from math import ceil, log -from hashes import double_sha256 +from electrumobelisk.hashes import double_sha256 def branch_length(hash_count): diff --git a/electrumobelisk/protocol.py b/electrumobelisk/protocol.py @@ -17,7 +17,8 @@ import asyncio import json -from zeromq import Client +from electrumobelisk.util import is_non_negative_integer, safe_hexlify +from electrumobelisk.zeromq import Client VERSION = 0.0 DONATION_ADDR = "bc1q7an9p5pz6pjwjk4r48zke2yfaevafzpglg26mz" @@ -89,7 +90,19 @@ class ElectrumProtocol(asyncio.Protocol): async def blockchain_block_header(self, query): self.log.debug("query: %s", query) - return {"result": "foo"} + # 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"} + if not is_non_negative_integer(cp_height): + return {"error": "Invalid cp_height"} + + _ec, data = await self.bx.block_header(index) + if _ec and _ec != 0: + return {"error": "Request corrupted"} + return {"result": safe_hexlify(data)} async def handle_query(self, writer, query): # pylint: disable=R0915,R0912,R0911 """Electrum protocol method handlers""" @@ -105,12 +118,15 @@ class ElectrumProtocol(asyncio.Protocol): if method == "blockchain.block.header": self.log.debug("blockchain.block.header") + if "params" not in query: + return await self._send_error(writer, "Malformed query", + query["id"]) resp = await self.blockchain_block_header(query) if "error" in resp: - await self._send_error(writer, resp["error"], query["id"]) - else: - await self._send_response(writer, resp["result"], query["id"]) - return + return await self._send_error(writer, resp["error"], + query["id"]) + return await self._send_response(writer, resp["result"], + query["id"]) if method == "blockchain.block.headers": self.log.debug("blockchain.block.headers") diff --git a/electrumobelisk/util.py b/electrumobelisk/util.py @@ -15,13 +15,21 @@ # 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/>. """Utility functions""" +from binascii import hexlify def is_integer(val): + """Check if val is of type int""" return isinstance(val, int) def is_non_negative_integer(val): + """Check if val is of type int and non-negative""" if is_integer(val): return val >= 0 return False + + +def safe_hexlify(val): + """hexlify and return a string""" + return str(hexlify(val), "utf-8") diff --git a/electrumobelisk/zeromq.py b/electrumobelisk/zeromq.py @@ -17,12 +17,13 @@ """ZeroMQ implementation for libbitcoin""" import asyncio import struct +from binascii import unhexlify from random import randint import zmq import zmq.asyncio -from libbitcoin_errors import make_error_code, ErrorCode +from electrumobelisk.libbitcoin_errors import make_error_code, ErrorCode def create_random_id(): @@ -31,6 +32,19 @@ def create_random_id(): return randint(0, max_uint32) +def pack_block_index(index): + if isinstance(index, str): + index = unhexlify(index) + assert len(index) == 32 + return index + if isinstance(index, int): + return struct.pack("<I", index) + + raise ValueError( + f"Unknown index type {type(index)} v:{index}, should be int or bytearray" + ) + + class ClientSettings: """Class implementing ZMQ client settings""" def __init__(self, timeout=10, context=None, loop=None): @@ -41,7 +55,7 @@ class ClientSettings: @property def context(self): """ZMQ context property""" - if not self.context: + if not self._context: ctx = zmq.asyncio.Context() ctx.linger = 500 # in milliseconds self._context = ctx @@ -234,3 +248,9 @@ class Client: assert response.command == request.command assert response.request_id == request.id_ return response.error_code, response.data + + async def block_header(self, index): + """Fetch a block header by its height or integer index""" + command = b"blockchain.fetch_block_header" + data = pack_block_index(index) + return await self._simple_request(command, data)