URI: 
       tImplement blockchain.block.header - 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 f707ca4997dfa23685946434974ddb3c9db3552d
   DIR parent 24a277654df91c955e2edc9ec8dc9a20bcf112b4
  HTML Author: parazyd <parazyd@dyne.org>
       Date:   Wed,  7 Apr 2021 17:37:03 +0200
       
       Implement blockchain.block.header
       
       Diffstat:
         M electrumobelisk/merkle.py           |       2 +-
         M electrumobelisk/protocol.py         |      28 ++++++++++++++++++++++------
         M electrumobelisk/util.py             |       8 ++++++++
         M electrumobelisk/zeromq.py           |      24 ++++++++++++++++++++++--
       
       4 files changed, 53 insertions(+), 9 deletions(-)
       ---
   DIR diff --git a/electrumobelisk/merkle.py b/electrumobelisk/merkle.py
       t@@ -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):
   DIR diff --git a/electrumobelisk/protocol.py b/electrumobelisk/protocol.py
       t@@ -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"
       t@@ -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"""
       t@@ -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")
   DIR diff --git a/electrumobelisk/util.py b/electrumobelisk/util.py
       t@@ -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")
   DIR diff --git a/electrumobelisk/zeromq.py b/electrumobelisk/zeromq.py
       t@@ -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():
       t@@ -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):
       t@@ -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
       t@@ -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)