mirror of
https://github.com/aljazceru/python-teos.git
synced 2025-12-17 22:24:23 +01:00
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:
@@ -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
109
tests/bitcoin_sim_tests.py
Normal 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!")
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user