diff options
Diffstat (limited to 'test')
31 files changed, 968 insertions, 99 deletions
diff --git a/test/functional/feature_block.py b/test/functional/feature_block.py index 462deeae32..850cb8334c 100755 --- a/test/functional/feature_block.py +++ b/test/functional/feature_block.py @@ -1297,7 +1297,7 @@ class FullBlockTest(BitcoinTestFramework): blocks2 = [] for i in range(89, LARGE_REORG_SIZE + 89): blocks2.append(self.next_block("alt" + str(i))) - self.send_blocks(blocks2, False, force_send=True) + self.send_blocks(blocks2, False, force_send=False) # extend alt chain to trigger re-org block = self.next_block("alt" + str(chain1_tip + 1)) diff --git a/test/functional/feature_config_args.py b/test/functional/feature_config_args.py index 6c51a5ac31..eb31bca29a 100755 --- a/test/functional/feature_config_args.py +++ b/test/functional/feature_config_args.py @@ -186,11 +186,12 @@ class ConfArgsTest(BitcoinTestFramework): with self.nodes[0].assert_debug_log(expected_msgs=[ "Loaded 0 addresses from peers.dat", "DNS seeding disabled", - "Adding fixed seeds as -dnsseed=0, -addnode is not provided and all -seednode(s) attempted\n", + "Adding fixed seeds as -dnsseed=0 (or IPv4/IPv6 connections are disabled via -onlynet), -addnode is not provided and all -seednode(s) attempted\n", ]): self.start_node(0, extra_args=['-dnsseed=0', '-fixedseeds=1']) assert time.time() - start < 60 self.stop_node(0) + self.nodes[0].assert_start_raises_init_error(['-dnsseed=1', '-onlynet=i2p', '-i2psam=127.0.0.1:7656'], "Error: Incompatible options: -dnsseed=1 was explicitly specified, but -onlynet forbids connections to IPv4/IPv6") # No peers.dat exists and dns seeds are disabled. # We expect the node will not add fixed seeds when explicitly disabled. diff --git a/test/functional/feature_proxy.py b/test/functional/feature_proxy.py index dd3cdc96ca..d02d56d068 100755 --- a/test/functional/feature_proxy.py +++ b/test/functional/feature_proxy.py @@ -332,20 +332,27 @@ class ProxyTest(BitcoinTestFramework): msg = "Error: Invalid -i2psam address or hostname: 'def:xyz'" self.nodes[1].assert_start_raises_init_error(expected_msg=msg) + self.log.info("Test passing -onlynet=onion with -onion=0/-noonion raises expected init error") msg = ( "Error: Outbound connections restricted to Tor (-onlynet=onion) but " - "the proxy for reaching the Tor network is not provided (no -proxy= " - "and no -onion= given) or it is explicitly forbidden (-onion=0)" + "the proxy for reaching the Tor network is explicitly forbidden: -onion=0" ) - self.log.info("Test passing -onlynet=onion without -proxy or -onion raises expected init error") - self.nodes[1].extra_args = ["-onlynet=onion"] - self.nodes[1].assert_start_raises_init_error(expected_msg=msg) - - self.log.info("Test passing -onlynet=onion with -onion=0/-noonion raises expected init error") for arg in ["-onion=0", "-noonion"]: self.nodes[1].extra_args = ["-onlynet=onion", arg] self.nodes[1].assert_start_raises_init_error(expected_msg=msg) + self.log.info("Test passing -onlynet=onion without -proxy, -onion or -listenonion raises expected init error") + self.nodes[1].extra_args = ["-onlynet=onion", "-listenonion=0"] + msg = ( + "Error: Outbound connections restricted to Tor (-onlynet=onion) but the proxy for " + "reaching the Tor network is not provided: none of -proxy, -onion or -listenonion is given" + ) + self.nodes[1].assert_start_raises_init_error(expected_msg=msg) + + self.log.info("Test passing -onlynet=onion without -proxy or -onion but with -listenonion=1 is ok") + self.start_node(1, extra_args=["-onlynet=onion", "-listenonion=1"]) + self.stop_node(1) + self.log.info("Test passing unknown network to -onlynet raises expected init error") self.nodes[1].extra_args = ["-onlynet=abc"] msg = "Error: Unknown network specified in -onlynet: 'abc'" diff --git a/test/functional/mempool_accept.py b/test/functional/mempool_accept.py index 65b37a4975..02ec18140c 100755 --- a/test/functional/mempool_accept.py +++ b/test/functional/mempool_accept.py @@ -65,7 +65,7 @@ class MempoolAcceptanceTest(BitcoinTestFramework): assert_equal(node.getmempoolinfo()['size'], self.mempool_size) self.log.info('Should not accept garbage to testmempoolaccept') - assert_raises_rpc_error(-3, 'Expected type array, got string', lambda: node.testmempoolaccept(rawtxs='ff00baar')) + assert_raises_rpc_error(-3, 'JSON value of type string is not of expected type array', lambda: node.testmempoolaccept(rawtxs='ff00baar')) assert_raises_rpc_error(-8, 'Array must contain between 1 and 25 transactions.', lambda: node.testmempoolaccept(rawtxs=['ff22']*26)) assert_raises_rpc_error(-8, 'Array must contain between 1 and 25 transactions.', lambda: node.testmempoolaccept(rawtxs=[])) assert_raises_rpc_error(-22, 'TX decode failed', lambda: node.testmempoolaccept(rawtxs=['ff00baar'])) diff --git a/test/functional/mempool_expiry.py b/test/functional/mempool_expiry.py index 47ae0c762b..21721177e6 100755 --- a/test/functional/mempool_expiry.py +++ b/test/functional/mempool_expiry.py @@ -13,6 +13,7 @@ definable expiry timeout via the '-mempoolexpiry=<n>' command line argument from datetime import timedelta from test_framework.blocktools import COINBASE_MATURITY +from test_framework.messages import DEFAULT_MEMPOOL_EXPIRY_HOURS from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, @@ -20,7 +21,6 @@ from test_framework.util import ( ) from test_framework.wallet import MiniWallet -DEFAULT_MEMPOOL_EXPIRY_HOURS = 336 # hours CUSTOM_MEMPOOL_EXPIRY = 10 # hours diff --git a/test/functional/p2p_compactblocks.py b/test/functional/p2p_compactblocks.py index 5e50e1ebce..3cbb948e3c 100755 --- a/test/functional/p2p_compactblocks.py +++ b/test/functional/p2p_compactblocks.py @@ -615,6 +615,27 @@ class CompactBlocksTest(BitcoinTestFramework): bad_peer.send_message(msg) bad_peer.wait_for_disconnect() + def test_low_work_compactblocks(self, test_node): + # A compactblock with insufficient work won't get its header included + node = self.nodes[0] + hashPrevBlock = int(node.getblockhash(node.getblockcount() - 150), 16) + block = self.build_block_on_tip(node) + block.hashPrevBlock = hashPrevBlock + block.solve() + + comp_block = HeaderAndShortIDs() + comp_block.initialize_from_block(block) + with self.nodes[0].assert_debug_log(['[net] Ignoring low-work compact block from peer 0']): + test_node.send_and_ping(msg_cmpctblock(comp_block.to_p2p())) + + tips = node.getchaintips() + found = False + for x in tips: + if x["hash"] == block.hash: + found = True + break + assert not found + def test_compactblocks_not_at_tip(self, test_node): node = self.nodes[0] # Test that requesting old compactblocks doesn't work. @@ -833,6 +854,9 @@ class CompactBlocksTest(BitcoinTestFramework): self.log.info("Testing compactblock requests/announcements not at chain tip...") self.test_compactblocks_not_at_tip(self.segwit_node) + self.log.info("Testing handling of low-work compact blocks...") + self.test_low_work_compactblocks(self.segwit_node) + self.log.info("Testing handling of incorrect blocktxn responses...") self.test_incorrect_blocktxn_response(self.segwit_node) diff --git a/test/functional/p2p_dos_header_tree.py b/test/functional/p2p_dos_header_tree.py index fde1e4bfa2..7e26994511 100755 --- a/test/functional/p2p_dos_header_tree.py +++ b/test/functional/p2p_dos_header_tree.py @@ -22,6 +22,7 @@ class RejectLowDifficultyHeadersTest(BitcoinTestFramework): self.setup_clean_chain = True self.chain = 'testnet3' # Use testnet chain because it has an early checkpoint self.num_nodes = 2 + self.extra_args = [["-minimumchainwork=0x0"], ["-minimumchainwork=0x0"]] def add_options(self, parser): parser.add_argument( @@ -62,7 +63,7 @@ class RejectLowDifficultyHeadersTest(BitcoinTestFramework): self.log.info("Feed all fork headers (succeeds without checkpoint)") # On node 0 it succeeds because checkpoints are disabled - self.restart_node(0, extra_args=['-nocheckpoints']) + self.restart_node(0, extra_args=['-nocheckpoints', "-minimumchainwork=0x0"]) peer_no_checkpoint = self.nodes[0].add_p2p_connection(P2PInterface()) peer_no_checkpoint.send_and_ping(msg_headers(self.headers_fork)) assert { diff --git a/test/functional/p2p_headers_sync_with_minchainwork.py b/test/functional/p2p_headers_sync_with_minchainwork.py new file mode 100755 index 0000000000..991e3348ed --- /dev/null +++ b/test/functional/p2p_headers_sync_with_minchainwork.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019-2021 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 that we reject low difficulty headers to prevent our block tree from filling up with useless bloat""" + +from test_framework.test_framework import BitcoinTestFramework + +from test_framework.p2p import ( + P2PInterface, +) + +from test_framework.messages import ( + msg_headers, +) + +from test_framework.blocktools import ( + NORMAL_GBT_REQUEST_PARAMS, + create_block, +) + +from test_framework.util import assert_equal + +NODE1_BLOCKS_REQUIRED = 15 +NODE2_BLOCKS_REQUIRED = 2047 + + +class RejectLowDifficultyHeadersTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 4 + # Node0 has no required chainwork; node1 requires 15 blocks on top of the genesis block; node2 requires 2047 + self.extra_args = [["-minimumchainwork=0x0", "-checkblockindex=0"], ["-minimumchainwork=0x1f", "-checkblockindex=0"], ["-minimumchainwork=0x1000", "-checkblockindex=0"], ["-minimumchainwork=0x1000", "-checkblockindex=0", "-whitelist=noban@127.0.0.1"]] + + def setup_network(self): + self.setup_nodes() + self.reconnect_all() + self.sync_all() + + def disconnect_all(self): + self.disconnect_nodes(0, 1) + self.disconnect_nodes(0, 2) + self.disconnect_nodes(0, 3) + + def reconnect_all(self): + self.connect_nodes(0, 1) + self.connect_nodes(0, 2) + self.connect_nodes(0, 3) + + def test_chains_sync_when_long_enough(self): + self.log.info("Generate blocks on the node with no required chainwork, and verify nodes 1 and 2 have no new headers in their headers tree") + with self.nodes[1].assert_debug_log(expected_msgs=["[net] Ignoring low-work chain (height=14)"]), self.nodes[2].assert_debug_log(expected_msgs=["[net] Ignoring low-work chain (height=14)"]), self.nodes[3].assert_debug_log(expected_msgs=["Synchronizing blockheaders, height: 14"]): + self.generate(self.nodes[0], NODE1_BLOCKS_REQUIRED-1, sync_fun=self.no_op) + + # Node3 should always allow headers due to noban permissions + self.log.info("Check that node3 will sync headers (due to noban permissions)") + + def check_node3_chaintips(num_tips, tip_hash, height): + node3_chaintips = self.nodes[3].getchaintips() + assert(len(node3_chaintips) == num_tips) + assert { + 'height': height, + 'hash': tip_hash, + 'branchlen': height, + 'status': 'headers-only', + } in node3_chaintips + + check_node3_chaintips(2, self.nodes[0].getbestblockhash(), NODE1_BLOCKS_REQUIRED-1) + + for node in self.nodes[1:3]: + chaintips = node.getchaintips() + assert(len(chaintips) == 1) + assert { + 'height': 0, + 'hash': '0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206', + 'branchlen': 0, + 'status': 'active', + } in chaintips + + self.log.info("Generate more blocks to satisfy node1's minchainwork requirement, and verify node2 still has no new headers in headers tree") + with self.nodes[2].assert_debug_log(expected_msgs=["[net] Ignoring low-work chain (height=15)"]), self.nodes[3].assert_debug_log(expected_msgs=["Synchronizing blockheaders, height: 15"]): + self.generate(self.nodes[0], NODE1_BLOCKS_REQUIRED - self.nodes[0].getblockcount(), sync_fun=self.no_op) + self.sync_blocks(self.nodes[0:2]) # node3 will sync headers (noban permissions) but not blocks (due to minchainwork) + + assert { + 'height': 0, + 'hash': '0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206', + 'branchlen': 0, + 'status': 'active', + } in self.nodes[2].getchaintips() + + assert(len(self.nodes[2].getchaintips()) == 1) + + self.log.info("Check that node3 accepted these headers as well") + check_node3_chaintips(2, self.nodes[0].getbestblockhash(), NODE1_BLOCKS_REQUIRED) + + self.log.info("Generate long chain for node0/node1/node3") + self.generate(self.nodes[0], NODE2_BLOCKS_REQUIRED-self.nodes[0].getblockcount(), sync_fun=self.no_op) + + self.log.info("Verify that node2 and node3 will sync the chain when it gets long enough") + self.sync_blocks() + + def test_peerinfo_includes_headers_presync_height(self): + self.log.info("Test that getpeerinfo() includes headers presync height") + + # Disconnect network, so that we can find our own peer connection more + # easily + self.disconnect_all() + + p2p = self.nodes[0].add_p2p_connection(P2PInterface()) + node = self.nodes[0] + + # Ensure we have a long chain already + current_height = self.nodes[0].getblockcount() + if (current_height < 3000): + self.generate(node, 3000-current_height, sync_fun=self.no_op) + + # Send a group of 2000 headers, forking from genesis. + new_blocks = [] + hashPrevBlock = int(node.getblockhash(0), 16) + for i in range(2000): + block = create_block(hashprev = hashPrevBlock, tmpl=node.getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)) + block.solve() + new_blocks.append(block) + hashPrevBlock = block.sha256 + + headers_message = msg_headers(headers=new_blocks) + p2p.send_and_ping(headers_message) + + # getpeerinfo should show a sync in progress + assert_equal(node.getpeerinfo()[0]['presynced_headers'], 2000) + + def test_large_reorgs_can_succeed(self): + self.log.info("Test that a 2000+ block reorg, starting from a point that is more than 2000 blocks before a locator entry, can succeed") + + self.sync_all() # Ensure all nodes are synced. + self.disconnect_all() + + # locator(block at height T) will have heights: + # [T, T-1, ..., T-10, T-12, T-16, T-24, T-40, T-72, T-136, T-264, + # T-520, T-1032, T-2056, T-4104, ...] + # So mine a number of blocks > 4104 to ensure that the first window of + # received headers during a sync are fully between locator entries. + BLOCKS_TO_MINE = 4110 + + self.generate(self.nodes[0], BLOCKS_TO_MINE, sync_fun=self.no_op) + self.generate(self.nodes[1], BLOCKS_TO_MINE+2, sync_fun=self.no_op) + + self.reconnect_all() + + self.sync_blocks(timeout=300) # Ensure tips eventually agree + + + def run_test(self): + self.test_chains_sync_when_long_enough() + + self.test_large_reorgs_can_succeed() + + self.test_peerinfo_includes_headers_presync_height() + + + +if __name__ == '__main__': + RejectLowDifficultyHeadersTest().main() diff --git a/test/functional/p2p_i2p_sessions.py b/test/functional/p2p_i2p_sessions.py new file mode 100755 index 0000000000..4e52522b81 --- /dev/null +++ b/test/functional/p2p_i2p_sessions.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# Copyright (c) 2022-2022 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 whether persistent or transient I2P sessions are being used, based on `-i2pacceptincoming`. +""" + +from test_framework.test_framework import BitcoinTestFramework + + +class I2PSessions(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 2 + # The test assumes that an I2P SAM proxy is not listening here. + self.extra_args = [ + ["-i2psam=127.0.0.1:60000", "-i2pacceptincoming=1"], + ["-i2psam=127.0.0.1:60000", "-i2pacceptincoming=0"], + ] + + def run_test(self): + addr = "zsxwyo6qcn3chqzwxnseusqgsnuw3maqnztkiypyfxtya4snkoka.b32.i2p" + + self.log.info("Ensure we create a persistent session when -i2pacceptincoming=1") + node0 = self.nodes[0] + with node0.assert_debug_log(expected_msgs=[f"Creating persistent SAM session"]): + node0.addnode(node=addr, command="onetry") + + self.log.info("Ensure we create a transient session when -i2pacceptincoming=0") + node1 = self.nodes[1] + with node1.assert_debug_log(expected_msgs=[f"Creating transient SAM session"]): + node1.addnode(node=addr, command="onetry") + + +if __name__ == '__main__': + I2PSessions().main() diff --git a/test/functional/p2p_leak.py b/test/functional/p2p_leak.py index af8e45d578..936c22197c 100755 --- a/test/functional/p2p_leak.py +++ b/test/functional/p2p_leak.py @@ -138,6 +138,9 @@ class P2PLeakTest(BitcoinTestFramework): # Give the node enough time to possibly leak out a message time.sleep(PEER_TIMEOUT + 2) + self.log.info("Connect peer to ensure the net thread runs the disconnect logic at least once") + self.nodes[0].add_p2p_connection(P2PInterface()) + # Make sure only expected messages came in assert not no_version_idle_peer.unexpected_msg assert not no_version_idle_peer.got_wtxidrelay @@ -169,7 +172,7 @@ class P2PLeakTest(BitcoinTestFramework): self.log.info('Check that old peers are disconnected') p2p_old_peer = self.nodes[0].add_p2p_connection(P2PInterface(), send_version=False, wait_for_verack=False) - with self.nodes[0].assert_debug_log(['peer=4 using obsolete version 31799; disconnecting']): + with self.nodes[0].assert_debug_log(["using obsolete version 31799; disconnecting"]): p2p_old_peer.send_message(self.create_old_version(31799)) p2p_old_peer.wait_for_disconnect() diff --git a/test/functional/p2p_unrequested_blocks.py b/test/functional/p2p_unrequested_blocks.py index 76d9b045ce..5030e7af26 100755 --- a/test/functional/p2p_unrequested_blocks.py +++ b/test/functional/p2p_unrequested_blocks.py @@ -72,6 +72,13 @@ class AcceptBlockTest(BitcoinTestFramework): def setup_network(self): self.setup_nodes() + def check_hash_in_chaintips(self, node, blockhash): + tips = node.getchaintips() + for x in tips: + if x["hash"] == blockhash: + return True + return False + def run_test(self): test_node = self.nodes[0].add_p2p_connection(P2PInterface()) min_work_node = self.nodes[1].add_p2p_connection(P2PInterface()) @@ -89,10 +96,15 @@ class AcceptBlockTest(BitcoinTestFramework): blocks_h2[i].solve() block_time += 1 test_node.send_and_ping(msg_block(blocks_h2[0])) - min_work_node.send_and_ping(msg_block(blocks_h2[1])) + + with self.nodes[1].assert_debug_log(expected_msgs=[f"AcceptBlockHeader: not adding new block header {blocks_h2[1].hash}, missing anti-dos proof-of-work validation"]): + min_work_node.send_and_ping(msg_block(blocks_h2[1])) assert_equal(self.nodes[0].getblockcount(), 2) assert_equal(self.nodes[1].getblockcount(), 1) + + # Ensure that the header of the second block was also not accepted by node1 + assert_equal(self.check_hash_in_chaintips(self.nodes[1], blocks_h2[1].hash), False) self.log.info("First height 2 block accepted by node0; correctly rejected by node1") # 3. Send another block that builds on genesis. diff --git a/test/functional/rpc_blockchain.py b/test/functional/rpc_blockchain.py index d07b144905..d8147b3355 100755 --- a/test/functional/rpc_blockchain.py +++ b/test/functional/rpc_blockchain.py @@ -38,6 +38,7 @@ from test_framework.messages import ( msg_block, ) from test_framework.p2p import P2PInterface +from test_framework.script import hash256 from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, @@ -452,8 +453,9 @@ class BlockchainTest(BitcoinTestFramework): # (Previously this was broken based on setting # `rpc/blockchain.cpp:latestblock` incorrectly.) # - b20hash = node.getblockhash(20) - b20 = node.getblock(b20hash) + fork_height = current_height - 100 # choose something vaguely near our tip + fork_hash = node.getblockhash(fork_height) + fork_block = node.getblock(fork_hash) def solve_and_send_block(prevhash, height, time): b = create_block(prevhash, create_coinbase(height), time) @@ -461,10 +463,10 @@ class BlockchainTest(BitcoinTestFramework): peer.send_and_ping(msg_block(b)) return b - b21f = solve_and_send_block(int(b20hash, 16), 21, b20['time'] + 1) - b22f = solve_and_send_block(b21f.sha256, 22, b21f.nTime + 1) + b1 = solve_and_send_block(int(fork_hash, 16), fork_height+1, fork_block['time'] + 1) + b2 = solve_and_send_block(b1.sha256, fork_height+2, b1.nTime + 1) - node.invalidateblock(b22f.hash) + node.invalidateblock(b2.hash) def assert_waitforheight(height, timeout=2): assert_equal( @@ -484,6 +486,10 @@ class BlockchainTest(BitcoinTestFramework): self.wallet.send_self_transfer(fee_rate=fee_per_kb, from_node=node) blockhash = self.generate(node, 1)[0] + def assert_hexblock_hashes(verbosity): + block = node.getblock(blockhash, verbosity) + assert_equal(blockhash, hash256(bytes.fromhex(block[:160]))[::-1].hex()) + def assert_fee_not_in_block(verbosity): block = node.getblock(blockhash, verbosity) assert 'fee' not in block['tx'][1] @@ -518,8 +524,13 @@ class BlockchainTest(BitcoinTestFramework): for vin in tx["vin"]: assert "prevout" not in vin + self.log.info("Test that getblock with verbosity 0 hashes to expected value") + assert_hexblock_hashes(0) + assert_hexblock_hashes(False) + self.log.info("Test that getblock with verbosity 1 doesn't include fee") assert_fee_not_in_block(1) + assert_fee_not_in_block(True) self.log.info('Test that getblock with verbosity 2 and 3 includes expected fee') assert_fee_in_block(2) diff --git a/test/functional/rpc_estimatefee.py b/test/functional/rpc_estimatefee.py index 51b7efb4c3..b057400887 100755 --- a/test/functional/rpc_estimatefee.py +++ b/test/functional/rpc_estimatefee.py @@ -22,15 +22,15 @@ class EstimateFeeTest(BitcoinTestFramework): assert_raises_rpc_error(-1, "estimaterawfee", self.nodes[0].estimaterawfee) # wrong type for conf_target - assert_raises_rpc_error(-3, "Expected type number, got string", self.nodes[0].estimatesmartfee, 'foo') - assert_raises_rpc_error(-3, "Expected type number, got string", self.nodes[0].estimaterawfee, 'foo') + assert_raises_rpc_error(-3, "JSON value of type string is not of expected type number", self.nodes[0].estimatesmartfee, 'foo') + assert_raises_rpc_error(-3, "JSON value of type string is not of expected type number", self.nodes[0].estimaterawfee, 'foo') # wrong type for estimatesmartfee(estimate_mode) - assert_raises_rpc_error(-3, "Expected type string, got number", self.nodes[0].estimatesmartfee, 1, 1) + assert_raises_rpc_error(-3, "JSON value of type number is not of expected type string", self.nodes[0].estimatesmartfee, 1, 1) assert_raises_rpc_error(-8, 'Invalid estimate_mode parameter, must be one of: "unset", "economical", "conservative"', self.nodes[0].estimatesmartfee, 1, 'foo') # wrong type for estimaterawfee(threshold) - assert_raises_rpc_error(-3, "Expected type number, got string", self.nodes[0].estimaterawfee, 1, 'foo') + assert_raises_rpc_error(-3, "JSON value of type string is not of expected type number", self.nodes[0].estimaterawfee, 1, 'foo') # extra params assert_raises_rpc_error(-1, "estimatesmartfee", self.nodes[0].estimatesmartfee, 1, 'ECONOMICAL', 1) diff --git a/test/functional/rpc_fundrawtransaction.py b/test/functional/rpc_fundrawtransaction.py index 5a870de6c7..a963bb5e2d 100755 --- a/test/functional/rpc_fundrawtransaction.py +++ b/test/functional/rpc_fundrawtransaction.py @@ -106,6 +106,7 @@ class RawTransactionsTest(BitcoinTestFramework): self.generate(self.nodes[2], 1) self.generate(self.nodes[0], 121) + self.test_add_inputs_default_value() self.test_weight_calculation() self.test_change_position() self.test_simple() @@ -635,7 +636,7 @@ class RawTransactionsTest(BitcoinTestFramework): self.log.info("Test fundrawtxn fee with many inputs") # Empty node1, send some small coins from node0 to node1. - self.nodes[1].sendtoaddress(self.nodes[0].getnewaddress(), self.nodes[1].getbalance(), "", "", True) + self.nodes[1].sendall(recipients=[self.nodes[0].getnewaddress()]) self.generate(self.nodes[1], 1) for _ in range(20): @@ -661,7 +662,7 @@ class RawTransactionsTest(BitcoinTestFramework): self.log.info("Test fundrawtxn sign+send with many inputs") # Again, empty node1, send some small coins from node0 to node1. - self.nodes[1].sendtoaddress(self.nodes[0].getnewaddress(), self.nodes[1].getbalance(), "", "", True) + self.nodes[1].sendall(recipients=[self.nodes[0].getnewaddress()]) self.generate(self.nodes[1], 1) for _ in range(20): @@ -1073,6 +1074,121 @@ class RawTransactionsTest(BitcoinTestFramework): self.nodes[2].unloadwallet("extfund") + def test_add_inputs_default_value(self): + self.log.info("Test 'add_inputs' default value") + + # Create and fund the wallet with 5 BTC + self.nodes[2].createwallet("test_preset_inputs") + wallet = self.nodes[2].get_wallet_rpc("test_preset_inputs") + addr1 = wallet.getnewaddress(address_type="bech32") + self.nodes[0].sendtoaddress(addr1, 5) + self.generate(self.nodes[0], 1) + + # Covered cases: + # 1. Default add_inputs value with no preset inputs (add_inputs=true): + # Expect: automatically add coins from the wallet to the tx. + # 2. Default add_inputs value with preset inputs (add_inputs=false): + # Expect: disallow automatic coin selection. + # 3. Explicit add_inputs=true and preset inputs (with preset inputs not-covering the target amount). + # Expect: include inputs from the wallet. + # 4. Explicit add_inputs=true and preset inputs (with preset inputs covering the target amount). + # Expect: only preset inputs are used. + # 5. Explicit add_inputs=true, no preset inputs (same as (1) but with an explicit set): + # Expect: include inputs from the wallet. + + # Case (1), 'send' command + # 'add_inputs' value is true unless "inputs" are specified, in such case, add_inputs=false. + # So, the wallet will automatically select coins and create the transaction if only the outputs are provided. + tx = wallet.send(outputs=[{addr1: 3}]) + assert tx["complete"] + + # Case (2), 'send' command + # Select an input manually, which doesn't cover the entire output amount and + # verify that the dynamically set 'add_inputs=false' value works. + + # Fund wallet with 2 outputs, 5 BTC each. + addr2 = wallet.getnewaddress(address_type="bech32") + source_tx = self.nodes[0].send(outputs=[{addr1: 5}, {addr2: 5}], options={"change_position": 0}) + self.generate(self.nodes[0], 1) + + # Select only one input. + options = { + "inputs": [ + { + "txid": source_tx["txid"], + "vout": 1 # change position was hardcoded to index 0 + } + ] + } + assert_raises_rpc_error(-4, "Insufficient funds", wallet.send, outputs=[{addr1: 8}], options=options) + + # Case (3), Explicit add_inputs=true and preset inputs (with preset inputs not-covering the target amount) + options["add_inputs"] = True + options["add_to_wallet"] = False + tx = wallet.send(outputs=[{addr1: 8}], options=options) + assert tx["complete"] + + # Case (4), Explicit add_inputs=true and preset inputs (with preset inputs covering the target amount) + options["inputs"].append({ + "txid": source_tx["txid"], + "vout": 2 # change position was hardcoded to index 0 + }) + tx = wallet.send(outputs=[{addr1: 8}], options=options) + assert tx["complete"] + # Check that only the preset inputs were added to the tx + decoded_psbt_inputs = self.nodes[0].decodepsbt(tx["psbt"])['tx']['vin'] + assert_equal(len(decoded_psbt_inputs), 2) + for input in decoded_psbt_inputs: + assert_equal(input["txid"], source_tx["txid"]) + + # Case (5), assert that inputs are added to the tx by explicitly setting add_inputs=true + options = {"add_inputs": True, "add_to_wallet": True} + tx = wallet.send(outputs=[{addr1: 8}], options=options) + assert tx["complete"] + + ################################################ + + # Case (1), 'walletcreatefundedpsbt' command + # Default add_inputs value with no preset inputs (add_inputs=true) + inputs = [] + outputs = {self.nodes[1].getnewaddress(): 8} + assert "psbt" in wallet.walletcreatefundedpsbt(inputs=inputs, outputs=outputs) + + # Case (2), 'walletcreatefundedpsbt' command + # Default add_inputs value with preset inputs (add_inputs=false). + inputs = [{ + "txid": source_tx["txid"], + "vout": 1 # change position was hardcoded to index 0 + }] + outputs = {self.nodes[1].getnewaddress(): 8} + assert_raises_rpc_error(-4, "Insufficient funds", wallet.walletcreatefundedpsbt, inputs=inputs, outputs=outputs) + + # Case (3), Explicit add_inputs=true and preset inputs (with preset inputs not-covering the target amount) + options["add_inputs"] = True + options["add_to_wallet"] = False + assert "psbt" in wallet.walletcreatefundedpsbt(outputs=[{addr1: 8}], inputs=inputs, options=options) + + # Case (4), Explicit add_inputs=true and preset inputs (with preset inputs covering the target amount) + inputs.append({ + "txid": source_tx["txid"], + "vout": 2 # change position was hardcoded to index 0 + }) + psbt_tx = wallet.walletcreatefundedpsbt(outputs=[{addr1: 8}], inputs=inputs, options=options) + # Check that only the preset inputs were added to the tx + decoded_psbt_inputs = self.nodes[0].decodepsbt(psbt_tx["psbt"])['tx']['vin'] + assert_equal(len(decoded_psbt_inputs), 2) + for input in decoded_psbt_inputs: + assert_equal(input["txid"], source_tx["txid"]) + + # Case (5), 'walletcreatefundedpsbt' command + # Explicit add_inputs=true, no preset inputs + options = { + "add_inputs": True + } + assert "psbt" in wallet.walletcreatefundedpsbt(inputs=[], outputs=outputs, options=options) + + self.nodes[2].unloadwallet("test_preset_inputs") + def test_weight_calculation(self): self.log.info("Test weight calculation with external inputs") diff --git a/test/functional/rpc_getblockfrompeer.py b/test/functional/rpc_getblockfrompeer.py index 41e430d87e..278a343b2b 100755 --- a/test/functional/rpc_getblockfrompeer.py +++ b/test/functional/rpc_getblockfrompeer.py @@ -56,8 +56,8 @@ class GetBlockFromPeerTest(BitcoinTestFramework): self.log.info("Arguments must be valid") assert_raises_rpc_error(-8, "hash must be of length 64 (not 4, for '1234')", self.nodes[0].getblockfrompeer, "1234", peer_0_peer_1_id) - assert_raises_rpc_error(-3, "Expected type string, got number", self.nodes[0].getblockfrompeer, 1234, peer_0_peer_1_id) - assert_raises_rpc_error(-3, "Expected type number, got string", self.nodes[0].getblockfrompeer, short_tip, "0") + assert_raises_rpc_error(-3, "JSON value of type number is not of expected type string", self.nodes[0].getblockfrompeer, 1234, peer_0_peer_1_id) + assert_raises_rpc_error(-3, "JSON value of type string is not of expected type number", self.nodes[0].getblockfrompeer, short_tip, "0") self.log.info("We must already have the header") assert_raises_rpc_error(-1, "Block header missing", self.nodes[0].getblockfrompeer, "00" * 32, 0) diff --git a/test/functional/rpc_getdescriptorinfo.py b/test/functional/rpc_getdescriptorinfo.py index 5e6fd66aab..1b0f411e52 100755 --- a/test/functional/rpc_getdescriptorinfo.py +++ b/test/functional/rpc_getdescriptorinfo.py @@ -29,7 +29,7 @@ class DescriptorTest(BitcoinTestFramework): def run_test(self): assert_raises_rpc_error(-1, 'getdescriptorinfo', self.nodes[0].getdescriptorinfo) - assert_raises_rpc_error(-3, 'Expected type string', self.nodes[0].getdescriptorinfo, 1) + assert_raises_rpc_error(-3, 'JSON value of type number is not of expected type string', self.nodes[0].getdescriptorinfo, 1) assert_raises_rpc_error(-5, "'' is not a valid descriptor function", self.nodes[0].getdescriptorinfo, "") # P2PK output with the specified public key. diff --git a/test/functional/rpc_invalidateblock.py b/test/functional/rpc_invalidateblock.py index f1c2537ef9..1e33e7ca9c 100755 --- a/test/functional/rpc_invalidateblock.py +++ b/test/functional/rpc_invalidateblock.py @@ -8,6 +8,7 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.address import ADDRESS_BCRT1_UNSPENDABLE_DESCRIPTOR from test_framework.util import ( assert_equal, + assert_raises_rpc_error, ) @@ -83,6 +84,10 @@ class InvalidateTest(BitcoinTestFramework): # Should be back at the tip by now assert_equal(self.nodes[1].getbestblockhash(), blocks[-1]) + self.log.info("Verify that invalidating an unknown block throws an error") + assert_raises_rpc_error(-5, "Block not found", self.nodes[1].invalidateblock, "00" * 32) + assert_equal(self.nodes[1].getbestblockhash(), blocks[-1]) + if __name__ == '__main__': InvalidateTest().main() diff --git a/test/functional/rpc_rawtransaction.py b/test/functional/rpc_rawtransaction.py index a858292dd4..f1fae13b4a 100755 --- a/test/functional/rpc_rawtransaction.py +++ b/test/functional/rpc_rawtransaction.py @@ -180,7 +180,7 @@ class RawTransactionsTest(BitcoinTestFramework): assert_raises_rpc_error(-1, "createrawtransaction", self.nodes[0].createrawtransaction, [], {}, 0, False, 'foo') # Test `createrawtransaction` invalid `inputs` - assert_raises_rpc_error(-3, "Expected type array", self.nodes[0].createrawtransaction, 'foo', {}) + assert_raises_rpc_error(-3, "JSON value of type string is not of expected type array", self.nodes[0].createrawtransaction, 'foo', {}) assert_raises_rpc_error(-1, "JSON value of type string is not of expected type object", self.nodes[0].createrawtransaction, ['foo'], {}) assert_raises_rpc_error(-1, "JSON value of type null is not of expected type string", self.nodes[0].createrawtransaction, [{}], {}) assert_raises_rpc_error(-8, "txid must be of length 64 (not 3, for 'foo')", self.nodes[0].createrawtransaction, [{'txid': 'foo'}], {}) @@ -226,12 +226,12 @@ class RawTransactionsTest(BitcoinTestFramework): self.nodes[0].createrawtransaction, [{'txid': TXID, 'vout': 0, 'sequence': MAX_BIP125_RBF_SEQUENCE+1}], {}, 0, True) # Test `createrawtransaction` invalid `locktime` - assert_raises_rpc_error(-3, "Expected type number", self.nodes[0].createrawtransaction, [], {}, 'foo') + assert_raises_rpc_error(-3, "JSON value of type string is not of expected type number", self.nodes[0].createrawtransaction, [], {}, 'foo') assert_raises_rpc_error(-8, "Invalid parameter, locktime out of range", self.nodes[0].createrawtransaction, [], {}, -1) assert_raises_rpc_error(-8, "Invalid parameter, locktime out of range", self.nodes[0].createrawtransaction, [], {}, 4294967296) # Test `createrawtransaction` invalid `replaceable` - assert_raises_rpc_error(-3, "Expected type bool", self.nodes[0].createrawtransaction, [], {}, 0, 'foo') + assert_raises_rpc_error(-3, "JSON value of type string is not of expected type bool", self.nodes[0].createrawtransaction, [], {}, 0, 'foo') # Test that createrawtransaction accepts an array and object as outputs # One output diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py index 4e757b64ca..8a928a1e50 100755 --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -71,6 +71,7 @@ DEFAULT_DESCENDANT_LIMIT = 25 # default max number of in-mempool descendants # Default setting for -datacarriersize. 80 bytes of data, +1 for OP_RETURN, +2 for the pushdata opcodes. MAX_OP_RETURN_RELAY = 83 +DEFAULT_MEMPOOL_EXPIRY_HOURS = 336 # hours def sha256(s): return hashlib.sha256(s).digest() diff --git a/test/functional/test_framework/test_node.py b/test/functional/test_framework/test_node.py index 03f6c8adea..e35cae006f 100755 --- a/test/functional/test_framework/test_node.py +++ b/test/functional/test_framework/test_node.py @@ -118,6 +118,8 @@ class TestNode(): self.args.append("-logthreadnames") if self.version_is_at_least(219900): self.args.append("-logsourcelocations") + if self.version_is_at_least(239000): + self.args.append("-loglevel=trace") self.cli = TestNodeCLI(bitcoin_cli, self.datadir) self.use_cli = use_cli diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 37d0c4f87e..c47e4d9b84 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -186,6 +186,7 @@ BASE_SCRIPTS = [ 'wallet_signrawtransactionwithwallet.py --legacy-wallet', 'wallet_signrawtransactionwithwallet.py --descriptors', 'rpc_signrawtransactionwithkey.py', + 'p2p_headers_sync_with_minchainwork.py', 'rpc_rawtransaction.py --legacy-wallet', 'wallet_groups.py --legacy-wallet', 'wallet_transactiontime_rescan.py --descriptors', @@ -330,6 +331,7 @@ BASE_SCRIPTS = [ 'feature_blocksdir.py', 'wallet_startup.py', 'p2p_i2p_ports.py', + 'p2p_i2p_sessions.py', 'feature_config_args.py', 'feature_presegwit_node_upgrade.py', 'feature_settings.py', @@ -339,6 +341,7 @@ BASE_SCRIPTS = [ 'feature_dirsymlinks.py', 'feature_help.py', 'feature_shutdown.py', + 'wallet_migration.py', 'p2p_ibd_txrelay.py', # Don't append tests at the end to avoid merge conflicts # Put them in a random line within the section that fits their approximate run-time @@ -552,14 +555,14 @@ def run_tests(*, test_list, src_dir, build_dir, tmpdir, jobs=1, enable_coverage= while i < test_count: if failfast and not all_passed: break - for test_result, testdir, stdout, stderr in job_queue.get_next(): + for test_result, testdir, stdout, stderr, skip_reason in job_queue.get_next(): test_results.append(test_result) i += 1 done_str = "{}/{} - {}{}{}".format(i, test_count, BOLD[1], test_result.name, BOLD[0]) if test_result.status == "Passed": logging.debug("%s passed, Duration: %s s" % (done_str, test_result.time)) elif test_result.status == "Skipped": - logging.debug("%s skipped" % (done_str)) + logging.debug(f"{done_str} skipped ({skip_reason})") else: all_passed = False print("%s failed, Duration: %s s\n" % (done_str, test_result.time)) @@ -683,10 +686,12 @@ class TestHandler: log_out.seek(0), log_err.seek(0) [stdout, stderr] = [log_file.read().decode('utf-8') for log_file in (log_out, log_err)] log_out.close(), log_err.close() + skip_reason = None if proc.returncode == TEST_EXIT_PASSED and stderr == "": status = "Passed" elif proc.returncode == TEST_EXIT_SKIPPED: status = "Skipped" + skip_reason = re.search(r"Test Skipped: (.*)", stdout).group(1) else: status = "Failed" self.num_running -= 1 @@ -695,7 +700,7 @@ class TestHandler: clearline = '\r' + (' ' * dot_count) + '\r' print(clearline, end='', flush=True) dot_count = 0 - ret.append((TestResult(name, status, int(time.time() - start_time)), testdir, stdout, stderr)) + ret.append((TestResult(name, status, int(time.time() - start_time)), testdir, stdout, stderr, skip_reason)) if ret: return ret if self.use_term_control: diff --git a/test/functional/tool_wallet.py b/test/functional/tool_wallet.py index 2cb9dc4523..1e5ce513cb 100755 --- a/test/functional/tool_wallet.py +++ b/test/functional/tool_wallet.py @@ -68,7 +68,7 @@ class ToolWalletTest(BitcoinTestFramework): result = 'unchanged' if new == old else 'increased!' self.log.debug('Wallet file timestamp {}'.format(result)) - def get_expected_info_output(self, name="", transactions=0, keypool=2, address=0): + def get_expected_info_output(self, name="", transactions=0, keypool=2, address=0, imported_privs=0): wallet_name = self.default_wallet_name if name == "" else name if self.options.descriptors: output_types = 4 # p2pkh, p2sh, segwit, bech32m @@ -83,7 +83,7 @@ class ToolWalletTest(BitcoinTestFramework): Keypool Size: %d Transactions: %d Address Book: %d - ''' % (wallet_name, keypool * output_types, transactions, address)) + ''' % (wallet_name, keypool * output_types, transactions, imported_privs * 3 + address)) else: output_types = 3 # p2pkh, p2sh, segwit. Legacy wallets do not support bech32m. return textwrap.dedent('''\ @@ -97,7 +97,7 @@ class ToolWalletTest(BitcoinTestFramework): Keypool Size: %d Transactions: %d Address Book: %d - ''' % (wallet_name, keypool, transactions, address * output_types)) + ''' % (wallet_name, keypool, transactions, (address + imported_privs) * output_types)) def read_dump(self, filename): dump = OrderedDict() @@ -219,7 +219,7 @@ class ToolWalletTest(BitcoinTestFramework): # shasum_before = self.wallet_shasum() timestamp_before = self.wallet_timestamp() self.log.debug('Wallet file timestamp before calling info: {}'.format(timestamp_before)) - out = self.get_expected_info_output(address=1) + out = self.get_expected_info_output(imported_privs=1) self.assert_tool_output(out, '-wallet=' + self.default_wallet_name, 'info') timestamp_after = self.wallet_timestamp() self.log.debug('Wallet file timestamp after calling info: {}'.format(timestamp_after)) @@ -250,7 +250,7 @@ class ToolWalletTest(BitcoinTestFramework): shasum_before = self.wallet_shasum() timestamp_before = self.wallet_timestamp() self.log.debug('Wallet file timestamp before calling info: {}'.format(timestamp_before)) - out = self.get_expected_info_output(transactions=1, address=1) + out = self.get_expected_info_output(transactions=1, imported_privs=1) self.assert_tool_output(out, '-wallet=' + self.default_wallet_name, 'info') shasum_after = self.wallet_shasum() timestamp_after = self.wallet_timestamp() diff --git a/test/functional/wallet_balance.py b/test/functional/wallet_balance.py index d2ed97ca76..ec58ace4a2 100755 --- a/test/functional/wallet_balance.py +++ b/test/functional/wallet_balance.py @@ -266,7 +266,6 @@ class WalletTest(BitcoinTestFramework): self.nodes[1].invalidateblock(block_reorg) assert_equal(self.nodes[0].getbalance(minconf=0), 0) # wallet txs not in the mempool are untrusted self.generatetoaddress(self.nodes[0], 1, ADDRESS_WATCHONLY, sync_fun=self.no_op) - assert_equal(self.nodes[0].getbalance(minconf=0), 0) # wallet txs not in the mempool are untrusted # Now confirm tx_orig self.restart_node(1, ['-persistmempool=0']) diff --git a/test/functional/wallet_basic.py b/test/functional/wallet_basic.py index ed603a2857..917721fef8 100755 --- a/test/functional/wallet_basic.py +++ b/test/functional/wallet_basic.py @@ -585,15 +585,9 @@ class WalletTest(BitcoinTestFramework): # ==Check that wallet prefers to use coins that don't exceed mempool limits ===== - # Get all non-zero utxos together + # Get all non-zero utxos together and split into two chains chain_addrs = [self.nodes[0].getnewaddress(), self.nodes[0].getnewaddress()] - singletxid = self.nodes[0].sendtoaddress(chain_addrs[0], self.nodes[0].getbalance(), "", "", True) - self.generate(self.nodes[0], 1, sync_fun=self.no_op) - node0_balance = self.nodes[0].getbalance() - # Split into two chains - rawtx = self.nodes[0].createrawtransaction([{"txid": singletxid, "vout": 0}], {chain_addrs[0]: node0_balance / 2 - Decimal('0.01'), chain_addrs[1]: node0_balance / 2 - Decimal('0.01')}) - signedtx = self.nodes[0].signrawtransactionwithwallet(rawtx) - singletxid = self.nodes[0].sendrawtransaction(hexstring=signedtx["hex"], maxfeerate=0) + self.nodes[0].sendall(recipients=chain_addrs) self.generate(self.nodes[0], 1, sync_fun=self.no_op) # Make a long chain of unconfirmed payments without hitting mempool limit @@ -701,37 +695,38 @@ class WalletTest(BitcoinTestFramework): txid_feeReason_four = self.nodes[2].sendmany(dummy='', amounts={address: 5}, verbose=False) assert_equal(self.nodes[2].gettransaction(txid_feeReason_four)['txid'], txid_feeReason_four) - self.log.info("Testing 'listunspent' outputs the parent descriptor(s) of coins") - # Create two multisig descriptors, and send a UTxO each. - multi_a = descsum_create("wsh(multi(1,tpubD6NzVbkrYhZ4YBNjUo96Jxd1u4XKWgnoc7LsA1jz3Yc2NiDbhtfBhaBtemB73n9V5vtJHwU6FVXwggTbeoJWQ1rzdz8ysDuQkpnaHyvnvzR/*,tpubD6NzVbkrYhZ4YHdDGMAYGaWxMSC1B6tPRTHuU5t3BcfcS3nrF523iFm5waFd1pP3ZvJt4Jr8XmCmsTBNx5suhcSgtzpGjGMASR3tau1hJz4/*))") - multi_b = descsum_create("wsh(multi(1,tpubD6NzVbkrYhZ4YHdDGMAYGaWxMSC1B6tPRTHuU5t3BcfcS3nrF523iFm5waFd1pP3ZvJt4Jr8XmCmsTBNx5suhcSgtzpGjGMASR3tau1hJz4/*,tpubD6NzVbkrYhZ4Y2RLiuEzNQkntjmsLpPYDm3LTRBYynUQtDtpzeUKAcb9sYthSFL3YR74cdFgF5mW8yKxv2W2CWuZDFR2dUpE5PF9kbrVXNZ/*))") - addr_a = self.nodes[0].deriveaddresses(multi_a, 0)[0] - addr_b = self.nodes[0].deriveaddresses(multi_b, 0)[0] - txid_a = self.nodes[0].sendtoaddress(addr_a, 0.01) - txid_b = self.nodes[0].sendtoaddress(addr_b, 0.01) - self.generate(self.nodes[0], 1, sync_fun=self.no_op) - # Now import the descriptors, make sure we can identify on which descriptor each coin was received. - self.nodes[0].createwallet(wallet_name="wo", descriptors=True, disable_private_keys=True) - wo_wallet = self.nodes[0].get_wallet_rpc("wo") - wo_wallet.importdescriptors([ - { - "desc": multi_a, - "active": False, - "timestamp": "now", - }, - { - "desc": multi_b, - "active": False, - "timestamp": "now", - }, - ]) - coins = wo_wallet.listunspent(minconf=0) - assert_equal(len(coins), 2) - coin_a = next(c for c in coins if c["txid"] == txid_a) - assert_equal(coin_a["parent_descs"][0], multi_a) - coin_b = next(c for c in coins if c["txid"] == txid_b) - assert_equal(coin_b["parent_descs"][0], multi_b) - self.nodes[0].unloadwallet("wo") + if self.options.descriptors: + self.log.info("Testing 'listunspent' outputs the parent descriptor(s) of coins") + # Create two multisig descriptors, and send a UTxO each. + multi_a = descsum_create("wsh(multi(1,tpubD6NzVbkrYhZ4YBNjUo96Jxd1u4XKWgnoc7LsA1jz3Yc2NiDbhtfBhaBtemB73n9V5vtJHwU6FVXwggTbeoJWQ1rzdz8ysDuQkpnaHyvnvzR/*,tpubD6NzVbkrYhZ4YHdDGMAYGaWxMSC1B6tPRTHuU5t3BcfcS3nrF523iFm5waFd1pP3ZvJt4Jr8XmCmsTBNx5suhcSgtzpGjGMASR3tau1hJz4/*))") + multi_b = descsum_create("wsh(multi(1,tpubD6NzVbkrYhZ4YHdDGMAYGaWxMSC1B6tPRTHuU5t3BcfcS3nrF523iFm5waFd1pP3ZvJt4Jr8XmCmsTBNx5suhcSgtzpGjGMASR3tau1hJz4/*,tpubD6NzVbkrYhZ4Y2RLiuEzNQkntjmsLpPYDm3LTRBYynUQtDtpzeUKAcb9sYthSFL3YR74cdFgF5mW8yKxv2W2CWuZDFR2dUpE5PF9kbrVXNZ/*))") + addr_a = self.nodes[0].deriveaddresses(multi_a, 0)[0] + addr_b = self.nodes[0].deriveaddresses(multi_b, 0)[0] + txid_a = self.nodes[0].sendtoaddress(addr_a, 0.01) + txid_b = self.nodes[0].sendtoaddress(addr_b, 0.01) + self.generate(self.nodes[0], 1, sync_fun=self.no_op) + # Now import the descriptors, make sure we can identify on which descriptor each coin was received. + self.nodes[0].createwallet(wallet_name="wo", descriptors=True, disable_private_keys=True) + wo_wallet = self.nodes[0].get_wallet_rpc("wo") + wo_wallet.importdescriptors([ + { + "desc": multi_a, + "active": False, + "timestamp": "now", + }, + { + "desc": multi_b, + "active": False, + "timestamp": "now", + }, + ]) + coins = wo_wallet.listunspent(minconf=0) + assert_equal(len(coins), 2) + coin_a = next(c for c in coins if c["txid"] == txid_a) + assert_equal(coin_a["parent_descs"][0], multi_a) + coin_b = next(c for c in coins if c["txid"] == txid_b) + assert_equal(coin_b["parent_descs"][0], multi_b) + self.nodes[0].unloadwallet("wo") if __name__ == '__main__': diff --git a/test/functional/wallet_bumpfee.py b/test/functional/wallet_bumpfee.py index bd3943d66b..f4ae697292 100755 --- a/test/functional/wallet_bumpfee.py +++ b/test/functional/wallet_bumpfee.py @@ -624,7 +624,7 @@ def test_no_more_inputs_fails(self, rbf_node, dest_address): # feerate rbf requires confirmed outputs when change output doesn't exist or is insufficient self.generatetoaddress(rbf_node, 1, dest_address) # spend all funds, no change output - rbfid = rbf_node.sendtoaddress(rbf_node.getnewaddress(), rbf_node.getbalance(), "", "", True) + rbfid = rbf_node.sendall(recipients=[rbf_node.getnewaddress()])['txid'] assert_raises_rpc_error(-4, "Unable to create transaction. Insufficient funds", rbf_node.bumpfee, rbfid) self.clear_mempool() diff --git a/test/functional/wallet_groups.py b/test/functional/wallet_groups.py index 866c411dba..e5e4cf03bf 100755 --- a/test/functional/wallet_groups.py +++ b/test/functional/wallet_groups.py @@ -26,9 +26,10 @@ class WalletGroupTest(BitcoinTestFramework): ["-maxapsfee=0.00002719"], ["-maxapsfee=0.00002720"], ] - # whitelist peers to speed up tx relay / mempool sync + for args in self.extra_args: - args.append("-whitelist=noban@127.0.0.1") + args.append("-whitelist=noban@127.0.0.1") # whitelist peers to speed up tx relay / mempool sync + args.append(f"-paytxfee={20 * 1e3 / 1e8}") # apply feerate of 20 sats/vB across all nodes self.rpc_timeout = 480 @@ -154,7 +155,7 @@ class WalletGroupTest(BitcoinTestFramework): assert_equal(2, len(tx6["vout"])) # Empty out node2's wallet - self.nodes[2].sendtoaddress(address=self.nodes[0].getnewaddress(), amount=self.nodes[2].getbalance(), subtractfeefromamount=True) + self.nodes[2].sendall(recipients=[self.nodes[0].getnewaddress()]) self.sync_all() self.generate(self.nodes[0], 1) diff --git a/test/functional/wallet_listsinceblock.py b/test/functional/wallet_listsinceblock.py index 204bd67fb1..f259449bef 100755 --- a/test/functional/wallet_listsinceblock.py +++ b/test/functional/wallet_listsinceblock.py @@ -42,7 +42,8 @@ class ListSinceBlockTest(BitcoinTestFramework): self.test_double_send() self.double_spends_filtered() self.test_targetconfirmations() - self.test_desc() + if self.options.descriptors: + self.test_desc() self.test_send_to_self() def test_no_blockhash(self): diff --git a/test/functional/wallet_migration.py b/test/functional/wallet_migration.py new file mode 100755 index 0000000000..3c1cb6ac32 --- /dev/null +++ b/test/functional/wallet_migration.py @@ -0,0 +1,407 @@ +#!/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 Migrating a wallet from legacy to descriptor.""" + +import os +import random +from test_framework.descriptors import descsum_create +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, + find_vout_for_address, +) +from test_framework.wallet_util import ( + get_generate_key, +) + + +class WalletMigrationTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 1 + self.extra_args = [[]] + self.supports_cli = False + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + self.skip_if_no_sqlite() + self.skip_if_no_bdb() + + def assert_is_sqlite(self, wallet_name): + wallet_file_path = os.path.join(self.nodes[0].datadir, "regtest/wallets", wallet_name, self.wallet_data_filename) + with open(wallet_file_path, 'rb') as f: + file_magic = f.read(16) + assert_equal(file_magic, b'SQLite format 3\x00') + assert_equal(self.nodes[0].get_wallet_rpc(wallet_name).getwalletinfo()["format"], "sqlite") + + def create_legacy_wallet(self, wallet_name): + self.nodes[0].createwallet(wallet_name=wallet_name) + wallet = self.nodes[0].get_wallet_rpc(wallet_name) + assert_equal(wallet.getwalletinfo()["descriptors"], False) + assert_equal(wallet.getwalletinfo()["format"], "bdb") + return wallet + + def assert_addr_info_equal(self, addr_info, addr_info_old): + assert_equal(addr_info["address"], addr_info_old["address"]) + assert_equal(addr_info["scriptPubKey"], addr_info_old["scriptPubKey"]) + assert_equal(addr_info["ismine"], addr_info_old["ismine"]) + assert_equal(addr_info["hdkeypath"], addr_info_old["hdkeypath"]) + assert_equal(addr_info["solvable"], addr_info_old["solvable"]) + assert_equal(addr_info["ischange"], addr_info_old["ischange"]) + assert_equal(addr_info["hdmasterfingerprint"], addr_info_old["hdmasterfingerprint"]) + + def assert_list_txs_equal(self, received_list_txs, expected_list_txs): + for d in received_list_txs: + if "parent_descs" in d: + del d["parent_descs"] + for d in expected_list_txs: + if "parent_descs" in d: + del d["parent_descs"] + assert_equal(received_list_txs, expected_list_txs) + + def test_basic(self): + default = self.nodes[0].get_wallet_rpc(self.default_wallet_name) + + self.log.info("Test migration of a basic keys only wallet without balance") + basic0 = self.create_legacy_wallet("basic0") + + addr = basic0.getnewaddress() + change = basic0.getrawchangeaddress() + + old_addr_info = basic0.getaddressinfo(addr) + old_change_addr_info = basic0.getaddressinfo(change) + assert_equal(old_addr_info["ismine"], True) + assert_equal(old_addr_info["hdkeypath"], "m/0'/0'/0'") + assert_equal(old_change_addr_info["ismine"], True) + assert_equal(old_change_addr_info["hdkeypath"], "m/0'/1'/0'") + + # Note: migration could take a while. + basic0.migratewallet() + + # Verify created descriptors + assert_equal(basic0.getwalletinfo()["descriptors"], True) + self.assert_is_sqlite("basic0") + + # The wallet should create the following descriptors: + # * BIP32 descriptors in the form of "0'/0'/*" and "0'/1'/*" (2 descriptors) + # * BIP44 descriptors in the form of "44'/1'/0'/0/*" and "44'/1'/0'/1/*" (2 descriptors) + # * BIP49 descriptors, P2SH(P2WPKH), in the form of "86'/1'/0'/0/*" and "86'/1'/0'/1/*" (2 descriptors) + # * BIP84 descriptors, P2WPKH, in the form of "84'/1'/0'/1/*" and "84'/1'/0'/1/*" (2 descriptors) + # * BIP86 descriptors, P2TR, in the form of "86'/1'/0'/0/*" and "86'/1'/0'/1/*" (2 descriptors) + # * A combo(PK) descriptor for the wallet master key. + # So, should have a total of 11 descriptors on it. + assert_equal(len(basic0.listdescriptors()["descriptors"]), 11) + + # Compare addresses info + addr_info = basic0.getaddressinfo(addr) + change_addr_info = basic0.getaddressinfo(change) + self.assert_addr_info_equal(addr_info, old_addr_info) + self.assert_addr_info_equal(change_addr_info, old_change_addr_info) + + addr_info = basic0.getaddressinfo(basic0.getnewaddress("", "bech32")) + assert_equal(addr_info["hdkeypath"], "m/84'/1'/0'/0/0") + + self.log.info("Test migration of a basic keys only wallet with a balance") + basic1 = self.create_legacy_wallet("basic1") + + for _ in range(0, 10): + default.sendtoaddress(basic1.getnewaddress(), 1) + + self.generate(self.nodes[0], 1) + + for _ in range(0, 5): + basic1.sendtoaddress(default.getnewaddress(), 0.5) + + self.generate(self.nodes[0], 1) + bal = basic1.getbalance() + txs = basic1.listtransactions() + + basic1.migratewallet() + assert_equal(basic1.getwalletinfo()["descriptors"], True) + self.assert_is_sqlite("basic1") + assert_equal(basic1.getbalance(), bal) + self.assert_list_txs_equal(basic1.listtransactions(), txs) + + # restart node and verify that everything is still there + self.restart_node(0) + default = self.nodes[0].get_wallet_rpc(self.default_wallet_name) + self.nodes[0].loadwallet("basic1") + basic1 = self.nodes[0].get_wallet_rpc("basic1") + assert_equal(basic1.getwalletinfo()["descriptors"], True) + self.assert_is_sqlite("basic1") + assert_equal(basic1.getbalance(), bal) + self.assert_list_txs_equal(basic1.listtransactions(), txs) + + self.log.info("Test migration of a wallet with balance received on the seed") + basic2 = self.create_legacy_wallet("basic2") + basic2_seed = get_generate_key() + basic2.sethdseed(True, basic2_seed.privkey) + assert_equal(basic2.getbalance(), 0) + + # Receive coins on different output types for the same seed + basic2_balance = 0 + for addr in [basic2_seed.p2pkh_addr, basic2_seed.p2wpkh_addr, basic2_seed.p2sh_p2wpkh_addr]: + send_value = random.randint(1, 4) + default.sendtoaddress(addr, send_value) + basic2_balance += send_value + self.generate(self.nodes[0], 1) + assert_equal(basic2.getbalance(), basic2_balance) + basic2_txs = basic2.listtransactions() + + # Now migrate and test that we still see have the same balance/transactions + basic2.migratewallet() + assert_equal(basic2.getwalletinfo()["descriptors"], True) + self.assert_is_sqlite("basic2") + assert_equal(basic2.getbalance(), basic2_balance) + self.assert_list_txs_equal(basic2.listtransactions(), basic2_txs) + + def test_multisig(self): + default = self.nodes[0].get_wallet_rpc(self.default_wallet_name) + + # Contrived case where all the multisig keys are in a single wallet + self.log.info("Test migration of a wallet with all keys for a multisig") + multisig0 = self.create_legacy_wallet("multisig0") + addr1 = multisig0.getnewaddress() + addr2 = multisig0.getnewaddress() + addr3 = multisig0.getnewaddress() + + ms_info = multisig0.addmultisigaddress(2, [addr1, addr2, addr3]) + + multisig0.migratewallet() + assert_equal(multisig0.getwalletinfo()["descriptors"], True) + self.assert_is_sqlite("multisig0") + ms_addr_info = multisig0.getaddressinfo(ms_info["address"]) + assert_equal(ms_addr_info["ismine"], True) + assert_equal(ms_addr_info["desc"], ms_info["descriptor"]) + assert_equal("multisig0_watchonly" in self.nodes[0].listwallets(), False) + assert_equal("multisig0_solvables" in self.nodes[0].listwallets(), False) + + pub1 = multisig0.getaddressinfo(addr1)["pubkey"] + pub2 = multisig0.getaddressinfo(addr2)["pubkey"] + + # Some keys in multisig do not belong to this wallet + self.log.info("Test migration of a wallet that has some keys in a multisig") + self.nodes[0].createwallet(wallet_name="multisig1") + multisig1 = self.nodes[0].get_wallet_rpc("multisig1") + ms_info = multisig1.addmultisigaddress(2, [multisig1.getnewaddress(), pub1, pub2]) + ms_info2 = multisig1.addmultisigaddress(2, [multisig1.getnewaddress(), pub1, pub2]) + assert_equal(multisig1.getwalletinfo()["descriptors"], False) + + addr1 = ms_info["address"] + addr2 = ms_info2["address"] + txid = default.sendtoaddress(addr1, 10) + multisig1.importaddress(addr1) + assert_equal(multisig1.getaddressinfo(addr1)["ismine"], False) + assert_equal(multisig1.getaddressinfo(addr1)["iswatchonly"], True) + assert_equal(multisig1.getaddressinfo(addr1)["solvable"], True) + self.generate(self.nodes[0], 1) + multisig1.gettransaction(txid) + assert_equal(multisig1.getbalances()["watchonly"]["trusted"], 10) + assert_equal(multisig1.getaddressinfo(addr2)["ismine"], False) + assert_equal(multisig1.getaddressinfo(addr2)["iswatchonly"], False) + assert_equal(multisig1.getaddressinfo(addr2)["solvable"], True) + + # Migrating multisig1 should see the multisig is no longer part of multisig1 + # A new wallet multisig1_watchonly is created which has the multisig address + # Transaction to multisig is in multisig1_watchonly and not multisig1 + multisig1.migratewallet() + assert_equal(multisig1.getwalletinfo()["descriptors"], True) + self.assert_is_sqlite("multisig1") + assert_equal(multisig1.getaddressinfo(addr1)["ismine"], False) + assert_equal(multisig1.getaddressinfo(addr1)["iswatchonly"], False) + assert_equal(multisig1.getaddressinfo(addr1)["solvable"], False) + assert_raises_rpc_error(-5, "Invalid or non-wallet transaction id", multisig1.gettransaction, txid) + assert_equal(multisig1.getbalance(), 0) + assert_equal(multisig1.listtransactions(), []) + + assert_equal("multisig1_watchonly" in self.nodes[0].listwallets(), True) + ms1_watchonly = self.nodes[0].get_wallet_rpc("multisig1_watchonly") + ms1_wallet_info = ms1_watchonly.getwalletinfo() + assert_equal(ms1_wallet_info['descriptors'], True) + assert_equal(ms1_wallet_info['private_keys_enabled'], False) + self.assert_is_sqlite("multisig1_watchonly") + assert_equal(ms1_watchonly.getaddressinfo(addr1)["ismine"], True) + assert_equal(ms1_watchonly.getaddressinfo(addr1)["solvable"], True) + # Because addr2 was not being watched, it isn't in multisig1_watchonly but rather multisig1_solvables + assert_equal(ms1_watchonly.getaddressinfo(addr2)["ismine"], False) + assert_equal(ms1_watchonly.getaddressinfo(addr2)["solvable"], False) + ms1_watchonly.gettransaction(txid) + assert_equal(ms1_watchonly.getbalance(), 10) + + # Migrating multisig1 should see the second multisig is no longer part of multisig1 + # A new wallet multisig1_solvables is created which has the second address + # This should have no transactions + assert_equal("multisig1_solvables" in self.nodes[0].listwallets(), True) + ms1_solvable = self.nodes[0].get_wallet_rpc("multisig1_solvables") + ms1_wallet_info = ms1_solvable.getwalletinfo() + assert_equal(ms1_wallet_info['descriptors'], True) + assert_equal(ms1_wallet_info['private_keys_enabled'], False) + self.assert_is_sqlite("multisig1_solvables") + assert_equal(ms1_solvable.getaddressinfo(addr1)["ismine"], False) + assert_equal(ms1_solvable.getaddressinfo(addr1)["solvable"], False) + assert_equal(ms1_solvable.getaddressinfo(addr2)["ismine"], True) + assert_equal(ms1_solvable.getaddressinfo(addr2)["solvable"], True) + assert_equal(ms1_solvable.getbalance(), 0) + assert_equal(ms1_solvable.listtransactions(), []) + + + def test_other_watchonly(self): + default = self.nodes[0].get_wallet_rpc(self.default_wallet_name) + + # Wallet with an imported address. Should be the same thing as the multisig test + self.log.info("Test migration of a wallet with watchonly imports") + self.nodes[0].createwallet(wallet_name="imports0") + imports0 = self.nodes[0].get_wallet_rpc("imports0") + assert_equal(imports0.getwalletinfo()["descriptors"], False) + + # Exteranl address label + imports0.setlabel(default.getnewaddress(), "external") + + # Normal non-watchonly tx + received_addr = imports0.getnewaddress() + imports0.setlabel(received_addr, "Receiving") + received_txid = default.sendtoaddress(received_addr, 10) + + # Watchonly tx + import_addr = default.getnewaddress() + imports0.importaddress(import_addr) + imports0.setlabel(import_addr, "imported") + received_watchonly_txid = default.sendtoaddress(import_addr, 10) + + # Received watchonly tx that is then spent + import_sent_addr = default.getnewaddress() + imports0.importaddress(import_sent_addr) + received_sent_watchonly_txid = default.sendtoaddress(import_sent_addr, 10) + received_sent_watchonly_vout = find_vout_for_address(self.nodes[0], received_sent_watchonly_txid, import_sent_addr) + send = default.sendall(recipients=[default.getnewaddress()], options={"inputs": [{"txid": received_sent_watchonly_txid, "vout": received_sent_watchonly_vout}]}) + sent_watchonly_txid = send["txid"] + + self.generate(self.nodes[0], 1) + + balances = imports0.getbalances() + spendable_bal = balances["mine"]["trusted"] + watchonly_bal = balances["watchonly"]["trusted"] + assert_equal(len(imports0.listtransactions(include_watchonly=True)), 4) + + # Migrate + imports0.migratewallet() + assert_equal(imports0.getwalletinfo()["descriptors"], True) + self.assert_is_sqlite("imports0") + assert_raises_rpc_error(-5, "Invalid or non-wallet transaction id", imports0.gettransaction, received_watchonly_txid) + assert_raises_rpc_error(-5, "Invalid or non-wallet transaction id", imports0.gettransaction, received_sent_watchonly_txid) + assert_raises_rpc_error(-5, "Invalid or non-wallet transaction id", imports0.gettransaction, sent_watchonly_txid) + assert_equal(len(imports0.listtransactions(include_watchonly=True)), 1) + imports0.gettransaction(received_txid) + assert_equal(imports0.getbalance(), spendable_bal) + + assert_equal("imports0_watchonly" in self.nodes[0].listwallets(), True) + watchonly = self.nodes[0].get_wallet_rpc("imports0_watchonly") + watchonly_info = watchonly.getwalletinfo() + assert_equal(watchonly_info["descriptors"], True) + self.assert_is_sqlite("imports0_watchonly") + assert_equal(watchonly_info["private_keys_enabled"], False) + watchonly.gettransaction(received_watchonly_txid) + watchonly.gettransaction(received_sent_watchonly_txid) + watchonly.gettransaction(sent_watchonly_txid) + assert_equal(watchonly.getbalance(), watchonly_bal) + assert_raises_rpc_error(-5, "Invalid or non-wallet transaction id", watchonly.gettransaction, received_txid) + assert_equal(len(watchonly.listtransactions(include_watchonly=True)), 3) + + def test_no_privkeys(self): + default = self.nodes[0].get_wallet_rpc(self.default_wallet_name) + + # Migrating an actual watchonly wallet should not create a new watchonly wallet + self.log.info("Test migration of a pure watchonly wallet") + self.nodes[0].createwallet(wallet_name="watchonly0", disable_private_keys=True) + watchonly0 = self.nodes[0].get_wallet_rpc("watchonly0") + info = watchonly0.getwalletinfo() + assert_equal(info["descriptors"], False) + assert_equal(info["private_keys_enabled"], False) + + addr = default.getnewaddress() + desc = default.getaddressinfo(addr)["desc"] + res = watchonly0.importmulti([ + { + "desc": desc, + "watchonly": True, + "timestamp": "now", + }]) + assert_equal(res[0]['success'], True) + default.sendtoaddress(addr, 10) + self.generate(self.nodes[0], 1) + + watchonly0.migratewallet() + assert_equal("watchonly0_watchonly" in self.nodes[0].listwallets(), False) + info = watchonly0.getwalletinfo() + assert_equal(info["descriptors"], True) + assert_equal(info["private_keys_enabled"], False) + self.assert_is_sqlite("watchonly0") + + # Migrating a wallet with pubkeys added to the keypool + self.log.info("Test migration of a pure watchonly wallet with pubkeys in keypool") + self.nodes[0].createwallet(wallet_name="watchonly1", disable_private_keys=True) + watchonly1 = self.nodes[0].get_wallet_rpc("watchonly1") + info = watchonly1.getwalletinfo() + assert_equal(info["descriptors"], False) + assert_equal(info["private_keys_enabled"], False) + + addr1 = default.getnewaddress(address_type="bech32") + addr2 = default.getnewaddress(address_type="bech32") + desc1 = default.getaddressinfo(addr1)["desc"] + desc2 = default.getaddressinfo(addr2)["desc"] + res = watchonly1.importmulti([ + { + "desc": desc1, + "keypool": True, + "timestamp": "now", + }, + { + "desc": desc2, + "keypool": True, + "timestamp": "now", + } + ]) + assert_equal(res[0]["success"], True) + assert_equal(res[1]["success"], True) + # Before migrating, we can fetch addr1 from the keypool + assert_equal(watchonly1.getnewaddress(address_type="bech32"), addr1) + + watchonly1.migratewallet() + info = watchonly1.getwalletinfo() + assert_equal(info["descriptors"], True) + assert_equal(info["private_keys_enabled"], False) + self.assert_is_sqlite("watchonly1") + # After migrating, the "keypool" is empty + assert_raises_rpc_error(-4, "Error: This wallet has no available keys", watchonly1.getnewaddress) + + def test_pk_coinbases(self): + self.log.info("Test migration of a wallet using old pk() coinbases") + wallet = self.create_legacy_wallet("pkcb") + + addr = wallet.getnewaddress() + addr_info = wallet.getaddressinfo(addr) + desc = descsum_create("pk(" + addr_info["pubkey"] + ")") + + self.nodes[0].generatetodescriptor(1, desc, invalid_call=False) + + bals = wallet.getbalances() + + wallet.migratewallet() + + assert_equal(bals, wallet.getbalances()) + + def run_test(self): + self.generate(self.nodes[0], 101) + + # TODO: Test the actual records in the wallet for these tests too. The behavior may be correct, but the data written may not be what we actually want + self.test_basic() + self.test_multisig() + self.test_other_watchonly() + self.test_no_privkeys() + self.test_pk_coinbases() + +if __name__ == '__main__': + WalletMigrationTest().main() diff --git a/test/functional/wallet_resendwallettransactions.py b/test/functional/wallet_resendwallettransactions.py index 6552bfe60c..b3d02fbfc9 100755 --- a/test/functional/wallet_resendwallettransactions.py +++ b/test/functional/wallet_resendwallettransactions.py @@ -9,10 +9,13 @@ from test_framework.blocktools import ( create_block, create_coinbase, ) +from test_framework.messages import DEFAULT_MEMPOOL_EXPIRY_HOURS from test_framework.p2p import P2PTxInvStore from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import assert_equal - +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, +) class ResendWalletTransactionsTest(BitcoinTestFramework): def set_test_params(self): @@ -27,13 +30,9 @@ class ResendWalletTransactionsTest(BitcoinTestFramework): peer_first = node.add_p2p_connection(P2PTxInvStore()) self.log.info("Create a new transaction and wait until it's broadcast") - txid = node.sendtoaddress(node.getnewaddress(), 1) - - # Wallet rebroadcast is first scheduled 1 sec after startup (see - # nNextResend in ResendWalletTransactions()). Tell scheduler to call - # MaybeResendWalletTxn now to initialize nNextResend before the first - # setmocktime call below. - node.mockscheduler(1) + parent_utxo, indep_utxo = node.listunspent()[:2] + addr = node.getnewaddress() + txid = node.send(outputs=[{addr: 1}], options={"inputs": [parent_utxo]})["txid"] # Can take a few seconds due to transaction trickling peer_first.wait_for_broadcast([txid]) @@ -51,7 +50,7 @@ class ResendWalletTransactionsTest(BitcoinTestFramework): block.solve() node.submitblock(block.serialize().hex()) - # Set correct m_best_block_time, which is used in ResendWalletTransactions + # Set correct m_best_block_time, which is used in ResubmitWalletTransactions node.syncwithvalidationinterfacequeue() now = int(time.time()) @@ -60,20 +59,67 @@ class ResendWalletTransactionsTest(BitcoinTestFramework): twelve_hrs = 12 * 60 * 60 two_min = 2 * 60 node.setmocktime(now + twelve_hrs - two_min) - node.mockscheduler(1) # Tell scheduler to call MaybeResendWalletTxn now + node.mockscheduler(60) # Tell scheduler to call MaybeResendWalletTxs now assert_equal(int(txid, 16) in peer_second.get_invs(), False) self.log.info("Bump time & check that transaction is rebroadcast") # Transaction should be rebroadcast approximately 24 hours in the future, # but can range from 12-36. So bump 36 hours to be sure. - with node.assert_debug_log(['ResendWalletTransactions: resubmit 1 unconfirmed transactions']): + with node.assert_debug_log(['resubmit 1 unconfirmed transactions']): node.setmocktime(now + 36 * 60 * 60) - # Tell scheduler to call MaybeResendWalletTxn now. - node.mockscheduler(1) + # Tell scheduler to call MaybeResendWalletTxs now. + node.mockscheduler(60) # Give some time for trickle to occur node.setmocktime(now + 36 * 60 * 60 + 600) peer_second.wait_for_broadcast([txid]) + self.log.info("Chain of unconfirmed not-in-mempool txs are rebroadcast") + # This tests that the node broadcasts the parent transaction before the child transaction. + # To test that scenario, we need a method to reliably get a child transaction placed + # in mapWallet positioned before the parent. We cannot predict the position in mapWallet, + # but we can observe it using listreceivedbyaddress and other related RPCs. + # + # So we will create the child transaction, use listreceivedbyaddress to see what the + # ordering of mapWallet is, if the child is not before the parent, we will create a new + # child (via bumpfee) and remove the old child (via removeprunedfunds) until we get the + # ordering of child before parent. + child_txid = node.send(outputs=[{addr: 0.5}], options={"inputs": [{"txid":txid, "vout":0}]})["txid"] + while True: + txids = node.listreceivedbyaddress(minconf=0, address_filter=addr)[0]["txids"] + if txids == [child_txid, txid]: + break + bumped = node.bumpfee(child_txid) + # The scheduler queue creates a copy of the added tx after + # send/bumpfee and re-adds it to the wallet (undoing the next + # removeprunedfunds). So empty the scheduler queue: + node.syncwithvalidationinterfacequeue() + node.removeprunedfunds(child_txid) + child_txid = bumped["txid"] + entry_time = node.getmempoolentry(child_txid)["time"] + + block_time = entry_time + 6 * 60 + node.setmocktime(block_time) + block = create_block(int(node.getbestblockhash(), 16), create_coinbase(node.getblockcount() + 1), block_time) + block.solve() + node.submitblock(block.serialize().hex()) + # Set correct m_best_block_time, which is used in ResubmitWalletTransactions + node.syncwithvalidationinterfacequeue() + + # Evict these txs from the mempool + evict_time = block_time + 60 * 60 * DEFAULT_MEMPOOL_EXPIRY_HOURS + 5 + node.setmocktime(evict_time) + indep_send = node.send(outputs=[{node.getnewaddress(): 1}], options={"inputs": [indep_utxo]}) + node.getmempoolentry(indep_send["txid"]) + assert_raises_rpc_error(-5, "Transaction not in mempool", node.getmempoolentry, txid) + assert_raises_rpc_error(-5, "Transaction not in mempool", node.getmempoolentry, child_txid) + + # Rebroadcast and check that parent and child are both in the mempool + with node.assert_debug_log(['resubmit 2 unconfirmed transactions']): + node.setmocktime(evict_time + 36 * 60 * 60) # 36 hrs is the upper limit of the resend timer + node.mockscheduler(60) + node.getmempoolentry(txid) + node.getmempoolentry(child_txid) + if __name__ == '__main__': ResendWalletTransactionsTest().main() diff --git a/test/functional/wallet_sendall.py b/test/functional/wallet_sendall.py index aa8d2a9d2c..db4f32fe16 100755 --- a/test/functional/wallet_sendall.py +++ b/test/functional/wallet_sendall.py @@ -264,6 +264,32 @@ class SendallTest(BitcoinTestFramework): recipients=[self.remainder_target], options={"inputs": [utxo], "send_max": True}) + @cleanup + def sendall_fails_on_high_fee(self): + self.log.info("Test sendall fails if the transaction fee exceeds the maxtxfee") + self.add_utxos([21]) + + assert_raises_rpc_error( + -4, + "Fee exceeds maximum configured by user", + self.wallet.sendall, + recipients=[self.remainder_target], + fee_rate=100000) + + # This tests needs to be the last one otherwise @cleanup will fail with "Transaction too large" error + def sendall_fails_with_transaction_too_large(self): + self.log.info("Test that sendall fails if resulting transaction is too large") + # create many inputs + outputs = {self.wallet.getnewaddress(): 0.000025 for _ in range(1600)} + self.def_wallet.sendmany(amounts=outputs) + self.generate(self.nodes[0], 1) + + assert_raises_rpc_error( + -4, + "Transaction too large.", + self.wallet.sendall, + recipients=[self.remainder_target]) + def run_test(self): self.nodes[0].createwallet("activewallet") self.wallet = self.nodes[0].get_wallet_rpc("activewallet") @@ -312,5 +338,11 @@ class SendallTest(BitcoinTestFramework): # Sendall fails when using send_max while specifying inputs self.sendall_fails_on_specific_inputs_with_send_max() + # Sendall fails when providing a fee that is too high + self.sendall_fails_on_high_fee() + + # Sendall fails when many inputs result to too large transaction + self.sendall_fails_with_transaction_too_large() + if __name__ == '__main__': SendallTest().main() diff --git a/test/sanitizer_suppressions/tsan b/test/sanitizer_suppressions/tsan index 3acf575d07..d331991273 100644 --- a/test/sanitizer_suppressions/tsan +++ b/test/sanitizer_suppressions/tsan @@ -13,7 +13,7 @@ race:zmq::* race:bitcoin-qt # deadlock (TODO fix) -deadlock:CChainState::ConnectTip +deadlock:Chainstate::ConnectTip # Intentional deadlock in tests deadlock:sync_tests::potential_deadlock_detected |