diff options
Diffstat (limited to 'qa/rpc-tests')
-rw-r--r-- | qa/rpc-tests/README.md | 29 | ||||
-rw-r--r-- | qa/rpc-tests/netutil.py | 134 | ||||
-rwxr-xr-x | qa/rpc-tests/rpcbind_test.py | 152 | ||||
-rwxr-xr-x | qa/rpc-tests/smartfees.py | 142 | ||||
-rw-r--r-- | qa/rpc-tests/util.py | 201 |
5 files changed, 617 insertions, 41 deletions
diff --git a/qa/rpc-tests/README.md b/qa/rpc-tests/README.md index 835ff11057..616c0525f4 100644 --- a/qa/rpc-tests/README.md +++ b/qa/rpc-tests/README.md @@ -1,22 +1,26 @@ Regression tests of RPC interface ================================= -python-bitcoinrpc: git subtree of https://github.com/jgarzik/python-bitcoinrpc +### [python-bitcoinrpc](https://github.com/jgarzik/python-bitcoinrpc) +Git subtree of [https://github.com/jgarzik/python-bitcoinrpc](https://github.com/jgarzik/python-bitcoinrpc). Changes to python-bitcoinrpc should be made upstream, and then -pulled here using git subtree +pulled here using git subtree. -skeleton.py : Copy this to create new regression tests. +### [skeleton.py](skeleton.py) +Copy this to create new regression tests. -listtransactions.py : Tests for the listtransactions RPC call +### [listtransactions.py](listtransactions.py) +Tests for the listtransactions RPC call. -util.py : generally useful functions +### [util.py](util.sh) +Generally useful functions. Bash-based tests, to be ported to Python: ----------------------------------------- -wallet.sh : Exercise wallet send/receive code. -walletbackup.sh : Exercise wallet backup / dump / import -txnmall.sh : Test proper accounting of malleable transactions -conflictedbalance.sh : More testing of malleable transaction handling +- wallet.sh : Exercise wallet send/receive code. +- walletbackup.sh : Exercise wallet backup / dump / import +- txnmall.sh : Test proper accounting of malleable transactions +- conflictedbalance.sh : More testing of malleable transaction handling Notes ===== @@ -32,5 +36,8 @@ test state. If you get into a bad state, you should be able to recover with: - rm -rf cache - killall bitcoind + +```bash +rm -rf cache +killall bitcoind +``` diff --git a/qa/rpc-tests/netutil.py b/qa/rpc-tests/netutil.py new file mode 100644 index 0000000000..9bea2e355e --- /dev/null +++ b/qa/rpc-tests/netutil.py @@ -0,0 +1,134 @@ +# Linux network utilities +import sys +import socket +import fcntl +import struct +import array +import os +import binascii + +# Roughly based on http://voorloopnul.com/blog/a-python-netstat-in-less-than-100-lines-of-code/ by Ricardo Pascal +STATE_ESTABLISHED = '01' +STATE_SYN_SENT = '02' +STATE_SYN_RECV = '03' +STATE_FIN_WAIT1 = '04' +STATE_FIN_WAIT2 = '05' +STATE_TIME_WAIT = '06' +STATE_CLOSE = '07' +STATE_CLOSE_WAIT = '08' +STATE_LAST_ACK = '09' +STATE_LISTEN = '0A' +STATE_CLOSING = '0B' + +def get_socket_inodes(pid): + ''' + Get list of socket inodes for process pid. + ''' + base = '/proc/%i/fd' % pid + inodes = [] + for item in os.listdir(base): + target = os.readlink(os.path.join(base, item)) + if target.startswith('socket:'): + inodes.append(int(target[8:-1])) + return inodes + +def _remove_empty(array): + return [x for x in array if x !=''] + +def _convert_ip_port(array): + host,port = array.split(':') + # convert host from mangled-per-four-bytes form as used by kernel + host = binascii.unhexlify(host) + host_out = '' + for x in range(0, len(host)/4): + (val,) = struct.unpack('=I', host[x*4:(x+1)*4]) + host_out += '%08x' % val + + return host_out,int(port,16) + +def netstat(typ='tcp'): + ''' + Function to return a list with status of tcp connections at linux systems + To get pid of all network process running on system, you must run this script + as superuser + ''' + with open('/proc/net/'+typ,'r') as f: + content = f.readlines() + content.pop(0) + result = [] + for line in content: + line_array = _remove_empty(line.split(' ')) # Split lines and remove empty spaces. + tcp_id = line_array[0] + l_addr = _convert_ip_port(line_array[1]) + r_addr = _convert_ip_port(line_array[2]) + state = line_array[3] + inode = int(line_array[9]) # Need the inode to match with process pid. + nline = [tcp_id, l_addr, r_addr, state, inode] + result.append(nline) + return result + +def get_bind_addrs(pid): + ''' + Get bind addresses as (host,port) tuples for process pid. + ''' + inodes = get_socket_inodes(pid) + bind_addrs = [] + for conn in netstat('tcp') + netstat('tcp6'): + if conn[3] == STATE_LISTEN and conn[4] in inodes: + bind_addrs.append(conn[1]) + return bind_addrs + +# from: http://code.activestate.com/recipes/439093/ +def all_interfaces(): + ''' + Return all interfaces that are up + ''' + is_64bits = sys.maxsize > 2**32 + struct_size = 40 if is_64bits else 32 + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + max_possible = 8 # initial value + while True: + bytes = max_possible * struct_size + names = array.array('B', '\0' * bytes) + outbytes = struct.unpack('iL', fcntl.ioctl( + s.fileno(), + 0x8912, # SIOCGIFCONF + struct.pack('iL', bytes, names.buffer_info()[0]) + ))[0] + if outbytes == bytes: + max_possible *= 2 + else: + break + namestr = names.tostring() + return [(namestr[i:i+16].split('\0', 1)[0], + socket.inet_ntoa(namestr[i+20:i+24])) + for i in range(0, outbytes, struct_size)] + +def addr_to_hex(addr): + ''' + Convert string IPv4 or IPv6 address to binary address as returned by + get_bind_addrs. + Very naive implementation that certainly doesn't work for all IPv6 variants. + ''' + if '.' in addr: # IPv4 + addr = [int(x) for x in addr.split('.')] + elif ':' in addr: # IPv6 + sub = [[], []] # prefix, suffix + x = 0 + addr = addr.split(':') + for i,comp in enumerate(addr): + if comp == '': + if i == 0 or i == (len(addr)-1): # skip empty component at beginning or end + continue + x += 1 # :: skips to suffix + assert(x < 2) + else: # two bytes per component + val = int(comp, 16) + sub[x].append(val >> 8) + sub[x].append(val & 0xff) + nullbytes = 16 - len(sub[0]) - len(sub[1]) + assert((x == 0 and nullbytes == 0) or (x == 1 and nullbytes > 0)) + addr = sub[0] + ([0] * nullbytes) + sub[1] + else: + raise ValueError('Could not parse address %s' % addr) + return binascii.hexlify(bytearray(addr)) diff --git a/qa/rpc-tests/rpcbind_test.py b/qa/rpc-tests/rpcbind_test.py new file mode 100755 index 0000000000..a31f8d98ef --- /dev/null +++ b/qa/rpc-tests/rpcbind_test.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python +# Copyright (c) 2014 The Bitcoin Core developers +# Distributed under the MIT/X11 software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +# Test for -rpcbind, as well as -rpcallowip and -rpcconnect + +# Add python-bitcoinrpc to module search path: +import os +import sys +sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "python-bitcoinrpc")) + +import json +import shutil +import subprocess +import tempfile +import traceback + +from bitcoinrpc.authproxy import AuthServiceProxy, JSONRPCException +from util import * +from netutil import * + +def run_bind_test(tmpdir, allow_ips, connect_to, addresses, expected): + ''' + Start a node with requested rpcallowip and rpcbind parameters, + then try to connect, and check if the set of bound addresses + matches the expected set. + ''' + expected = [(addr_to_hex(addr), port) for (addr, port) in expected] + base_args = ['-disablewallet', '-nolisten'] + if allow_ips: + base_args += ['-rpcallowip=' + x for x in allow_ips] + binds = ['-rpcbind='+addr for addr in addresses] + nodes = start_nodes(1, tmpdir, [base_args + binds], connect_to) + try: + pid = bitcoind_processes[0].pid + assert_equal(set(get_bind_addrs(pid)), set(expected)) + finally: + stop_nodes(nodes) + wait_bitcoinds() + +def run_allowip_test(tmpdir, allow_ips, rpchost): + ''' + Start a node with rpcwallow IP, and request getinfo + at a non-localhost IP. + ''' + base_args = ['-disablewallet', '-nolisten'] + ['-rpcallowip='+x for x in allow_ips] + nodes = start_nodes(1, tmpdir, [base_args]) + try: + # connect to node through non-loopback interface + url = "http://rt:rt@%s:%d" % (rpchost, START_RPC_PORT,) + node = AuthServiceProxy(url) + node.getinfo() + finally: + node = None # make sure connection will be garbage collected and closed + stop_nodes(nodes) + wait_bitcoinds() + + +def run_test(tmpdir): + assert(sys.platform == 'linux2') # due to OS-specific network stats queries, this test works only on Linux + # find the first non-loopback interface for testing + non_loopback_ip = None + for name,ip in all_interfaces(): + if ip != '127.0.0.1': + non_loopback_ip = ip + break + if non_loopback_ip is None: + assert(not 'This test requires at least one non-loopback IPv4 interface') + print("Using interface %s for testing" % non_loopback_ip) + + # check default without rpcallowip (IPv4 and IPv6 localhost) + run_bind_test(tmpdir, None, '127.0.0.1', [], + [('127.0.0.1', 11100), ('::1', 11100)]) + # check default with rpcallowip (IPv6 any) + run_bind_test(tmpdir, ['127.0.0.1'], '127.0.0.1', [], + [('::0', 11100)]) + # check only IPv4 localhost (explicit) + run_bind_test(tmpdir, ['127.0.0.1'], '127.0.0.1', ['127.0.0.1'], + [('127.0.0.1', START_RPC_PORT)]) + # check only IPv4 localhost (explicit) with alternative port + run_bind_test(tmpdir, ['127.0.0.1'], '127.0.0.1:32171', ['127.0.0.1:32171'], + [('127.0.0.1', 32171)]) + # check only IPv4 localhost (explicit) with multiple alternative ports on same host + run_bind_test(tmpdir, ['127.0.0.1'], '127.0.0.1:32171', ['127.0.0.1:32171', '127.0.0.1:32172'], + [('127.0.0.1', 32171), ('127.0.0.1', 32172)]) + # check only IPv6 localhost (explicit) + run_bind_test(tmpdir, ['[::1]'], '[::1]', ['[::1]'], + [('::1', 11100)]) + # check both IPv4 and IPv6 localhost (explicit) + run_bind_test(tmpdir, ['127.0.0.1'], '127.0.0.1', ['127.0.0.1', '[::1]'], + [('127.0.0.1', START_RPC_PORT), ('::1', START_RPC_PORT)]) + # check only non-loopback interface + run_bind_test(tmpdir, [non_loopback_ip], non_loopback_ip, [non_loopback_ip], + [(non_loopback_ip, START_RPC_PORT)]) + + # Check that with invalid rpcallowip, we are denied + run_allowip_test(tmpdir, [non_loopback_ip], non_loopback_ip) + try: + run_allowip_test(tmpdir, ['1.1.1.1'], non_loopback_ip) + assert(not 'Connection not denied by rpcallowip as expected') + except ValueError: + pass + +def main(): + import optparse + + parser = optparse.OptionParser(usage="%prog [options]") + parser.add_option("--nocleanup", dest="nocleanup", default=False, action="store_true", + help="Leave bitcoinds and test.* datadir on exit or error") + parser.add_option("--srcdir", dest="srcdir", default="../../src", + help="Source directory containing bitcoind/bitcoin-cli (default: %default%)") + parser.add_option("--tmpdir", dest="tmpdir", default=tempfile.mkdtemp(prefix="test"), + help="Root directory for datadirs") + (options, args) = parser.parse_args() + + os.environ['PATH'] = options.srcdir+":"+os.environ['PATH'] + + check_json_precision() + + success = False + nodes = [] + try: + print("Initializing test directory "+options.tmpdir) + if not os.path.isdir(options.tmpdir): + os.makedirs(options.tmpdir) + initialize_chain(options.tmpdir) + + run_test(options.tmpdir) + + success = True + + except AssertionError as e: + print("Assertion failed: "+e.message) + except Exception as e: + print("Unexpected exception caught during testing: "+str(e)) + traceback.print_tb(sys.exc_info()[2]) + + if not options.nocleanup: + print("Cleaning up") + wait_bitcoinds() + shutil.rmtree(options.tmpdir) + + if success: + print("Tests successful") + sys.exit(0) + else: + print("Failed") + sys.exit(1) + +if __name__ == '__main__': + main() diff --git a/qa/rpc-tests/smartfees.py b/qa/rpc-tests/smartfees.py new file mode 100755 index 0000000000..e8abbfba19 --- /dev/null +++ b/qa/rpc-tests/smartfees.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python + +# +# Test fee estimation code +# + +# Add python-bitcoinrpc to module search path: +import os +import sys +sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "python-bitcoinrpc")) + +import json +import random +import shutil +import subprocess +import tempfile +import traceback + +from bitcoinrpc.authproxy import AuthServiceProxy, JSONRPCException +from util import * + + +def run_test(nodes, test_dir): + nodes.append(start_node(0, test_dir, + ["-debug=mempool", "-debug=estimatefee"])) + # Node1 mines small-but-not-tiny blocks, and allows free transactions. + # NOTE: the CreateNewBlock code starts counting block size at 1,000 bytes, + # so blockmaxsize of 2,000 is really just 1,000 bytes (room enough for + # 6 or 7 transactions) + nodes.append(start_node(1, test_dir, + ["-blockprioritysize=1500", "-blockmaxsize=2000", + "-debug=mempool", "-debug=estimatefee"])) + connect_nodes(nodes[1], 0) + + # Node2 is a stingy miner, that + # produces very small blocks (room for only 3 or so transactions) + node2args = [ "-blockprioritysize=0", "-blockmaxsize=1500", + "-debug=mempool", "-debug=estimatefee"] + nodes.append(start_node(2, test_dir, node2args)) + connect_nodes(nodes[2], 0) + + sync_blocks(nodes) + + # Prime the memory pool with pairs of transactions + # (high-priority, random fee and zero-priority, random fee) + min_fee = Decimal("0.001") + fees_per_kb = []; + for i in range(12): + (txid, txhex, fee) = random_zeropri_transaction(nodes, Decimal("1.1"), + min_fee, min_fee, 20) + tx_kbytes = (len(txhex)/2)/1000.0 + fees_per_kb.append(float(fee)/tx_kbytes) + + # Mine blocks with node2 until the memory pool clears: + count_start = nodes[2].getblockcount() + while len(nodes[2].getrawmempool()) > 0: + nodes[2].setgenerate(True, 1) + sync_blocks(nodes) + + all_estimates = [ nodes[0].estimatefee(i) for i in range(1,20) ] + print("Fee estimates, super-stingy miner: "+str([str(e) for e in all_estimates])) + + # Estimates should be within the bounds of what transactions fees actually were: + delta = 1.0e-6 # account for rounding error + for e in filter(lambda x: x >= 0, all_estimates): + if float(e)+delta < min(fees_per_kb) or float(e)-delta > max(fees_per_kb): + raise AssertionError("Estimated fee (%f) out of range (%f,%f)"%(float(e), min_fee_kb, max_fee_kb)) + + # Generate transactions while mining 30 more blocks, this time with node1: + for i in range(30): + for j in range(random.randrange(6-4,6+4)): + (txid, txhex, fee) = random_transaction(nodes, Decimal("1.1"), + Decimal("0.0"), min_fee, 20) + tx_kbytes = (len(txhex)/2)/1000.0 + fees_per_kb.append(float(fee)/tx_kbytes) + nodes[1].setgenerate(True, 1) + sync_blocks(nodes) + + all_estimates = [ nodes[0].estimatefee(i) for i in range(1,20) ] + print("Fee estimates, more generous miner: "+str([ str(e) for e in all_estimates])) + for e in filter(lambda x: x >= 0, all_estimates): + if float(e)+delta < min(fees_per_kb) or float(e)-delta > max(fees_per_kb): + raise AssertionError("Estimated fee (%f) out of range (%f,%f)"%(float(e), min_fee_kb, max_fee_kb)) + + # Finish by mining a normal-sized block: + while len(nodes[0].getrawmempool()) > 0: + nodes[0].setgenerate(True, 1) + sync_blocks(nodes) + + final_estimates = [ nodes[0].estimatefee(i) for i in range(1,20) ] + print("Final fee estimates: "+str([ str(e) for e in final_estimates])) + +def main(): + import optparse + + parser = optparse.OptionParser(usage="%prog [options]") + parser.add_option("--nocleanup", dest="nocleanup", default=False, action="store_true", + help="Leave bitcoinds and test.* datadir on exit or error") + parser.add_option("--srcdir", dest="srcdir", default="../../src", + help="Source directory containing bitcoind/bitcoin-cli (default: %default%)") + parser.add_option("--tmpdir", dest="tmpdir", default=tempfile.mkdtemp(prefix="test"), + help="Root directory for datadirs") + (options, args) = parser.parse_args() + + os.environ['PATH'] = options.srcdir+":"+os.environ['PATH'] + + check_json_precision() + + success = False + nodes = [] + try: + print("Initializing test directory "+options.tmpdir) + print(" node0 running at: 127.0.0.1:%d"%(p2p_port(0))) + if not os.path.isdir(options.tmpdir): + os.makedirs(options.tmpdir) + initialize_chain(options.tmpdir) + + run_test(nodes, options.tmpdir) + + success = True + + except AssertionError as e: + print("Assertion failed: "+e.message) + except Exception as e: + print("Unexpected exception caught during testing: "+str(e)) + traceback.print_tb(sys.exc_info()[2]) + + if not options.nocleanup: + print("Cleaning up") + stop_nodes(nodes) + wait_bitcoinds() + shutil.rmtree(options.tmpdir) + + if success: + print("Tests successful") + sys.exit(0) + else: + print("Failed") + sys.exit(1) + +if __name__ == '__main__': + main() diff --git a/qa/rpc-tests/util.py b/qa/rpc-tests/util.py index fa0700f1c8..eded098c7c 100644 --- a/qa/rpc-tests/util.py +++ b/qa/rpc-tests/util.py @@ -12,15 +12,19 @@ sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "python from decimal import Decimal import json +import random import shutil import subprocess import time +import re from bitcoinrpc.authproxy import AuthServiceProxy, JSONRPCException from util import * -START_P2P_PORT=11000 -START_RPC_PORT=11100 +def p2p_port(n): + return 11000 + n + os.getpid()%999 +def rpc_port(n): + return 12000 + n + os.getpid()%999 def check_json_precision(): """Make sure json library being used does not lose precision converting BTC values""" @@ -57,6 +61,18 @@ def sync_mempools(rpc_connections): bitcoind_processes = [] +def initialize_datadir(dir, n): + datadir = os.path.join(dir, "node"+str(n)) + if not os.path.isdir(datadir): + os.makedirs(datadir) + with open(os.path.join(datadir, "bitcoin.conf"), 'w') as f: + f.write("regtest=1\n"); + f.write("rpcuser=rt\n"); + f.write("rpcpassword=rt\n"); + f.write("port="+str(p2p_port(n))+"\n"); + f.write("rpcport="+str(rpc_port(n))+"\n"); + return datadir + def initialize_chain(test_dir): """ Create (or copy from cache) a 200-block-long chain and @@ -65,27 +81,21 @@ def initialize_chain(test_dir): """ if not os.path.isdir(os.path.join("cache", "node0")): + devnull = open("/dev/null", "w+") # Create cache directories, run bitcoinds: for i in range(4): - datadir = os.path.join("cache", "node"+str(i)) - os.makedirs(datadir) - with open(os.path.join(datadir, "bitcoin.conf"), 'w') as f: - f.write("regtest=1\n"); - f.write("rpcuser=rt\n"); - f.write("rpcpassword=rt\n"); - f.write("port="+str(START_P2P_PORT+i)+"\n"); - f.write("rpcport="+str(START_RPC_PORT+i)+"\n"); + datadir=initialize_datadir("cache", i) args = [ "bitcoind", "-keypool=1", "-datadir="+datadir ] if i > 0: - args.append("-connect=127.0.0.1:"+str(START_P2P_PORT)) + args.append("-connect=127.0.0.1:"+str(p2p_port(0))) bitcoind_processes.append(subprocess.Popen(args)) - subprocess.check_output([ "bitcoin-cli", "-datadir="+datadir, - "-rpcwait", "getblockcount"]) - + subprocess.check_call([ "bitcoin-cli", "-datadir="+datadir, + "-rpcwait", "getblockcount"], stdout=devnull) + devnull.close() rpcs = [] for i in range(4): try: - url = "http://rt:rt@127.0.0.1:%d"%(START_RPC_PORT+i,) + url = "http://rt:rt@127.0.0.1:%d"%(rpc_port(i),) rpcs.append(AuthServiceProxy(url)) except: sys.stderr.write("Error connecting to "+url+"\n") @@ -110,21 +120,50 @@ def initialize_chain(test_dir): from_dir = os.path.join("cache", "node"+str(i)) to_dir = os.path.join(test_dir, "node"+str(i)) shutil.copytree(from_dir, to_dir) + initialize_datadir(test_dir, i) # Overwrite port/rpcport in bitcoin.conf + +def _rpchost_to_args(rpchost): + '''Convert optional IP:port spec to rpcconnect/rpcport args''' + if rpchost is None: + return [] + + match = re.match('(\[[0-9a-fA-f:]+\]|[^:]+)(?::([0-9]+))?$', rpchost) + if not match: + raise ValueError('Invalid RPC host spec ' + rpchost) + + rpcconnect = match.group(1) + rpcport = match.group(2) -def start_nodes(num_nodes, dir): - # Start bitcoinds, and wait for RPC interface to be up and running: - for i in range(num_nodes): - datadir = os.path.join(dir, "node"+str(i)) - args = [ "bitcoind", "-datadir="+datadir ] - bitcoind_processes.append(subprocess.Popen(args)) - subprocess.check_output([ "bitcoin-cli", "-datadir="+datadir, - "-rpcwait", "getblockcount"]) - # Create&return JSON-RPC connections - rpc_connections = [] - for i in range(num_nodes): - url = "http://rt:rt@127.0.0.1:%d"%(START_RPC_PORT+i,) - rpc_connections.append(AuthServiceProxy(url)) - return rpc_connections + if rpcconnect.startswith('['): # remove IPv6 [...] wrapping + rpcconnect = rpcconnect[1:-1] + + rv = ['-rpcconnect=' + rpcconnect] + if rpcport: + rv += ['-rpcport=' + rpcport] + return rv + +def start_node(i, dir, extra_args=None, rpchost=None): + """ + Start a bitcoind and return RPC connection to it + """ + datadir = os.path.join(dir, "node"+str(i)) + args = [ "bitcoind", "-datadir="+datadir, "-keypool=1" ] + if extra_args is not None: args.extend(extra_args) + bitcoind_processes.append(subprocess.Popen(args)) + devnull = open("/dev/null", "w+") + subprocess.check_call([ "bitcoin-cli", "-datadir="+datadir] + + _rpchost_to_args(rpchost) + + ["-rpcwait", "getblockcount"], stdout=devnull) + devnull.close() + url = "http://rt:rt@%s:%d" % (rpchost or '127.0.0.1', rpc_port(i)) + return AuthServiceProxy(url) + +def start_nodes(num_nodes, dir, extra_args=None, rpchost=None): + """ + Start multiple bitcoinds, return RPC connections to them + """ + if extra_args is None: extra_args = [ None for i in range(num_nodes) ] + return [ start_node(i, dir, extra_args[i], rpchost) for i in range(num_nodes) ] def debug_log(dir, n_node): return os.path.join(dir, "node"+str(n_node), "regtest", "debug.log") @@ -141,9 +180,111 @@ def wait_bitcoinds(): del bitcoind_processes[:] def connect_nodes(from_connection, node_num): - ip_port = "127.0.0.1:"+str(START_P2P_PORT+node_num) + ip_port = "127.0.0.1:"+str(p2p_port(node_num)) from_connection.addnode(ip_port, "onetry") +def find_output(node, txid, amount): + """ + Return index to output of txid with value amount + Raises exception if there is none. + """ + txdata = node.getrawtransaction(txid, 1) + for i in range(len(txdata["vout"])): + if txdata["vout"][i]["value"] == amount: + return i + raise RuntimeError("find_output txid %s : %s not found"%(txid,str(amount))) + +def gather_inputs(from_node, amount_needed): + """ + Return a random set of unspent txouts that are enough to pay amount_needed + """ + utxo = from_node.listunspent(1) + random.shuffle(utxo) + inputs = [] + total_in = Decimal("0.00000000") + while total_in < amount_needed and len(utxo) > 0: + t = utxo.pop() + total_in += t["amount"] + inputs.append({ "txid" : t["txid"], "vout" : t["vout"], "address" : t["address"] } ) + if total_in < amount_needed: + raise RuntimeError("Insufficient funds: need %d, have %d"%(amount+fee*2, total_in)) + return (total_in, inputs) + +def make_change(from_node, amount_in, amount_out, fee): + """ + Create change output(s), return them + """ + outputs = {} + amount = amount_out+fee + change = amount_in - amount + if change > amount*2: + # Create an extra change output to break up big inputs + outputs[from_node.getnewaddress()] = float(change/2) + change = change/2 + if change > 0: + outputs[from_node.getnewaddress()] = float(change) + return outputs + +def send_zeropri_transaction(from_node, to_node, amount, fee): + """ + Create&broadcast a zero-priority transaction. + Returns (txid, hex-encoded-txdata) + Ensures transaction is zero-priority by first creating a send-to-self, + then using it's output + """ + + # Create a send-to-self with confirmed inputs: + self_address = from_node.getnewaddress() + (total_in, inputs) = gather_inputs(from_node, amount+fee*2) + outputs = make_change(from_node, total_in, amount+fee, fee) + outputs[self_address] = float(amount+fee) + + self_rawtx = from_node.createrawtransaction(inputs, outputs) + self_signresult = from_node.signrawtransaction(self_rawtx) + self_txid = from_node.sendrawtransaction(self_signresult["hex"], True) + + vout = find_output(from_node, self_txid, amount+fee) + # Now immediately spend the output to create a 1-input, 1-output + # zero-priority transaction: + inputs = [ { "txid" : self_txid, "vout" : vout } ] + outputs = { to_node.getnewaddress() : float(amount) } + + rawtx = from_node.createrawtransaction(inputs, outputs) + signresult = from_node.signrawtransaction(rawtx) + txid = from_node.sendrawtransaction(signresult["hex"], True) + + return (txid, signresult["hex"]) + +def random_zeropri_transaction(nodes, amount, min_fee, fee_increment, fee_variants): + """ + Create a random zero-priority transaction. + Returns (txid, hex-encoded-transaction-data, fee) + """ + from_node = random.choice(nodes) + to_node = random.choice(nodes) + fee = min_fee + fee_increment*random.randint(0,fee_variants) + (txid, txhex) = send_zeropri_transaction(from_node, to_node, amount, fee) + return (txid, txhex, fee) + +def random_transaction(nodes, amount, min_fee, fee_increment, fee_variants): + """ + Create a random transaction. + Returns (txid, hex-encoded-transaction-data, fee) + """ + from_node = random.choice(nodes) + to_node = random.choice(nodes) + fee = min_fee + fee_increment*random.randint(0,fee_variants) + + (total_in, inputs) = gather_inputs(from_node, amount+fee) + outputs = make_change(from_node, total_in, amount, fee) + outputs[to_node.getnewaddress()] = float(amount) + + rawtx = from_node.createrawtransaction(inputs, outputs) + signresult = from_node.signrawtransaction(rawtx) + txid = from_node.sendrawtransaction(signresult["hex"], True) + + return (txid, signresult["hex"], fee) + def assert_equal(thing1, thing2): if thing1 != thing2: raise AssertionError("%s != %s"%(str(thing1),str(thing2))) |