From 51bafa323accbcd1268537d0ef6c950b4cb16edc Mon Sep 17 00:00:00 2001 From: Sergi Delgado Segura Date: Fri, 16 Aug 2019 17:01:16 +0100 Subject: [PATCH] Improves the simulator and adds some tests The simulator now return the proper error messages when incorrect data is pased on RPC calls. Functionality for forks is missing. --- pisa/tools.py | 5 ++ tests/bitcoin_sim_tests.py | 109 +++++++++++++++++++++++++++++++++++++ tests/bitcoind_sim.py | 97 +++++++++++++++++++++++---------- 3 files changed, 182 insertions(+), 29 deletions(-) create mode 100644 tests/bitcoin_sim_tests.py diff --git a/pisa/tools.py b/pisa/tools.py index 0155509..42ad73f 100644 --- a/pisa/tools.py +++ b/pisa/tools.py @@ -1,3 +1,4 @@ +import re from pisa.utils.authproxy import JSONRPCException from pisa.rpc_errors import RPC_INVALID_ADDRESS_OR_KEY from http.client import HTTPException @@ -55,3 +56,7 @@ def in_correct_network(bitcoin_cli, network): return correct_network + +def check_txid_format(txid): + # TODO: #12-check-txid-regexp + return isinstance(txid, str) and re.search(r'^[0-9A-Fa-f]{64}$', txid) is not None diff --git a/tests/bitcoin_sim_tests.py b/tests/bitcoin_sim_tests.py new file mode 100644 index 0000000..dd77baa --- /dev/null +++ b/tests/bitcoin_sim_tests.py @@ -0,0 +1,109 @@ +import os +import binascii +from pisa.utils.authproxy import AuthServiceProxy, JSONRPCException +from pisa.conf import BTC_RPC_USER, BTC_RPC_PASSWD, BTC_RPC_HOST, BTC_RPC_PORT +from pisa.tools import check_txid_format + + +bitcoin_cli = AuthServiceProxy("http://%s:%s@%s:%d" % (BTC_RPC_USER, BTC_RPC_PASSWD, BTC_RPC_HOST, BTC_RPC_PORT)) + +# Help should always return 0 +assert(bitcoin_cli.help() == 0) + +# getblockhash should return a blockid (which matches the txid format) +block_hash = bitcoin_cli.getblockhash(0) +assert(check_txid_format(block_hash)) + +# Check that the values are within range and of the proper format (all should fail) +values = [-1, 500, None, '', '111', [], 1.1] +print("getblockhash fails ({}):".format(len(values))) + +for v in values: + try: + block_hash = bitcoin_cli.getblockhash(v) + assert False + except JSONRPCException as e: + print('\t{}'.format(e)) + +# getblock should return a list of transactions and the height +block = bitcoin_cli.getblock(block_hash) +assert(isinstance(block.get('tx'), list)) +assert(len(block.get('tx')) != 0) +assert(isinstance(block.get('height'), int)) + +# Some fails +values += ["a"*64, binascii.hexlify(os.urandom(32)).decode()] +print("\ngetblock fails ({}):".format(len(values))) + +for v in values: + try: + block = bitcoin_cli.getblock(v) + assert False + except JSONRPCException as e: + print('\t{}'.format(e)) + +# decoderawtransaction should only return if the given transaction matches a txid format +coinbase_tx = block.get('tx')[0] +tx = bitcoin_cli.decoderawtransaction(coinbase_tx) +assert(isinstance(tx, dict)) +assert(isinstance(tx.get('txid'), str)) +assert(check_txid_format(tx.get('txid'))) + +# Therefore should also work for a random formatted 32-byte hex in our simulation +random_tx = binascii.hexlify(os.urandom(32)).decode() +tx = bitcoin_cli.decoderawtransaction(random_tx) +assert(isinstance(tx, dict)) +assert(isinstance(tx.get('txid'), str)) +assert(check_txid_format(tx.get('txid'))) + +# But it should fail for not proper formatted one +values = [1, None, '', "a"*63, "b"*65, [], binascii.hexlify(os.urandom(31)).hex()] +print("\ndecoderawtransaction fails ({}):".format(len(values))) + +for v in values: + try: + block = bitcoin_cli.decoderawtransaction(v) + assert False + except JSONRPCException as e: + print('\t{}'.format(e)) + +# sendrawtransaction should only allow txids that the simulator has not mined yet +bitcoin_cli.sendrawtransaction(binascii.hexlify(os.urandom(32)).decode()) + +# Any data not matching the txid format or that matches with an already mined transaction should fail +values += [coinbase_tx] + +print("\nsendrawtransaction fails ({}):".format(len(values))) + +for v in values: + try: + block = bitcoin_cli.sendrawtransaction(v) + assert False + except JSONRPCException as e: + print('\t{}'.format(e)) + +# getrawtransaction should work for existing transactions, and fail for non-existing ones +tx = bitcoin_cli.getrawtransaction(coinbase_tx) + +assert(isinstance(tx, dict)) +assert(isinstance(tx.get('confirmations'), int)) + +print("\nsendrawtransaction fails ({}):".format(len(values))) + +for v in values: + try: + block = bitcoin_cli.sendrawtransaction(v) + assert False + except JSONRPCException as e: + print('\t{}'.format(e)) + +# getblockcount should always return a positive integer +bc = bitcoin_cli.getblockcount() +assert (isinstance(bc, int)) +assert (bc >= 0) + +print("\nAll tests passed!") + + + + diff --git a/tests/bitcoind_sim.py b/tests/bitcoind_sim.py index e2b8b39..01ce712 100644 --- a/tests/bitcoind_sim.py +++ b/tests/bitcoind_sim.py @@ -2,6 +2,8 @@ from pisa.conf import FEED_PROTOCOL, FEED_ADDR, FEED_PORT from flask import Flask, request, Response, abort from tests.zmq_publisher import ZMQPublisher from threading import Thread +from pisa.rpc_errors import * +from pisa.tools import check_txid_format import binascii import json import os @@ -41,70 +43,107 @@ def process_request(): simulator. getblockhash: a block hash is only queried by pisad on bootstrapping to check the network bitcoind is - running on. It always asks for the genesis block. Since this is ment to be for testing we - will return the testnet3 genesis block hash. + running on. help: help is only used as a sample command to test if bitcoind is running when bootstrapping pisad. It will return a 200/OK with no data. """ - global sent_transactions + global mempool request_data = request.get_json() method = request_data.get('method') response = {"id": 0, "result": 0, "error": None} + no_param_err = {"code": RPC_MISC_ERROR, "message": "JSON value is not a {} as expected"} if method == "decoderawtransaction": txid = get_param(request_data) - if txid: - response["result"] = {"txid": txid} + if isinstance(txid, str): + if check_txid_format(txid): + response["result"] = {"txid": txid} + + else: + response["error"] = {"code": RPC_DESERIALIZATION_ERROR, "message": "TX decode failed"} + + else: + response["error"] = no_param_err + response["error"]["message"] = response["error"]["message"].format("string") elif method == "sendrawtransaction": + # TODO: A way of rejecting transactions should be added to test edge cases. txid = get_param(request_data) - if txid: - sent_transactions.append(txid) + if isinstance(txid, str): + if check_txid_format(txid): + if txid not in mempool and txid not in list(mined_transactions.keys()): + mempool.append(txid) - # FIXME: If the same transaction is sent twice it should return an error informing that the transaction is - # already known + else: + response["error"] = {"code": RPC_VERIFY_ALREADY_IN_CHAIN, + "message": "Transaction already in block chain"} + + else: + response["error"] = {"code": RPC_DESERIALIZATION_ERROR, "message": "TX decode failed"} + + else: + response["error"] = no_param_err + response["error"]["message"] = response["error"]["message"].format("string") elif method == "getrawtransaction": txid = get_param(request_data) - if txid: + if isinstance(txid, str): block = blocks.get(mined_transactions.get(txid)) if block: - response["result"] = {"confirmations": block_count - block.get('height')} + response["result"] = {"confirmations": len(blockchain) - block.get('height')} else: - # FIXME: if the transaction cannot be found it should return an error. Check bitcoind - return abort(500) + response["error"] = {'code': RPC_INVALID_ADDRESS_OR_KEY, + 'message': 'No such mempool or blockchain transaction. Use gettransaction for ' + 'wallet transactions.'} + else: + response["error"] = no_param_err + response["error"]["message"] = response["error"]["message"].format("string") elif method == "getblockcount": - response["result"] = block_count + response["result"] = len(blockchain) elif method == "getblock": blockid = get_param(request_data) - if blockid: - response["result"] = blocks.get(blockid) + if isinstance(blockid, str): + block = blocks.get(blockid) + + if block: + response["result"] = block + + else: + response["error"] = {"code": RPC_INVALID_ADDRESS_OR_KEY, "message": "Block not found"} + + else: + response["error"] = no_param_err + response["error"]["message"] = response["error"]["message"].format("string") elif method == "getblockhash": height = get_param(request_data) - if height == 0: - # testnet3 genesis block hash - response["result"] = "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943" + if isinstance(height, int): + if 0 <= height <= len(blockchain): + response["result"] = blockchain[height] + + else: + response["error"] = {"code": RPC_INVALID_PARAMETER, "message": "Block height out of range"} else: - return abort(500, "Unsupported method") + response["error"] = no_param_err + response["error"]["message"] = response["error"]["message"].format("integer") elif method == "help": pass else: - return abort(500, "Unsupported method") + return abort(404, "Method not found") return Response(json.dumps(response), status=200, mimetype='application/json') @@ -124,26 +163,26 @@ def load_data(): def simulate_mining(): - global sent_transactions, mined_transactions, blocks, block_count + global mempool, mined_transactions, blocks, blockchain while True: block_hash = binascii.hexlify(os.urandom(32)).decode('utf-8') coinbase_tx_hash = binascii.hexlify(os.urandom(32)).decode('utf-8') txs_to_mine = [coinbase_tx_hash] - if len(sent_transactions) != 0: + if len(mempool) != 0: # We'll mine up to 100 txs per block - txs_to_mine += sent_transactions[:99] - sent_transactions = sent_transactions[99:] + txs_to_mine += mempool[:99] + mempool = mempool[99:] # Keep track of the mined transaction (to respond to getrawtransaction) for tx in txs_to_mine: mined_transactions[tx] = block_hash - blocks[block_hash] = {"tx": txs_to_mine, "height": block_count} + blocks[block_hash] = {"tx": txs_to_mine, "height": len(blockchain)} mining_simulator.publish_data(binascii.unhexlify(block_hash)) + blockchain.append(block_hash) - block_count += 1 time.sleep(10) @@ -151,10 +190,10 @@ if __name__ == '__main__': mining_simulator = ZMQPublisher(topic=b'hashblock', feed_protocol=FEED_PROTOCOL, feed_addr=FEED_ADDR, feed_port=FEED_PORT) - sent_transactions = [] + mempool = [] mined_transactions = {} blocks = {} - block_count = 0 + blockchain = [] mining_thread = Thread(target=simulate_mining) mining_thread.start()