obelisk

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

test_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)