diff options
Diffstat (limited to 'test/functional')
-rwxr-xr-x | test/functional/feature_block.py | 2 | ||||
-rwxr-xr-x | test/functional/p2p_compactblocks.py | 24 | ||||
-rwxr-xr-x | test/functional/p2p_dos_header_tree.py | 3 | ||||
-rwxr-xr-x | test/functional/p2p_headers_sync_with_minchainwork.py | 164 | ||||
-rwxr-xr-x | test/functional/p2p_i2p_sessions.py | 36 | ||||
-rwxr-xr-x | test/functional/p2p_unrequested_blocks.py | 14 | ||||
-rwxr-xr-x | test/functional/rpc_blockchain.py | 11 | ||||
-rwxr-xr-x | test/functional/rpc_fundrawtransaction.py | 4 | ||||
-rwxr-xr-x | test/functional/test_framework/test_node.py | 2 | ||||
-rwxr-xr-x | test/functional/test_runner.py | 3 | ||||
-rwxr-xr-x | test/functional/tool_wallet.py | 10 | ||||
-rwxr-xr-x | test/functional/wallet_balance.py | 1 | ||||
-rwxr-xr-x | test/functional/wallet_basic.py | 10 | ||||
-rwxr-xr-x | test/functional/wallet_bumpfee.py | 2 | ||||
-rwxr-xr-x | test/functional/wallet_groups.py | 2 | ||||
-rwxr-xr-x | test/functional/wallet_migration.py | 407 | ||||
-rwxr-xr-x | test/functional/wallet_resendwallettransactions.py | 12 |
17 files changed, 675 insertions, 32 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/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..016375222d --- /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.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.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_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..227e255536 100755 --- a/test/functional/rpc_blockchain.py +++ b/test/functional/rpc_blockchain.py @@ -452,8 +452,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 +462,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( diff --git a/test/functional/rpc_fundrawtransaction.py b/test/functional/rpc_fundrawtransaction.py index 5a870de6c7..2c33617750 100755 --- a/test/functional/rpc_fundrawtransaction.py +++ b/test/functional/rpc_fundrawtransaction.py @@ -635,7 +635,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 +661,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): 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..267d8e2177 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 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..5d3d78c2dc 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 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..9f0f694a3e 100755 --- a/test/functional/wallet_groups.py +++ b/test/functional/wallet_groups.py @@ -154,7 +154,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_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..4ef259efe1 100755 --- a/test/functional/wallet_resendwallettransactions.py +++ b/test/functional/wallet_resendwallettransactions.py @@ -29,11 +29,11 @@ class ResendWalletTransactionsTest(BitcoinTestFramework): 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 + # Wallet rebroadcast is first scheduled 1 min sec after startup (see # nNextResend in ResendWalletTransactions()). Tell scheduler to call - # MaybeResendWalletTxn now to initialize nNextResend before the first + # MaybeResendWalletTxs now to initialize nNextResend before the first # setmocktime call below. - node.mockscheduler(1) + node.mockscheduler(60) # Can take a few seconds due to transaction trickling peer_first.wait_for_broadcast([txid]) @@ -60,7 +60,7 @@ 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") @@ -68,8 +68,8 @@ class ResendWalletTransactionsTest(BitcoinTestFramework): # but can range from 12-36. So bump 36 hours to be sure. with node.assert_debug_log(['ResendWalletTransactions: 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]) |