obelisk

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

commit 9b6accd058e54b69c16596c303fa67682134f34c
parent f1a7d430089c56ba5f6446a2eae245ce50c86967
Author: parazyd <parazyd@dyne.org>
Date:   Wed,  7 Apr 2021 19:14:06 +0200

Implement blockchain.transaction.id_from_pos

Diffstat:
Melectrumobelisk/protocol.py | 40+++++++++++++++++++++++++++++++++++++---
Melectrumobelisk/util.py | 28++++++++++++++++++++++++++++
Melectrumobelisk/zeromq.py | 22++++++++++++++++++++++
3 files changed, 87 insertions(+), 3 deletions(-)

diff --git a/electrumobelisk/protocol.py b/electrumobelisk/protocol.py @@ -17,7 +17,13 @@ import asyncio import json -from electrumobelisk.util import is_non_negative_integer, safe_hexlify +from electrumobelisk.merkle import merkle_branch +from electrumobelisk.util import ( + is_boolean, + is_hash256_str, + is_non_negative_integer, + safe_hexlify, +) from electrumobelisk.zeromq import Client VERSION = 0.0 @@ -235,8 +241,36 @@ class ElectrumProtocol(asyncio.Protocol): # pylint: disable=R0904,R0902 async def blockchain_transaction_get_merkle(self, query): return - async def blockchain_transaction_from_pos(self, query): - return + async def blockchain_transaction_from_pos(self, query): # pylint: disable=R0911 + if "params" not in query or len(query["params"]) < 2: + return {"error": "malformed request"} + 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"} + if not is_non_negative_integer(tx_pos): + return {"error": "tx_pos is not a non-negative integer"} + if not is_boolean(merkle): + return {"error": "merkle is not a boolean value"} + + _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"} + + if len(hashes) - 1 < tx_pos: + return {"error": "index not in block"} + + # Decouple from tuples + hashes = [i[0] for i in hashes] + txid = safe_hexlify(hashes[tx_pos][::-1]) + + if not merkle: + return {"result": txid} + branch = merkle_branch(hashes, tx_pos) + return {"result": {"tx_hash": txid, "merkle": branch}} async def mempool_get_fee_histogram(self, query): # pylint: disable=W0613 # Help wanted diff --git a/electrumobelisk/util.py b/electrumobelisk/util.py @@ -30,6 +30,34 @@ def is_non_negative_integer(val): return False +def is_boolean(val): + """Check if val is of type bool""" + return isinstance(val, bool) + + +def is_hex_str(text): + """Check if text is a hex string""" + if not isinstance(text, str): + return False + try: + b = bytes.fromhex(text) + except: + return False + # Forbid whitespaces in text: + if len(text) != 2 * len(b): + return False + return True + + +def is_hash256_str(text): + """Check if text is a sha256 hash""" + if not isinstance(text, str): + return False + if len(text) != 64: + return False + return is_hex_str(text) + + 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 @@ -45,6 +45,19 @@ def pack_block_index(index): ) +def unpack_table(row_fmt, data): + # Get the number of rows + row_size = struct.calcsize(row_fmt) + nrows = len(data) // row_size + # Unpack + rows = [] + for idx in range(nrows): + offset = idx * row_size + row = struct.unpack_from(row_fmt, data, offset) + rows.append(row) + return rows + + class ClientSettings: """Class implementing ZMQ client settings""" def __init__(self, timeout=10, context=None, loop=None): @@ -254,3 +267,12 @@ class Client: command = b"blockchain.fetch_block_header" data = pack_block_index(index) return await self._simple_request(command, data) + + async def fetch_block_transaction_hashes(self, index): + """Fetch transaction hashes in a block at height index""" + command = b"blockchain.fetch_block_transaction_hashes" + data = pack_block_index(index) + error_code, data = await self._simple_request(command, data) + if error_code: + return error_code, None + return error_code, unpack_table("32s", data)