diff --git a/test/functional/feature_filelock.py b/test/functional/feature_filelock.py index f4153302b..809745af2 100755 --- a/test/functional/feature_filelock.py +++ b/test/functional/feature_filelock.py @@ -1,48 +1,48 @@ #!/usr/bin/env python3 # Copyright (c) 2018 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Check that it's not possible to start a second bitcoind instance using the same datadir or wallet.""" import os from test_framework.test_framework import BitcoinTestFramework from test_framework.test_node import ErrorMatch class FilelockTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 2 def setup_network(self): self.add_nodes(self.num_nodes, extra_args=None) self.nodes[0].start(['-wallet=']) self.nodes[0].wait_for_rpc_connection() def run_test(self): datadir = os.path.join(self.nodes[0].datadir, self.chain) self.log.info("Using datadir {}".format(datadir)) self.log.info( "Check that we can't start a second bitcoind instance using the same datadir") expected_msg = "Error: Cannot obtain a lock on data directory {0}. {1} is probably already running.".format( datadir, self.config['environment']['PACKAGE_NAME']) self.nodes[1].assert_start_raises_init_error(extra_args=[ '-datadir={}'.format(self.nodes[0].datadir), '-noserver'], expected_msg=expected_msg) if self.is_wallet_compiled(): wallet_dir = os.path.join(datadir, 'wallets') self.log.info( "Check that we can't start a second bitcoind instance using the same wallet") expected_msg = "Error: Error initializing wallet database environment" self.nodes[1].assert_start_raises_init_error( extra_args=[ '-walletdir={}'.format(wallet_dir), - '-wallet=', + '-wallet=' + self.default_wallet_name, '-noserver'], expected_msg=expected_msg, match=ErrorMatch.PARTIAL_REGEX) if __name__ == '__main__': FilelockTest().main() diff --git a/test/functional/interface_bitcoin_cli.py b/test/functional/interface_bitcoin_cli.py index 140e24ef6..3fc8bdc6f 100755 --- a/test/functional/interface_bitcoin_cli.py +++ b/test/functional/interface_bitcoin_cli.py @@ -1,334 +1,334 @@ #!/usr/bin/env python3 # Copyright (c) 2017-2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test bitcoin-cli""" from decimal import Decimal from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_raises_process_error, assert_raises_rpc_error, get_auth_cookie, ) # The block reward of coinbaseoutput.nValue (50) BTC/block matures after # COINBASE_MATURITY (100) blocks. Therefore, after mining 101 blocks we expect # node 0 to have a balance of (BLOCKS - COINBASE_MATURITY) * 50 BTC/block. BLOCKS = 101 BALANCE = (BLOCKS - 100) * 50000000 JSON_PARSING_ERROR = 'error: Error parsing JSON: foo' BLOCKS_VALUE_OF_ZERO = 'error: the first argument (number of blocks to generate, default: 1) must be an integer value greater than zero' TOO_MANY_ARGS = 'error: too many arguments (maximum 2 for nblocks and maxtries)' WALLET_NOT_LOADED = 'Requested wallet does not exist or is not loaded' WALLET_NOT_SPECIFIED = 'Wallet file not specified' class TestBitcoinCli(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 def skip_test_if_missing_module(self): self.skip_if_no_cli() def run_test(self): """Main test logic""" self.nodes[0].generate(BLOCKS) self.log.info( "Compare responses from getblockchaininfo RPC and `bitcoin-cli getblockchaininfo`") cli_response = self.nodes[0].cli.getblockchaininfo() rpc_response = self.nodes[0].getblockchaininfo() assert_equal(cli_response, rpc_response) user, password = get_auth_cookie(self.nodes[0].datadir, self.chain) self.log.info("Test -stdinrpcpass option") assert_equal(BLOCKS, self.nodes[0].cli( '-rpcuser={}'.format(user), '-stdinrpcpass', input=password).getblockcount()) assert_raises_process_error(1, "Incorrect rpcuser or rpcpassword", self.nodes[0].cli( '-rpcuser={}'.format(user), '-stdinrpcpass', input="foo").echo) self.log.info("Test -stdin and -stdinrpcpass") assert_equal(["foo", "bar"], self.nodes[0].cli('-rpcuser={}'.format(user), '-stdin', '-stdinrpcpass', input=password + "\nfoo\nbar").echo()) assert_raises_process_error(1, "Incorrect rpcuser or rpcpassword", self.nodes[0].cli( '-rpcuser={}'.format(user), '-stdin', '-stdinrpcpass', input="foo").echo) self.log.info("Test connecting to a non-existing server") assert_raises_process_error( 1, "Could not connect to the server", self.nodes[0].cli('-rpcport=1').echo) self.log.info("Test connecting with non-existing RPC cookie file") assert_raises_process_error(1, "Could not locate RPC credentials", self.nodes[0].cli( '-rpccookiefile=does-not-exist', '-rpcpassword=').echo) self.log.info("Test -getinfo with arguments fails") assert_raises_process_error( 1, "-getinfo takes no arguments", self.nodes[0].cli('-getinfo').help) self.log.info( "Test -getinfo returns expected network and blockchain info") if self.is_wallet_compiled(): self.nodes[0].encryptwallet(password) cli_get_info = self.nodes[0].cli().send_cli('-getinfo') network_info = self.nodes[0].getnetworkinfo() blockchain_info = self.nodes[0].getblockchaininfo() assert_equal(cli_get_info['version'], network_info['version']) assert_equal(cli_get_info['blocks'], blockchain_info['blocks']) assert_equal(cli_get_info['headers'], blockchain_info['headers']) assert_equal(cli_get_info['timeoffset'], network_info['timeoffset']) assert_equal( cli_get_info['connections'], { 'in': network_info['connections_in'], 'out': network_info['connections_out'], 'total': network_info['connections'] } ) assert_equal(cli_get_info['proxy'], network_info['networks'][0]['proxy']) assert_equal(cli_get_info['difficulty'], blockchain_info['difficulty']) assert_equal(cli_get_info['chain'], blockchain_info['chain']) if self.is_wallet_compiled(): self.log.info( "Test -getinfo and bitcoin-cli getwalletinfo return expected wallet info") assert_equal(cli_get_info['balance'], BALANCE) assert 'balances' not in cli_get_info.keys() wallet_info = self.nodes[0].getwalletinfo() assert_equal( cli_get_info['keypoolsize'], wallet_info['keypoolsize']) assert_equal( cli_get_info['unlocked_until'], wallet_info['unlocked_until']) assert_equal(cli_get_info['paytxfee'], wallet_info['paytxfee']) assert_equal(cli_get_info['relayfee'], network_info['relayfee']) assert_equal(self.nodes[0].cli.getwalletinfo(), wallet_info) # Setup to test -getinfo, -generate, and -rpcwallet= with multiple # wallets. - wallets = ['', 'Encrypted', 'secret'] + wallets = [self.default_wallet_name, 'Encrypted', 'secret'] amounts = [ BALANCE + Decimal('9999995.50'), Decimal(9000000), Decimal(31000000)] self.nodes[0].createwallet(wallet_name=wallets[1]) self.nodes[0].createwallet(wallet_name=wallets[2]) w1 = self.nodes[0].get_wallet_rpc(wallets[0]) w2 = self.nodes[0].get_wallet_rpc(wallets[1]) w3 = self.nodes[0].get_wallet_rpc(wallets[2]) rpcwallet2 = '-rpcwallet={}'.format(wallets[1]) rpcwallet3 = '-rpcwallet={}'.format(wallets[2]) w1.walletpassphrase(password, self.rpc_timeout) w2.encryptwallet(password) w1.sendtoaddress(w2.getnewaddress(), amounts[1]) w1.sendtoaddress(w3.getnewaddress(), amounts[2]) # Mine a block to confirm; adds a block reward (50 BTC) to the # default wallet. self.nodes[0].generate(1) self.log.info( "Test -getinfo with multiple wallets and -rpcwallet returns specified wallet balance") for i in range(len(wallets)): cli_get_info = self.nodes[0].cli( '-getinfo', '-rpcwallet={}'.format(wallets[i])).send_cli() assert 'balances' not in cli_get_info.keys() assert_equal(cli_get_info['balance'], amounts[i]) self.log.info( "Test -getinfo with multiple wallets and " "-rpcwallet=non-existing-wallet returns no balances") cli_get_info_keys = self.nodes[0].cli( '-getinfo', '-rpcwallet=does-not-exist').send_cli().keys() assert 'balance' not in cli_get_info_keys assert 'balances' not in cli_get_info_keys self.log.info( "Test -getinfo with multiple wallets returns all loaded " "wallet names and balances") assert_equal(set(self.nodes[0].listwallets()), set(wallets)) cli_get_info = self.nodes[0].cli('-getinfo').send_cli() assert 'balance' not in cli_get_info.keys() assert_equal(cli_get_info['balances'], {k: v for k, v in zip(wallets, amounts)}) # Unload the default wallet and re-verify. self.nodes[0].unloadwallet(wallets[0]) assert wallets[0] not in self.nodes[0].listwallets() cli_get_info = self.nodes[0].cli('-getinfo').send_cli() assert 'balance' not in cli_get_info.keys() assert_equal(cli_get_info['balances'], {k: v for k, v in zip(wallets[1:], amounts[1:])}) self.log.info( "Test -getinfo after unloading all wallets except a " "non-default one returns its balance") self.nodes[0].unloadwallet(wallets[2]) assert_equal(self.nodes[0].listwallets(), [wallets[1]]) cli_get_info = self.nodes[0].cli('-getinfo').send_cli() assert 'balances' not in cli_get_info.keys() assert_equal(cli_get_info['balance'], amounts[1]) self.log.info( "Test -getinfo with -rpcwallet=remaining-non-default-wallet" " returns only its balance") cli_get_info = self.nodes[0].cli('-getinfo', rpcwallet2).send_cli() assert 'balances' not in cli_get_info.keys() assert_equal(cli_get_info['balance'], amounts[1]) self.log.info( "Test -getinfo with -rpcwallet=unloaded wallet returns" " no balances") cli_get_info = self.nodes[0].cli('-getinfo', rpcwallet3).send_cli() assert 'balance' not in cli_get_info_keys assert 'balances' not in cli_get_info_keys # Test bitcoin-cli -generate. n1 = 3 n2 = 4 w2.walletpassphrase(password, self.rpc_timeout) blocks = self.nodes[0].getblockcount() self.log.info('Test -generate with no args') generate = self.nodes[0].cli('-generate').send_cli() assert_equal(set(generate.keys()), {'address', 'blocks'}) assert_equal(len(generate["blocks"]), 1) assert_equal(self.nodes[0].getblockcount(), blocks + 1) self.log.info('Test -generate with bad args') assert_raises_process_error( 1, JSON_PARSING_ERROR, self.nodes[0].cli( '-generate', 'foo').echo) assert_raises_process_error( 1, BLOCKS_VALUE_OF_ZERO, self.nodes[0].cli( '-generate', 0).echo) assert_raises_process_error( 1, TOO_MANY_ARGS, self.nodes[0].cli( '-generate', 1, 2, 3).echo) self.log.info('Test -generate with nblocks') generate = self.nodes[0].cli('-generate', n1).send_cli() assert_equal(set(generate.keys()), {'address', 'blocks'}) assert_equal(len(generate["blocks"]), n1) assert_equal(self.nodes[0].getblockcount(), blocks + 1 + n1) self.log.info('Test -generate with nblocks and maxtries') generate = self.nodes[0].cli('-generate', n2, 1000000).send_cli() assert_equal(set(generate.keys()), {'address', 'blocks'}) assert_equal(len(generate["blocks"]), n2) assert_equal(self.nodes[0].getblockcount(), blocks + 1 + n1 + n2) self.log.info('Test -generate -rpcwallet in single-wallet mode') generate = self.nodes[0].cli(rpcwallet2, '-generate').send_cli() assert_equal(set(generate.keys()), {'address', 'blocks'}) assert_equal(len(generate["blocks"]), 1) assert_equal(self.nodes[0].getblockcount(), blocks + 2 + n1 + n2) self.log.info( 'Test -generate -rpcwallet=unloaded wallet raises RPC error') assert_raises_rpc_error(-18, WALLET_NOT_LOADED, self.nodes[0].cli(rpcwallet3, '-generate').echo) assert_raises_rpc_error(-18, WALLET_NOT_LOADED, self.nodes[0].cli(rpcwallet3, '-generate', 'foo').echo) assert_raises_rpc_error(-18, WALLET_NOT_LOADED, self.nodes[0].cli(rpcwallet3, '-generate', 0).echo) assert_raises_rpc_error(-18, WALLET_NOT_LOADED, self.nodes[0].cli(rpcwallet3, '-generate', 1, 2, 3).echo) # Test bitcoin-cli -generate with -rpcwallet in multiwallet mode. self.nodes[0].loadwallet(wallets[2]) n3 = 4 n4 = 10 blocks = self.nodes[0].getblockcount() self.log.info('Test -generate -rpcwallet with no args') generate = self.nodes[0].cli(rpcwallet2, '-generate').send_cli() assert_equal(set(generate.keys()), {'address', 'blocks'}) assert_equal(len(generate["blocks"]), 1) assert_equal(self.nodes[0].getblockcount(), blocks + 1) self.log.info('Test -generate -rpcwallet with bad args') assert_raises_process_error( 1, JSON_PARSING_ERROR, self.nodes[0].cli( rpcwallet2, '-generate', 'foo').echo) assert_raises_process_error( 1, BLOCKS_VALUE_OF_ZERO, self.nodes[0].cli( rpcwallet2, '-generate', 0).echo) assert_raises_process_error( 1, TOO_MANY_ARGS, self.nodes[0].cli( rpcwallet2, '-generate', 1, 2, 3).echo) self.log.info('Test -generate -rpcwallet with nblocks') generate = self.nodes[0].cli( rpcwallet2, '-generate', n3).send_cli() assert_equal(set(generate.keys()), {'address', 'blocks'}) assert_equal(len(generate["blocks"]), n3) assert_equal(self.nodes[0].getblockcount(), blocks + 1 + n3) self.log.info( 'Test -generate -rpcwallet with nblocks and maxtries') generate = self.nodes[0].cli( rpcwallet2, '-generate', n4, 1000000).send_cli() assert_equal(set(generate.keys()), {'address', 'blocks'}) assert_equal(len(generate["blocks"]), n4) assert_equal(self.nodes[0].getblockcount(), blocks + 1 + n3 + n4) self.log.info( 'Test -generate without -rpcwallet in multiwallet mode raises RPC error') assert_raises_rpc_error(-19, WALLET_NOT_SPECIFIED, self.nodes[0].cli('-generate').echo) assert_raises_rpc_error(-19, WALLET_NOT_SPECIFIED, self.nodes[0].cli('-generate', 'foo').echo) assert_raises_rpc_error(-19, WALLET_NOT_SPECIFIED, self.nodes[0].cli('-generate', 0).echo) assert_raises_rpc_error(-19, WALLET_NOT_SPECIFIED, self.nodes[0].cli('-generate', 1, 2, 3).echo) else: self.log.info( "*** Wallet not compiled; cli getwalletinfo and -getinfo wallet tests skipped") # maintain block parity with the wallet_compiled conditional branch self.nodes[0].generate(25) self.log.info("Test -version with node stopped") self.stop_node(0) cli_response = self.nodes[0].cli().send_cli('-version') assert "{} RPC client version".format( self.config['environment']['PACKAGE_NAME']) in cli_response self.log.info( "Test -rpcwait option successfully waits for RPC connection") # Start node without RPC connection. self.nodes[0].start() # ensure cookie file is available to avoid race condition self.nodes[0].wait_for_cookie_credentials() blocks = self.nodes[0].cli('-rpcwait').send_cli('getblockcount') self.nodes[0].wait_for_rpc_connection() assert_equal(blocks, BLOCKS + 25) if __name__ == '__main__': TestBitcoinCli().main() diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index f59ecdd20..feaab63a6 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -1,812 +1,814 @@ #!/usr/bin/env python3 # Copyright (c) 2014-2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Base class for RPC testing.""" import argparse import configparser import logging import os import pdb import random import shutil import sys import tempfile import time from enum import Enum from typing import Optional from . import coverage from .authproxy import JSONRPCException from .avatools import get_proof_ids from .p2p import NetworkThread from .test_node import TestNode from .util import ( MAX_NODES, PortSeed, assert_equal, check_json_precision, connect_nodes, disconnect_nodes, get_datadir_path, initialize_datadir, p2p_port, rpc_port, wait_until_helper, ) class TestStatus(Enum): PASSED = 1 FAILED = 2 SKIPPED = 3 TEST_EXIT_PASSED = 0 TEST_EXIT_FAILED = 1 TEST_EXIT_SKIPPED = 77 # Timestamp is Dec. 1st, 2019 at 00:00:00 TIMESTAMP_IN_THE_PAST = 1575158400 TMPDIR_PREFIX = "bitcoin_func_test_" class SkipTest(Exception): """This exception is raised to skip a test""" def __init__(self, message): self.message = message class BitcoinTestMetaClass(type): """Metaclass for BitcoinTestFramework. Ensures that any attempt to register a subclass of `BitcoinTestFramework` adheres to a standard whereby the subclass overrides `set_test_params` and `run_test` but DOES NOT override either `__init__` or `main`. If any of those standards are violated, a ``TypeError`` is raised.""" def __new__(cls, clsname, bases, dct): if not clsname == 'BitcoinTestFramework': if not ('run_test' in dct and 'set_test_params' in dct): raise TypeError("BitcoinTestFramework subclasses must override " "'run_test' and 'set_test_params'") if '__init__' in dct or 'main' in dct: raise TypeError("BitcoinTestFramework subclasses may not override " "'__init__' or 'main'") return super().__new__(cls, clsname, bases, dct) class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): """Base class for a bitcoin test script. Individual bitcoin test scripts should subclass this class and override the set_test_params() and run_test() methods. Individual tests can also override the following methods to customize the test setup: - add_options() - setup_chain() - setup_network() - setup_nodes() The __init__() and main() methods should not be overridden. This class also contains various public and private helper methods.""" chain: Optional[str] = None setup_clean_chain: Optional[bool] = None def __init__(self): """Sets test framework defaults. Do not override this method. Instead, override the set_test_params() method""" self.chain = 'regtest' self.setup_clean_chain = False self.nodes = [] self.network_thread = None # Wait for up to 60 seconds for the RPC server to respond self.rpc_timeout = 60 self.supports_cli = True self.bind_to_localhost_only = True + self.default_wallet_name = "" + self.wallet_data_filename = "wallet.dat" # We run parse_args before set_test_params for tests who need to # know the parser options during setup. self.parse_args() self.set_test_params() if self.options.timeout_factor == 0: self.options.timeout_factor = 99999 # optionally, increase timeout by a factor self.rpc_timeout = int(self.rpc_timeout * self.options.timeout_factor) def main(self): """Main function. This should not be overridden by the subclass test scripts.""" assert hasattr( self, "num_nodes"), "Test must set self.num_nodes in set_test_params()" try: self.setup() self.run_test() except JSONRPCException: self.log.exception("JSONRPC error") self.success = TestStatus.FAILED except SkipTest as e: self.log.warning("Test Skipped: {}".format(e.message)) self.success = TestStatus.SKIPPED except AssertionError: self.log.exception("Assertion failed") self.success = TestStatus.FAILED except KeyError: self.log.exception("Key error") self.success = TestStatus.FAILED except Exception: self.log.exception("Unexpected exception caught during testing") self.success = TestStatus.FAILED except KeyboardInterrupt: self.log.warning("Exiting after keyboard interrupt") self.success = TestStatus.FAILED finally: exit_code = self.shutdown() sys.exit(exit_code) def parse_args(self): parser = argparse.ArgumentParser(usage="%(prog)s [options]") parser.add_argument("--nocleanup", dest="nocleanup", default=False, action="store_true", help="Leave bitcoinds and test.* datadir on exit or error") parser.add_argument("--noshutdown", dest="noshutdown", default=False, action="store_true", help="Don't stop bitcoinds after the test execution") parser.add_argument("--cachedir", dest="cachedir", default=os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + "/../../cache"), help="Directory for caching pregenerated datadirs (default: %(default)s)") parser.add_argument("--tmpdir", dest="tmpdir", help="Root directory for datadirs") parser.add_argument("-l", "--loglevel", dest="loglevel", default="INFO", help="log events at this level and higher to the console. Can be set to DEBUG, INFO, WARNING, ERROR or CRITICAL. Passing --loglevel DEBUG will output all logs to console. Note that logs at all levels are always written to the test_framework.log file in the temporary test directory.") parser.add_argument("--tracerpc", dest="trace_rpc", default=False, action="store_true", help="Print out all RPC calls as they are made") parser.add_argument("--portseed", dest="port_seed", default=os.getpid(), type=int, help="The seed to use for assigning port numbers (default: current process id)") parser.add_argument("--coveragedir", dest="coveragedir", help="Write tested RPC commands into this directory") parser.add_argument("--configfile", dest="configfile", default=os.path.abspath(os.path.dirname(os.path.realpath( __file__)) + "/../../config.ini"), help="Location of the test framework config file (default: %(default)s)") parser.add_argument("--pdbonfailure", dest="pdbonfailure", default=False, action="store_true", help="Attach a python debugger if test fails") parser.add_argument("--usecli", dest="usecli", default=False, action="store_true", help="use bitcoin-cli instead of RPC for all commands") parser.add_argument("--perf", dest="perf", default=False, action="store_true", help="profile running nodes with perf for the duration of the test") parser.add_argument("--valgrind", dest="valgrind", default=False, action="store_true", help="run nodes under the valgrind memory error detector: expect at least a ~10x slowdown, valgrind 3.14 or later required") parser.add_argument("--randomseed", type=int, help="set a random seed for deterministically reproducing a previous test run") parser.add_argument("--descriptors", default=False, action="store_true", help="Run test using a descriptor wallet") parser.add_argument("--with-axionactivation", dest="axionactivation", default=False, action="store_true", help="Activate axion update on timestamp {}".format(TIMESTAMP_IN_THE_PAST)) parser.add_argument( '--timeout-factor', dest="timeout_factor", type=float, default=1.0, help='adjust test timeouts by a factor. ' 'Setting it to 0 disables all timeouts') self.add_options(parser) self.options = parser.parse_args() def setup(self): """Call this method to start up the test framework object with options set.""" PortSeed.n = self.options.port_seed check_json_precision() self.options.cachedir = os.path.abspath(self.options.cachedir) config = configparser.ConfigParser() config.read_file(open(self.options.configfile, encoding='utf-8')) self.config = config fname_bitcoind = os.path.join( config["environment"]["BUILDDIR"], "src", "bitcoind" + config["environment"]["EXEEXT"] ) fname_bitcoincli = os.path.join( config["environment"]["BUILDDIR"], "src", "bitcoin-cli" + config["environment"]["EXEEXT"] ) self.options.bitcoind = os.getenv("BITCOIND", default=fname_bitcoind) self.options.bitcoincli = os.getenv( "BITCOINCLI", default=fname_bitcoincli) self.options.emulator = config["environment"]["EMULATOR"] or None os.environ['PATH'] = config['environment']['BUILDDIR'] + os.pathsep + \ config['environment']['BUILDDIR'] + os.path.sep + "qt" + os.pathsep + \ os.environ['PATH'] # Set up temp directory and start logging if self.options.tmpdir: self.options.tmpdir = os.path.abspath(self.options.tmpdir) os.makedirs(self.options.tmpdir, exist_ok=False) else: self.options.tmpdir = tempfile.mkdtemp(prefix=TMPDIR_PREFIX) self._start_logging() # Seed the PRNG. Note that test runs are reproducible if and only if # a single thread accesses the PRNG. For more information, see # https://docs.python.org/3/library/random.html#notes-on-reproducibility. # The network thread shouldn't access random. If we need to change the # network thread to access randomness, it should instantiate its own # random.Random object. seed = self.options.randomseed if seed is None: seed = random.randrange(sys.maxsize) else: self.log.debug("User supplied random seed {}".format(seed)) random.seed(seed) self.log.debug("PRNG seed is: {}".format(seed)) self.log.debug('Setting up network thread') self.network_thread = NetworkThread() self.network_thread.start() if self.options.usecli: if not self.supports_cli: raise SkipTest( "--usecli specified but test does not support using CLI") self.skip_if_no_cli() self.skip_test_if_missing_module() self.setup_chain() self.setup_network() self.success = TestStatus.PASSED def shutdown(self): """Call this method to shut down the test framework object.""" if self.success == TestStatus.FAILED and self.options.pdbonfailure: print("Testcase failed. Attaching python debugger. Enter ? for help") pdb.set_trace() self.log.debug('Closing down network thread') self.network_thread.close() if not self.options.noshutdown: self.log.info("Stopping nodes") if self.nodes: self.stop_nodes() else: for node in self.nodes: node.cleanup_on_exit = False self.log.info( "Note: bitcoinds were not stopped and may still be running") should_clean_up = ( not self.options.nocleanup and not self.options.noshutdown and self.success != TestStatus.FAILED and not self.options.perf ) if should_clean_up: self.log.info("Cleaning up {} on exit".format(self.options.tmpdir)) cleanup_tree_on_exit = True elif self.options.perf: self.log.warning( "Not cleaning up dir {} due to perf data".format( self.options.tmpdir)) cleanup_tree_on_exit = False else: self.log.warning( "Not cleaning up dir {}".format(self.options.tmpdir)) cleanup_tree_on_exit = False if self.success == TestStatus.PASSED: self.log.info("Tests successful") exit_code = TEST_EXIT_PASSED elif self.success == TestStatus.SKIPPED: self.log.info("Test skipped") exit_code = TEST_EXIT_SKIPPED else: self.log.error( "Test failed. Test logging available at {}/test_framework.log".format(self.options.tmpdir)) self.log.error("") self.log.error("Hint: Call {} '{}' to consolidate all logs".format(os.path.normpath( os.path.dirname(os.path.realpath(__file__)) + "/../combine_logs.py"), self.options.tmpdir)) self.log.error("") self.log.error( "If this failure happened unexpectedly or intermittently, please" " file a bug and provide a link or upload of the combined log.") self.log.error(self.config['environment']['PACKAGE_BUGREPORT']) self.log.error("") exit_code = TEST_EXIT_FAILED # Logging.shutdown will not remove stream- and filehandlers, so we must # do it explicitly. Handlers are removed so the next test run can apply # different log handler settings. # See: https://docs.python.org/3/library/logging.html#logging.shutdown for h in list(self.log.handlers): h.flush() h.close() self.log.removeHandler(h) rpc_logger = logging.getLogger("BitcoinRPC") for h in list(rpc_logger.handlers): h.flush() rpc_logger.removeHandler(h) if cleanup_tree_on_exit: shutil.rmtree(self.options.tmpdir) self.nodes.clear() return exit_code # Methods to override in subclass test scripts. def set_test_params(self): """Tests must this method to change default values for number of nodes, topology, etc""" raise NotImplementedError def add_options(self, parser): """Override this method to add command-line options to the test""" pass def skip_test_if_missing_module(self): """Override this method to skip a test if a module is not compiled""" pass def setup_chain(self): """Override this method to customize blockchain setup""" self.log.info("Initializing test directory " + self.options.tmpdir) if self.setup_clean_chain: self._initialize_chain_clean() else: self._initialize_chain() def setup_network(self): """Override this method to customize test network topology""" self.setup_nodes() # Connect the nodes as a "chain". This allows us # to split the network between nodes 1 and 2 to get # two halves that can work on competing chains. # # Topology looks like this: # node0 <-- node1 <-- node2 <-- node3 # # If all nodes are in IBD (clean chain from genesis), node0 is assumed to be the source of blocks (miner). To # ensure block propagation, all nodes will establish outgoing connections toward node0. # See fPreferredDownload in net_processing. # # If further outbound connections are needed, they can be added at the beginning of the test with e.g. # self.connect_nodes(1, 2) for i in range(self.num_nodes - 1): self.connect_nodes(i + 1, i) self.sync_all() def setup_nodes(self): """Override this method to customize test node setup""" extra_args = [[]] * self.num_nodes wallets = [[]] * self.num_nodes if hasattr(self, "extra_args"): extra_args = self.extra_args wallets = [[x for x in eargs if x.startswith('-wallet=')] for eargs in extra_args] extra_args = [x + ['-nowallet'] for x in extra_args] self.add_nodes(self.num_nodes, extra_args) self.start_nodes() for i, n in enumerate(self.nodes): n.extra_args.pop() if '-wallet=0' in n.extra_args or '-nowallet' in n.extra_args or '-disablewallet' in n.extra_args or not self.is_wallet_compiled(): continue if '-wallet=' not in wallets[i] and not any( [x.startswith('-wallet=') for x in wallets[i]]): wallets[i].append('-wallet=') for w in wallets[i]: wallet_name = w.split('=', 1)[1] n.createwallet( wallet_name=wallet_name, descriptors=self.options.descriptors) self.import_deterministic_coinbase_privkeys() if not self.setup_clean_chain: for n in self.nodes: assert_equal(n.getblockchaininfo()["blocks"], 199) # To ensure that all nodes are out of IBD, the most recent block # must have a timestamp not too old (see IsInitialBlockDownload()). self.log.debug('Generate a block with current time') block_hash = self.nodes[0].generate(1)[0] block = self.nodes[0].getblock(blockhash=block_hash, verbosity=0) for n in self.nodes: n.submitblock(block) chain_info = n.getblockchaininfo() assert_equal(chain_info["blocks"], 200) assert_equal(chain_info["initialblockdownload"], False) def import_deterministic_coinbase_privkeys(self): for n in self.nodes: try: n.getwalletinfo() except JSONRPCException as e: assert str(e).startswith('Method not found') continue n.importprivkey( privkey=n.get_deterministic_priv_key().key, label='coinbase') def run_test(self): """Tests must override this method to define test logic""" raise NotImplementedError # Public helper methods. These can be accessed by the subclass test # scripts. def add_nodes(self, num_nodes: int, extra_args=None, *, host=None, binary=None): """Instantiate TestNode objects. Should only be called once after the nodes have been specified in set_test_params().""" if self.bind_to_localhost_only: extra_confs = [["bind=127.0.0.1"]] * num_nodes else: extra_confs = [[]] * num_nodes if extra_args is None: extra_args = [[]] * num_nodes if binary is None: binary = [self.options.bitcoind] * num_nodes assert_equal(len(extra_confs), num_nodes) assert_equal(len(extra_args), num_nodes) assert_equal(len(binary), num_nodes) for i in range(num_nodes): self.nodes.append(TestNode( i, get_datadir_path(self.options.tmpdir, i), chain=self.chain, host=host, rpc_port=rpc_port(i), p2p_port=p2p_port(i), timewait=self.rpc_timeout, timeout_factor=self.options.timeout_factor, bitcoind=binary[i], bitcoin_cli=self.options.bitcoincli, coverage_dir=self.options.coveragedir, cwd=self.options.tmpdir, extra_conf=extra_confs[i], extra_args=extra_args[i], use_cli=self.options.usecli, emulator=self.options.emulator, start_perf=self.options.perf, use_valgrind=self.options.valgrind, descriptors=self.options.descriptors, )) if self.options.axionactivation: self.nodes[i].extend_default_args( ["-axionactivationtime={}".format(TIMESTAMP_IN_THE_PAST)]) def start_node(self, i, *args, **kwargs): """Start a bitcoind""" node = self.nodes[i] node.start(*args, **kwargs) node.wait_for_rpc_connection() if self.options.coveragedir is not None: coverage.write_all_rpc_commands(self.options.coveragedir, node.rpc) def start_nodes(self, extra_args=None, *args, **kwargs): """Start multiple bitcoinds""" if extra_args is None: extra_args = [None] * self.num_nodes assert_equal(len(extra_args), self.num_nodes) try: for i, node in enumerate(self.nodes): node.start(extra_args[i], *args, **kwargs) for node in self.nodes: node.wait_for_rpc_connection() except BaseException: # If one node failed to start, stop the others self.stop_nodes() raise if self.options.coveragedir is not None: for node in self.nodes: coverage.write_all_rpc_commands( self.options.coveragedir, node.rpc) def stop_node(self, i, expected_stderr='', wait=0): """Stop a bitcoind test node""" self.nodes[i].stop_node(expected_stderr, wait=wait) def stop_nodes(self, wait=0): """Stop multiple bitcoind test nodes""" for node in self.nodes: # Issue RPC to stop nodes node.stop_node(wait=wait, wait_until_stopped=False) for node in self.nodes: # Wait for nodes to stop node.wait_until_stopped() def restart_node(self, i, extra_args=None): """Stop and start a test node""" self.stop_node(i) self.start_node(i, extra_args) def wait_for_node_exit(self, i, timeout): self.nodes[i].process.wait(timeout) def connect_nodes(self, a, b): connect_nodes(self.nodes[a], self.nodes[b]) def disconnect_nodes(self, a, b): disconnect_nodes(self.nodes[a], self.nodes[b]) def split_network(self): """ Split the network of four nodes into nodes 0/1 and 2/3. """ self.disconnect_nodes(1, 2) self.sync_all(self.nodes[:2]) self.sync_all(self.nodes[2:]) def join_network(self): """ Join the (previously split) network halves together. """ self.connect_nodes(1, 2) self.sync_all() def sync_blocks(self, nodes=None, 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. """ rpc_connections = nodes or self.nodes timeout = int(timeout * self.options.timeout_factor) stop_time = time.time() + timeout while time.time() <= stop_time: best_hash = [x.getbestblockhash() for x in rpc_connections] if best_hash.count(best_hash[0]) == len(rpc_connections): return # Check that each peer has at least one connection assert (all([len(x.getpeerinfo()) for x in rpc_connections])) time.sleep(wait) raise AssertionError("Block sync timed out after {}s:{}".format( timeout, "".join("\n {!r}".format(b) for b in best_hash), )) def sync_mempools(self, nodes=None, wait=1, timeout=60, flush_scheduler=True): """ Wait until everybody has the same transactions in their memory pools """ rpc_connections = nodes or self.nodes timeout = int(timeout * self.options.timeout_factor) stop_time = time.time() + timeout while time.time() <= stop_time: pool = [set(r.getrawmempool()) for r in rpc_connections] if pool.count(pool[0]) == len(rpc_connections): if flush_scheduler: for r in rpc_connections: r.syncwithvalidationinterfacequeue() return # Check that each peer has at least one connection assert (all([len(x.getpeerinfo()) for x in rpc_connections])) time.sleep(wait) raise AssertionError("Mempool sync timed out after {}s:{}".format( timeout, "".join("\n {!r}".format(m) for m in pool), )) def sync_proofs(self, nodes=None, wait=1, timeout=60): """ Wait until everybody has the same proofs in their proof pools """ rpc_connections = nodes or self.nodes timeout = int(timeout * self.options.timeout_factor) stop_time = time.time() + timeout def format_ids(id_list): """Convert ProodIDs to hex strings for easier debugging""" return list(f"{i:064x}" for i in id_list) while time.time() <= stop_time: nodes_proofs = [ set(format_ids(get_proof_ids(r))) for r in rpc_connections] if nodes_proofs.count(nodes_proofs[0]) == len(rpc_connections): return # Check that each peer has at least one connection assert (all([len(x.getpeerinfo()) for x in rpc_connections])) time.sleep(wait) raise AssertionError("Proofs sync timed out after {}s:{}".format( timeout, "".join("\n {!r}".format(m) for m in nodes_proofs), )) def sync_all(self, nodes=None): self.sync_blocks(nodes) self.sync_mempools(nodes) def wait_until(self, test_function, timeout=60): return wait_until_helper(test_function, timeout=timeout, timeout_factor=self.options.timeout_factor) # Private helper methods. These should not be accessed by the subclass # test scripts. def _start_logging(self): # Add logger and logging handlers self.log = logging.getLogger('TestFramework') self.log.setLevel(logging.DEBUG) # Create file handler to log all messages fh = logging.FileHandler( self.options.tmpdir + '/test_framework.log', encoding='utf-8') fh.setLevel(logging.DEBUG) # Create console handler to log messages to stderr. By default this # logs only error messages, but can be configured with --loglevel. ch = logging.StreamHandler(sys.stdout) # User can provide log level as a number or string (eg DEBUG). loglevel # was caught as a string, so try to convert it to an int 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) formatter = logging.Formatter( fmt='%(asctime)s.%(msecs)03d000Z %(name)s (%(levelname)s): %(message)s', datefmt='%Y-%m-%dT%H:%M:%S') formatter.converter = time.gmtime fh.setFormatter(formatter) ch.setFormatter(formatter) # add the handlers to the logger self.log.addHandler(fh) self.log.addHandler(ch) if self.options.trace_rpc: rpc_logger = logging.getLogger("BitcoinRPC") rpc_logger.setLevel(logging.DEBUG) rpc_handler = logging.StreamHandler(sys.stdout) rpc_handler.setLevel(logging.DEBUG) rpc_logger.addHandler(rpc_handler) def _initialize_chain(self): """Initialize a pre-mined blockchain for use by the test. Create a cache of a 199-block-long chain Afterward, create num_nodes copies from the cache.""" # Use node 0 to create the cache for all other nodes CACHE_NODE_ID = 0 cache_node_dir = get_datadir_path(self.options.cachedir, CACHE_NODE_ID) assert self.num_nodes <= MAX_NODES if not os.path.isdir(cache_node_dir): self.log.debug( "Creating cache directory {}".format(cache_node_dir)) initialize_datadir( self.options.cachedir, CACHE_NODE_ID, self.chain) self.nodes.append( TestNode( CACHE_NODE_ID, cache_node_dir, chain=self.chain, extra_conf=["bind=127.0.0.1"], extra_args=['-disablewallet'], host=None, rpc_port=rpc_port(CACHE_NODE_ID), p2p_port=p2p_port(CACHE_NODE_ID), timewait=self.rpc_timeout, timeout_factor=self.options.timeout_factor, bitcoind=self.options.bitcoind, bitcoin_cli=self.options.bitcoincli, coverage_dir=None, cwd=self.options.tmpdir, descriptors=self.options.descriptors, emulator=self.options.emulator, )) if self.options.axionactivation: self.nodes[CACHE_NODE_ID].extend_default_args( ["-axionactivationtime={}".format(TIMESTAMP_IN_THE_PAST)]) self.start_node(CACHE_NODE_ID) cache_node = self.nodes[CACHE_NODE_ID] # Wait for RPC connections to be ready cache_node.wait_for_rpc_connection() # Set a time in the past, so that blocks don't end up in the future cache_node.setmocktime( cache_node.getblockheader( cache_node.getbestblockhash())['time']) # Create a 199-block-long chain; each of the 4 first nodes # gets 25 mature blocks and 25 immature. # The 4th node gets only 24 immature blocks so that the very last # block in the cache does not age too much (have an old tip age). # This is needed so that we are out of IBD when the test starts, # see the tip age check in IsInitialBlockDownload(). for i in range(8): cache_node.generatetoaddress( nblocks=25 if i != 7 else 24, address=TestNode.PRIV_KEYS[i % 4].address, ) assert_equal(cache_node.getblockchaininfo()["blocks"], 199) # Shut it down, and clean up cache directories: self.stop_nodes() self.nodes = [] def cache_path(*paths): return os.path.join(cache_node_dir, self.chain, *paths) # Remove empty wallets dir os.rmdir(cache_path('wallets')) for entry in os.listdir(cache_path()): # Only keep chainstate and blocks folder if entry not in ['chainstate', 'blocks']: os.remove(cache_path(entry)) for i in range(self.num_nodes): self.log.debug( "Copy cache directory {} to node {}".format( cache_node_dir, i)) to_dir = get_datadir_path(self.options.tmpdir, i) shutil.copytree(cache_node_dir, to_dir) # Overwrite port/rpcport in bitcoin.conf initialize_datadir(self.options.tmpdir, i, self.chain) def _initialize_chain_clean(self): """Initialize empty blockchain for use by the test. Create an empty blockchain and num_nodes wallets. Useful if a test case wants complete control over initialization.""" for i in range(self.num_nodes): initialize_datadir(self.options.tmpdir, i, self.chain) def skip_if_no_py3_zmq(self): """Attempt to import the zmq package and skip the test if the import fails.""" try: import zmq # noqa except ImportError: raise SkipTest("python3-zmq module not available.") def skip_if_no_bitcoind_zmq(self): """Skip the running test if bitcoind has not been compiled with zmq support.""" if not self.is_zmq_compiled(): raise SkipTest("bitcoind has not been built with zmq enabled.") def skip_if_no_wallet(self): """Skip the running test if wallet has not been compiled.""" if not self.is_wallet_compiled(): raise SkipTest("wallet has not been compiled.") def skip_if_no_wallet_tool(self): """Skip the running test if bitcoin-wallet has not been compiled.""" if not self.is_wallet_tool_compiled(): raise SkipTest("bitcoin-wallet has not been compiled") def skip_if_no_cli(self): """Skip the running test if bitcoin-cli has not been compiled.""" if not self.is_cli_compiled(): raise SkipTest("bitcoin-cli has not been compiled.") def is_cli_compiled(self): """Checks whether bitcoin-cli was compiled.""" return self.config["components"].getboolean("ENABLE_CLI") def is_wallet_compiled(self): """Checks whether the wallet module was compiled.""" return self.config["components"].getboolean("ENABLE_WALLET") def is_wallet_tool_compiled(self): """Checks whether bitcoin-wallet was compiled.""" return self.config["components"].getboolean("ENABLE_WALLET_TOOL") def is_zmq_compiled(self): """Checks whether the zmq module was compiled.""" return self.config["components"].getboolean("ENABLE_ZMQ") diff --git a/test/functional/tool_wallet.py b/test/functional/tool_wallet.py index 0e528e72e..2fa93b601 100755 --- a/test/functional/tool_wallet.py +++ b/test/functional/tool_wallet.py @@ -1,260 +1,265 @@ #!/usr/bin/env python3 # Copyright (c) 2018-2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test bitcoin-wallet.""" import hashlib import os import stat import subprocess import textwrap from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal BUFFER_SIZE = 16 * 1024 class ToolWalletTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 self.setup_clean_chain = True self.rpc_timeout = 120 def skip_test_if_missing_module(self): self.skip_if_no_wallet() self.skip_if_no_wallet_tool() def bitcoin_wallet_process(self, *args): binary = self.config["environment"]["BUILDDIR"] + \ '/src/bitcoin-wallet' + self.config["environment"]["EXEEXT"] args = ['-datadir={}'.format(self.nodes[0].datadir), '-chain={}'.format(self.chain)] + list(args) command_line = [binary] + args if self.config["environment"]["EMULATOR"]: command_line = [ self.config["environment"]["EMULATOR"]] + command_line return subprocess.Popen(command_line, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) def assert_raises_tool_error(self, error, *args): p = self.bitcoin_wallet_process(*args) stdout, stderr = p.communicate() assert_equal(p.poll(), 1) assert_equal(stdout, '') assert_equal(stderr.strip(), error) def assert_tool_output(self, output, *args): p = self.bitcoin_wallet_process(*args) stdout, stderr = p.communicate() assert_equal(stderr, '') assert_equal(stdout, output) assert_equal(p.poll(), 0) def wallet_shasum(self): h = hashlib.sha1() mv = memoryview(bytearray(BUFFER_SIZE)) with open(self.wallet_path, 'rb', buffering=0) as f: for n in iter(lambda: f.readinto(mv), 0): h.update(mv[:n]) return h.hexdigest() def wallet_timestamp(self): return os.path.getmtime(self.wallet_path) def wallet_permissions(self): return oct(os.lstat(self.wallet_path).st_mode)[-3:] def log_wallet_timestamp_comparison(self, old, new): result = 'unchanged' if new == old else 'increased!' self.log.debug('Wallet file timestamp {}'.format(result)) def test_invalid_tool_commands_and_args(self): self.log.info( 'Testing that various invalid commands raise with specific error messages') self.assert_raises_tool_error('Invalid command: foo', 'foo') # `bitcoin-wallet help` raises an error. Use `bitcoin-wallet -help`. self.assert_raises_tool_error('Invalid command: help', 'help') self.assert_raises_tool_error( 'Error: two methods provided (info and create). Only one method should be provided.', 'info', 'create') self.assert_raises_tool_error( 'Error parsing command line arguments: Invalid parameter -foo', '-foo') locked_dir = os.path.join(self.options.tmpdir, "node0", "regtest", "wallets") self.assert_raises_tool_error( f'Error initializing wallet database environment "{locked_dir}"!', '-wallet=wallet.dat', 'info', ) path = os.path.join(self.options.tmpdir, "node0", "regtest", "wallets", "nonexistent.dat") self.assert_raises_tool_error( f"Failed to load database path '{path}'. Path does not exist.", '-wallet=nonexistent.dat', 'info') def test_tool_wallet_info(self): # Stop the node to close the wallet to call the info command. self.stop_node(0) self.log.info('Calling wallet tool info, testing output') # # TODO: Wallet tool info should work with wallet file permissions set to # read-only without raising: # "Error loading wallet.dat. Is wallet being used by another process?" # The following lines should be uncommented and the tests still succeed: # # self.log.debug('Setting wallet file permissions to 400 (read-only)') # os.chmod(self.wallet_path, stat.S_IRUSR) # assert self.wallet_permissions() in ['400', '666'] # Sanity check. 666 because Appveyor. # shasum_before = self.wallet_shasum() timestamp_before = self.wallet_timestamp() self.log.debug( 'Wallet file timestamp before calling info: {}'.format(timestamp_before)) out = textwrap.dedent('''\ Wallet info =========== Encrypted: no HD (hd seed available): yes Keypool Size: 2 Transactions: 0 Address Book: 1 ''') self.assert_tool_output(out, '-wallet=wallet.dat', 'info') timestamp_after = self.wallet_timestamp() self.log.debug( 'Wallet file timestamp after calling info: {}'.format(timestamp_after)) self.log_wallet_timestamp_comparison(timestamp_before, timestamp_after) self.log.debug( 'Setting wallet file permissions back to 600 (read/write)') os.chmod(self.wallet_path, stat.S_IRUSR | stat.S_IWUSR) # Sanity check. 666 because Appveyor. assert self.wallet_permissions() in ['600', '666'] # # TODO: Wallet tool info should not write to the wallet file. # The following lines should be uncommented and the tests still succeed: # # assert_equal(timestamp_before, timestamp_after) # shasum_after = self.wallet_shasum() # assert_equal(shasum_before, shasum_after) # self.log.debug('Wallet file shasum unchanged\n') def test_tool_wallet_info_after_transaction(self): """ Mutate the wallet with a transaction to verify that the info command output changes accordingly. """ self.start_node(0) self.log.info('Generating transaction to mutate wallet') self.nodes[0].generate(1) self.stop_node(0) self.log.info( 'Calling wallet tool info after generating a transaction, testing output') shasum_before = self.wallet_shasum() timestamp_before = self.wallet_timestamp() self.log.debug( 'Wallet file timestamp before calling info: {}'.format(timestamp_before)) out = textwrap.dedent('''\ Wallet info =========== Encrypted: no HD (hd seed available): yes Keypool Size: 2 Transactions: 1 Address Book: 1 ''') self.assert_tool_output(out, '-wallet=wallet.dat', 'info') shasum_after = self.wallet_shasum() timestamp_after = self.wallet_timestamp() self.log.debug( 'Wallet file timestamp after calling info: {}'.format(timestamp_after)) self.log_wallet_timestamp_comparison(timestamp_before, timestamp_after) # # TODO: Wallet tool info should not write to the wallet file. # This assertion should be uncommented and succeed: # assert_equal(timestamp_before, timestamp_after) assert_equal(shasum_before, shasum_after) self.log.debug('Wallet file shasum unchanged\n') def test_tool_wallet_create_on_existing_wallet(self): self.log.info( 'Calling wallet tool create on an existing wallet, testing output') shasum_before = self.wallet_shasum() timestamp_before = self.wallet_timestamp() self.log.debug( 'Wallet file timestamp before calling create: {}'.format(timestamp_before)) out = textwrap.dedent('''\ Topping up keypool... Wallet info =========== Encrypted: no HD (hd seed available): yes Keypool Size: 2000 Transactions: 0 Address Book: 0 ''') self.assert_tool_output(out, '-wallet=foo', 'create') shasum_after = self.wallet_shasum() timestamp_after = self.wallet_timestamp() self.log.debug( 'Wallet file timestamp after calling create: {}'.format(timestamp_after)) self.log_wallet_timestamp_comparison(timestamp_before, timestamp_after) assert_equal(timestamp_before, timestamp_after) assert_equal(shasum_before, shasum_after) self.log.debug('Wallet file shasum unchanged\n') def test_getwalletinfo_on_different_wallet(self): self.log.info('Starting node with arg -wallet=foo') self.start_node(0, ['-wallet=foo']) self.log.info( 'Calling getwalletinfo on a different wallet ("foo"), testing output') shasum_before = self.wallet_shasum() timestamp_before = self.wallet_timestamp() self.log.debug( 'Wallet file timestamp before calling getwalletinfo: {}'.format(timestamp_before)) out = self.nodes[0].getwalletinfo() self.stop_node(0) shasum_after = self.wallet_shasum() timestamp_after = self.wallet_timestamp() self.log.debug( 'Wallet file timestamp after calling getwalletinfo: {}'.format(timestamp_after)) assert_equal(0, out['txcount']) assert_equal(1000, out['keypoolsize']) assert_equal(1000, out['keypoolsize_hd_internal']) assert_equal(True, 'hdseedid' in out) self.log_wallet_timestamp_comparison(timestamp_before, timestamp_after) assert_equal(timestamp_before, timestamp_after) assert_equal(shasum_after, shasum_before) self.log.debug('Wallet file shasum unchanged\n') def test_salvage(self): # TODO: Check salvage actually salvages and doesn't break things. # https://github.com/bitcoin/bitcoin/issues/7463 self.log.info('Check salvage') self.start_node(0, ['-wallet=salvage']) self.stop_node(0) self.assert_tool_output('', '-wallet=salvage', 'salvage') def run_test(self): self.wallet_path = os.path.join( - self.nodes[0].datadir, self.chain, 'wallets', 'wallet.dat') + self.nodes[0].datadir, + self.chain, + 'wallets', + self.default_wallet_name, + self.wallet_data_filename + ) self.test_invalid_tool_commands_and_args() # Warning: The following tests are order-dependent. self.test_tool_wallet_info() self.test_tool_wallet_info_after_transaction() self.test_tool_wallet_create_on_existing_wallet() self.test_getwalletinfo_on_different_wallet() self.test_salvage() if __name__ == '__main__': ToolWalletTest().main() diff --git a/test/functional/wallet_backup.py b/test/functional/wallet_backup.py index 126a22131..a2fcc6f12 100755 --- a/test/functional/wallet_backup.py +++ b/test/functional/wallet_backup.py @@ -1,249 +1,259 @@ #!/usr/bin/env python3 # Copyright (c) 2014-2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the wallet backup features. Test case is: 4 nodes. 1 2 and 3 send transactions between each other, fourth node is a miner. 1 2 3 each mine a block to start, then Miner creates 100 blocks so 1 2 3 each have 50 mature coins to spend. Then 5 iterations of 1/2/3 sending coins amongst themselves to get transactions in the wallets, and the miner mining one block. Wallets are backed up using dumpwallet/backupwallet. Then 5 more iterations of transactions and mining a block. Miner then generates 101 more blocks, so any transaction fees paid mature. Sanity check: Sum(1,2,3,4 balances) == 114*50 1/2/3 are shutdown, and their wallets erased. Then restore using wallet.dat backup. And confirm 1/2/3/4 balances are same as before. Shutdown again, restore using importwallet, and confirm again balances are correct. """ import os import shutil from decimal import Decimal from random import randint from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_raises_rpc_error, connect_nodes, ) class WalletBackupTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 4 self.setup_clean_chain = True # nodes 1, 2,3 are spenders, let's give them a keypool=100 # whitelist all peers to speed up tx relay / mempool sync self.extra_args = [ ["-whitelist=noban@127.0.0.1", "-keypool=100", "-wallet="], ["-whitelist=noban@127.0.0.1", "-keypool=100", "-wallet="], ["-whitelist=noban@127.0.0.1", "-keypool=100", "-wallet="], ["-whitelist=noban@127.0.0.1", "-wallet="], ] self.rpc_timeout = 120 def skip_test_if_missing_module(self): self.skip_if_no_wallet() def setup_network(self): self.setup_nodes() connect_nodes(self.nodes[0], self.nodes[3]) connect_nodes(self.nodes[1], self.nodes[3]) connect_nodes(self.nodes[2], self.nodes[3]) connect_nodes(self.nodes[2], self.nodes[0]) self.sync_all() def one_send(self, from_node, to_address): if (randint(1, 2) == 1): amount = Decimal(randint(1, 10)) * Decimal(100000) self.nodes[from_node].sendtoaddress(to_address, amount) def do_one_round(self): a0 = self.nodes[0].getnewaddress() a1 = self.nodes[1].getnewaddress() a2 = self.nodes[2].getnewaddress() self.one_send(0, a1) self.one_send(0, a2) self.one_send(1, a0) self.one_send(1, a2) self.one_send(2, a0) self.one_send(2, a1) # Have the miner (node3) mine a block. # Must sync mempools before mining. self.sync_mempools() self.nodes[3].generate(1) self.sync_blocks() # As above, this mirrors the original bash test. def start_three(self): self.start_node(0) self.start_node(1) self.start_node(2) connect_nodes(self.nodes[0], self.nodes[3]) connect_nodes(self.nodes[1], self.nodes[3]) connect_nodes(self.nodes[2], self.nodes[3]) connect_nodes(self.nodes[2], self.nodes[0]) def stop_three(self): self.stop_node(0) self.stop_node(1) self.stop_node(2) def erase_three(self): - os.remove(os.path.join( - self.nodes[0].datadir, self.chain, 'wallets', 'wallet.dat')) - os.remove(os.path.join( - self.nodes[1].datadir, self.chain, 'wallets', 'wallet.dat')) - os.remove(os.path.join( - self.nodes[2].datadir, self.chain, 'wallets', 'wallet.dat')) + os.remove( + os.path.join(self.nodes[0].datadir, self.chain, 'wallets', + self.default_wallet_name, self.wallet_data_filename)) + os.remove( + os.path.join(self.nodes[1].datadir, self.chain, 'wallets', + self.default_wallet_name, self.wallet_data_filename)) + os.remove( + os.path.join(self.nodes[2].datadir, self.chain, 'wallets', + self.default_wallet_name, self.wallet_data_filename)) def run_test(self): self.log.info("Generating initial blockchain") self.nodes[0].generate(1) self.sync_blocks() self.nodes[1].generate(1) self.sync_blocks() self.nodes[2].generate(1) self.sync_blocks() self.nodes[3].generate(100) self.sync_blocks() assert_equal(self.nodes[0].getbalance(), 50000000) assert_equal(self.nodes[1].getbalance(), 50000000) assert_equal(self.nodes[2].getbalance(), 50000000) assert_equal(self.nodes[3].getbalance(), 0) self.log.info("Creating transactions") # Five rounds of sending each other transactions. for _ in range(5): self.do_one_round() self.log.info("Backing up") self.nodes[0].backupwallet(os.path.join( self.nodes[0].datadir, 'wallet.bak')) self.nodes[0].dumpwallet(os.path.join( self.nodes[0].datadir, 'wallet.dump')) self.nodes[1].backupwallet(os.path.join( self.nodes[1].datadir, 'wallet.bak')) self.nodes[1].dumpwallet(os.path.join( self.nodes[1].datadir, 'wallet.dump')) self.nodes[2].backupwallet(os.path.join( self.nodes[2].datadir, 'wallet.bak')) self.nodes[2].dumpwallet(os.path.join( self.nodes[2].datadir, 'wallet.dump')) self.log.info("More transactions") for _ in range(5): self.do_one_round() # Generate 101 more blocks, so any fees paid mature self.nodes[3].generate(101) self.sync_all() balance0 = self.nodes[0].getbalance() balance1 = self.nodes[1].getbalance() balance2 = self.nodes[2].getbalance() balance3 = self.nodes[3].getbalance() total = balance0 + balance1 + balance2 + balance3 # At this point, there are 214 blocks (103 for setup, then 10 rounds, then 101.) # 114 are mature, so the sum of all wallets should be 114 * 50 = 5700. assert_equal(total, 5700000000) ## # Test restoring spender wallets from backups ## self.log.info("Restoring using wallet.dat") self.stop_three() self.erase_three() # Start node2 with no chain shutil.rmtree( os.path.join( self.nodes[2].datadir, self.chain, 'blocks')) shutil.rmtree(os.path.join( self.nodes[2].datadir, self.chain, 'chainstate')) # Restore wallets from backup - shutil.copyfile(os.path.join(self.nodes[0].datadir, 'wallet.bak'), os.path.join( - self.nodes[0].datadir, self.chain, 'wallets', 'wallet.dat')) - shutil.copyfile(os.path.join(self.nodes[1].datadir, 'wallet.bak'), os.path.join( - self.nodes[1].datadir, self.chain, 'wallets', 'wallet.dat')) - shutil.copyfile(os.path.join(self.nodes[2].datadir, 'wallet.bak'), os.path.join( - self.nodes[2].datadir, self.chain, 'wallets', 'wallet.dat')) + shutil.copyfile( + os.path.join(self.nodes[0].datadir, 'wallet.bak'), + os.path.join(self.nodes[0].datadir, self.chain, 'wallets', + self.default_wallet_name, self.wallet_data_filename)) + shutil.copyfile( + os.path.join(self.nodes[1].datadir, 'wallet.bak'), + os.path.join(self.nodes[1].datadir, self.chain, 'wallets', + self.default_wallet_name, self.wallet_data_filename)) + shutil.copyfile( + os.path.join(self.nodes[2].datadir, 'wallet.bak'), + os.path.join(self.nodes[2].datadir, self.chain, 'wallets', + self.default_wallet_name, self.wallet_data_filename)) self.log.info("Re-starting nodes") self.start_three() self.sync_blocks() assert_equal(self.nodes[0].getbalance(), balance0) assert_equal(self.nodes[1].getbalance(), balance1) assert_equal(self.nodes[2].getbalance(), balance2) self.log.info("Restoring using dumped wallet") self.stop_three() self.erase_three() # start node2 with no chain shutil.rmtree( os.path.join( self.nodes[2].datadir, self.chain, 'blocks')) shutil.rmtree(os.path.join( self.nodes[2].datadir, self.chain, 'chainstate')) self.start_three() assert_equal(self.nodes[0].getbalance(), 0) assert_equal(self.nodes[1].getbalance(), 0) assert_equal(self.nodes[2].getbalance(), 0) self.nodes[0].importwallet(os.path.join( self.nodes[0].datadir, 'wallet.dump')) self.nodes[1].importwallet(os.path.join( self.nodes[1].datadir, 'wallet.dump')) self.nodes[2].importwallet(os.path.join( self.nodes[2].datadir, 'wallet.dump')) self.sync_blocks() assert_equal(self.nodes[0].getbalance(), balance0) assert_equal(self.nodes[1].getbalance(), balance1) assert_equal(self.nodes[2].getbalance(), balance2) # Backup to source wallet file must fail sourcePaths = [ - os.path.join(self.nodes[0].datadir, - self.chain, 'wallets', 'wallet.dat'), - os.path.join(self.nodes[0].datadir, self.chain, - '.', 'wallets', 'wallet.dat'), - os.path.join(self.nodes[0].datadir, self.chain, 'wallets', ''), + os.path.join(self.nodes[0].datadir, self.chain, 'wallets', + self.default_wallet_name, self.wallet_data_filename), + os.path.join(self.nodes[0].datadir, self.chain, '.', 'wallets', + self.default_wallet_name, self.wallet_data_filename), + os.path.join(self.nodes[0].datadir, self.chain, 'wallets', + self.default_wallet_name), os.path.join(self.nodes[0].datadir, self.chain, 'wallets')] for sourcePath in sourcePaths: assert_raises_rpc_error(-4, "backup failed", self.nodes[0].backupwallet, sourcePath) if __name__ == '__main__': WalletBackupTest().main() diff --git a/test/functional/wallet_balance.py b/test/functional/wallet_balance.py index aa226f45c..64282fa57 100755 --- a/test/functional/wallet_balance.py +++ b/test/functional/wallet_balance.py @@ -1,327 +1,327 @@ #!/usr/bin/env python3 # Copyright (c) 2018-2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the wallet balance RPC methods.""" import struct from decimal import Decimal from test_framework.address import ( ADDRESS_ECREG_UNSPENDABLE as ADDRESS_WATCHONLY, ) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_raises_rpc_error, connect_nodes, ) def create_transactions(node, address, amt, fees): # Create and sign raw transactions from node to address for amt. # Creates a transaction for each fee and returns an array # of the raw transactions. utxos = [u for u in node.listunspent(0) if u['spendable']] # Create transactions inputs = [] ins_total = 0 for utxo in utxos: inputs.append({"txid": utxo["txid"], "vout": utxo["vout"]}) ins_total += utxo['amount'] if ins_total >= amt + max(fees): break # make sure there was enough utxos assert ins_total >= amt + max(fees) txs = [] for fee in fees: outputs = {address: amt} # prevent 0 change output if ins_total > amt + fee: outputs[node.getrawchangeaddress()] = ins_total - amt - fee raw_tx = node.createrawtransaction(inputs, outputs, 0) raw_tx = node.signrawtransactionwithwallet(raw_tx) assert_equal(raw_tx['complete'], True) txs.append(raw_tx) return txs class WalletTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 self.setup_clean_chain = True self.extra_args = [ # Limit mempool descendants as a hack to have wallet txs rejected # from the mempool ['-limitdescendantcount=3'], [], ] def skip_test_if_missing_module(self): self.skip_if_no_wallet() def run_test(self): self.nodes[0].importaddress(ADDRESS_WATCHONLY) # Check that nodes don't own any UTXOs assert_equal(len(self.nodes[0].listunspent()), 0) assert_equal(len(self.nodes[1].listunspent()), 0) self.log.info("Check that only node 0 is watching an address") assert 'watchonly' in self.nodes[0].getbalances() assert 'watchonly' not in self.nodes[1].getbalances() self.log.info("Mining blocks ...") self.nodes[0].generate(1) self.sync_all() self.nodes[1].generate(1) self.nodes[1].generatetoaddress(101, ADDRESS_WATCHONLY) self.sync_all() assert_equal(self.nodes[0].getbalances()['mine']['trusted'], 50000000) assert_equal(self.nodes[0].getwalletinfo()['balance'], 50000000) assert_equal(self.nodes[1].getbalances()['mine']['trusted'], 50000000) assert_equal(self.nodes[0].getbalances()[ 'watchonly']['immature'], 5000000000) assert 'watchonly' not in self.nodes[1].getbalances() assert_equal(self.nodes[0].getbalance(), 50000000) assert_equal(self.nodes[1].getbalance(), 50000000) self.log.info("Test getbalance with different arguments") assert_equal(self.nodes[0].getbalance("*"), 50000000) assert_equal(self.nodes[0].getbalance("*", 1), 50000000) assert_equal(self.nodes[0].getbalance("*", 1, True), 100000000) assert_equal(self.nodes[0].getbalance(minconf=1), 50000000) assert_equal( self.nodes[0].getbalance( minconf=0, include_watchonly=True), 100000000) assert_equal( self.nodes[1].getbalance( minconf=0, include_watchonly=True), 50000000) # Send 40 BTC from 0 to 1 and 60 BTC from 1 to 0. txs = create_transactions( self.nodes[0], self.nodes[1].getnewaddress(), 40000000, [Decimal('10000')]) self.nodes[0].sendrawtransaction(txs[0]['hex']) # sending on both nodes is faster than waiting for propagation self.nodes[1].sendrawtransaction(txs[0]['hex']) self.sync_all() txs = create_transactions(self.nodes[1], self.nodes[0].getnewaddress(), 60000000, [ Decimal('10000'), Decimal('20000')]) self.nodes[1].sendrawtransaction(txs[0]['hex']) # sending on both nodes is faster than waiting for propagation self.nodes[0].sendrawtransaction(txs[0]['hex']) self.sync_all() # First argument of getbalance must be set to "*" assert_raises_rpc_error(-32, "dummy first argument must be excluded or set to \"*\"", self.nodes[1].getbalance, "") self.log.info("Test balances with unconfirmed inputs") # Before `test_balance()`, we have had two nodes with a balance of 50 # each and then we: # # 1) Sent 40 from node A to node B with fee 0.01 # 2) Sent 60 from node B to node A with fee 0.01 # # Then we check the balances: # # 1) As is # 2) With transaction 2 from above with 2x the fee # # Prior to #16766, in this situation, the node would immediately report # a balance of 30 on node B as unconfirmed and trusted. # # After #16766, we show that balance as unconfirmed. # # The balance is indeed "trusted" and "confirmed" insofar as removing # the mempool transactions would return at least that much money. But # the algorithm after #16766 marks it as unconfirmed because the 'taint' # tracking of transaction trust for summing balances doesn't consider # which inputs belong to a user. In this case, the change output in # question could be "destroyed" by replace the 1st transaction above. # # The post #16766 behavior is correct; we shouldn't be treating those # funds as confirmed. If you want to rely on that specific UTXO existing # which has given you that balance, you cannot, as a third party # spending the other input would destroy that unconfirmed. # # For example, if the test transactions were: # # 1) Sent 40 from node A to node B with fee 0.01 # 2) Sent 10 from node B to node A with fee 0.01 # # Then our node would report a confirmed balance of 40 + 50 - 10 = 80 # BTC, which is more than would be available if transaction 1 were # replaced. def test_balances(*, fee_node_1=0): # getbalances expected_balances_0 = {'mine': {'immature': Decimal('0E-2'), # change from node 0's send 'trusted': Decimal('9990000'), 'untrusted_pending': Decimal('60000000.0')}, 'watchonly': {'immature': Decimal('5000000000'), 'trusted': Decimal('50000000.0'), 'untrusted_pending': Decimal('0E-2')}} expected_balances_1 = {'mine': {'immature': Decimal('0E-2'), # node 1's send had an unsafe input 'trusted': Decimal('0E-2'), # Doesn't include output of node # 0's send since it was spent 'untrusted_pending': Decimal('30000000.0') - fee_node_1}} assert_equal(self.nodes[0].getbalances(), expected_balances_0) assert_equal(self.nodes[1].getbalances(), expected_balances_1) # getbalance without any arguments includes unconfirmed transactions, but not untrusted transactions # change from node 0's send assert_equal(self.nodes[0].getbalance(), Decimal('9990000')) # node 1's send had an unsafe input assert_equal(self.nodes[1].getbalance(), Decimal('0')) # Same with minconf=0 assert_equal( self.nodes[0].getbalance( minconf=0), Decimal('9990000')) assert_equal(self.nodes[1].getbalance(minconf=0), Decimal('0')) # getbalance with a minconf incorrectly excludes coins that have been spent more recently than the minconf blocks ago # TODO: fix getbalance tracking of coin spentness depth assert_equal(self.nodes[0].getbalance(minconf=1), Decimal('0')) assert_equal(self.nodes[1].getbalance(minconf=1), Decimal('0')) # getunconfirmedbalance # output of node 1's spend assert_equal( self.nodes[0].getunconfirmedbalance(), Decimal('60000000')) # Doesn't include output of node 0's send since it was spent assert_equal( self.nodes[1].getunconfirmedbalance(), Decimal('30000000') - fee_node_1) # getwalletinfo.unconfirmed_balance assert_equal(self.nodes[0].getwalletinfo()[ "unconfirmed_balance"], Decimal('60000000')) assert_equal( self.nodes[1].getwalletinfo()["unconfirmed_balance"], Decimal('30000000') - fee_node_1) test_balances(fee_node_1=Decimal('10000')) # In the original Core version of this test, Node 1 would've bumped # the fee by 0.01 here to resend, but this is XEC, so it has 10000 XEC # left to spend on goods and services self.sync_all() self.log.info( "Test getbalance and getbalances.mine.untrusted_pending with conflicted unconfirmed inputs") test_balances(fee_node_1=Decimal('10000')) self.nodes[1].generatetoaddress(1, ADDRESS_WATCHONLY) self.sync_all() # balances are correct after the transactions are confirmed # node 1's send plus change from node 0's send balance_node0 = Decimal('69990000') # change from node 0's send balance_node1 = Decimal('29990000') assert_equal(self.nodes[0].getbalances()[ 'mine']['trusted'], balance_node0) assert_equal(self.nodes[1].getbalances()[ 'mine']['trusted'], balance_node1) assert_equal(self.nodes[0].getbalance(), balance_node0) assert_equal(self.nodes[1].getbalance(), balance_node1) # Send total balance away from node 1 txs = create_transactions(self.nodes[1], self.nodes[0].getnewaddress( ), Decimal('29970000'), [Decimal('10000')]) self.nodes[1].sendrawtransaction(txs[0]['hex']) self.nodes[1].generatetoaddress(2, ADDRESS_WATCHONLY) self.sync_all() # getbalance with a minconf incorrectly excludes coins that have been spent more recently than the minconf blocks ago # TODO: fix getbalance tracking of coin spentness depth # getbalance with minconf=3 should still show the old balance assert_equal(self.nodes[1].getbalance(minconf=3), Decimal('0')) # getbalance with minconf=2 will show the new balance. assert_equal(self.nodes[1].getbalance(minconf=2), Decimal('10000')) # check mempool transactions count for wallet unconfirmed balance after # dynamically loading the wallet. before = self.nodes[1].getbalances()['mine']['untrusted_pending'] dst = self.nodes[1].getnewaddress() - self.nodes[1].unloadwallet('') + self.nodes[1].unloadwallet(self.default_wallet_name) self.nodes[0].sendtoaddress(dst, 100000) self.sync_all() - self.nodes[1].loadwallet('') + self.nodes[1].loadwallet(self.default_wallet_name) after = self.nodes[1].getbalances()['mine']['untrusted_pending'] assert_equal(before + Decimal('100000'), after) # Create 3 more wallet txs, where the last is not accepted to the # mempool because it is the third descendant of the tx above for _ in range(3): # Set amount high enough such that all coins are spent by each tx txid = self.nodes[0].sendtoaddress( self.nodes[0].getnewaddress(), 99000000) self.log.info('Check that wallet txs not in the mempool are untrusted') assert txid not in self.nodes[0].getrawmempool() assert_equal(self.nodes[0].gettransaction(txid)['trusted'], False) assert_equal(self.nodes[0].getbalance(minconf=0), 0) self.log.info("Test replacement and reorg of non-mempool tx") tx_orig = self.nodes[0].gettransaction(txid)['hex'] # Increase fee by 1 coin tx_replace = tx_orig.replace( struct.pack(" uri-path).", node.getwalletinfo) w1, w2, w3, w4, *_ = wallets node.generatetoaddress(nblocks=101, address=w1.getnewaddress()) assert_equal(w1.getbalance(), 100000000) assert_equal(w2.getbalance(), 0) assert_equal(w3.getbalance(), 0) assert_equal(w4.getbalance(), 0) w1.sendtoaddress(w2.getnewaddress(), 1000000) w1.sendtoaddress(w3.getnewaddress(), 2000000) w1.sendtoaddress(w4.getnewaddress(), 3000000) node.generatetoaddress(nblocks=1, address=w1.getnewaddress()) assert_equal(w2.getbalance(), 1000000) assert_equal(w3.getbalance(), 2000000) assert_equal(w4.getbalance(), 3000000) batch = w1.batch([w1.getblockchaininfo.get_request(), w1.getwalletinfo.get_request()]) assert_equal(batch[0]["result"]["chain"], self.chain) assert_equal(batch[1]["result"]["walletname"], "w1") self.log.info('Check for per-wallet settxfee call') assert_equal(w1.getwalletinfo()['paytxfee'], 0) assert_equal(w2.getwalletinfo()['paytxfee'], 0) w2.settxfee(1000) assert_equal(w1.getwalletinfo()['paytxfee'], 0) assert_equal(w2.getwalletinfo()['paytxfee'], Decimal('1000.00')) self.log.info("Test dynamic wallet loading") self.restart_node(0, ['-nowallet']) assert_equal(node.listwallets(), []) assert_raises_rpc_error(-32601, "Method not found", node.getwalletinfo) self.log.info("Load first wallet") loadwallet_name = node.loadwallet(wallet_names[0]) assert_equal(loadwallet_name['name'], wallet_names[0]) assert_equal(node.listwallets(), wallet_names[0:1]) node.getwalletinfo() w1 = node.get_wallet_rpc(wallet_names[0]) w1.getwalletinfo() self.log.info("Load second wallet") loadwallet_name = node.loadwallet(wallet_names[1]) assert_equal(loadwallet_name['name'], wallet_names[1]) assert_equal(node.listwallets(), wallet_names[0:2]) assert_raises_rpc_error(-19, "Wallet file not specified", node.getwalletinfo) w2 = node.get_wallet_rpc(wallet_names[1]) w2.getwalletinfo() self.log.info("Concurrent wallet loading") threads = [] for _ in range(3): n = node.cli if self.options.usecli else get_rpc_proxy( node.url, 1, timeout=600, coveragedir=node.coverage_dir) t = Thread(target=test_load_unload, args=(n, wallet_names[2], )) t.start() threads.append(t) for t in threads: t.join() global got_loading_error assert_equal(got_loading_error, True) self.log.info("Load remaining wallets") for wallet_name in wallet_names[2:]: loadwallet_name = self.nodes[0].loadwallet(wallet_name) assert_equal(loadwallet_name['name'], wallet_name) assert_equal(set(self.nodes[0].listwallets()), set(wallet_names)) # Fail to load if wallet doesn't exist path = os.path.join(self.options.tmpdir, "node0", "regtest", "wallets", "wallets") assert_raises_rpc_error( -18, "Wallet file verification failed. Failed to load database path " "'{}'. Path does not exist.".format(path), self.nodes[0].loadwallet, 'wallets') # Fail to load duplicate wallets path = os.path.join( self.options.tmpdir, "node0", "regtest", "wallets", "w1", - "wallet.dat") + self.wallet_data_filename) assert_raises_rpc_error( -4, "Wallet file verification failed. Refusing to load database. Data file '{}' is already loaded.".format( path), self.nodes[0].loadwallet, wallet_names[0]) # Fail to load duplicate wallets by different ways (directory and # filepath) path = os.path.join( self.options.tmpdir, "node0", "regtest", "wallets", - "wallet.dat") + self.wallet_data_filename) assert_raises_rpc_error( -4, "Wallet file verification failed. Refusing to load database. Data file '{}' is already loaded.".format( path), self.nodes[0].loadwallet, - 'wallet.dat') + self.wallet_data_filename) # Fail to load if one wallet is a copy of another assert_raises_rpc_error(-4, "BerkeleyDatabase: Can't open database w8_copy (duplicates fileid", self.nodes[0].loadwallet, 'w8_copy') # Fail to load if one wallet is a copy of another. # Test this twice to make sure that we don't re-introduce # https://github.com/bitcoin/bitcoin/issues/14304 assert_raises_rpc_error(-4, "BerkeleyDatabase: Can't open database w8_copy (duplicates fileid", self.nodes[0].loadwallet, 'w8_copy') # Fail to load if wallet file is a symlink if os.name != 'nt': assert_raises_rpc_error( -4, "Wallet file verification failed. Invalid -wallet path 'w8_symlink'", self.nodes[0].loadwallet, 'w8_symlink') # Fail to load if a directory is specified that doesn't contain a # wallet os.mkdir(wallet_dir('empty_wallet_dir')) path = os.path.join(self.options.tmpdir, "node0", "regtest", "wallets", "empty_wallet_dir") assert_raises_rpc_error( -18, "Wallet file verification failed. Failed to load database " "path '{}'. Data is not in recognized format.".format(path), self.nodes[0].loadwallet, 'empty_wallet_dir') self.log.info("Test dynamic wallet creation.") # Fail to create a wallet if it already exists. path = os.path.join(self.options.tmpdir, "node0", "regtest", "wallets", "w2") assert_raises_rpc_error( -4, f"Failed to create database path '{path}'. Database already exists.", self.nodes[0].createwallet, 'w2') # Successfully create a wallet with a new name loadwallet_name = self.nodes[0].createwallet('w9') assert_equal(loadwallet_name['name'], 'w9') w9 = node.get_wallet_rpc('w9') assert_equal(w9.getwalletinfo()['walletname'], 'w9') assert 'w9' in self.nodes[0].listwallets() # Successfully create a wallet using a full path new_wallet_dir = os.path.join(self.options.tmpdir, 'new_walletdir') new_wallet_name = os.path.join(new_wallet_dir, 'w10') loadwallet_name = self.nodes[0].createwallet(new_wallet_name) assert_equal(loadwallet_name['name'], new_wallet_name) w10 = node.get_wallet_rpc(new_wallet_name) assert_equal(w10.getwalletinfo()['walletname'], new_wallet_name) assert new_wallet_name in self.nodes[0].listwallets() self.log.info("Test dynamic wallet unloading") # Test `unloadwallet` errors assert_raises_rpc_error(-1, "JSON value is not a string as expected", self.nodes[0].unloadwallet) assert_raises_rpc_error(-18, "Requested wallet does not exist or is not loaded", self.nodes[0].unloadwallet, "dummy") assert_raises_rpc_error(-18, "Requested wallet does not exist or is not loaded", node.get_wallet_rpc("dummy").unloadwallet) assert_raises_rpc_error(-8, "Cannot unload the requested wallet", w1.unloadwallet, "w2"), # Successfully unload the specified wallet name self.nodes[0].unloadwallet("w1") assert 'w1' not in self.nodes[0].listwallets() # Successfully unload the wallet referenced by the request endpoint # Also ensure unload works during walletpassphrase timeout w2.encryptwallet('test') w2.walletpassphrase('test', 1) w2.unloadwallet() time.sleep(1.1) assert 'w2' not in self.nodes[0].listwallets() # Successfully unload all wallets for wallet_name in self.nodes[0].listwallets(): self.nodes[0].unloadwallet(wallet_name) assert_equal(self.nodes[0].listwallets(), []) assert_raises_rpc_error(-32601, "Method not found (wallet method is disabled because no wallet is loaded)", self.nodes[0].getwalletinfo) # Successfully load a previously unloaded wallet self.nodes[0].loadwallet('w1') assert_equal(self.nodes[0].listwallets(), ['w1']) assert_equal(w1.getwalletinfo()['walletname'], 'w1') - assert_equal(sorted(map(lambda w: w['name'], self.nodes[0].listwalletdir()['wallets'])), [ - '', os.path.join('sub', 'w5'), 'w', 'w1', 'w2', 'w3', 'w7', 'w7_symlink', 'w8', 'w8_copy', 'w9']) + assert_equal(sorted(map(lambda w: w['name'], + self.nodes[0].listwalletdir()['wallets'])), + [self.default_wallet_name, os.path.join('sub', 'w5'), 'w', + 'w1', 'w2', 'w3', 'w7', 'w7_symlink', 'w8', 'w8_copy', + 'w9']) # Test backing up and restoring wallets self.log.info("Test wallet backup") self.restart_node(0, ['-nowallet']) for wallet_name in wallet_names: self.nodes[0].loadwallet(wallet_name) for wallet_name in wallet_names: rpc = self.nodes[0].get_wallet_rpc(wallet_name) addr = rpc.getnewaddress() backup = os.path.join(self.options.tmpdir, 'backup.dat') rpc.backupwallet(backup) self.nodes[0].unloadwallet(wallet_name) shutil.copyfile(empty_wallet, wallet_file(wallet_name)) self.nodes[0].loadwallet(wallet_name) assert_equal(rpc.getaddressinfo(addr)['ismine'], False) self.nodes[0].unloadwallet(wallet_name) shutil.copyfile(backup, wallet_file(wallet_name)) self.nodes[0].loadwallet(wallet_name) assert_equal(rpc.getaddressinfo(addr)['ismine'], True) # Test .walletlock file is closed self.start_node(1) wallet = os.path.join(self.options.tmpdir, 'my_wallet') self.nodes[0].createwallet(wallet) assert_raises_rpc_error(-4, "Error initializing wallet database environment", self.nodes[1].loadwallet, wallet) self.nodes[0].unloadwallet(wallet) self.nodes[1].loadwallet(wallet) if __name__ == '__main__': MultiWalletTest().main() diff --git a/test/functional/wallet_reorgsrestore.py b/test/functional/wallet_reorgsrestore.py index 358559941..b419272a9 100755 --- a/test/functional/wallet_reorgsrestore.py +++ b/test/functional/wallet_reorgsrestore.py @@ -1,123 +1,124 @@ #!/usr/bin/env python3 # Copyright (c) 2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test tx status in case of reorgs while wallet being shutdown. Wallet txn status rely on block connection/disconnection for its accuracy. In case of reorgs happening while wallet being shutdown block updates are not going to be received. At wallet loading, we check against chain if confirmed txn are still in chain and change their status if block in which they have been included has been disconnected. """ import os import shutil from decimal import Decimal from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, connect_nodes, disconnect_nodes class ReorgsRestoreTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 3 self.extra_args = [["-noparkdeepreorg"]] * self.num_nodes def skip_test_if_missing_module(self): self.skip_if_no_wallet() def run_test(self): # Send a tx from which to conflict outputs later txid_conflict_from = self.nodes[0].sendtoaddress( self.nodes[0].getnewaddress(), Decimal("10000000")) self.nodes[0].generate(1) self.sync_blocks() # Disconnect node1 from others to reorg its chain later disconnect_nodes(self.nodes[0], self.nodes[1]) disconnect_nodes(self.nodes[1], self.nodes[2]) connect_nodes(self.nodes[0], self.nodes[2]) # Send a tx to be unconfirmed later txid = self.nodes[0].sendtoaddress( self.nodes[0].getnewaddress(), Decimal("10000000")) tx = self.nodes[0].gettransaction(txid) self.nodes[0].generate(4) tx_before_reorg = self.nodes[0].gettransaction(txid) assert_equal(tx_before_reorg["confirmations"], 4) # Disconnect node0 from node2 to broadcast a conflict on their # respective chains disconnect_nodes(self.nodes[0], self.nodes[2]) nA = next(tx_out["vout"] for tx_out in self.nodes[0].gettransaction( txid_conflict_from)["details"] if tx_out["amount"] == Decimal("10000000")) inputs = [] inputs.append({"txid": txid_conflict_from, "vout": nA}) outputs_1 = {} outputs_2 = {} # Create a conflicted tx broadcast on node0 chain and conflicting tx # broadcast on node1 chain. Both spend from txid_conflict_from outputs_1[self.nodes[0].getnewaddress()] = Decimal("9999980") outputs_2[self.nodes[0].getnewaddress()] = Decimal("9999980") conflicted = self.nodes[0].signrawtransactionwithwallet( self.nodes[0].createrawtransaction(inputs, outputs_1)) conflicting = self.nodes[0].signrawtransactionwithwallet( self.nodes[0].createrawtransaction(inputs, outputs_2)) conflicted_txid = self.nodes[0].sendrawtransaction(conflicted["hex"]) self.nodes[0].generate(1) conflicting_txid = self.nodes[2].sendrawtransaction(conflicting["hex"]) self.nodes[2].generate(9) # Reconnect node0 and node2 and check that conflicted_txid is # effectively conflicted connect_nodes(self.nodes[0], self.nodes[2]) self.sync_blocks([self.nodes[0], self.nodes[2]]) conflicted = self.nodes[0].gettransaction(conflicted_txid) conflicting = self.nodes[0].gettransaction(conflicting_txid) assert_equal(conflicted["confirmations"], -9) assert_equal(conflicted["walletconflicts"][0], conflicting["txid"]) # Node0 wallet is shutdown self.restart_node(0) # The block chain re-orgs and the tx is included in a different block self.nodes[1].generate(9) self.nodes[1].sendrawtransaction(tx["hex"]) self.nodes[1].generate(1) self.nodes[1].sendrawtransaction(conflicted["hex"]) self.nodes[1].generate(1) # Node0 wallet file is loaded on longest sync'ed node1 self.stop_node(1) self.nodes[0].backupwallet( os.path.join( self.nodes[0].datadir, 'wallet.bak')) shutil.copyfile( os.path.join( self.nodes[0].datadir, 'wallet.bak'), os.path.join( self.nodes[1].datadir, self.chain, - 'wallet.dat')) + self.default_wallet_name, + self.wallet_data_filename)) self.start_node(1) tx_after_reorg = self.nodes[1].gettransaction(txid) # Check that normal confirmed tx is confirmed again but with different # blockhash assert_equal(tx_after_reorg["confirmations"], 2) assert(tx_before_reorg["blockhash"] != tx_after_reorg["blockhash"]) conflicted_after_reorg = self.nodes[1].gettransaction(conflicted_txid) # Check that conflicted tx is confirmed again with blockhash different # than previously conflicting tx assert_equal(conflicted_after_reorg["confirmations"], 1) assert(conflicting["blockhash"] != conflicted_after_reorg["blockhash"]) if __name__ == '__main__': ReorgsRestoreTest().main() diff --git a/test/functional/wallet_send.py b/test/functional/wallet_send.py index dfaffaf0b..14cc9964d 100755 --- a/test/functional/wallet_send.py +++ b/test/functional/wallet_send.py @@ -1,423 +1,423 @@ #!/usr/bin/env python3 # Copyright (c) 2020 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the send RPC command.""" from decimal import Decimal from test_framework.authproxy import JSONRPCException from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_fee_amount, assert_greater_than, assert_raises_rpc_error, ) class WalletSendTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 # whitelist all peers to speed up tx relay / mempool sync self.extra_args = [ ["-whitelist=127.0.0.1", ], ["-whitelist=127.0.0.1", ], ] def skip_test_if_missing_module(self): self.skip_if_no_wallet() def test_send(self, from_wallet, to_wallet=None, amount=None, data=None, add_to_wallet=None, psbt=None, inputs=None, add_inputs=None, change_address=None, change_position=None, include_watching=None, locktime=None, lock_unspents=None, subtract_fee_from_outputs=None, fee_rate=None, expect_error=None): assert (amount is None) != (data is None) from_balance_before = from_wallet.getbalance() if to_wallet is None: assert amount is None else: to_untrusted_pending_before = \ to_wallet.getbalances()["mine"]["untrusted_pending"] if amount: dest = to_wallet.getnewaddress() outputs = {dest: amount} else: outputs = {"data": data} # Construct options dictionary options = {} if add_to_wallet is not None: options["add_to_wallet"] = add_to_wallet else: add_to_wallet = ( False if psbt else from_wallet.getwalletinfo()["private_keys_enabled"] ) if psbt is not None: options["psbt"] = psbt if inputs is not None: options["inputs"] = inputs if add_inputs is not None: options["add_inputs"] = add_inputs if change_address is not None: options["change_address"] = change_address if change_position is not None: options["change_position"] = change_position if include_watching is not None: options["include_watching"] = include_watching if locktime is not None: options["locktime"] = locktime if lock_unspents is not None: options["lock_unspents"] = lock_unspents if subtract_fee_from_outputs is not None: options["subtract_fee_from_outputs"] = subtract_fee_from_outputs if fee_rate is not None: options["fee_rate"] = fee_rate if len(options.keys()) == 0: options = None if expect_error is None: res = from_wallet.send( outputs=outputs, options=options) else: try: assert_raises_rpc_error( expect_error[0], expect_error[1], from_wallet.send, outputs=outputs, options=options) except AssertionError: # Provide debug info if the test fails self.log.error("Unexpected successful result:") self.log.error(options) res = from_wallet.send( outputs=outputs, options=options) self.log.error(res) if "txid" in res and add_to_wallet: self.log.error("Transaction details:") try: tx = from_wallet.gettransaction(res["txid"]) self.log.error(tx) self.log.error( "testmempoolaccept (transaction may already be in mempool):") self.log.error( from_wallet.testmempoolaccept([tx["hex"]])) except JSONRPCException as exc: self.log.error(exc) raise return if locktime: return res if (from_wallet.getwalletinfo()["private_keys_enabled"] and not include_watching): assert_equal(res["complete"], True) assert "txid" in res else: assert_equal(res["complete"], False) assert "txid" not in res assert "psbt" in res if add_to_wallet and not include_watching: # Ensure transaction exists in the wallet: tx = from_wallet.gettransaction(res["txid"]) assert tx # Ensure transaction exists in the mempool: tx = from_wallet.getrawtransaction(res["txid"], True) assert tx if amount: if subtract_fee_from_outputs: assert_equal( from_balance_before - from_wallet.getbalance(), amount) else: assert_greater_than( from_balance_before - from_wallet.getbalance(), amount) else: assert next( (out for out in tx["vout"] if out["scriptPubKey"] ["asm"] == "OP_RETURN 35"), None) else: assert_equal(from_balance_before, from_wallet.getbalance()) if to_wallet: self.sync_mempools() if add_to_wallet: if not subtract_fee_from_outputs: assert_equal( to_wallet.getbalances()["mine"]["untrusted_pending"], to_untrusted_pending_before + Decimal( amount if amount else 0)) else: assert_equal( to_wallet.getbalances()["mine"]["untrusted_pending"], to_untrusted_pending_before) return res def run_test(self): self.log.info("Setup wallets...") # w0 is a wallet with coinbase rewards - w0 = self.nodes[0].get_wallet_rpc("") + w0 = self.nodes[0].get_wallet_rpc(self.default_wallet_name) # w1 is a regular wallet self.nodes[1].createwallet(wallet_name="w1") w1 = self.nodes[1].get_wallet_rpc("w1") # w2 contains the private keys for w3 self.nodes[1].createwallet(wallet_name="w2") w2 = self.nodes[1].get_wallet_rpc("w2") # w3 is a watch-only wallet, based on w2 self.nodes[1].createwallet(wallet_name="w3", disable_private_keys=True) w3 = self.nodes[1].get_wallet_rpc("w3") for _ in range(3): a2_receive = w2.getnewaddress() # doesn't actually use change derivation a2_change = w2.getrawchangeaddress() res = w3.importmulti([{ "desc": w2.getaddressinfo(a2_receive)["desc"], "timestamp": "now", "keypool": True, "watchonly": True }, { "desc": w2.getaddressinfo(a2_change)["desc"], "timestamp": "now", "keypool": True, "internal": True, "watchonly": True }]) assert_equal(res, [{"success": True}, {"success": True}]) # fund w3 w0.sendtoaddress(a2_receive, 10_000_000) self.nodes[0].generate(1) self.sync_blocks() # w4 has private keys enabled, but only contains watch-only keys (from # w2) self.nodes[1].createwallet( wallet_name="w4", disable_private_keys=False) w4 = self.nodes[1].get_wallet_rpc("w4") for _ in range(3): a2_receive = w2.getnewaddress() res = w4.importmulti([{ "desc": w2.getaddressinfo(a2_receive)["desc"], "timestamp": "now", "keypool": False, "watchonly": True }]) assert_equal(res, [{"success": True}]) # fund w4 w0.sendtoaddress(a2_receive, 10_000_000) self.nodes[0].generate(1) self.sync_blocks() self.log.info("Send to address...") self.test_send(from_wallet=w0, to_wallet=w1, amount=1_000_000) self.test_send( from_wallet=w0, to_wallet=w1, amount=1_000_000, add_to_wallet=True) self.log.info("Don't broadcast...") res = self.test_send( from_wallet=w0, to_wallet=w1, amount=1_000_000, add_to_wallet=False) assert(res["hex"]) self.log.info("Return PSBT...") res = self.test_send( from_wallet=w0, to_wallet=w1, amount=1_000_000, psbt=True) assert(res["psbt"]) self.log.info( "Create transaction that spends to address, but don't broadcast...") self.test_send( from_wallet=w0, to_wallet=w1, amount=1_000_000, add_to_wallet=False) self.log.info("Create PSBT from watch-only wallet w3, sign with w2...") res = self.test_send(from_wallet=w3, to_wallet=w1, amount=1_000_000) res = w2.walletprocesspsbt(res["psbt"]) assert res["complete"] self.log.info( "Create PSBT from wallet w4 with watch-only keys, sign with w2...") self.test_send(from_wallet=w4, to_wallet=w1, amount=1_000_000, expect_error=(-4, "Insufficient funds")) res = self.test_send( from_wallet=w4, to_wallet=w1, amount=1_000_000, include_watching=True, add_to_wallet=False) res = w2.walletprocesspsbt(res["psbt"]) assert res["complete"] self.log.info("Create OP_RETURN...") self.test_send(from_wallet=w0, to_wallet=w1, amount=1_000_000) self.test_send(from_wallet=w0, data="Hello World", expect_error=(-8, "Data must be hexadecimal string (not 'Hello World')")) self.test_send(from_wallet=w0, data="23") res = self.test_send(from_wallet=w3, data="23") res = w2.walletprocesspsbt(res["psbt"]) assert res["complete"] self.log.info("Set fee rate...") res = self.test_send( from_wallet=w0, to_wallet=w1, amount=1_000_000, fee_rate=Decimal("20.00"), add_to_wallet=False) fee = self.nodes[1].decodepsbt(res["psbt"])["fee"] assert_fee_amount(fee, Decimal(len(res["hex"]) / 2), Decimal("20.00")) self.test_send(from_wallet=w0, to_wallet=w1, amount=1_000_000, fee_rate=-1, expect_error=(-3, "Amount out of range")) # Fee rate of 0.1 satoshi per byte should throw an error self.test_send(from_wallet=w0, to_wallet=w1, amount=1_000_000, fee_rate=Decimal("1.00"), expect_error=(-4, "Fee rate (1.00 XEC/kB) is lower than the minimum fee rate setting (10.00 XEC/kB)")) # TODO: Return hex if fee rate is below -maxmempool # res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1_000_000, # feeRate=Decimal("1.00"), add_to_wallet=False) # assert res["hex"] # hex = res["hex"] # res = self.nodes[0].testmempoolaccept([hex]) # assert not res[0]["allowed"] # assert_equal(res[0]["reject-reason"], "...") # low fee # assert_fee_amount(fee, Decimal(len(res["hex"]) / 2), Decimal("1.00")) self.log.info( "If inputs are specified, do not automatically add more...") res = self.test_send( from_wallet=w0, to_wallet=w1, amount=51_000_000, inputs=[], add_to_wallet=False) assert res["complete"] utxo1 = w0.listunspent()[0] assert_equal(utxo1["amount"], 50_000_000) self.test_send(from_wallet=w0, to_wallet=w1, amount=51_000_000, inputs=[utxo1], expect_error=(-4, "Insufficient funds")) self.test_send(from_wallet=w0, to_wallet=w1, amount=51_000_000, inputs=[utxo1], add_inputs=False, expect_error=(-4, "Insufficient funds")) res = self.test_send( from_wallet=w0, to_wallet=w1, amount=51_000_000, inputs=[utxo1], add_inputs=True, add_to_wallet=False) assert res["complete"] self.log.info("Manual change address and position...") self.test_send(from_wallet=w0, to_wallet=w1, amount=1_000_000, change_address="not an address", expect_error=(-5, "Change address must be a valid bitcoin address")) change_address = w0.getnewaddress() self.test_send( from_wallet=w0, to_wallet=w1, amount=1_000_000, add_to_wallet=False, change_address=change_address) assert res["complete"] res = self.test_send( from_wallet=w0, to_wallet=w1, amount=1_000_000, add_to_wallet=False, change_address=change_address, change_position=0) assert res["complete"] assert_equal( self.nodes[0].decodepsbt( res["psbt"])["tx"]["vout"][0]["scriptPubKey"]["addresses"], [change_address]) self.log.info("Set lock time...") height = self.nodes[0].getblockchaininfo()["blocks"] res = self.test_send( from_wallet=w0, to_wallet=w1, amount=1_000_000, locktime=height + 1) assert res["complete"] assert res["txid"] txid = res["txid"] # Although the wallet finishes the transaction, it can't be added to # the mempool yet: hex = self.nodes[0].gettransaction(res["txid"])["hex"] res = self.nodes[0].testmempoolaccept([hex]) assert not res[0]["allowed"] assert_equal(res[0]["reject-reason"], "bad-txns-nonfinal") # It shouldn't be confirmed in the next block self.nodes[0].generate(1) assert_equal(self.nodes[0].gettransaction(txid)["confirmations"], 0) # The mempool should allow it now: res = self.nodes[0].testmempoolaccept([hex]) assert res[0]["allowed"] # Don't wait for wallet to add it to the mempool: res = self.nodes[0].sendrawtransaction(hex) self.nodes[0].generate(1) assert_equal(self.nodes[0].gettransaction(txid)["confirmations"], 1) self.log.info("Lock unspents...") utxo1 = w0.listunspent()[0] assert_greater_than(utxo1["amount"], 1) res = self.test_send( from_wallet=w0, to_wallet=w1, amount=1_000_000, inputs=[utxo1], add_to_wallet=False, lock_unspents=True) assert res["complete"] locked_coins = w0.listlockunspent() assert_equal(len(locked_coins), 1) # Locked coins are automatically unlocked when manually selected res = self.test_send( from_wallet=w0, to_wallet=w1, amount=1_000_000, inputs=[utxo1], add_to_wallet=False) assert res["complete"] self.log.info("Subtract fee from output") self.test_send( from_wallet=w0, to_wallet=w1, amount=1_000_000, subtract_fee_from_outputs=[0]) if __name__ == '__main__': WalletSendTest().main()