diff --git a/test/functional/abc-cmdline.py b/test/functional/abc-cmdline.py --- a/test/functional/abc-cmdline.py +++ b/test/functional/abc-cmdline.py @@ -11,10 +11,7 @@ import re from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import ( - assert_equal, - assert_start_raises_init_error -) +from test_framework.util import assert_equal from test_framework.cdefs import LEGACY_MAX_BLOCK_SIZE, DEFAULT_MAX_BLOCK_SIZE MAX_GENERATED_BLOCK_SIZE_ERROR = ( @@ -56,11 +53,11 @@ self.log.info(" Attempt to set below legacy limit of 1MB - try %d bytes" % LEGACY_MAX_BLOCK_SIZE) self.stop_node(0) - assert_start_raises_init_error(0, self.options.tmpdir, [ - "-excessiveblocksize=%d" % LEGACY_MAX_BLOCK_SIZE], 'Error: Excessive block size must be > 1,000,000 bytes (1MB)') + self.assert_start_raises_init_error(0, self.options.tmpdir, [ + "-excessiveblocksize=%d" % LEGACY_MAX_BLOCK_SIZE], 'Error: Excessive block size must be > 1,000,000 bytes (1MB)') self.log.info(" Attempt to set below blockmaxsize (mining limit)") - assert_start_raises_init_error(0, self.options.tmpdir, [ - '-blockmaxsize=1500000', '-excessiveblocksize=1300000'], 'Error: ' + MAX_GENERATED_BLOCK_SIZE_ERROR) + self.assert_start_raises_init_error(0, self.options.tmpdir, [ + '-blockmaxsize=1500000', '-excessiveblocksize=1300000'], 'Error: ' + MAX_GENERATED_BLOCK_SIZE_ERROR) # Make sure we leave the test with a node running as this is what thee # framework expects. diff --git a/test/functional/bip9-softforks.py b/test/functional/bip9-softforks.py --- a/test/functional/bip9-softforks.py +++ b/test/functional/bip9-softforks.py @@ -3,17 +3,6 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -from test_framework.blockstore import BlockStore -from test_framework.test_framework import ComparisonTestFramework -from test_framework.util import * -from test_framework.mininode import CTransaction, NetworkThread -from test_framework.blocktools import create_coinbase, create_block -from test_framework.comptool import TestInstance, TestManager -from test_framework.script import CScript, OP_1NEGATE, OP_CHECKSEQUENCEVERIFY, OP_DROP -from io import BytesIO -import time -import itertools - ''' This test is meant to exercise BIP forks Connect to a single node. @@ -28,6 +17,17 @@ test that enforcement has triggered ''' +from test_framework.test_framework import ComparisonTestFramework +from test_framework.util import * +from test_framework.mininode import CTransaction, NetworkThread +from test_framework.blocktools import create_coinbase, create_block +from test_framework.comptool import TestInstance, TestManager +from test_framework.script import CScript, OP_1NEGATE, OP_CHECKSEQUENCEVERIFY, OP_DROP +from io import BytesIO +import time +import shutil +import itertools + class BIP9SoftForksTest(ComparisonTestFramework): diff --git a/test/functional/blockchain.py b/test/functional/blockchain.py --- a/test/functional/blockchain.py +++ b/test/functional/blockchain.py @@ -22,15 +22,14 @@ import http.client import subprocess -from test_framework.test_framework import BitcoinTestFramework +from test_framework.test_framework import ( + BitcoinTestFramework, BITCOIND_PROC_WAIT_TIMEOUT) from test_framework.util import ( assert_equal, assert_raises, assert_raises_jsonrpc, assert_is_hex_string, assert_is_hash_string, - bitcoind_processes, - BITCOIND_PROC_WAIT_TIMEOUT, ) @@ -168,13 +167,13 @@ assert_equal(self.nodes[0].getblockcount(), 206) self.log.debug('Node should not stop at this height') assert_raises(subprocess.TimeoutExpired, - lambda: bitcoind_processes[0].wait(timeout=3)) + lambda: self.bitcoind_processes[0].wait(timeout=3)) try: self.nodes[0].generate(1) except (ConnectionError, http.client.BadStatusLine): pass # The node already shut down before response self.log.debug('Node should stop at this height...') - bitcoind_processes[0].wait(timeout=BITCOIND_PROC_WAIT_TIMEOUT) + self.bitcoind_processes[0].wait(timeout=BITCOIND_PROC_WAIT_TIMEOUT) self.nodes[0] = self.start_node(0, self.options.tmpdir) assert_equal(self.nodes[0].getblockcount(), 207) diff --git a/test/functional/fundrawtransaction.py b/test/functional/fundrawtransaction.py --- a/test/functional/fundrawtransaction.py +++ b/test/functional/fundrawtransaction.py @@ -3,7 +3,7 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -from test_framework.test_framework import BitcoinTestFramework +from test_framework.test_framework import BitcoinTestFramework, BITCOIND_PROC_WAIT_TIMEOUT from test_framework.util import * @@ -463,7 +463,7 @@ self.stop_node(2) self.stop_node(3) self.nodes[1].encryptwallet("test") - self.nodes.pop(1) + self.bitcoind_processes[1].wait(timeout=BITCOIND_PROC_WAIT_TIMEOUT) self.nodes = self.start_nodes(self.num_nodes, self.options.tmpdir) # This test is not meant to test fee estimation and we'd like diff --git a/test/functional/keypool.py b/test/functional/keypool.py --- a/test/functional/keypool.py +++ b/test/functional/keypool.py @@ -22,7 +22,7 @@ # Encrypt wallet and wait to terminate nodes[0].encryptwallet('test') - bitcoind_processes[0].wait() + self.bitcoind_processes[0].wait() # Restart node 0 nodes[0] = self.start_node(0, self.options.tmpdir) # Keep creating keys diff --git a/test/functional/listtransactions.py b/test/functional/listtransactions.py --- a/test/functional/listtransactions.py +++ b/test/functional/listtransactions.py @@ -27,7 +27,7 @@ def setup_nodes(self): # This test requires mocktime - enable_mocktime() + self.enable_mocktime() self.nodes = self.start_nodes(self.num_nodes, self.options.tmpdir) def run_test(self): diff --git a/test/functional/receivedby.py b/test/functional/receivedby.py --- a/test/functional/receivedby.py +++ b/test/functional/receivedby.py @@ -34,7 +34,7 @@ def setup_nodes(self): # This test requires mocktime - enable_mocktime() + self.enable_mocktime() self.nodes = self.start_nodes(self.num_nodes, self.options.tmpdir) def run_test(self): diff --git a/test/functional/rpcbind_test.py b/test/functional/rpcbind_test.py --- a/test/functional/rpcbind_test.py +++ b/test/functional/rpcbind_test.py @@ -36,7 +36,7 @@ binds = ['-rpcbind=' + addr for addr in addresses] self.nodes = self.start_nodes(self.num_nodes, self.options.tmpdir, [ base_args + binds], connect_to) - pid = bitcoind_processes[0].pid + pid = self.bitcoind_processes[0].pid assert_equal(set(get_bind_addrs(pid)), set(expected)) self.stop_nodes() diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -4,7 +4,9 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. from collections import deque +import errno from enum import Enum +import http.client import logging import optparse import os @@ -15,17 +17,15 @@ import time import traceback +from .authproxy import JSONRPCException +from . import coverage from .util import ( - PortSeed, MAX_NODES, - bitcoind_processes, + PortSeed, + assert_equal, check_json_precision, connect_nodes_bi, - disable_mocktime, disconnect_nodes, - enable_coverage, - enable_mocktime, - get_mocktime, get_rpc_proxy, initialize_datadir, get_datadir_path, @@ -33,15 +33,9 @@ p2p_port, rpc_url, set_node_times, - _start_node, - _start_nodes, - _stop_node, - _stop_nodes, sync_blocks, sync_mempools, - wait_for_bitcoind_start, ) -from .authproxy import JSONRPCException class TestStatus(Enum): @@ -54,6 +48,8 @@ TEST_EXIT_FAILED = 1 TEST_EXIT_SKIPPED = 77 +BITCOIND_PROC_WAIT_TIMEOUT = 60 + class BitcoinTestFramework(object): """Base class for a bitcoin test script. @@ -74,7 +70,9 @@ def __init__(self): self.num_nodes = 4 self.setup_clean_chain = False - self.nodes = None + self.nodes = [] + self.bitcoind_processes = {} + self.mocktime = 0 def add_options(self, parser): pass @@ -105,16 +103,14 @@ extra_args = None if hasattr(self, "extra_args"): extra_args = self.extra_args - self.nodes = _start_nodes( + self.nodes = self.start_nodes( self.num_nodes, self.options.tmpdir, extra_args) def run_test(self): raise NotImplementedError # Main function. This should not be overridden by the subclass test scripts. - def main(self): - 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") @@ -141,9 +137,6 @@ self.add_options(parser) (self.options, self.args) = parser.parse_args() - if self.options.coveragedir: - enable_coverage(self.options.coveragedir) - PortSeed.n = self.options.port_seed os.environ['PATH'] = self.options.srcdir + ":" + \ @@ -224,16 +217,95 @@ # Public helper methods. These can be accessed by the subclass test scripts. def start_node(self, i, dirname, extra_args=None, rpchost=None, timewait=None, binary=None, stderr=None): - return _start_node(i, dirname, extra_args, rpchost, timewait, binary, stderr) + """Start a bitcoind and return RPC connection to it""" + + datadir = os.path.join(dirname, "node" + str(i)) + if binary is None: + binary = os.getenv("BITCOIND", "bitcoind") + args = [binary, "-datadir=" + datadir, "-server", "-keypool=1", "-discover=0", "-rest", "-logtimemicros", "-debug", + "-debugexclude=libevent", "-debugexclude=leveldb", "-mocktime=" + str(self.mocktime), "-uacomment=testnode%d" % i] + if extra_args is not None: + args.extend(extra_args) + self.bitcoind_processes[i] = subprocess.Popen(args, stderr=stderr) + self.log.debug( + "initialize_chain: bitcoind started, waiting for RPC to come up") + self._wait_for_bitcoind_start( + self.bitcoind_processes[i], datadir, i, rpchost) + self.log.debug("initialize_chain: RPC successfully started") + proxy = get_rpc_proxy( + rpc_url(datadir, i, rpchost), i, timeout=timewait) + + if self.options.coveragedir: + coverage.write_all_rpc_commands(self.options.coveragedir, proxy) + + return proxy def start_nodes(self, num_nodes, dirname, extra_args=None, rpchost=None, timewait=None, binary=None): - return _start_nodes(num_nodes, dirname, extra_args, rpchost, timewait, binary) + """Start multiple bitcoinds, return RPC connections to them""" + + if extra_args is None: + extra_args = [None] * num_nodes + if binary is None: + binary = [None] * num_nodes + assert_equal(len(extra_args), num_nodes) + assert_equal(len(binary), num_nodes) + rpcs = [] + try: + for i in range(num_nodes): + rpcs.append(self.start_node( + i, dirname, extra_args[i], rpchost, timewait=timewait, binary=binary[i])) + except: + # If one node failed to start, stop the others + # TODO: abusing self.nodes in this way is a little hacky. + # Eventually we should do a better job of tracking nodes + self.nodes.extend(rpcs) + self.stop_nodes() + self.nodes = [] + raise + return rpcs + + def stop_node(self, i): + """Stop a bitcoind test node""" - def stop_node(self, num_node): - _stop_node(self.nodes[num_node], num_node) + self.log.debug("Stopping node %d" % i) + try: + self.nodes[i].stop() + except http.client.CannotSendRequest as e: + self.log.exception("Unable to stop node") + return_code = self.bitcoind_processes[i].wait( + timeout=BITCOIND_PROC_WAIT_TIMEOUT) + del self.bitcoind_processes[i] + assert_equal(return_code, 0) def stop_nodes(self): - _stop_nodes(self.nodes) + """Stop multiple bitcoind test nodes""" + + for i in range(len(self.nodes)): + self.stop_node(i) + assert not self.bitcoind_processes.values() # All connections must be gone now + + def assert_start_raises_init_error(self, i, dirname, extra_args=None, expected_msg=None): + with tempfile.SpooledTemporaryFile(max_size=2**16) as log_stderr: + try: + self.start_node(i, dirname, extra_args, stderr=log_stderr) + self.stop_node(i) + except Exception as e: + assert 'bitcoind exited' in str(e) # node must have shutdown + if expected_msg is not None: + log_stderr.seek(0) + stderr = log_stderr.read().decode('utf-8') + if expected_msg not in stderr: + raise AssertionError( + "Expected error \"" + expected_msg + "\" not found in:\n" + stderr) + else: + if expected_msg is None: + assert_msg = "bitcoind should have exited with an error" + else: + assert_msg = "bitcoind should have exited with expected error " + expected_msg + raise AssertionError(assert_msg) + + def wait_for_node_exit(self, i, timeout): + self.bitcoind_processes[i].wait(timeout) def split_network(self): """ @@ -258,6 +330,21 @@ sync_blocks(group) sync_mempools(group) + def enable_mocktime(self): + """Enable mocktime for the script. + + mocktime may be needed for scripts that use the cached version of the + blockchain. If the cached version of the blockchain is used without + mocktime then the mempools will not sync due to IBD. + + For backwared compatibility of the python scripts with previous + versions of the cache, this helper function sets mocktime to Jan 1, + 2014 + (201 * 10 * 60)""" + self.mocktime = 1388534400 + (201 * 10 * 60) + + def disable_mocktime(self): + self.mocktime = 0 + # Private helper methods. These should not be accessed by the subclass test scripts. def _start_logging(self): @@ -275,8 +362,7 @@ ll = int( self.options.loglevel) if self.options.loglevel.isdigit() else self.options.loglevel.upper() ch.setLevel(ll) - # Format logs the same as bitcoind's debug.log with microprecision (so - # log files can be concatenated and sorted) + # Format logs the same as bitcoind's debug.log with microprecision (so log files can be concatenated and sorted) formatter = logging.Formatter( fmt='%(asctime)s.%(msecs)03d000 %(name)s (%(levelname)s): %(message)s', datefmt='%Y-%m-%d %H:%M:%S') formatter.converter = time.gmtime @@ -322,10 +408,11 @@ "-keypool=1", "-datadir=" + datadir, "-discover=0"] if i > 0: args.append("-connect=127.0.0.1:" + str(p2p_port(0))) - bitcoind_processes[i] = subprocess.Popen(args) + self.bitcoind_processes[i] = subprocess.Popen(args) self.log.debug( "initialize_chain: bitcoind started, waiting for RPC to come up") - wait_for_bitcoind_start(bitcoind_processes[i], datadir, i) + self._wait_for_bitcoind_start( + self.bitcoind_processes[i], datadir, i) self.log.debug("initialize_chain: RPC successfully started") self.nodes = [] @@ -344,8 +431,8 @@ # # blocks are created with timestamps 10 minutes apart # starting from 2010 minutes in the past - enable_mocktime() - block_time = get_mocktime() - (201 * 10 * 60) + self.enable_mocktime() + block_time = self.mocktime - (201 * 10 * 60) for i in range(2): for peer in range(4): for j in range(25): @@ -358,7 +445,7 @@ # Shut them down, and clean up cache directories: self.stop_nodes() self.nodes = [] - disable_mocktime() + self.disable_mocktime() for i in range(MAX_NODES): os.remove(log_filename(cachedir, i, "debug.log")) os.remove(log_filename(cachedir, i, "db.log")) @@ -380,21 +467,40 @@ for i in range(num_nodes): initialize_datadir(test_dir, i) -# Test framework for doing p2p comparison testing, which sets up some bitcoind -# binaries: -# 1 binary: test binary -# 2 binaries: 1 test binary, 1 ref binary -# n>2 binaries: 1 test binary, n-1 ref binaries - - -class SkipTest(Exception): - """This exception is raised to skip a test""" - - def __init__(self, message): - self.message = message + def _wait_for_bitcoind_start(self, process, datadir, i, rpchost=None): + """Wait for bitcoind to start. + + This means that RPC is accessible and fully initialized. + Raise an exception if bitcoind exits during initialization.""" + while True: + if process.poll() is not None: + raise Exception( + 'bitcoind exited with status %i during initialization' % process.returncode) + try: + # Check if .cookie file to be created + rpc = get_rpc_proxy( + rpc_url(datadir, i, rpchost), i, coveragedir=self.options.coveragedir) + rpc.getblockcount() + break # break out of loop on success + except IOError as e: + if e.errno != errno.ECONNREFUSED: # Port not yet open? + raise # unknown IO error + except JSONRPCException as e: # Initialization phase + if e.error['code'] != -28: # RPC in warmup? + raise # unknown JSON RPC exception + except ValueError as e: # cookie file not found and no rpcuser or rpcassword. bitcoind still starting + if "No RPC credentials" not in str(e): + raise + time.sleep(0.25) class ComparisonTestFramework(BitcoinTestFramework): + """Test framework for doing p2p comparison testing + + Sets up some bitcoind binaries: + - 1 binary: test binary + - 2 binaries: 1 test binary, 1 ref binary + - n>2 binaries: 1 test binary, n-1 ref binaries""" def __init__(self): super().__init__() @@ -417,3 +523,10 @@ self.num_nodes, self.options.tmpdir, extra_args, binary=[self.options.testbinary] + [self.options.refbinary] * (self.num_nodes - 1)) + + +class SkipTest(Exception): + """This exception is raised to skip a test""" + + def __init__(self, message): + self.message = message diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py --- a/test/functional/test_framework/util.py +++ b/test/functional/test_framework/util.py @@ -3,114 +3,156 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. - -# -# Helpful routines for regression testing -# - from base64 import b64encode from binascii import hexlify, unhexlify from decimal import Decimal, ROUND_DOWN import json -import http.client +import logging import os import sys import random -import shutil -import subprocess -import tempfile -import time import re -import errno -import logging +import time from . import coverage from .authproxy import AuthServiceProxy, JSONRPCException -DEFAULT_BITCOIND = 'bitcoind' -COVERAGE_DIR = None - logger = logging.getLogger("TestFramework.utils") -# The maximum number of nodes a single test can spawn -MAX_NODES = 8 -# Don't assign rpc or p2p ports lower than this -PORT_MIN = 11000 -# The number of ports to "reserve" for p2p and rpc, each -PORT_RANGE = 5000 - -BITCOIND_PROC_WAIT_TIMEOUT = 60 +# Assert functions +################## -class PortSeed: - # Must be initialized with a unique integer for each process - n = None - +def assert_fee_amount(fee, tx_size, fee_per_kB): + """Assert the fee was in range""" + target_fee = tx_size * fee_per_kB / 1000 + if fee < target_fee: + raise AssertionError( + "Fee of %s BTC too low! (Should be %s BTC)" % (str(fee), str(target_fee))) + # allow the wallet's estimation to be at most 2 bytes off + if fee > (tx_size + 2) * fee_per_kB / 1000: + raise AssertionError( + "Fee of %s BTC too high! (Should be %s BTC)" % (str(fee), str(target_fee))) -# Set Mocktime default to OFF. -# MOCKTIME is only needed for scripts that use the -# cached version of the blockchain. If the cached -# version of the blockchain is used without MOCKTIME -# then the mempools will not sync due to IBD. -MOCKTIME = 0 +def assert_equal(thing1, thing2, *args): + if thing1 != thing2 or any(thing1 != arg for arg in args): + raise AssertionError("not(%s)" % " == ".join(str(arg) + for arg in (thing1, thing2) + args)) -def enable_mocktime(): - # For backwared compatibility of the python scripts - # with previous versions of the cache, set MOCKTIME - # to Jan 1, 2014 + (201 * 10 * 60) - global MOCKTIME - MOCKTIME = 1388534400 + (201 * 10 * 60) +def assert_greater_than(thing1, thing2): + if thing1 <= thing2: + raise AssertionError("%s <= %s" % (str(thing1), str(thing2))) -def disable_mocktime(): - global MOCKTIME - MOCKTIME = 0 +def assert_greater_than_or_equal(thing1, thing2): + if thing1 < thing2: + raise AssertionError("%s < %s" % (str(thing1), str(thing2))) -def get_mocktime(): - return MOCKTIME +def assert_raises(exc, fun, *args, **kwds): + assert_raises_message(exc, None, fun, *args, **kwds) -def enable_coverage(dirname): - """Maintain a log of which RPC calls are made during testing.""" - global COVERAGE_DIR - COVERAGE_DIR = dirname +def assert_raises_message(exc, message, fun, *args, **kwds): + try: + fun(*args, **kwds) + except exc as e: + if message is not None and message not in e.error['message']: + raise AssertionError( + "Expected substring not found:" + e.error['message']) + except Exception as e: + raise AssertionError( + "Unexpected exception raised: " + type(e).__name__) + else: + raise AssertionError("No exception raised") -def get_rpc_proxy(url, node_number, timeout=None): - """ - Args: - url (str): URL of the RPC server to call - node_number (int): the node number (or id) that this calls to - Kwargs: - timeout (int): HTTP timeout in seconds +def assert_raises_jsonrpc(code, message, fun, *args, **kwds): + """Run an RPC and verify that a specific JSONRPC exception code and message is raised. - Returns: - AuthServiceProxy. convenience object for making RPC calls. + Calls function `fun` with arguments `args` and `kwds`. Catches a JSONRPCException + and verifies that the error code and message are as expected. Throws AssertionError if + no JSONRPCException was returned or if the error code/message are not as expected. + Args: + code (int), optional: the error code returned by the RPC call (defined + in src/rpc/protocol.h). Set to None if checking the error code is not required. + message (string), optional: [a substring of] the error string returned by the + RPC call. Set to None if checking the error string is not required + fun (function): the function to call. This should be the name of an RPC. + args*: positional arguments for the function. + kwds**: named arguments for the function. """ - proxy_kwargs = {} - if timeout is not None: - proxy_kwargs['timeout'] = timeout + try: + fun(*args, **kwds) + except JSONRPCException as e: + # JSONRPCException was thrown as expected. Check the code and message values are correct. + if (code is not None) and (code != e.error["code"]): + raise AssertionError( + "Unexpected JSONRPC error code %i" % e.error["code"]) + if (message is not None) and (message not in e.error['message']): + raise AssertionError( + "Expected substring not found:" + e.error['message']) + except Exception as e: + raise AssertionError( + "Unexpected exception raised: " + type(e).__name__) + else: + raise AssertionError("No exception raised") - proxy = AuthServiceProxy(url, **proxy_kwargs) - proxy.url = url # store URL on proxy for info - coverage_logfile = coverage.get_filename( - COVERAGE_DIR, node_number) if COVERAGE_DIR else None +def assert_is_hex_string(string): + try: + int(string, 16) + except Exception as e: + raise AssertionError( + "Couldn't interpret %r as hexadecimal; raised: %s" % (string, e)) - return coverage.AuthServiceProxyWrapper(proxy, coverage_logfile) +def assert_is_hash_string(string, length=64): + if not isinstance(string, str): + raise AssertionError("Expected a string, got type %r" % type(string)) + elif length and len(string) != length: + raise AssertionError( + "String of length %d expected; got %d" % (length, len(string))) + elif not re.match('[abcdef0-9]+$', string): + raise AssertionError( + "String %r contains invalid characters for a hash." % string) -def p2p_port(n): - assert(n <= MAX_NODES) - return PORT_MIN + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES) +def assert_array_result(object_array, to_match, expected, should_not_find=False): + """ + Pass in array of JSON objects, a dictionary with key/value pairs + to match against, and another dictionary with expected key/value + pairs. + If the should_not_find flag is true, to_match should not be found + in object_array + """ + if should_not_find: + assert_equal(expected, {}) + num_matched = 0 + for item in object_array: + all_match = True + for key, value in to_match.items(): + if item[key] != value: + all_match = False + if not all_match: + continue + elif should_not_find: + num_matched = num_matched + 1 + for key, value in expected.items(): + if item[key] != value: + raise AssertionError("%s : expected %s=%s" % + (str(item), str(key), str(value))) + num_matched = num_matched + 1 + if num_matched == 0 and not should_not_find: + raise AssertionError("No objects matched %s" % (str(to_match))) + if num_matched > 0 and should_not_find: + raise AssertionError("Objects were found %s" % (str(to_match))) -def rpc_port(n): - return PORT_MIN + PORT_RANGE + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES) +# Utility functions +################### def check_json_precision(): @@ -137,65 +179,75 @@ return b64encode(string.encode('utf-8')).decode('ascii') -def sync_blocks(rpc_connections, *, wait=1, timeout=60): - """ - Wait until everybody has the same tip. +def satoshi_round(amount): + return Decimal(amount).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN) - sync_blocks needs to be called with an rpc_connections set that has least - one node already synced to the latest, stable tip, otherwise there's a - chance it might return before all nodes are stably synced. - """ - # Use getblockcount() instead of waitforblockheight() to determine the - # initial max height because the two RPCs look at different internal global - # variables (chainActive vs latestBlock) and the former gets updated - # earlier. - maxheight = max(x.getblockcount() for x in rpc_connections) - start_time = cur_time = time.time() - while cur_time <= start_time + timeout: - tips = [r.waitforblockheight(maxheight, int(wait * 1000)) - for r in rpc_connections] - if all(t["height"] == maxheight for t in tips): - if all(t["hash"] == tips[0]["hash"] for t in tips): - return - raise AssertionError("Block sync failed, mismatched block hashes:{}".format( - "".join("\n {!r}".format(tip) for tip in tips))) - cur_time = time.time() - raise AssertionError("Block sync to height {} timed out:{}".format( - maxheight, "".join("\n {!r}".format(tip) for tip in tips))) +# RPC/P2P connection constants and functions +############################################ -def sync_chain(rpc_connections, *, wait=1, timeout=60): - """ - Wait until everybody has the same best block - """ - while timeout > 0: - best_hash = [x.getbestblockhash() for x in rpc_connections] - if best_hash == [best_hash[0]] * len(best_hash): - return - time.sleep(wait) - timeout -= wait - raise AssertionError("Chain sync failed: Best block hashes don't match") +# The maximum number of nodes a single test can spawn +MAX_NODES = 8 +# Don't assign rpc or p2p ports lower than this +PORT_MIN = 11000 +# The number of ports to "reserve" for p2p and rpc, each +PORT_RANGE = 5000 -def sync_mempools(rpc_connections, *, wait=1, timeout=60): +class PortSeed: + # Must be initialized with a unique integer for each process + n = None + + +def get_rpc_proxy(url, node_number, timeout=None, coveragedir=None): """ - Wait until everybody has the same transactions in their memory - pools + Args: + url (str): URL of the RPC server to call + node_number (int): the node number (or id) that this calls to + + Kwargs: + timeout (int): HTTP timeout in seconds + + Returns: + AuthServiceProxy. convenience object for making RPC calls. + """ - while timeout > 0: - pool = set(rpc_connections[0].getrawmempool()) - num_match = 1 - for i in range(1, len(rpc_connections)): - if set(rpc_connections[i].getrawmempool()) == pool: - num_match = num_match + 1 - if num_match == len(rpc_connections): - return - time.sleep(wait) - timeout -= wait - raise AssertionError("Mempool sync failed") + proxy_kwargs = {} + if timeout is not None: + proxy_kwargs['timeout'] = timeout + proxy = AuthServiceProxy(url, **proxy_kwargs) + proxy.url = url # store URL on proxy for info -bitcoind_processes = {} + coverage_logfile = coverage.get_filename( + coveragedir, node_number) if coveragedir else None + + return coverage.AuthServiceProxyWrapper(proxy, coverage_logfile) + + +def p2p_port(n): + assert(n <= MAX_NODES) + return PORT_MIN + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES) + + +def rpc_port(n): + return PORT_MIN + PORT_RANGE + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES) + + +def rpc_url(datadir, i, rpchost=None): + rpc_u, rpc_p = get_auth_cookie(datadir, i) + host = '127.0.0.1' + port = rpc_port(i) + if rpchost: + parts = rpchost.split(':') + if len(parts) == 2: + host, port = parts + else: + host = rpchost + return "http://%s:%s@%s:%d" % (rpc_u, rpc_p, host, int(port)) + +# Node functions +################ def initialize_datadir(dirname, n): @@ -238,168 +290,13 @@ return user, password -def rpc_url(datadir, i, rpchost=None): - rpc_u, rpc_p = get_auth_cookie(datadir, i) - host = '127.0.0.1' - port = rpc_port(i) - if rpchost: - parts = rpchost.split(':') - if len(parts) == 2: - host, port = parts - else: - host = rpchost - return "http://%s:%s@%s:%d" % (rpc_u, rpc_p, host, int(port)) - - -def wait_for_bitcoind_start(process, datadir, i, rpchost=None): - ''' - Wait for bitcoind to start. This means that RPC is accessible and fully initialized. - Raise an exception if bitcoind exits during initialization. - ''' - while True: - if process.poll() is not None: - raise Exception( - 'bitcoind exited with status %i during initialization' % process.returncode) - try: - # Check if .cookie file to be created - rpc = get_rpc_proxy(rpc_url(datadir, i, rpchost), i) - blocks = rpc.getblockcount() - break # break out of loop on success - except IOError as e: - if e.errno != errno.ECONNREFUSED: # Port not yet open? - raise # unknown IO error - except JSONRPCException as e: # Initialization phase - if e.error['code'] != -28: # RPC in warmup? - raise # unknown JSON RPC exception - except ValueError as e: # cookie file not found and no rpcuser or rpcassword. bitcoind still starting - if "No RPC credentials" not in str(e): - raise - time.sleep(0.25) - - -def locate_bitcoind_binary(): - """ - Find bitcoind binary if possible. - """ - bitcoind_binary = os.getenv("BITCOIND", DEFAULT_BITCOIND) - if os.path.exists(bitcoind_binary): - return bitcoind_binary - - if os.path.exists(os.path.join('src', DEFAULT_BITCOIND)): - bitcoind_binary = os.path.abspath( - os.path.join('src', DEFAULT_BITCOIND)) - elif bitcoind_binary == 'bitcoind' or not os.path.exists(bitcoind_binary): - # If BITCOIND was specified and exists, use it, otherwise look for source. - # get_srcdir() already returns an absolute path - src_dir_cand = get_srcdir(sys.argv[0]) - if src_dir_cand and os.path.exists( - os.path.join(src_dir_cand, 'src', DEFAULT_BITCOIND)): - bitcoind_binary = os.path.join( - src_dir_cand, 'src', DEFAULT_BITCOIND) - else: - sys.stderr.write("Unable to locate bitcoind for this test.\n") - sys.exit(1) - return bitcoind_binary - - -def _start_node(i, dirname, extra_args=None, rpchost=None, timewait=None, binary=None, stderr=None): - """Start a bitcoind and return RPC connection to it - - This function should only be called from within test_framework, not by individual test scripts.""" - - datadir = os.path.join(dirname, "node" + str(i)) - if binary is None: - binary = locate_bitcoind_binary() - args = [binary, "-datadir=" + datadir, "-server", "-keypool=1", "-discover=0", "-rest", "-logtimemicros", - "-debug", "-debugexclude=libevent", "-mocktime=" + str(get_mocktime()), "-uacomment=testnode%d" % i] - if extra_args is not None: - args.extend(extra_args) - bitcoind_processes[i] = subprocess.Popen(args, stderr=stderr) - logger.debug( - "initialize_chain: bitcoind started, waiting for RPC to come up") - wait_for_bitcoind_start(bitcoind_processes[i], datadir, i, rpchost) - logger.debug("initialize_chain: RPC successfully started") - proxy = get_rpc_proxy(rpc_url(datadir, i, rpchost), i, timeout=timewait) - - if COVERAGE_DIR: - coverage.write_all_rpc_commands(COVERAGE_DIR, proxy) - - return proxy - - -def assert_start_raises_init_error(i, dirname, extra_args=None, expected_msg=None): - with tempfile.SpooledTemporaryFile(max_size=2**16) as log_stderr: - try: - node = _start_node(i, dirname, extra_args, stderr=log_stderr) - _stop_node(node, i) - except Exception as e: - assert 'bitcoind exited' in str(e) # node must have shutdown - if expected_msg is not None: - log_stderr.seek(0) - stderr = log_stderr.read().decode('utf-8') - if expected_msg not in stderr: - raise AssertionError( - "Expected error \"" + expected_msg + "\" not found in:\n" + stderr) - else: - if expected_msg is None: - assert_msg = "bitcoind should have exited with an error" - else: - assert_msg = "bitcoind should have exited with expected error " + expected_msg - raise AssertionError(assert_msg) - - -def _start_nodes(num_nodes, dirname, extra_args=None, rpchost=None, timewait=None, binary=None): - """Start multiple bitcoinds, return RPC connections to them - - This function should only be called from within test_framework, not by individual test scripts.""" - - if extra_args is None: - extra_args = [None for _ in range(num_nodes)] - if binary is None: - binary = [None for _ in range(num_nodes)] - assert_equal(len(extra_args), num_nodes) - if binary is None: - binary = [None for _ in range(num_nodes)] - assert_equal(len(binary), num_nodes) - rpcs = [] - try: - for i in range(num_nodes): - rpcs.append(_start_node( - i, dirname, extra_args[i], rpchost, timewait=timewait, binary=binary[i])) - except: # If one node failed to start, stop the others - _stop_nodes(rpcs) - raise - return rpcs - - def log_filename(dirname, n_node, logname): return os.path.join(dirname, "node" + str(n_node), "regtest", logname) -def _stop_node(node, i): - """Stop a bitcoind test node - - This function should only be called from within test_framework, not by individual test scripts.""" - - logger.debug("Stopping node %d" % i) - try: - node.stop() - except http.client.CannotSendRequest as e: - logger.exception("Unable to stop node") - return_code = bitcoind_processes[i].wait( - timeout=BITCOIND_PROC_WAIT_TIMEOUT) - assert_equal(return_code, 0) - del bitcoind_processes[i] - - -def _stop_nodes(nodes): - """Stop multiple bitcoind test nodes - - This function should only be called from within test_framework, not by individual test scripts.""" - - for i, node in enumerate(nodes): - _stop_node(node, i) - assert not bitcoind_processes.values() # All connections must be gone now +def get_bip9_status(node, key): + info = node.getblockchaininfo() + return info['bip9_softforks'][key] def set_node_times(nodes, t): @@ -433,6 +330,67 @@ connect_nodes(nodes[b], a) +def sync_blocks(rpc_connections, *, wait=1, timeout=60): + """ + Wait until everybody has the same tip. + + sync_blocks needs to be called with an rpc_connections set that has least + one node already synced to the latest, stable tip, otherwise there's a + chance it might return before all nodes are stably synced. + """ + # Use getblockcount() instead of waitforblockheight() to determine the + # initial max height because the two RPCs look at different internal global + # variables (chainActive vs latestBlock) and the former gets updated + # earlier. + maxheight = max(x.getblockcount() for x in rpc_connections) + start_time = cur_time = time.time() + while cur_time <= start_time + timeout: + tips = [r.waitforblockheight(maxheight, int(wait * 1000)) + for r in rpc_connections] + if all(t["height"] == maxheight for t in tips): + if all(t["hash"] == tips[0]["hash"] for t in tips): + return + raise AssertionError("Block sync failed, mismatched block hashes:{}".format( + "".join("\n {!r}".format(tip) for tip in tips))) + cur_time = time.time() + raise AssertionError("Block sync to height {} timed out:{}".format( + maxheight, "".join("\n {!r}".format(tip) for tip in tips))) + + +def sync_chain(rpc_connections, *, wait=1, timeout=60): + """ + Wait until everybody has the same best block + """ + while timeout > 0: + best_hash = [x.getbestblockhash() for x in rpc_connections] + if best_hash == [best_hash[0]] * len(best_hash): + return + time.sleep(wait) + timeout -= wait + raise AssertionError("Chain sync failed: Best block hashes don't match") + + +def sync_mempools(rpc_connections, *, wait=1, timeout=60): + """ + Wait until everybody has the same transactions in their memory + pools + """ + while timeout > 0: + pool = set(rpc_connections[0].getrawmempool()) + num_match = 1 + for i in range(1, len(rpc_connections)): + if set(rpc_connections[i].getrawmempool()) == pool: + num_match = num_match + 1 + if num_match == len(rpc_connections): + return + time.sleep(wait) + timeout -= wait + raise AssertionError("Mempool sync failed") + +# Transaction/Block functions +############################# + + def find_output(node, txid, amount): """ Return index to output of txid with value amount @@ -442,8 +400,8 @@ 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))) + raise RuntimeError("find_output txid %s : %s not found" % + (txid, str(amount))) def gather_inputs(from_node, amount_needed, confirmations_required=1): @@ -461,8 +419,8 @@ 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_needed, total_in)) + raise RuntimeError("Insufficient funds: need %d, have %d" % + (amount_needed, total_in)) return (total_in, inputs) @@ -477,8 +435,8 @@ # Create an extra change output to break up big inputs change_address = from_node.getnewaddress() # Split change in two, being careful of rounding: - outputs[change_address] = Decimal(change / 2).quantize( - Decimal('0.00000001'), rounding=ROUND_DOWN) + outputs[change_address] = Decimal( + change / 2).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN) change = amount_in - amount - outputs[change_address] if change > 0: outputs[from_node.getnewaddress()] = change @@ -548,139 +506,6 @@ return (txid, signresult["hex"], fee) - -def assert_fee_amount(fee, tx_size, fee_per_kB): - """Assert the fee was in range""" - target_fee = tx_size * fee_per_kB / 1000 - if fee < target_fee: - raise AssertionError( - "Fee of %s BTC too low! (Should be %s BTC)" % (str(fee), str(target_fee))) - # allow the wallet's estimation to be at most 2 bytes off - if fee > (tx_size + 2) * fee_per_kB / 1000: - raise AssertionError( - "Fee of %s BTC too high! (Should be %s BTC)" % (str(fee), str(target_fee))) - - -def assert_equal(thing1, thing2, *args): - if thing1 != thing2 or any(thing1 != arg for arg in args): - raise AssertionError("not(%s)" % " == ".join(str(arg) - for arg in (thing1, thing2) + args)) - - -def assert_greater_than(thing1, thing2): - if thing1 <= thing2: - raise AssertionError("%s <= %s" % (str(thing1), str(thing2))) - - -def assert_greater_than_or_equal(thing1, thing2): - if thing1 < thing2: - raise AssertionError("%s < %s" % (str(thing1), str(thing2))) - - -def assert_raises(exc, fun, *args, **kwds): - assert_raises_message(exc, None, fun, *args, **kwds) - - -def assert_raises_message(exc, message, fun, *args, **kwds): - try: - fun(*args, **kwds) - except exc as e: - if message is not None and message not in e.error['message']: - raise AssertionError( - "Expected substring not found:" + e.error['message']) - except Exception as e: - raise AssertionError( - "Unexpected exception raised: " + type(e).__name__) - else: - raise AssertionError("No exception raised") - - -def assert_raises_jsonrpc(code, message, fun, *args, **kwds): - """Run an RPC and verify that a specific JSONRPC exception code and message is raised. - - Calls function `fun` with arguments `args` and `kwds`. Catches a JSONRPCException - and verifies that the error code and message are as expected. Throws AssertionError if - no JSONRPCException was returned or if the error code/message are not as expected. - - Args: - code (int), optional: the error code returned by the RPC call (defined - in src/rpc/protocol.h). Set to None if checking the error code is not required. - message (string), optional: [a substring of] the error string returned by the - RPC call. Set to None if checking the error string is not required - fun (function): the function to call. This should be the name of an RPC. - args*: positional arguments for the function. - kwds**: named arguments for the function. - """ - try: - fun(*args, **kwds) - except JSONRPCException as e: - # JSONRPCException was thrown as expected. Check the code and message values are correct. - if (code is not None) and (code != e.error["code"]): - raise AssertionError( - "Unexpected JSONRPC error code %i" % e.error["code"]) - if (message is not None) and (message not in e.error['message']): - raise AssertionError( - "Expected substring not found:" + e.error['message']) - except Exception as e: - raise AssertionError( - "Unexpected exception raised: " + type(e).__name__) - else: - raise AssertionError("No exception raised") - - -def assert_is_hex_string(string): - try: - int(string, 16) - except Exception as e: - raise AssertionError( - "Couldn't interpret %r as hexadecimal; raised: %s" % (string, e)) - - -def assert_is_hash_string(string, length=64): - if not isinstance(string, str): - raise AssertionError("Expected a string, got type %r" % type(string)) - elif length and len(string) != length: - raise AssertionError( - "String of length %d expected; got %d" % (length, len(string))) - elif not re.match('[abcdef0-9]+$', string): - raise AssertionError( - "String %r contains invalid characters for a hash." % string) - - -def assert_array_result(object_array, to_match, expected, should_not_find=False): - """ - Pass in array of JSON objects, a dictionary with key/value pairs - to match against, and another dictionary with expected key/value - pairs. - If the should_not_find flag is true, to_match should not be found - in object_array - """ - if should_not_find == True: - assert_equal(expected, {}) - num_matched = 0 - for item in object_array: - all_match = True - for key, value in to_match.items(): - if item[key] != value: - all_match = False - if not all_match: - continue - elif should_not_find == True: - num_matched = num_matched + 1 - for key, value in expected.items(): - if item[key] != value: - raise AssertionError( - "%s : expected %s=%s" % (str(item), str(key), str(value))) - num_matched = num_matched + 1 - if num_matched == 0 and should_not_find != True: - raise AssertionError("No objects matched %s" % (str(to_match))) - if num_matched > 0 and should_not_find == True: - raise AssertionError("Objects were found %s" % (str(to_match))) - - -def satoshi_round(amount): - return Decimal(amount).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN) - # Helper to create at least "count" utxos # Pass in a fee that is sufficient for relay and mining new transactions. @@ -704,7 +529,7 @@ raw_tx = node.createrawtransaction(inputs, outputs) signed_tx = node.signrawtransaction( raw_tx, None, None, "ALL|FORKID")["hex"] - txid = node.sendrawtransaction(signed_tx) + node.sendrawtransaction(signed_tx) while (node.getmempoolinfo()['size'] > 0): node.generate(1) @@ -782,11 +607,6 @@ node.generate(1) -def get_bip9_status(node, key): - info = node.getblockchaininfo() - return info['bip9_softforks'][key] - - def get_srcdir(calling_script=None): """ Try to find out the base folder containing the 'src' folder. @@ -794,6 +614,8 @@ Otherwise it goes on a search and rescue mission. Returns None if it cannot find a suitable folder. + + TODO: This is only used for cdefs, consider moving that there. """ def contains_src(path_to_check): if not path_to_check: diff --git a/test/functional/wallet-dump.py b/test/functional/wallet-dump.py --- a/test/functional/wallet-dump.py +++ b/test/functional/wallet-dump.py @@ -4,7 +4,7 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import (assert_equal, bitcoind_processes) +from test_framework.util import assert_equal def read_dump(file_name, addrs, hd_master_addr_old): @@ -95,7 +95,7 @@ # encrypt wallet, restart, unlock and dump self.nodes[0].encryptwallet('test') - bitcoind_processes[0].wait() + self.bitcoind_processes[0].wait() self.nodes[0] = self.start_node( 0, self.options.tmpdir, self.extra_args[0]) self.nodes[0].walletpassphrase('test', 10) diff --git a/test/functional/wallet-hd.py b/test/functional/wallet-hd.py --- a/test/functional/wallet-hd.py +++ b/test/functional/wallet-hd.py @@ -7,7 +7,6 @@ from test_framework.util import ( assert_equal, connect_nodes_bi, - assert_start_raises_init_error ) import os import shutil @@ -26,8 +25,8 @@ # Make sure can't switch off usehd after wallet creation self.stop_node(1) - assert_start_raises_init_error(1, self.options.tmpdir, [ - '-usehd=0'], 'already existing HD wallet') + self.assert_start_raises_init_error( + 1, self.options.tmpdir, ['-usehd=0'], 'already existing HD wallet') self.nodes[1] = self.start_node( 1, self.options.tmpdir, self.extra_args[1]) connect_nodes_bi(self.nodes, 0, 1) diff --git a/test/functional/walletbackup.py b/test/functional/walletbackup.py --- a/test/functional/walletbackup.py +++ b/test/functional/walletbackup.py @@ -36,6 +36,7 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import * from random import randint +import shutil class WalletBackupTest(BitcoinTestFramework):