obelisk

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

commit f28cf131ba9a7bacfbffd4081e420ecd15e57a6f
parent 5bbe4f0c3658f82518243e59429f75c5acb31d34
Author: parazyd <parazyd@dyne.org>
Date:   Thu, 15 Apr 2021 19:59:45 +0200

Refactor tests.

Diffstat:
Mobelisk/merkle.py | 17++++++++++++++---
Mobelisk/protocol.py | 7+++++--
Mres/format_code.py | 4++--
Mtests/test_electrum_protocol.py | 350++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
4 files changed, 215 insertions(+), 163 deletions(-)

diff --git a/obelisk/merkle.py b/obelisk/merkle.py @@ -22,10 +22,14 @@ from obelisk.util import double_sha256 def branch_length(hash_count): """Return the length of a merkle branch given the number of hashes""" + if not isinstance(hash_count, int): + raise TypeError("hash_count must be an integer") + if hash_count < 1: + raise ValueError("hash_count must be at least 1") return ceil(log(hash_count, 2)) -def merkle_branch_and_root(hashes, index): +def merkle_branch_and_root(hashes, index, length=None): """Return a (merkle branch, merkle_root) pair given hashes, and the index of one of those hashes. """ @@ -35,7 +39,14 @@ def merkle_branch_and_root(hashes, index): # This also asserts hashes is not empty if not 0 <= index < len(hashes): raise ValueError("index out of range") - length = branch_length(len(hashes)) + natural_length = branch_length(len(hashes)) + if length is None: + length = natural_length + else: + if not isinstance(length, int): + raise TypeError("length must be an integer") + if length < natural_length: + raise ValueError("length out of range") branch = [] for _ in range(length): @@ -52,6 +63,6 @@ def merkle_branch_and_root(hashes, index): def merkle_branch(tx_hashes, tx_pos): """Return a merkle branch given hashes and the tx position""" - branch, _root = merkle_branch_and_root(tx_hashes, tx_pos) + branch, _ = merkle_branch_and_root(tx_hashes, tx_pos) branch = [bytes(reversed(h)).hex() for h in branch] return branch diff --git a/obelisk/protocol.py b/obelisk/protocol.py @@ -108,7 +108,7 @@ class ElectrumProtocol(asyncio.Protocol): # pylint: disable=R0904,R0902 "blockchain.transaction.get_merkle": self.blockchain_transaction_get_merkle, "blockchain.transaction.id_from_pos": - self.blockchain_transaction_from_pos, + self.blockchain_transaction_id_from_pos, "mempool.get_fee_histogram": self.mempool_get_fee_histogram, "server_add_peer": @@ -451,6 +451,9 @@ class ElectrumProtocol(asyncio.Protocol): # pylint: disable=R0904,R0902 "tx_hash": hash_to_hex_str(rec["hash"]), "height": rec["height"], }) + + if len(ret) >= 2: + ret.reverse() return {"result": ret} async def scripthash_notifier(self, writer, scripthash): @@ -610,7 +613,7 @@ class ElectrumProtocol(asyncio.Protocol): # pylint: disable=R0904,R0902 } return {"result": res} - async def blockchain_transaction_from_pos(self, writer, query): # pylint: disable=R0911,W0613 + async def blockchain_transaction_id_from_pos(self, writer, query): # pylint: disable=R0911,W0613 """Method: blockchain.transaction.id_from_pos Return a transaction hash and optionally a merkle proof, given a block height and a position in the block. diff --git a/res/format_code.py b/res/format_code.py @@ -5,5 +5,5 @@ # yapf - https://github.com/google/yapf from subprocess import run -run(["black", "-l", "80", "."]) -run(["yapf", "--style", "google", "-i", "-r", "."]) +run(["black", "-l", "80", "."], check=True) +run(["yapf", "--style", "google", "-i", "-r", "."], check=True) diff --git a/tests/test_electrum_protocol.py b/tests/test_electrum_protocol.py @@ -14,167 +14,198 @@ # # 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/>. +""" +Test unit for the Electrum protocol. Takes results from testnet +blockstream.info:143 server as value reference. + +See bottom of file for test orchestration. +""" import asyncio +import json import sys +import traceback from logging import getLogger +from pprint import pprint +from socket import socket, AF_INET, SOCK_STREAM from obelisk.protocol import ElectrumProtocol +from obelisk.zeromq import create_random_id -# -# See bottom of this file for test orchestration. -# - -ENDPOINTS = { +libbitcoin = { "query": "tcp://testnet2.libbitcoin.net:29091", "heart": "tcp://testnet2.libbitcoin.net:29092", "block": "tcp://testnet2.libbitcoin.net:29093", "trans": "tcp://testnet2.libbitcoin.net:29094", } +blockstream = ("blockstream.info", 143) +bs = None # Socket -async def test_blockchain_block_header(protocol, writer): - expect = "01000000c54675276e0401706aa93db6494dd7d1058b19424f23c8d7c01076da000000001c4375c8056b0ded0fa3d7fc1b5511eaf53216aed72ea95e1b5d19eccbe855f91a184a4dffff001d0336a226" - query = {"params": [123]} - data = await protocol.blockchain_block_header(writer, query) - assert data["result"] == expect +def get_expect(method, params): + global bs + req = { + "json-rpc": "2.0", + "id": create_random_id(), + "method": method, + "params": params, + } + bs.send(json.dumps(req).encode("utf-8") + b"\n") + recv_buf = bytearray() + while True: + data = bs.recv(4096) + if not data or len(data) == 0: + raise ValueError("No data received from blockstream") + recv_buf.extend(data) + lb = recv_buf.find(b"\n") + if lb == -1: + continue + while lb != -1: + line = recv_buf[:lb].rstrip() + recv_buf = recv_buf[lb + 1:] + lb = recv_buf.find(b"\n") + line = line.decode("utf-8") + resp = json.loads(line) + return resp -async def test_blockchain_block_headers(protocol, writer): - expect = "01000000c54675276e0401706aa93db6494dd7d1058b19424f23c8d7c01076da000000001c4375c8056b0ded0fa3d7fc1b5511eaf53216aed72ea95e1b5d19eccbe855f91a184a4dffff001d0336a22601000000bca72b7ccb44f1f0dd803f2c321143c9dda7f5a2a6ed87c76aac918a000000004266985f02f11bdffa559a233f5600c95c04bd70340e75673cadaf3ef6ac72b448194a4dffff001d035c84d801000000769d6d6e4672a620669baa56dd39d066523e461762ad3610fb2055b400000000c50652340352ad79b799b870e3fa2c80804d0fc54063b413e0e2d6dc66ca3f9a55194a4dffff001d022510a4" - query = {"params": [123, 3]} - data = await protocol.blockchain_block_headers(writer, query) - assert data["result"]["hex"] == expect +async def test_blockchain_block_header(protocol, writer): + method = "blockchain.block.header" + params = [123] + expect = get_expect(method, params) + data = await protocol.blockchain_block_header(writer, {"params": params}) + assert data["result"] == expect["result"] + + # params = [123, 130] + # expect = get_expect(method, params) + # data = await protocol.blockchain_block_header(writer, {"params": params}) + + # assert data["result"]["header"] == expect["result"]["header"] + # assert data["result"]["branch"] == expect["result"]["branch"] + # assert data["result"]["root"] == expect["result"]["root"] -async def test_blockchain_estimatefee(protocol, writer): - expect = -1 - query = {"params": []} - data = await protocol.blockchain_estimatefee(writer, query) - assert data["result"] == expect +async def test_blockchain_block_headers(protocol, writer): + method = "blockchain.block.headers" + params = [123, 3] + expect = get_expect(method, params) + data = await protocol.blockchain_block_headers(writer, {"params": params}) + assert data["result"]["hex"] == expect["result"]["hex"] -async def test_blockchain_relayfee(protocol, writer): - expect = 0.00001 - query = {"params": []} - data = await protocol.blockchain_relayfee(writer, query) - assert data["result"] == expect + # params = [123, 3, 127] + # expect = get_expect(method, params) + # data = await protocol.blockchain_block_headers(writer, {"params": params}) + # assert data["result"]["branch"] == expect["result"]["branch"] + # assert data["result"]["root"] == expect["result"]["root"] + # assert data["result"]["hex"] == expect["result"]["hex"] async def test_blockchain_scripthash_get_balance(protocol, writer): - shs = [ - "c036b0ff3ad79662cd517cd5fe1fa0af07377b9262d16f276f11ced69aaa6921", - "92dd1eb7c042956d3dd9185a58a2578f61fee91347196604540838ccd0f8c08c", + method = "blockchain.scripthash.get_balance" + params = [ + "c036b0ff3ad79662cd517cd5fe1fa0af07377b9262d16f276f11ced69aaa6921" ] - expect = [ - { - "result": { - "confirmed": 0, - "unconfirmed": 0 - } - }, - { - "result": { - "confirmed": 831000, - "unconfirmed": 0 - } - }, + expect = get_expect(method, params) + data = await protocol.blockchain_scripthash_get_balance( + writer, {"params": params}) + assert data["result"]["unconfirmed"] == expect["result"]["unconfirmed"] + assert data["result"]["confirmed"] == expect["result"]["confirmed"] + + params = [ + "92dd1eb7c042956d3dd9185a58a2578f61fee91347196604540838ccd0f8c08c" ] - - data = [] - for i in shs: - params = {"params": [i]} - data.append(await - protocol.blockchain_scripthash_get_balance(writer, params)) - - for i in expect: - assert data[expect.index(i)] == i + expect = get_expect(method, params) + data = await protocol.blockchain_scripthash_get_balance( + writer, {"params": params}) + assert data["result"]["unconfirmed"] == expect["result"]["unconfirmed"] + assert data["result"]["confirmed"] == expect["result"]["confirmed"] async def test_blockchain_scripthash_get_history(protocol, writer): - shs = [ - "c036b0ff3ad79662cd517cd5fe1fa0af07377b9262d16f276f11ced69aaa6921", - "92dd1eb7c042956d3dd9185a58a2578f61fee91347196604540838ccd0f8c08c", + method = "blockchain.scripthash.get_history" + params = [ + "c036b0ff3ad79662cd517cd5fe1fa0af07377b9262d16f276f11ced69aaa6921" ] - expect = [ - ( - 1936167, - "084eba0e08c78b63e07535b74a5a849994d49afade95d0d205e4963e3f568600", - ), - ( - 1936171, - "705c4f265df23726c09c5acb80f9e8a85845c17d68974d89814383855c8545a2", - ), - ( - 1936171, - "705c4f265df23726c09c5acb80f9e8a85845c17d68974d89814383855c8545a2", - ), - ( - 1970700, - "a9c3c22cc2589284288b28e802ea81723d649210d59dfa7e03af00475f4cec20", - ), - ] - - res = [] - for i in shs: - params = {"params": [i]} - data = await protocol.blockchain_scripthash_get_history(writer, params) - if "result" in data: - for j in data["result"]: - res.append((j["height"], j["tx_hash"])) - - assert res == expect + expect = get_expect(method, params) + data = await protocol.blockchain_scripthash_get_history( + writer, {"params": params}) + assert len(data["result"]) == len(expect["result"]) + for i in range(len(expect["result"])): + assert data["result"][i]["tx_hash"] == expect["result"][i]["tx_hash"] + assert data["result"][i]["height"] == expect["result"][i]["height"] async def test_blockchain_scripthash_listunspent(protocol, writer): - shs = [ - "c036b0ff3ad79662cd517cd5fe1fa0af07377b9262d16f276f11ced69aaa6921", - "92dd1eb7c042956d3dd9185a58a2578f61fee91347196604540838ccd0f8c08c", + method = "blockchain.scripthash.listunspent" + params = [ + "c036b0ff3ad79662cd517cd5fe1fa0af07377b9262d16f276f11ced69aaa6921" ] + expect = get_expect(method, params) + data = await protocol.blockchain_scripthash_listunspent( + writer, {"params": params}) + assert data["result"] == expect["result"] - expect = [ - [], - [1, 731000, 1936171], - [1, 100000, 1970700], + params = [ + "92dd1eb7c042956d3dd9185a58a2578f61fee91347196604540838ccd0f8c08c" ] + expect = get_expect(method, params) + data = await protocol.blockchain_scripthash_listunspent( + writer, {"params": params}) - res = [] - for i in shs: - params = {"params": [i]} - data = await protocol.blockchain_scripthash_listunspent(writer, params) - if "result" in data and len(data["result"]) > 0: - for j in data["result"]: - res.append([j["tx_pos"], j["value"], j["height"]]) - else: - res.append([]) - - assert res == expect + assert len(data["result"]) == len(expect["result"]) + for i in range(len(expect["result"])): + assert data["result"][i]["value"] == expect["result"][i]["value"] + assert data["result"][i]["height"] == expect["result"][i]["height"] + assert data["result"][i]["tx_pos"] == expect["result"][i]["tx_pos"] + assert data["result"][i]["tx_hash"] == expect["result"][i]["tx_hash"] async def test_blockchain_transaction_get(protocol, writer): - expect = "020000000001011caa5f4ba91ff0ab77712851c1b17943e68f28d46bb0d96cbc13cdbef53c2b87000000001716001412e6e94028ab399b67c1232383d12f1dd3fc03b5feffffff02a40111000000000017a914ff1d7f4c85c562764ca16daa11e97d10eda52ebf87a0860100000000001976a9144a0360eac874a569e82ca6b17274d90bccbcab5e88ac0247304402205392417f5ffba2c0f3a501476fb6872368b2065c53bf18b2a201691fb88cdbe5022016c68ec9e094ba2b06d4bdc6af996ac74b580ab9728c622bb5304aaff04cb6980121031092742ffdf5901ceafcccec090c58170ce1d0ec26963ef7c7a2738a415a317e0b121e00" - params = { - "params": [ - "a9c3c22cc2589284288b28e802ea81723d649210d59dfa7e03af00475f4cec20" - ] - } - data = await protocol.blockchain_transaction_get(writer, params) - assert data["result"] == expect + method = "blockchain.transaction.get" + params = [ + "a9c3c22cc2589284288b28e802ea81723d649210d59dfa7e03af00475f4cec20" + ] + expect = get_expect(method, params) + data = await protocol.blockchain_transaction_get(writer, {"params": params}) + assert data["result"] == expect["result"] -async def test_blockchain_transaction_from_pos(protocol, writer): - expect = "f50f1c9b9551db0cc6916cb590bb6ccb5dea8adcb40e0bc103c4440e04c95e3d" - params = {"params": [1839411, 0]} - data = await protocol.blockchain_transaction_from_pos(writer, params) - assert data["result"] == expect - return "blockchain_transaction_from_pos", True +async def test_blockchain_transaction_get_merkle(protocol, writer): + method = "blockchain.transaction.get_merkle" + params = [ + "a9c3c22cc2589284288b28e802ea81723d649210d59dfa7e03af00475f4cec20", + 1970700, + ] + expect = get_expect(method, params) + data = await protocol.blockchain_transaction_get_merkle( + writer, {"params": params}) + assert data["result"]["block_height"] == expect["result"]["block_height"] + assert data["result"]["merkle"] == expect["result"]["merkle"] + assert data["result"]["pos"] == expect["result"]["pos"] + + +async def test_blockchain_transaction_id_from_pos(protocol, writer): + method = "blockchain.transaction.id_from_pos" + params = [1970700, 28] + expect = get_expect(method, params) + data = await protocol.blockchain_transaction_id_from_pos( + writer, {"params": params}) + assert data["result"] == expect["result"] + + params = [1970700, 28, True] + expect = get_expect(method, params) + data = await protocol.blockchain_transaction_id_from_pos( + writer, {"params": params}) + assert data["result"]["tx_hash"] == expect["result"]["tx_hash"] + assert data["result"]["merkle"] == expect["result"]["merkle"] async def test_server_ping(protocol, writer): - expect = None - params = {"params": []} - data = await protocol.server_ping(writer, params) - assert data["result"] == expect - return "server_ping", True + method = "server.ping" + params = [] + expect = get_expect(method, params) + data = await protocol.server_ping(writer, {"params": params}) + assert data["result"] == expect["result"] class MockWriter(asyncio.StreamWriter): @@ -190,58 +221,65 @@ class MockWriter(asyncio.StreamWriter): return True +# Test orchestration +orchestration = { + "blockchain_block_header": + test_blockchain_block_header, + "blockchain_block_headers": + test_blockchain_block_headers, + # "blockchain_estimatefee": test_blockchain_estimatefee, + # "blockchain_headers_subscribe": test_blockchain_headers_subscribe, + # "blockchain_relayfee": test_blockchain_relayfee, + "blockchain_scripthash_get_balance": + test_blockchain_scripthash_get_balance, + "blockchain_scripthash_get_history": + test_blockchain_scripthash_get_history, + # "blockchain_scripthash_get_mempool": test_blockchain_scripthash_get_mempool, + "blockchain_scripthash_listunspent": + test_blockchain_scripthash_listunspent, + # "blockchain_scripthash_subscribe": test_blockchain_scripthash_subscribe, + # "blockchain_scripthash_unsubscribe": test_blockchain_scripthash_unsubscribe, + # "blockchain_transaction_broadcast": test_blockchain_transaction_broadcast, + "blockchain_transaction_get": + test_blockchain_transaction_get, + "blockchain_transaction_get_merkle": + test_blockchain_transaction_get_merkle, + "blockchain_transaction_id_from_pos": + test_blockchain_transaction_id_from_pos, + # "mempool_get_fee_histogram": test_mempool_get_fee_histogram, + # "server_add_peer": test_server_add_peer, + # "server_donation_address": test_server_donation_address, + # "server_features": test_server_features, + # "server_peers_subscribe": test_server_peers_subscribe, + "server_ping": + test_server_ping, + # "server_version": test_server_version, +} + + async def main(): test_pass = [] test_fail = [] + global bs + bs = socket(AF_INET, SOCK_STREAM) + bs.connect(blockstream) + log = getLogger("obelisktest") - protocol = ElectrumProtocol(log, "testnet", ENDPOINTS, {}) + protocol = ElectrumProtocol(log, "testnet", libbitcoin, {}) writer = MockWriter() - functions = { - "blockchain_block_header": - test_blockchain_block_header, - "blockchain_block_hedaers": - test_blockchain_block_headers, - "blockchain_estimatefee": - test_blockchain_estimatefee, - # "blockchain_headers_subscribe": test_blockchain_headers_subscribe, - "blockchain_relayfee": - test_blockchain_relayfee, - "blockchain_scripthash_get_balance": - test_blockchain_scripthash_get_balance, - "blockchain_scripthash_get_history": - test_blockchain_scripthash_get_history, - # "blockchain_scripthash_get_mempool": test_blockchain_scripthash_get_mempool, - "blockchain_scripthash_listunspent": - test_blockchain_scripthash_listunspent, - # "blockchain_scripthash_subscribe": test_blockchain_scripthash_subscribe, - # "blockchain_scripthash_unsubscribe": test_blockchain_scripthash_unsubscribe, - # "blockchain_transaction_broadcast": test_blockchain_transaction_broadcast, - "blockchain_transaction_get": - test_blockchain_transaction_get, - # "blockchain_transaction_get_merkle": test_blockchain_transaction_get_merkle, - "blockchain_transaction_from_pos": - test_blockchain_transaction_from_pos, - # "mempool_get_fee_histogram": test_mempool_get_fee_histogram, - # "server_add_peer": test_server_add_peer, - # "server_banner": test_server_banner, - # "server_donation_address": test_server_donation_address, - # "server_features": test_server_features, - # "server_peers_subscribe": test_server_peers_subscribe, - "server_ping": - test_server_ping, - # "server_version": test_server_version, - } - for func in functions: + for func in orchestration: try: - await functions[func](protocol, writer) + await orchestration[func](protocol, writer) print(f"PASS: {func}") test_pass.append(func) except AssertionError: print(f"FAIL: {func}") + traceback.print_exc() test_fail.append(func) + bs.close() await protocol.stop() print()