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.
This commit is contained in:
Sergi Delgado Segura
2019-08-16 17:01:16 +01:00
parent 3ac2d446a3
commit 51bafa323a
3 changed files with 182 additions and 29 deletions

View File

@@ -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

109
tests/bitcoin_sim_tests.py Normal file
View File

@@ -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!")

View File

@@ -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:
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:
return abort(500, "Unsupported method")
response["error"] = {"code": RPC_INVALID_PARAMETER, "message": "Block height out of range"}
else:
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()