URI: 
       ttest_electrum_protocol.py - 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
       ---
       ttest_electrum_protocol.py (16141B)
       ---
            1 #!/usr/bin/env python3
            2 # Copyright (C) 2021 Ivan J. <parazyd@dyne.org>
            3 #
            4 # This file is part of obelisk
            5 #
            6 # This program is free software: you can redistribute it and/or modify
            7 # it under the terms of the GNU Affero General Public License version 3
            8 # as published by the Free Software Foundation.
            9 #
           10 # This program is distributed in the hope that it will be useful,
           11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
           12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
           13 # GNU Affero General Public License for more details.
           14 #
           15 # You should have received a copy of the GNU Affero General Public License
           16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
           17 """
           18 Test unit for the Electrum protocol. Takes results from testnet
           19 blockstream.info:143 server as value reference.
           20 
           21 See bottom of file for test orchestration.
           22 """
           23 import asyncio
           24 import json
           25 import sys
           26 import traceback
           27 from logging import getLogger
           28 from pprint import pprint
           29 from socket import socket, AF_INET, SOCK_STREAM
           30 
           31 from obelisk.errors_jsonrpc import JsonRPCError
           32 from obelisk.protocol import (
           33     ElectrumProtocol,
           34     VERSION,
           35     SERVER_PROTO_MIN,
           36     SERVER_PROTO_MAX,
           37 )
           38 from obelisk.zeromq import create_random_id
           39 
           40 libbitcoin = {
           41     "query": "tcp://testnet2.libbitcoin.net:29091",
           42     "heart": "tcp://testnet2.libbitcoin.net:29092",
           43     "block": "tcp://testnet2.libbitcoin.net:29093",
           44     "trans": "tcp://testnet2.libbitcoin.net:29094",
           45 }
           46 
           47 blockstream = ("blockstream.info", 143)
           48 bs = None  # Socket
           49 
           50 
           51 def get_expect(method, params):
           52     global bs
           53     req = {
           54         "json-rpc": "2.0",
           55         "id": create_random_id(),
           56         "method": method,
           57         "params": params
           58     }
           59     bs.send(json.dumps(req).encode("utf-8") + b"\n")
           60     recv_buf = bytearray()
           61     while True:
           62         data = bs.recv(4096)
           63         if not data or len(data) == 0:  # pragma: no cover
           64             raise ValueError("No data received from blockstream")
           65         recv_buf.extend(data)
           66         lb = recv_buf.find(b"\n")
           67         if lb == -1:  # pragma: no cover
           68             continue
           69         while lb != -1:
           70             line = recv_buf[:lb].rstrip()
           71             recv_buf = recv_buf[lb + 1:]
           72             lb = recv_buf.find(b"\n")
           73             line = line.decode("utf-8")
           74             resp = json.loads(line)
           75             return resp
           76 
           77 
           78 def assert_equal(data, expect):  # pragma: no cover
           79     try:
           80         assert data == expect
           81     except AssertionError:
           82         print("Got:")
           83         pprint(data)
           84         print("Expected:")
           85         pprint(expect)
           86         raise
           87 
           88 
           89 async def test_server_version(protocol, writer, method):
           90     params = ["obelisk 42", [SERVER_PROTO_MIN, SERVER_PROTO_MAX]]
           91     expect = {"result": [f"obelisk {VERSION}", SERVER_PROTO_MAX]}
           92     data = await protocol.server_version(writer, {"params": params})
           93     assert_equal(data["result"], expect["result"])
           94 
           95     params = ["obelisk", "0.0"]
           96     expect = JsonRPCError.protonotsupported()
           97     data = await protocol.server_version(writer, {"params": params})
           98     assert_equal(data, expect)
           99 
          100     params = ["obelisk"]
          101     expect = JsonRPCError.invalidparams()
          102     data = await protocol.server_version(writer, {"params": params})
          103     assert_equal(data, expect)
          104 
          105 
          106 async def test_ping(protocol, writer, method):
          107     params = []
          108     expect = get_expect(method, params)
          109     data = await protocol.ping(writer, {"params": params})
          110     assert_equal(data["result"], expect["result"])
          111 
          112 
          113 async def test_block_header(protocol, writer, method):
          114     params = [[123], [1, 5]]
          115     for i in params:
          116         expect = get_expect(method, i)
          117         data = await protocol.block_header(writer, {"params": i})
          118         assert_equal(data["result"], expect["result"])
          119 
          120     params = [[], [-3], [4, -1], [5, 3]]
          121     for i in params:
          122         expect = JsonRPCError.invalidparams()
          123         data = await protocol.block_header(writer, {"params": i})
          124         assert_equal(data, expect)
          125 
          126 
          127 async def test_block_headers(protocol, writer, method):
          128     params = [[123, 3], [11, 3, 14]]
          129     for i in params:
          130         expect = get_expect(method, i)
          131         data = await protocol.block_headers(writer, {"params": i})
          132         assert_equal(data["result"], expect["result"])
          133 
          134     params = [[], [1], [-3, 1], [4, -1], [7, 4, 4]]
          135     for i in params:
          136         expect = JsonRPCError.invalidparams()
          137         data = await protocol.block_headers(writer, {"params": i})
          138         assert_equal(data, expect)
          139 
          140 
          141 async def test_estimatefee(protocol, writer, method):
          142     params = [2]
          143     expect = 0.00001
          144     data = await protocol.estimatefee(writer, {"params": params})
          145     assert_equal(data["result"], expect)
          146 
          147 
          148 async def test_headers_subscribe(protocol, writer, method):
          149     params = [[]]
          150     for i in params:
          151         expect = get_expect(method, i)
          152         data = await protocol.headers_subscribe(writer, {"params": i})
          153         assert_equal(data["result"], expect["result"])
          154 
          155 
          156 async def test_relayfee(protocol, writer, method):
          157     expect = 0.00001
          158     data = await protocol.relayfee(writer, {"params": []})
          159     assert_equal(data["result"], expect)
          160 
          161 
          162 async def test_scripthash_get_balance(protocol, writer, method):
          163     params = [
          164         ["c036b0ff3ad79662cd517cd5fe1fa0af07377b9262d16f276f11ced69aaa6921"],
          165         ["92dd1eb7c042956d3dd9185a58a2578f61fee91347196604540838ccd0f8c08c"],
          166         ["b97b504af8fcf94a47d3ae5a346d38220f0751732d9b89a413568bfbf4b36ec6"],
          167     ]
          168     for i in params:
          169         expect = get_expect(method, i)
          170         data = await protocol.scripthash_get_balance(writer, {"params": i})
          171         assert_equal(data["result"], expect["result"])
          172 
          173     params = [
          174         [],
          175         ["foobar"],
          176         [
          177             "c036b0ff3ad79662cd517cd5fe1fa0af07377b9262d16f276f11ced69aaa6921",
          178             42,
          179         ],
          180     ]
          181     for i in params:
          182         expect = JsonRPCError.invalidparams()
          183         data = await protocol.scripthash_get_balance(writer, {"params": i})
          184         assert_equal(data, expect)
          185 
          186 
          187 async def test_scripthash_get_history(protocol, writer, method):
          188     params = [
          189         ["c036b0ff3ad79662cd517cd5fe1fa0af07377b9262d16f276f11ced69aaa6921"],
          190         ["b97b504af8fcf94a47d3ae5a346d38220f0751732d9b89a413568bfbf4b36ec6"],
          191     ]
          192     for i in params:
          193         expect = get_expect(method, i)
          194         data = await protocol.scripthash_get_history(writer, {"params": i})
          195         assert_equal(data["result"], expect["result"])
          196 
          197     params = [
          198         [],
          199         ["foobar"],
          200         [
          201             "c036b0ff3ad79662cd517cd5fe1fa0af07377b9262d16f276f11ced69aaa6921",
          202             42,
          203         ],
          204     ]
          205     for i in params:
          206         expect = JsonRPCError.invalidparams()
          207         data = await protocol.scripthash_get_history(writer, {"params": i})
          208         assert_equal(data, expect)
          209 
          210 
          211 async def test_scripthash_listunspent(protocol, writer, method):
          212     params = [
          213         ["c036b0ff3ad79662cd517cd5fe1fa0af07377b9262d16f276f11ced69aaa6921"],
          214         ["92dd1eb7c042956d3dd9185a58a2578f61fee91347196604540838ccd0f8c08c"],
          215         ["b97b504af8fcf94a47d3ae5a346d38220f0751732d9b89a413568bfbf4b36ec6"],
          216     ]
          217     for i in params:
          218         # Blockstream is broken here and doesn't return in ascending order.
          219         expect = get_expect(method, i)
          220         srt = sorted(expect["result"], key=lambda x: x["height"])
          221         data = await protocol.scripthash_listunspent(writer, {"params": i})
          222         assert_equal(data["result"], srt)
          223 
          224     params = [
          225         [],
          226         ["foobar"],
          227         [
          228             "c036b0ff3ad79662cd517cd5fe1fa0af07377b9262d16f276f11ced69aaa6921",
          229             42,
          230         ],
          231     ]
          232     for i in params:
          233         expect = JsonRPCError.invalidparams()
          234         data = await protocol.scripthash_listunspent(writer, {"params": i})
          235         assert_equal(data, expect)
          236 
          237 
          238 async def test_scripthash_subscribe(protocol, writer, method):
          239     params = [
          240         ["92dd1eb7c042956d3dd9185a58a2578f61fee91347196604540838ccd0f8c08c"],
          241     ]
          242     for i in params:
          243         expect = get_expect(method, i)
          244         data = await protocol.scripthash_subscribe(writer, {"params": i})
          245         assert_equal(data["result"], expect["result"])
          246 
          247     params = [
          248         [],
          249         ["foobar"],
          250         [
          251             "c036b0ff3ad79662cd517cd5fe1fa0af07377b9262d16f276f11ced69aaa6921",
          252             42,
          253         ],
          254     ]
          255     for i in params:
          256         expect = JsonRPCError.invalidparams()
          257         data = await protocol.scripthash_subscribe(writer, {"params": i})
          258         assert_equal(data, expect)
          259 
          260 
          261 async def test_scripthash_unsubscribe(protocol, writer, method):
          262     # Here blockstream doesn't even care
          263     params = [
          264         ["92dd1eb7c042956d3dd9185a58a2578f61fee91347196604540838ccd0f8c08c"],
          265     ]
          266     for i in params:
          267         data = await protocol.scripthash_unsubscribe(writer, {"params": i})
          268         assert data["result"] is True
          269 
          270     params = [
          271         [],
          272         ["foobar"],
          273         [
          274             "c036b0ff3ad79662cd517cd5fe1fa0af07377b9262d16f276f11ced69aaa6921",
          275             42,
          276         ],
          277     ]
          278     for i in params:
          279         expect = JsonRPCError.invalidparams()
          280         data = await protocol.scripthash_unsubscribe(writer, {"params": i})
          281         assert_equal(data, expect)
          282 
          283 
          284 async def test_transaction_get(protocol, writer, method):
          285     params = [
          286         ["a9c3c22cc2589284288b28e802ea81723d649210d59dfa7e03af00475f4cec20"],
          287     ]
          288     for i in params:
          289         expect = get_expect(method, i)
          290         data = await protocol.transaction_get(writer, {"params": i})
          291         assert_equal(data["result"], expect["result"])
          292 
          293     params = [[], [1], ["foo"], ["dead beef"]]
          294     for i in params:
          295         expect = JsonRPCError.invalidparams()
          296         data = await protocol.transaction_get(writer, {"params": i})
          297         assert_equal(data, expect)
          298 
          299 
          300 async def test_transaction_get_merkle(protocol, writer, method):
          301     params = [
          302         [
          303             "a9c3c22cc2589284288b28e802ea81723d649210d59dfa7e03af00475f4cec20",
          304             1970700,
          305         ],
          306     ]
          307     for i in params:
          308         expect = get_expect(method, i)
          309         data = await protocol.transaction_get_merkle(writer, {"params": i})
          310         assert_equal(data["result"], expect["result"])
          311 
          312     params = [
          313         [],
          314         ["foo", 1],
          315         [3, 1],
          316         [
          317             "a9c3c22cc2589284288b28e802ea81723d649210d59dfa7e03af00475f4cec20",
          318             -4,
          319         ],
          320         [
          321             "a9c3c22cc2589284288b28e802ea81723d649210d59dfa7e03af00475f4cec20",
          322             "foo",
          323         ],
          324     ]
          325     for i in params:
          326         expect = JsonRPCError.invalidparams()
          327         data = await protocol.transaction_get_merkle(writer, {"params": i})
          328         assert_equal(data, expect)
          329 
          330 
          331 async def test_transaction_id_from_pos(protocol, writer, method):
          332     params = [[1970700, 28], [1970700, 28, True]]
          333     for i in params:
          334         expect = get_expect(method, i)
          335         data = await protocol.transaction_id_from_pos(writer, {"params": i})
          336         assert_equal(data["result"], expect["result"])
          337 
          338     params = [[123], [-1, 1], [1, -1], [3, 42, 4]]
          339     for i in params:
          340         expect = JsonRPCError.invalidparams()
          341         data = await protocol.transaction_id_from_pos(writer, {"params": i})
          342         assert_equal(data, expect)
          343 
          344 
          345 async def test_get_fee_histogram(protocol, writer, method):
          346     data = await protocol.get_fee_histogram(writer, {"params": []})
          347     assert_equal(data["result"], [[0, 0]])
          348 
          349 
          350 async def test_add_peer(protocol, writer, method):
          351     data = await protocol.add_peer(writer, {"params": []})
          352     assert_equal(data["result"], False)
          353 
          354 
          355 async def test_banner(protocol, writer, method):
          356     data = await protocol.banner(writer, {"params": []})
          357     assert_equal(type(data["result"]), str)
          358 
          359 
          360 async def test_donation_address(protocol, writer, method):
          361     data = await protocol.donation_address(writer, {"params": []})
          362     assert_equal(type(data["result"]), str)
          363 
          364 
          365 async def test_peers_subscribe(protocol, writer, method):
          366     data = await protocol.peers_subscribe(writer, {"params": []})
          367     assert_equal(data["result"], [])
          368 
          369 
          370 async def test_send_notification(protocol, writer, method):
          371     params = ["sent notification"]
          372     expect = (json.dumps({
          373         "jsonrpc": "2.0",
          374         "method": method,
          375         "params": params
          376     }).encode("utf-8") + b"\n")
          377     await protocol._send_notification(writer, method, params)
          378     assert_equal(writer.mock, expect)
          379 
          380 
          381 async def test_send_reply(protocol, writer, method):
          382     error = {"error": {"code": 42, "message": 42}}
          383     result = {"result": 42}
          384 
          385     expect = (json.dumps({
          386         "jsonrpc": "2.0",
          387         "error": error["error"],
          388         "id": None
          389     }).encode("utf-8") + b"\n")
          390     await protocol._send_reply(writer, error, None)
          391     assert_equal(writer.mock, expect)
          392 
          393     expect = (json.dumps({
          394         "jsonrpc": "2.0",
          395         "result": result["result"],
          396         "id": 42
          397     }).encode("utf-8") + b"\n")
          398     await protocol._send_reply(writer, result, {"id": 42})
          399     assert_equal(writer.mock, expect)
          400 
          401 
          402 async def test_handle_query(protocol, writer, method):
          403     query = {"jsonrpc": "2.0", "method": method, "id": 42, "params": []}
          404     await protocol.handle_query(writer, query)
          405 
          406     method = "server.donation_address"
          407     query = {"jsonrpc": "2.0", "method": method, "id": 42, "params": []}
          408     await protocol.handle_query(writer, query)
          409 
          410     query = {"jsonrpc": "2.0", "method": method, "params": []}
          411     await protocol.handle_query(writer, query)
          412 
          413     query = {"jsonrpc": "2.0", "id": 42, "params": []}
          414     await protocol.handle_query(writer, query)
          415 
          416 
          417 class MockTransport:
          418 
          419     def __init__(self):
          420         self.peername = ("foo", 42)
          421 
          422     def get_extra_info(self, param):
          423         return self.peername
          424 
          425 
          426 class MockWriter(asyncio.StreamWriter):  # pragma: no cover
          427     """Mock class for StreamWriter"""
          428 
          429     def __init__(self):
          430         self.mock = None
          431         self._transport = MockTransport()
          432 
          433     def write(self, data):
          434         self.mock = data
          435         return True
          436 
          437     async def drain(self):
          438         return True
          439 
          440 
          441 # Test orchestration
          442 orchestration = {
          443     "server.version": test_server_version,
          444     "server.ping": test_ping,
          445     "blockchain.block.header": test_block_header,
          446     "blockchain.block.headers": test_block_headers,
          447     "blockchain.estimatefee": test_estimatefee,
          448     "blockchain.headers.subscribe": test_headers_subscribe,
          449     "blockchain.relayfee": test_relayfee,
          450     "blockchain.scripthash.get_balance": test_scripthash_get_balance,
          451     "blockchain.scripthash.get_history": test_scripthash_get_history,
          452     # "blockchain.scripthash.get_mempool": test_scripthash_get_mempool,
          453     "blockchain.scripthash.listunspent": test_scripthash_listunspent,
          454     "blockchain.scripthash.subscribe": test_scripthash_subscribe,
          455     "blockchain.scripthash.unsubscribe": test_scripthash_unsubscribe,
          456     # "blockchain.transaction.broadcast": test_transaction_broadcast,
          457     "blockchain.transaction.get": test_transaction_get,
          458     "blockchain.transaction.get_merkle": test_transaction_get_merkle,
          459     "blockchain.transaction.id_from_pos": test_transaction_id_from_pos,
          460     "mempool.get_fee_histogram": test_get_fee_histogram,
          461     "server.add_peer": test_add_peer,
          462     "server.banner": test_banner,
          463     "server.donation_address": test_donation_address,
          464     # "server.features": test_server_features,
          465     "server.peers_subscribe": test_peers_subscribe,
          466     "_send_notification": test_send_notification,
          467     "_send_reply": test_send_reply,
          468     "_handle_query": test_handle_query,
          469 }
          470 
          471 
          472 async def main():
          473     test_pass = []
          474     test_fail = []
          475 
          476     global bs
          477     bs = socket(AF_INET, SOCK_STREAM)
          478     bs.connect(blockstream)
          479 
          480     log = getLogger("obelisktest")
          481     protocol = ElectrumProtocol(log, "testnet", libbitcoin, {})
          482     writer = MockWriter()
          483 
          484     protocol.peers[protocol._get_peer(writer)] = {"tasks": [], "sh": {}}
          485 
          486     for func in orchestration:
          487         try:
          488             await orchestration[func](protocol, writer, func)
          489             print(f"PASS: {func}")
          490             test_pass.append(func)
          491         except AssertionError:  # pragma: no cover
          492             print(f"FAIL: {func}")
          493             traceback.print_exc()
          494             test_fail.append(func)
          495 
          496     bs.close()
          497     await protocol.stop()
          498 
          499     print()
          500     print(f"Tests passed: {len(test_pass)}")
          501     print(f"Tests failed: {len(test_fail)}")
          502 
          503     ret = 1 if len(test_fail) > 0 else 0
          504     sys.exit(ret)