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)