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)