diff options
Diffstat (limited to 'test/functional')
40 files changed, 1224 insertions, 605 deletions
diff --git a/test/functional/feature_backwards_compatibility.py b/test/functional/feature_backwards_compatibility.py index 21776d85c9..b161c71a85 100755 --- a/test/functional/feature_backwards_compatibility.py +++ b/test/functional/feature_backwards_compatibility.py @@ -27,6 +27,7 @@ from test_framework.descriptors import descsum_create from test_framework.util import ( assert_equal, + assert_raises_rpc_error, ) @@ -82,7 +83,7 @@ class BackwardsCompatibilityTest(BitcoinTestFramework): # w1: regular wallet, created on master: update this test when default # wallets can no longer be opened by older versions. - node_master.rpc.createwallet(wallet_name="w1") + node_master.createwallet(wallet_name="w1") wallet = node_master.get_wallet_rpc("w1") info = wallet.getwalletinfo() assert info['private_keys_enabled'] @@ -127,7 +128,7 @@ class BackwardsCompatibilityTest(BitcoinTestFramework): # w2: wallet with private keys disabled, created on master: update this # test when default wallets private keys disabled can no longer be # opened by older versions. - node_master.rpc.createwallet(wallet_name="w2", disable_private_keys=True) + node_master.createwallet(wallet_name="w2", disable_private_keys=True) wallet = node_master.get_wallet_rpc("w2") info = wallet.getwalletinfo() assert info['private_keys_enabled'] == False @@ -149,7 +150,7 @@ class BackwardsCompatibilityTest(BitcoinTestFramework): # w3: blank wallet, created on master: update this # test when default blank wallets can no longer be opened by older versions. - node_master.rpc.createwallet(wallet_name="w3", blank=True) + node_master.createwallet(wallet_name="w3", blank=True) wallet = node_master.get_wallet_rpc("w3") info = wallet.getwalletinfo() assert info['private_keys_enabled'] @@ -215,67 +216,89 @@ class BackwardsCompatibilityTest(BitcoinTestFramework): os.path.join(node_v19_wallets_dir, wallet) ) - # Open the wallets in v0.19 - node_v19.loadwallet("w1") - wallet = node_v19.get_wallet_rpc("w1") - info = wallet.getwalletinfo() - assert info['private_keys_enabled'] - assert info['keypoolsize'] > 0 - txs = wallet.listtransactions() - assert_equal(len(txs), 5) - assert_equal(txs[1]["txid"], tx1_id) - assert_equal(txs[2]["walletconflicts"], [tx1_id]) - assert_equal(txs[1]["replaced_by_txid"], tx2_id) - assert not(txs[1]["abandoned"]) - assert_equal(txs[1]["confirmations"], -1) - assert_equal(txs[2]["blockindex"], 1) - assert txs[3]["abandoned"] - assert_equal(txs[4]["walletconflicts"], [tx3_id]) - assert_equal(txs[3]["replaced_by_txid"], tx4_id) - assert not(hasattr(txs[3], "blockindex")) - - node_v19.loadwallet("w2") - wallet = node_v19.get_wallet_rpc("w2") - info = wallet.getwalletinfo() - assert info['private_keys_enabled'] == False - assert info['keypoolsize'] == 0 - - node_v19.loadwallet("w3") - wallet = node_v19.get_wallet_rpc("w3") - info = wallet.getwalletinfo() - assert info['private_keys_enabled'] - assert info['keypoolsize'] == 0 - - # Open the wallets in v0.18 - node_v18.loadwallet("w1") - wallet = node_v18.get_wallet_rpc("w1") - info = wallet.getwalletinfo() - assert info['private_keys_enabled'] - assert info['keypoolsize'] > 0 - txs = wallet.listtransactions() - assert_equal(len(txs), 5) - assert_equal(txs[1]["txid"], tx1_id) - assert_equal(txs[2]["walletconflicts"], [tx1_id]) - assert_equal(txs[1]["replaced_by_txid"], tx2_id) - assert not(txs[1]["abandoned"]) - assert_equal(txs[1]["confirmations"], -1) - assert_equal(txs[2]["blockindex"], 1) - assert txs[3]["abandoned"] - assert_equal(txs[4]["walletconflicts"], [tx3_id]) - assert_equal(txs[3]["replaced_by_txid"], tx4_id) - assert not(hasattr(txs[3], "blockindex")) - - node_v18.loadwallet("w2") - wallet = node_v18.get_wallet_rpc("w2") - info = wallet.getwalletinfo() - assert info['private_keys_enabled'] == False - assert info['keypoolsize'] == 0 - - node_v18.loadwallet("w3") - wallet = node_v18.get_wallet_rpc("w3") - info = wallet.getwalletinfo() - assert info['private_keys_enabled'] - assert info['keypoolsize'] == 0 + if not self.options.descriptors: + # Descriptor wallets break compatibility, only run this test for legacy wallet + # Open the wallets in v0.19 + node_v19.loadwallet("w1") + wallet = node_v19.get_wallet_rpc("w1") + info = wallet.getwalletinfo() + assert info['private_keys_enabled'] + assert info['keypoolsize'] > 0 + txs = wallet.listtransactions() + assert_equal(len(txs), 5) + assert_equal(txs[1]["txid"], tx1_id) + assert_equal(txs[2]["walletconflicts"], [tx1_id]) + assert_equal(txs[1]["replaced_by_txid"], tx2_id) + assert not(txs[1]["abandoned"]) + assert_equal(txs[1]["confirmations"], -1) + assert_equal(txs[2]["blockindex"], 1) + assert txs[3]["abandoned"] + assert_equal(txs[4]["walletconflicts"], [tx3_id]) + assert_equal(txs[3]["replaced_by_txid"], tx4_id) + assert not(hasattr(txs[3], "blockindex")) + + node_v19.loadwallet("w2") + wallet = node_v19.get_wallet_rpc("w2") + info = wallet.getwalletinfo() + assert info['private_keys_enabled'] == False + assert info['keypoolsize'] == 0 + + node_v19.loadwallet("w3") + wallet = node_v19.get_wallet_rpc("w3") + info = wallet.getwalletinfo() + assert info['private_keys_enabled'] + assert info['keypoolsize'] == 0 + + # Open the wallets in v0.18 + node_v18.loadwallet("w1") + wallet = node_v18.get_wallet_rpc("w1") + info = wallet.getwalletinfo() + assert info['private_keys_enabled'] + assert info['keypoolsize'] > 0 + txs = wallet.listtransactions() + assert_equal(len(txs), 5) + assert_equal(txs[1]["txid"], tx1_id) + assert_equal(txs[2]["walletconflicts"], [tx1_id]) + assert_equal(txs[1]["replaced_by_txid"], tx2_id) + assert not(txs[1]["abandoned"]) + assert_equal(txs[1]["confirmations"], -1) + assert_equal(txs[2]["blockindex"], 1) + assert txs[3]["abandoned"] + assert_equal(txs[4]["walletconflicts"], [tx3_id]) + assert_equal(txs[3]["replaced_by_txid"], tx4_id) + assert not(hasattr(txs[3], "blockindex")) + + node_v18.loadwallet("w2") + wallet = node_v18.get_wallet_rpc("w2") + info = wallet.getwalletinfo() + assert info['private_keys_enabled'] == False + assert info['keypoolsize'] == 0 + + node_v18.loadwallet("w3") + wallet = node_v18.get_wallet_rpc("w3") + info = wallet.getwalletinfo() + assert info['private_keys_enabled'] + assert info['keypoolsize'] == 0 + + node_v17.loadwallet("w1") + wallet = node_v17.get_wallet_rpc("w1") + info = wallet.getwalletinfo() + assert info['private_keys_enabled'] + assert info['keypoolsize'] > 0 + + node_v17.loadwallet("w2") + wallet = node_v17.get_wallet_rpc("w2") + info = wallet.getwalletinfo() + assert info['private_keys_enabled'] == False + assert info['keypoolsize'] == 0 + else: + # Descriptor wallets appear to be corrupted wallets to old software + assert_raises_rpc_error(-4, "Wallet file verification failed: wallet.dat corrupt, salvage failed", node_v19.loadwallet, "w1") + assert_raises_rpc_error(-4, "Wallet file verification failed: wallet.dat corrupt, salvage failed", node_v19.loadwallet, "w2") + assert_raises_rpc_error(-4, "Wallet file verification failed: wallet.dat corrupt, salvage failed", node_v19.loadwallet, "w3") + assert_raises_rpc_error(-4, "Wallet file verification failed: wallet.dat corrupt, salvage failed", node_v18.loadwallet, "w1") + assert_raises_rpc_error(-4, "Wallet file verification failed: wallet.dat corrupt, salvage failed", node_v18.loadwallet, "w2") + assert_raises_rpc_error(-4, "Wallet file verification failed: wallet.dat corrupt, salvage failed", node_v18.loadwallet, "w3") # Open the wallets in v0.17 node_v17.loadwallet("w1_v18") @@ -284,24 +307,12 @@ class BackwardsCompatibilityTest(BitcoinTestFramework): assert info['private_keys_enabled'] assert info['keypoolsize'] > 0 - node_v17.loadwallet("w1") - wallet = node_v17.get_wallet_rpc("w1") - info = wallet.getwalletinfo() - assert info['private_keys_enabled'] - assert info['keypoolsize'] > 0 - node_v17.loadwallet("w2_v18") wallet = node_v17.get_wallet_rpc("w2_v18") info = wallet.getwalletinfo() assert info['private_keys_enabled'] == False assert info['keypoolsize'] == 0 - node_v17.loadwallet("w2") - wallet = node_v17.get_wallet_rpc("w2") - info = wallet.getwalletinfo() - assert info['private_keys_enabled'] == False - assert info['keypoolsize'] == 0 - # RPC loadwallet failure causes bitcoind to exit, in addition to the RPC # call failure, so the following test won't work: # assert_raises_rpc_error(-4, "Wallet loading failed.", node_v17.loadwallet, 'w3_v18') @@ -309,14 +320,22 @@ class BackwardsCompatibilityTest(BitcoinTestFramework): # Instead, we stop node and try to launch it with the wallet: self.stop_node(4) node_v17.assert_start_raises_init_error(["-wallet=w3_v18"], "Error: Error loading w3_v18: Wallet requires newer version of Bitcoin Core") - node_v17.assert_start_raises_init_error(["-wallet=w3"], "Error: Error loading w3: Wallet requires newer version of Bitcoin Core") + if self.options.descriptors: + # Descriptor wallets appear to be corrupted wallets to old software + node_v17.assert_start_raises_init_error(["-wallet=w1"], "Error: wallet.dat corrupt, salvage failed") + node_v17.assert_start_raises_init_error(["-wallet=w2"], "Error: wallet.dat corrupt, salvage failed") + node_v17.assert_start_raises_init_error(["-wallet=w3"], "Error: wallet.dat corrupt, salvage failed") + else: + node_v17.assert_start_raises_init_error(["-wallet=w3"], "Error: Error loading w3: Wallet requires newer version of Bitcoin Core") self.start_node(4) - # Open most recent wallet in v0.16 (no loadwallet RPC) - self.restart_node(5, extra_args=["-wallet=w2"]) - wallet = node_v16.get_wallet_rpc("w2") - info = wallet.getwalletinfo() - assert info['keypoolsize'] == 1 + if not self.options.descriptors: + # Descriptor wallets break compatibility, only run this test for legacy wallets + # Open most recent wallet in v0.16 (no loadwallet RPC) + self.restart_node(5, extra_args=["-wallet=w2"]) + wallet = node_v16.get_wallet_rpc("w2") + info = wallet.getwalletinfo() + assert info['keypoolsize'] == 1 # Create upgrade wallet in v0.16 self.restart_node(-1, extra_args=["-wallet=u1_v16"]) diff --git a/test/functional/feature_block.py b/test/functional/feature_block.py index 19753d73ef..3cf7f3890c 100755 --- a/test/functional/feature_block.py +++ b/test/functional/feature_block.py @@ -1251,7 +1251,7 @@ class FullBlockTest(BitcoinTestFramework): blocks = [] spend = out[32] for i in range(89, LARGE_REORG_SIZE + 89): - b = self.next_block(i, spend, version=4) + b = self.next_block(i, spend) tx = CTransaction() script_length = MAX_BLOCK_BASE_SIZE - len(b.serialize()) - 69 script_output = CScript([b'\x00' * script_length]) @@ -1270,18 +1270,18 @@ class FullBlockTest(BitcoinTestFramework): self.move_tip(88) blocks2 = [] for i in range(89, LARGE_REORG_SIZE + 89): - blocks2.append(self.next_block("alt" + str(i), version=4)) + blocks2.append(self.next_block("alt" + str(i))) self.send_blocks(blocks2, False, force_send=True) # extend alt chain to trigger re-org - block = self.next_block("alt" + str(chain1_tip + 1), version=4) + block = self.next_block("alt" + str(chain1_tip + 1)) self.send_blocks([block], True, timeout=2440) # ... and re-org back to the first chain self.move_tip(chain1_tip) - block = self.next_block(chain1_tip + 1, version=4) + block = self.next_block(chain1_tip + 1) self.send_blocks([block], False, force_send=True) - block = self.next_block(chain1_tip + 2, version=4) + block = self.next_block(chain1_tip + 2) self.send_blocks([block], True, timeout=2440) self.log.info("Reject a block with an invalid block header version") @@ -1289,7 +1289,7 @@ class FullBlockTest(BitcoinTestFramework): self.send_blocks([b_v1], success=False, force_send=True, reject_reason='bad-version(0x00000001)', reconnect=True) self.move_tip(chain1_tip + 2) - b_cb34 = self.next_block('b_cb34', version=4) + b_cb34 = self.next_block('b_cb34') b_cb34.vtx[0].vin[0].scriptSig = b_cb34.vtx[0].vin[0].scriptSig[:-1] b_cb34.vtx[0].rehash() b_cb34.hashMerkleRoot = b_cb34.calc_merkle_root() @@ -1323,7 +1323,7 @@ class FullBlockTest(BitcoinTestFramework): tx.rehash() return tx - def next_block(self, number, spend=None, additional_coinbase_value=0, script=CScript([OP_TRUE]), *, version=1): + def next_block(self, number, spend=None, additional_coinbase_value=0, script=CScript([OP_TRUE]), *, version=4): if self.tip is None: base_block_hash = self.genesis_hash block_time = int(time.time()) + 1 diff --git a/test/functional/feature_config_args.py b/test/functional/feature_config_args.py index 60533e196f..3e28dae4b3 100755 --- a/test/functional/feature_config_args.py +++ b/test/functional/feature_config_args.py @@ -179,19 +179,15 @@ class ConfArgsTest(BitcoinTestFramework): # Create the directory and ensure the config file now works os.mkdir(new_data_dir) - self.start_node(0, ['-conf='+conf_file, '-wallet=w1']) + self.start_node(0, ['-conf='+conf_file]) self.stop_node(0) assert os.path.exists(os.path.join(new_data_dir, self.chain, 'blocks')) - if self.is_wallet_compiled(): - assert os.path.exists(os.path.join(new_data_dir, self.chain, 'wallets', 'w1')) # Ensure command line argument overrides datadir in conf os.mkdir(new_data_dir_2) self.nodes[0].datadir = new_data_dir_2 - self.start_node(0, ['-datadir='+new_data_dir_2, '-conf='+conf_file, '-wallet=w2']) + self.start_node(0, ['-datadir='+new_data_dir_2, '-conf='+conf_file]) assert os.path.exists(os.path.join(new_data_dir_2, self.chain, 'blocks')) - if self.is_wallet_compiled(): - assert os.path.exists(os.path.join(new_data_dir_2, self.chain, 'wallets', 'w2')) if __name__ == '__main__': diff --git a/test/functional/feature_csv_activation.py b/test/functional/feature_csv_activation.py index 39e8bca751..46ba18b9b5 100755 --- a/test/functional/feature_csv_activation.py +++ b/test/functional/feature_csv_activation.py @@ -150,7 +150,6 @@ class BIP68_112_113Test(BitcoinTestFramework): self.setup_clean_chain = True self.extra_args = [[ '-whitelist=noban@127.0.0.1', - '-blockversion=4', '-addresstype=legacy', '-par=1', # Use only one script thread to get the exact reject reason for testing ]] diff --git a/test/functional/feature_notifications.py b/test/functional/feature_notifications.py index cf102f321c..47bc8dbb49 100755 --- a/test/functional/feature_notifications.py +++ b/test/functional/feature_notifications.py @@ -39,11 +39,12 @@ class NotificationsTest(BitcoinTestFramework): # -alertnotify and -blocknotify on node0, walletnotify on node1 self.extra_args = [[ - "-alertnotify=echo > {}".format(os.path.join(self.alertnotify_dir, '%s')), - "-blocknotify=echo > {}".format(os.path.join(self.blocknotify_dir, '%s'))], - ["-blockversion=211", - "-rescan", - "-walletnotify=echo > {}".format(os.path.join(self.walletnotify_dir, notify_outputname('%w', '%s')))]] + "-alertnotify=echo > {}".format(os.path.join(self.alertnotify_dir, '%s')), + "-blocknotify=echo > {}".format(os.path.join(self.blocknotify_dir, '%s')), + ], [ + "-rescan", + "-walletnotify=echo > {}".format(os.path.join(self.walletnotify_dir, notify_outputname('%w', '%s'))), + ]] self.wallet_names = [self.default_wallet_name, self.wallet] super().setup_network() diff --git a/test/functional/feature_nulldummy.py b/test/functional/feature_nulldummy.py index d1196a4bbd..b0eac7056b 100755 --- a/test/functional/feature_nulldummy.py +++ b/test/functional/feature_nulldummy.py @@ -51,10 +51,18 @@ class NULLDUMMYTest(BitcoinTestFramework): self.skip_if_no_wallet() def run_test(self): - self.address = self.nodes[0].getnewaddress() - self.ms_address = self.nodes[0].addmultisigaddress(1, [self.address])['address'] - self.wit_address = self.nodes[0].getnewaddress(address_type='p2sh-segwit') - self.wit_ms_address = self.nodes[0].addmultisigaddress(1, [self.address], '', 'p2sh-segwit')['address'] + self.nodes[0].createwallet(wallet_name='wmulti', disable_private_keys=True) + wmulti = self.nodes[0].get_wallet_rpc('wmulti') + w0 = self.nodes[0].get_wallet_rpc(self.default_wallet_name) + self.address = w0.getnewaddress() + self.pubkey = w0.getaddressinfo(self.address)['pubkey'] + self.ms_address = wmulti.addmultisigaddress(1, [self.pubkey])['address'] + self.wit_address = w0.getnewaddress(address_type='p2sh-segwit') + self.wit_ms_address = wmulti.addmultisigaddress(1, [self.pubkey], '', 'p2sh-segwit')['address'] + if not self.options.descriptors: + # Legacy wallets need to import these so that they are watched by the wallet. This is unnecssary (and does not need to be tested) for descriptor wallets + wmulti.importaddress(self.ms_address) + wmulti.importaddress(self.wit_ms_address) self.coinbase_blocks = self.nodes[0].generate(2) # Block 2 coinbase_txid = [] diff --git a/test/functional/feature_proxy.py b/test/functional/feature_proxy.py index dfae58e860..05b658ed87 100755 --- a/test/functional/feature_proxy.py +++ b/test/functional/feature_proxy.py @@ -26,6 +26,8 @@ addnode connect to IPv4 addnode connect to IPv6 addnode connect to onion addnode connect to generic DNS name + +- Test getnetworkinfo for each node """ import socket @@ -41,12 +43,16 @@ from test_framework.util import ( from test_framework.netutil import test_ipv6_local RANGE_BEGIN = PORT_MIN + 2 * PORT_RANGE # Start after p2p and rpc ports -# From GetNetworkName() in netbase.cpp: -NET_UNROUTABLE = "" + +# Networks returned by RPC getpeerinfo, defined in src/netbase.cpp::GetNetworkName() +NET_UNROUTABLE = "unroutable" NET_IPV4 = "ipv4" NET_IPV6 = "ipv6" NET_ONION = "onion" +# Networks returned by RPC getnetworkinfo, defined in src/rpc/net.cpp::GetNetworksInfo() +NETWORKS = frozenset({NET_IPV4, NET_IPV6, NET_ONION}) + class ProxyTest(BitcoinTestFramework): def set_test_params(self): @@ -84,14 +90,14 @@ class ProxyTest(BitcoinTestFramework): self.serv3 = Socks5Server(self.conf3) self.serv3.start() - # Note: proxies are not used to connect to local nodes - # this is because the proxy to use is based on CService.GetNetwork(), which return NET_UNROUTABLE for localhost + # Note: proxies are not used to connect to local nodes. This is because the proxy to + # use is based on CService.GetNetwork(), which returns NET_UNROUTABLE for localhost. args = [ ['-listen', '-proxy=%s:%i' % (self.conf1.addr),'-proxyrandomize=1'], ['-listen', '-proxy=%s:%i' % (self.conf1.addr),'-onion=%s:%i' % (self.conf2.addr),'-proxyrandomize=0'], ['-listen', '-proxy=%s:%i' % (self.conf2.addr),'-proxyrandomize=1'], [] - ] + ] if self.have_ipv6: args[3] = ['-listen', '-proxy=[%s]:%i' % (self.conf3.addr),'-proxyrandomize=0', '-noonion'] self.add_nodes(self.num_nodes, extra_args=args) @@ -189,15 +195,17 @@ class ProxyTest(BitcoinTestFramework): r[x['name']] = x return r - # test RPC getnetworkinfo + self.log.info("Test RPC getnetworkinfo") n0 = networks_dict(self.nodes[0].getnetworkinfo()) - for net in ['ipv4','ipv6','onion']: + assert_equal(NETWORKS, n0.keys()) + for net in NETWORKS: assert_equal(n0[net]['proxy'], '%s:%i' % (self.conf1.addr)) assert_equal(n0[net]['proxy_randomize_credentials'], True) assert_equal(n0['onion']['reachable'], True) n1 = networks_dict(self.nodes[1].getnetworkinfo()) - for net in ['ipv4','ipv6']: + assert_equal(NETWORKS, n1.keys()) + for net in ['ipv4', 'ipv6']: assert_equal(n1[net]['proxy'], '%s:%i' % (self.conf1.addr)) assert_equal(n1[net]['proxy_randomize_credentials'], False) assert_equal(n1['onion']['proxy'], '%s:%i' % (self.conf2.addr)) @@ -205,14 +213,16 @@ class ProxyTest(BitcoinTestFramework): assert_equal(n1['onion']['reachable'], True) n2 = networks_dict(self.nodes[2].getnetworkinfo()) - for net in ['ipv4','ipv6','onion']: + assert_equal(NETWORKS, n2.keys()) + for net in NETWORKS: assert_equal(n2[net]['proxy'], '%s:%i' % (self.conf2.addr)) assert_equal(n2[net]['proxy_randomize_credentials'], True) assert_equal(n2['onion']['reachable'], True) if self.have_ipv6: n3 = networks_dict(self.nodes[3].getnetworkinfo()) - for net in ['ipv4','ipv6']: + assert_equal(NETWORKS, n3.keys()) + for net in NETWORKS: assert_equal(n3[net]['proxy'], '[%s]:%i' % (self.conf3.addr)) assert_equal(n3[net]['proxy_randomize_credentials'], False) assert_equal(n3['onion']['reachable'], False) diff --git a/test/functional/feature_taproot.py b/test/functional/feature_taproot.py index 7b534c1c2f..6e28cfb265 100755 --- a/test/functional/feature_taproot.py +++ b/test/functional/feature_taproot.py @@ -9,6 +9,7 @@ from test_framework.blocktools import ( create_block, add_witness_commitment, MAX_BLOCK_SIGOPS_WEIGHT, + NORMAL_GBT_REQUEST_PARAMS, WITNESS_SCALE_FACTOR, ) from test_framework.messages import ( @@ -1129,13 +1130,13 @@ def spenders_taproot_inactive(): ] tap = taproot_construct(pub, scripts) - # Test that keypath spending is valid & standard if compliant, but valid and nonstandard otherwise. - add_spender(spenders, "inactive/keypath_valid", key=sec, tap=tap) + # Test that keypath spending is valid & non-standard, regardless of validity. + add_spender(spenders, "inactive/keypath_valid", key=sec, tap=tap, standard=False) add_spender(spenders, "inactive/keypath_invalidsig", key=sec, tap=tap, standard=False, sighash=bitflipper(default_sighash)) add_spender(spenders, "inactive/keypath_empty", key=sec, tap=tap, standard=False, witness=[]) - # Same for scriptpath spending (but using future features like annex, leaf versions, or OP_SUCCESS is nonstandard). - add_spender(spenders, "inactive/scriptpath_valid", key=sec, tap=tap, leaf="pk", inputs=[getter("sign")]) + # Same for scriptpath spending (and features like annex, leaf versions, or OP_SUCCESS don't change this) + add_spender(spenders, "inactive/scriptpath_valid", key=sec, tap=tap, leaf="pk", standard=False, inputs=[getter("sign")]) add_spender(spenders, "inactive/scriptpath_invalidsig", key=sec, tap=tap, leaf="pk", standard=False, inputs=[getter("sign")], sighash=bitflipper(default_sighash)) add_spender(spenders, "inactive/scriptpath_invalidcb", key=sec, tap=tap, leaf="pk", standard=False, inputs=[getter("sign")], controlblock=bitflipper(default_controlblock)) add_spender(spenders, "inactive/scriptpath_valid_unkleaf", key=sec, tap=tap, leaf="future_leaf", standard=False, inputs=[getter("sign")]) @@ -1199,7 +1200,7 @@ class TaprootTest(BitcoinTestFramework): self.num_nodes = 2 self.setup_clean_chain = True # Node 0 has Taproot inactive, Node 1 active. - self.extra_args = [["-whitelist=127.0.0.1", "-par=1", "-vbparams=taproot:1:1"], ["-whitelist=127.0.0.1", "-par=1"]] + self.extra_args = [["-par=1", "-vbparams=taproot:1:1"], ["-par=1"]] def block_submit(self, node, txs, msg, err_msg, cb_pubkey=None, fees=0, sigops_weight=0, witness=False, accept=False): @@ -1218,7 +1219,7 @@ class TaprootTest(BitcoinTestFramework): witness and add_witness_commitment(block) block.rehash() block.solve() - block_response = node.submitblock(block.serialize(True).hex()) + block_response = node.submitblock(block.serialize().hex()) if err_msg is not None: assert block_response is not None and err_msg in block_response, "Missing error message '%s' from block response '%s': %s" % (err_msg, "(None)" if block_response is None else block_response, msg) if (accept): @@ -1436,22 +1437,36 @@ class TaprootTest(BitcoinTestFramework): self.log.info(" - Done") def run_test(self): - self.connect_nodes(0, 1) - # Post-taproot activation tests go first (pre-taproot tests' blocks are invalid post-taproot). self.log.info("Post-activation tests...") self.nodes[1].generate(101) self.test_spenders(self.nodes[1], spenders_taproot_active(), input_counts=[1, 2, 2, 2, 2, 3]) - # Transfer % of funds to pre-taproot node. + # Transfer funds to pre-taproot node. addr = self.nodes[0].getnewaddress() - self.nodes[1].sendtoaddress(address=addr, amount=int(self.nodes[1].getbalance() * 70000000) / 100000000) - self.nodes[1].generate(1) + rawtx = self.nodes[1].createrawtransaction( + inputs=[{ + 'txid': i['txid'], + 'vout': i['vout'] + } for i in self.nodes[1].listunspent()], + outputs={addr: self.nodes[1].getbalance()}, + ) + rawtx = self.nodes[1].signrawtransactionwithwallet(rawtx)['hex'] + # Transaction is too large to fit into the mempool, so put it into a block + block = create_block(tmpl=self.nodes[1].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS), txlist=[rawtx]) + add_witness_commitment(block) + block.rehash() + block.solve() + assert_equal(None, self.nodes[1].submitblock(block.serialize().hex())) self.sync_blocks() # Pre-taproot activation tests. self.log.info("Pre-activation tests...") - self.test_spenders(self.nodes[0], spenders_taproot_inactive(), input_counts=[1, 2, 2, 2, 2, 3]) + # Run each test twice; once in isolation, and once combined with others. Testing in isolation + # means that the standardness is verified in every test (as combined transactions are only standard + # when all their inputs are standard). + self.test_spenders(self.nodes[0], spenders_taproot_inactive(), input_counts=[1]) + self.test_spenders(self.nodes[0], spenders_taproot_inactive(), input_counts=[2, 3]) if __name__ == '__main__': diff --git a/test/functional/mining_basic.py b/test/functional/mining_basic.py index 80635e33c5..ba467c1517 100755 --- a/test/functional/mining_basic.py +++ b/test/functional/mining_basic.py @@ -13,6 +13,7 @@ from decimal import Decimal from test_framework.blocktools import ( create_coinbase, + NORMAL_GBT_REQUEST_PARAMS, TIME_GENESIS_BLOCK, ) from test_framework.messages import ( @@ -27,6 +28,9 @@ from test_framework.util import ( assert_raises_rpc_error, ) +VERSIONBITS_TOP_BITS = 0x20000000 +VERSIONBITS_DEPLOYMENT_TESTDUMMY_BIT = 28 + def assert_template(node, block, expect, rehash=True): if rehash: @@ -54,6 +58,14 @@ class MiningTest(BitcoinTestFramework): assert_equal(mining_info['blocks'], 200) assert_equal(mining_info['currentblocktx'], 0) assert_equal(mining_info['currentblockweight'], 4000) + + self.log.info('test blockversion') + self.restart_node(0, extra_args=['-mocktime={}'.format(t), '-blockversion=1337']) + self.connect_nodes(0, 1) + assert_equal(1337, self.nodes[0].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)['version']) + self.restart_node(0, extra_args=['-mocktime={}'.format(t)]) + self.connect_nodes(0, 1) + assert_equal(VERSIONBITS_TOP_BITS + (1 << VERSIONBITS_DEPLOYMENT_TESTDUMMY_BIT), self.nodes[0].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)['version']) self.restart_node(0) self.connect_nodes(0, 1) @@ -79,7 +91,7 @@ class MiningTest(BitcoinTestFramework): # Mine a block to leave initial block download node.generatetoaddress(1, node.get_deterministic_priv_key().address) - tmpl = node.getblocktemplate({'rules': ['segwit']}) + tmpl = node.getblocktemplate(NORMAL_GBT_REQUEST_PARAMS) self.log.info("getblocktemplate: Test capability advertised") assert 'proposal' in tmpl['capabilities'] assert 'coinbasetxn' not in tmpl diff --git a/test/functional/p2p_disconnect_ban.py b/test/functional/p2p_disconnect_ban.py index 3088a8aa46..a7f51b5364 100755 --- a/test/functional/p2p_disconnect_ban.py +++ b/test/functional/p2p_disconnect_ban.py @@ -18,8 +18,11 @@ class DisconnectBanTest(BitcoinTestFramework): def run_test(self): self.log.info("Connect nodes both way") + # By default, the test framework sets up an addnode connection from + # node 1 --> node0. By connecting node0 --> node 1, we're left with + # the two nodes being connected both ways. + # Topology will look like: node0 <--> node1 self.connect_nodes(0, 1) - self.connect_nodes(1, 0) self.log.info("Test setban and listbanned RPCs") diff --git a/test/functional/p2p_permissions.py b/test/functional/p2p_permissions.py index 653e3894af..ed82e6a2e2 100755 --- a/test/functional/p2p_permissions.py +++ b/test/functional/p2p_permissions.py @@ -153,11 +153,20 @@ class P2PPermissionsTests(BitcoinTestFramework): self.log.debug("Check that node[1] will not send an invalid tx to node[0]") tx.vout[0].nValue += 1 txid = tx.rehash() + # Send the transaction twice. The first time, it'll be rejected by ATMP because it conflicts + # with a mempool transaction. The second time, it'll be in the recentRejects filter. p2p_rebroadcast_wallet.send_txs_and_test( [tx], self.nodes[1], success=False, - reject_reason='Not relaying non-mempool transaction {} from forcerelay peer=0'.format(txid), + reject_reason='{} from peer=0 was not accepted: txn-mempool-conflict'.format(txid) + ) + + p2p_rebroadcast_wallet.send_txs_and_test( + [tx], + self.nodes[1], + success=False, + reject_reason='Not relaying non-mempool transaction {} from forcerelay peer=0'.format(txid) ) def checkpermission(self, args, expectedPermissions, whitelisted): diff --git a/test/functional/p2p_segwit.py b/test/functional/p2p_segwit.py index 5c15538418..e99ecd8026 100755 --- a/test/functional/p2p_segwit.py +++ b/test/functional/p2p_segwit.py @@ -2097,14 +2097,14 @@ class SegWitTest(BitcoinTestFramework): raw = self.nodes[0].createrawtransaction([{"txid": unspent['txid'], "vout": unspent['vout']}], {self.nodes[0].getnewaddress(): 1}) tx = FromHex(CTransaction(), raw) - assert_raises_rpc_error(-22, "TX decode failed", self.nodes[0].decoderawtransaction, serialize_with_bogus_witness(tx).hex()) + assert_raises_rpc_error(-22, "TX decode failed", self.nodes[0].decoderawtransaction, hexstring=serialize_with_bogus_witness(tx).hex(), iswitness=True) with self.nodes[0].assert_debug_log(['Superfluous witness record']): self.test_node.send_and_ping(msg_bogus_tx(tx)) raw = self.nodes[0].signrawtransactionwithwallet(raw) assert raw['complete'] raw = raw['hex'] tx = FromHex(CTransaction(), raw) - assert_raises_rpc_error(-22, "TX decode failed", self.nodes[0].decoderawtransaction, serialize_with_bogus_witness(tx).hex()) + assert_raises_rpc_error(-22, "TX decode failed", self.nodes[0].decoderawtransaction, hexstring=serialize_with_bogus_witness(tx).hex(), iswitness=True) with self.nodes[0].assert_debug_log(['Unknown transaction optional data']): self.test_node.send_and_ping(msg_bogus_tx(tx)) diff --git a/test/functional/rpc_fundrawtransaction.py b/test/functional/rpc_fundrawtransaction.py index 167c4671ef..503993162b 100755 --- a/test/functional/rpc_fundrawtransaction.py +++ b/test/functional/rpc_fundrawtransaction.py @@ -5,8 +5,10 @@ """Test the fundrawtransaction RPC.""" from decimal import Decimal +from test_framework.descriptors import descsum_create from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( + assert_approx, assert_equal, assert_fee_amount, assert_greater_than, @@ -88,6 +90,7 @@ class RawTransactionsTest(BitcoinTestFramework): self.test_op_return() self.test_watchonly() self.test_all_watched_funds() + self.test_feerate_with_conf_target_and_estimate_mode() self.test_option_feerate() self.test_address_reuse() self.test_option_subtract_fee_from_outputs() @@ -100,17 +103,19 @@ class RawTransactionsTest(BitcoinTestFramework): rawmatch = self.nodes[2].fundrawtransaction(rawmatch, {"changePosition":1, "subtractFeeFromOutputs":[0]}) assert_equal(rawmatch["changepos"], -1) + self.nodes[3].createwallet(wallet_name="wwatch", disable_private_keys=True) + wwatch = self.nodes[3].get_wallet_rpc('wwatch') watchonly_address = self.nodes[0].getnewaddress() watchonly_pubkey = self.nodes[0].getaddressinfo(watchonly_address)["pubkey"] self.watchonly_amount = Decimal(200) - self.nodes[3].importpubkey(watchonly_pubkey, "", True) + wwatch.importpubkey(watchonly_pubkey, "", True) self.watchonly_txid = self.nodes[0].sendtoaddress(watchonly_address, self.watchonly_amount) # Lock UTXO so nodes[0] doesn't accidentally spend it self.watchonly_vout = find_vout_for_address(self.nodes[0], self.watchonly_txid, watchonly_address) self.nodes[0].lockunspent(False, [{"txid": self.watchonly_txid, "vout": self.watchonly_vout}]) - self.nodes[0].sendtoaddress(self.nodes[3].getnewaddress(), self.watchonly_amount / 10) + self.nodes[0].sendtoaddress(self.nodes[3].get_wallet_rpc(self.default_wallet_name).getnewaddress(), self.watchonly_amount / 10) self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 1.5) self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 1.0) @@ -119,6 +124,8 @@ class RawTransactionsTest(BitcoinTestFramework): self.nodes[0].generate(1) self.sync_all() + wwatch.unloadwallet() + def test_simple(self): self.log.info("Test fundrawtxn") inputs = [ ] @@ -406,7 +413,7 @@ class RawTransactionsTest(BitcoinTestFramework): addr1Obj = self.nodes[1].getaddressinfo(addr1) addr2Obj = self.nodes[1].getaddressinfo(addr2) - mSigObj = self.nodes[1].addmultisigaddress(2, [addr1Obj['pubkey'], addr2Obj['pubkey']])['address'] + mSigObj = self.nodes[3].createmultisig(2, [addr1Obj['pubkey'], addr2Obj['pubkey']])['address'] inputs = [] outputs = {mSigObj:1.1} @@ -438,7 +445,7 @@ class RawTransactionsTest(BitcoinTestFramework): addr4Obj = self.nodes[1].getaddressinfo(addr4) addr5Obj = self.nodes[1].getaddressinfo(addr5) - mSigObj = self.nodes[1].addmultisigaddress( + mSigObj = self.nodes[1].createmultisig( 4, [ addr1Obj['pubkey'], @@ -464,7 +471,7 @@ class RawTransactionsTest(BitcoinTestFramework): def test_spend_2of2(self): """Spend a 2-of-2 multisig transaction over fundraw.""" - self.log.info("Test fundrawtxn spending 2-of-2 multisig") + self.log.info("Test fundpsbt spending 2-of-2 multisig") # Create 2-of-2 addr. addr1 = self.nodes[2].getnewaddress() @@ -473,13 +480,18 @@ class RawTransactionsTest(BitcoinTestFramework): addr1Obj = self.nodes[2].getaddressinfo(addr1) addr2Obj = self.nodes[2].getaddressinfo(addr2) - mSigObj = self.nodes[2].addmultisigaddress( + self.nodes[2].createwallet(wallet_name='wmulti', disable_private_keys=True) + wmulti = self.nodes[2].get_wallet_rpc('wmulti') + w2 = self.nodes[2].get_wallet_rpc(self.default_wallet_name) + mSigObj = wmulti.addmultisigaddress( 2, [ addr1Obj['pubkey'], addr2Obj['pubkey'], ] )['address'] + if not self.options.descriptors: + wmulti.importaddress(mSigObj) # Send 1.2 BTC to msig addr. self.nodes[0].sendtoaddress(mSigObj, 1.2) @@ -489,22 +501,39 @@ class RawTransactionsTest(BitcoinTestFramework): oldBalance = self.nodes[1].getbalance() inputs = [] outputs = {self.nodes[1].getnewaddress():1.1} - rawtx = self.nodes[2].createrawtransaction(inputs, outputs) - fundedTx = self.nodes[2].fundrawtransaction(rawtx) + funded_psbt = wmulti.walletcreatefundedpsbt(inputs=inputs, outputs=outputs, options={'changeAddress': w2.getrawchangeaddress()})['psbt'] - signedTx = self.nodes[2].signrawtransactionwithwallet(fundedTx['hex']) - self.nodes[2].sendrawtransaction(signedTx['hex']) + signed_psbt = w2.walletprocesspsbt(funded_psbt) + final_psbt = w2.finalizepsbt(signed_psbt['psbt']) + self.nodes[2].sendrawtransaction(final_psbt['hex']) self.nodes[2].generate(1) self.sync_all() # Make sure funds are received at node1. assert_equal(oldBalance+Decimal('1.10000000'), self.nodes[1].getbalance()) + wmulti.unloadwallet() + def test_locked_wallet(self): - self.log.info("Test fundrawtxn with locked wallet") + self.log.info("Test fundrawtxn with locked wallet and hardened derivation") self.nodes[1].encryptwallet("test") + if self.options.descriptors: + self.nodes[1].walletpassphrase('test', 10) + self.nodes[1].importdescriptors([{ + 'desc': descsum_create('wpkh(tprv8ZgxMBicQKsPdYeeZbPSKd2KYLmeVKtcFA7kqCxDvDR13MQ6us8HopUR2wLcS2ZKPhLyKsqpDL2FtL73LMHcgoCL7DXsciA8eX8nbjCR2eG/0h/*h)'), + 'timestamp': 'now', + 'active': True + }, + { + 'desc': descsum_create('wpkh(tprv8ZgxMBicQKsPdYeeZbPSKd2KYLmeVKtcFA7kqCxDvDR13MQ6us8HopUR2wLcS2ZKPhLyKsqpDL2FtL73LMHcgoCL7DXsciA8eX8nbjCR2eG/1h/*h)'), + 'timestamp': 'now', + 'active': True, + 'internal': True + }]) + self.nodes[1].walletlock() + # Drain the keypool. self.nodes[1].getnewaddress() self.nodes[1].getrawchangeaddress() @@ -621,7 +650,25 @@ class RawTransactionsTest(BitcoinTestFramework): outputs = {self.nodes[2].getnewaddress(): self.watchonly_amount / 2} rawtx = self.nodes[3].createrawtransaction(inputs, outputs) - result = self.nodes[3].fundrawtransaction(rawtx, {'includeWatching': True }) + self.nodes[3].loadwallet('wwatch') + wwatch = self.nodes[3].get_wallet_rpc('wwatch') + # Setup change addresses for the watchonly wallet + desc_import = [{ + "desc": descsum_create("wpkh(tpubD6NzVbkrYhZ4YNXVQbNhMK1WqguFsUXceaVJKbmno2aZ3B6QfbMeraaYvnBSGpV3vxLyTTK9DYT1yoEck4XUScMzXoQ2U2oSmE2JyMedq3H/1/*)"), + "timestamp": "now", + "internal": True, + "active": True, + "keypool": True, + "range": [0, 100], + "watchonly": True, + }] + if self.options.descriptors: + wwatch.importdescriptors(desc_import) + else: + wwatch.importmulti(desc_import) + + # Backward compatibility test (2nd params is includeWatching) + result = wwatch.fundrawtransaction(rawtx, True) res_dec = self.nodes[0].decoderawtransaction(result["hex"]) assert_equal(len(res_dec["vin"]), 1) assert_equal(res_dec["vin"][0]["txid"], self.watchonly_txid) @@ -629,6 +676,8 @@ class RawTransactionsTest(BitcoinTestFramework): assert "fee" in result.keys() assert_greater_than(result["changepos"], -1) + wwatch.unloadwallet() + def test_all_watched_funds(self): self.log.info("Test fundrawtxn using entirety of watched funds") @@ -636,17 +685,19 @@ class RawTransactionsTest(BitcoinTestFramework): outputs = {self.nodes[2].getnewaddress(): self.watchonly_amount} rawtx = self.nodes[3].createrawtransaction(inputs, outputs) - # Backward compatibility test (2nd param is includeWatching). - result = self.nodes[3].fundrawtransaction(rawtx, True) + self.nodes[3].loadwallet('wwatch') + wwatch = self.nodes[3].get_wallet_rpc('wwatch') + w3 = self.nodes[3].get_wallet_rpc(self.default_wallet_name) + result = wwatch.fundrawtransaction(rawtx, {'includeWatching': True, 'changeAddress': w3.getrawchangeaddress(), 'subtractFeeFromOutputs': [0]}) res_dec = self.nodes[0].decoderawtransaction(result["hex"]) - assert_equal(len(res_dec["vin"]), 2) - assert res_dec["vin"][0]["txid"] == self.watchonly_txid or res_dec["vin"][1]["txid"] == self.watchonly_txid + assert_equal(len(res_dec["vin"]), 1) + assert res_dec["vin"][0]["txid"] == self.watchonly_txid assert_greater_than(result["fee"], 0) - assert_greater_than(result["changepos"], -1) - assert_equal(result["fee"] + res_dec["vout"][result["changepos"]]["value"], self.watchonly_amount / 10) + assert_equal(result["changepos"], -1) + assert_equal(result["fee"] + res_dec["vout"][0]["value"], self.watchonly_amount) - signedtx = self.nodes[3].signrawtransactionwithwallet(result["hex"]) + signedtx = wwatch.signrawtransactionwithwallet(result["hex"]) assert not signedtx["complete"] signedtx = self.nodes[0].signrawtransactionwithwallet(signedtx["hex"]) assert signedtx["complete"] @@ -654,6 +705,8 @@ class RawTransactionsTest(BitcoinTestFramework): self.nodes[0].generate(1) self.sync_all() + wwatch.unloadwallet() + def test_option_feerate(self): self.log.info("Test fundrawtxn feeRate option") @@ -671,6 +724,59 @@ class RawTransactionsTest(BitcoinTestFramework): assert_fee_amount(result2['fee'], count_bytes(result2['hex']), 2 * result_fee_rate) assert_fee_amount(result3['fee'], count_bytes(result3['hex']), 10 * result_fee_rate) + def test_feerate_with_conf_target_and_estimate_mode(self): + self.log.info("Test fundrawtxn passing an explicit fee rate using conf_target and estimate_mode") + node = self.nodes[3] + # Make sure there is exactly one input so coin selection can't skew the result. + assert_equal(len(node.listunspent(1)), 1) + inputs = [] + outputs = {node.getnewaddress() : 1} + rawtx = node.createrawtransaction(inputs, outputs) + + for unit, fee_rate in {"btc/kb": 0.1, "sat/b": 10000}.items(): + self.log.info("Test fundrawtxn with conf_target {} estimate_mode {} produces expected fee".format(fee_rate, unit)) + # With no arguments passed, expect fee of 141 sats/b. + assert_approx(node.fundrawtransaction(rawtx)["fee"], vexp=0.00000141, vspan=0.00000001) + # Expect fee to be 10,000x higher when explicit fee 10,000x greater is specified. + result = node.fundrawtransaction(rawtx, {"conf_target": fee_rate, "estimate_mode": unit}) + assert_approx(result["fee"], vexp=0.0141, vspan=0.0001) + + for field, fee_rate in {"conf_target": 0.1, "estimate_mode": "sat/b"}.items(): + self.log.info("Test fundrawtxn raises RPC error if both feeRate and {} are passed".format(field)) + assert_raises_rpc_error( + -8, "Cannot specify both {} and feeRate".format(field), + lambda: node.fundrawtransaction(rawtx, {"feeRate": 0.1, field: fee_rate})) + + self.log.info("Test fundrawtxn with invalid estimate_mode settings") + for k, v in {"number": 42, "object": {"foo": "bar"}}.items(): + assert_raises_rpc_error(-3, "Expected type string for estimate_mode, got {}".format(k), + lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": v, "conf_target": 0.1})) + for mode in ["foo", Decimal("3.141592")]: + assert_raises_rpc_error(-8, "Invalid estimate_mode parameter", + lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": mode, "conf_target": 0.1})) + + self.log.info("Test fundrawtxn with invalid conf_target settings") + for mode in ["unset", "economical", "conservative", "btc/kb", "sat/b"]: + self.log.debug("{}".format(mode)) + for k, v in {"string": "", "object": {"foo": "bar"}}.items(): + assert_raises_rpc_error(-3, "Expected type number for conf_target, got {}".format(k), + lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": mode, "conf_target": v})) + if mode in ["btc/kb", "sat/b"]: + assert_raises_rpc_error(-3, "Amount out of range", + lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": mode, "conf_target": -1})) + assert_raises_rpc_error(-4, "Fee rate (0.00000000 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)", + lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": mode, "conf_target": 0})) + else: + for n in [-1, 0, 1009]: + assert_raises_rpc_error(-8, "Invalid conf_target, must be between 1 and 1008", + lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": mode, "conf_target": n})) + + for unit, fee_rate in {"sat/B": 0.99999999, "BTC/kB": 0.00000999}.items(): + self.log.info("- raises RPC error 'fee rate too low' if conf_target {} and estimate_mode {} are passed".format(fee_rate, unit)) + assert_raises_rpc_error(-4, "Fee rate (0.00000999 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)", + lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": unit, "conf_target": fee_rate, "add_inputs": True})) + + def test_address_reuse(self): """Test no address reuse occurs.""" self.log.info("Test fundrawtxn does not reuse addresses") diff --git a/test/functional/rpc_net.py b/test/functional/rpc_net.py index 71e8e1b22a..2efd090733 100755 --- a/test/functional/rpc_net.py +++ b/test/functional/rpc_net.py @@ -21,7 +21,6 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_approx, assert_equal, - assert_greater_than_or_equal, assert_greater_than, assert_raises_rpc_error, p2p_port, @@ -51,9 +50,12 @@ class NetTest(BitcoinTestFramework): def run_test(self): # Get out of IBD for the minfeefilter and getpeerinfo tests. self.nodes[0].generate(101) - # Connect nodes both ways. + + # By default, the test framework sets up an addnode connection from + # node 1 --> node0. By connecting node0 --> node 1, we're left with + # the two nodes being connected both ways. + # Topology will look like: node0 <--> node1 self.connect_nodes(0, 1) - self.connect_nodes(1, 0) self.sync_all() self.test_connection_count() @@ -69,36 +71,52 @@ class NetTest(BitcoinTestFramework): # After using `connect_nodes` to connect nodes 0 and 1 to each other. assert_equal(self.nodes[0].getconnectioncount(), 2) + def test_getpeerinfo(self): + self.log.info("Test getpeerinfo") + # Create a few getpeerinfo last_block/last_transaction values. + if self.is_wallet_compiled(): + self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 1) + self.nodes[1].generate(1) + self.sync_all() + time_now = int(time.time()) + peer_info = [x.getpeerinfo() for x in self.nodes] + # Verify last_block and last_transaction keys/values. + for node, peer, field in product(range(self.num_nodes), range(2), ['last_block', 'last_transaction']): + assert field in peer_info[node][peer].keys() + if peer_info[node][peer][field] != 0: + assert_approx(peer_info[node][peer][field], time_now, vspan=60) + # check both sides of bidirectional connection between nodes + # the address bound to on one side will be the source address for the other node + assert_equal(peer_info[0][0]['addrbind'], peer_info[1][0]['addr']) + assert_equal(peer_info[1][0]['addrbind'], peer_info[0][0]['addr']) + assert_equal(peer_info[0][0]['minfeefilter'], Decimal("0.00000500")) + assert_equal(peer_info[1][0]['minfeefilter'], Decimal("0.00001000")) + # check the `servicesnames` field + for info in peer_info: + assert_net_servicesnames(int(info[0]["services"], 0x10), info[0]["servicesnames"]) + + assert_equal(peer_info[0][0]['connection_type'], 'inbound') + assert_equal(peer_info[0][1]['connection_type'], 'manual') + + assert_equal(peer_info[1][0]['connection_type'], 'manual') + assert_equal(peer_info[1][1]['connection_type'], 'inbound') + def test_getnettotals(self): self.log.info("Test getnettotals") - # getnettotals totalbytesrecv and totalbytessent should be - # consistent with getpeerinfo. Since the RPC calls are not atomic, - # and messages might have been recvd or sent between RPC calls, call - # getnettotals before and after and verify that the returned values - # from getpeerinfo are bounded by those values. + # Test getnettotals and getpeerinfo by doing a ping. The bytes + # sent/received should increase by at least the size of one ping (32 + # bytes) and one pong (32 bytes). net_totals_before = self.nodes[0].getnettotals() - peer_info = self.nodes[0].getpeerinfo() - net_totals_after = self.nodes[0].getnettotals() - assert_equal(len(peer_info), 2) - peers_recv = sum([peer['bytesrecv'] for peer in peer_info]) - peers_sent = sum([peer['bytessent'] for peer in peer_info]) - - assert_greater_than_or_equal(peers_recv, net_totals_before['totalbytesrecv']) - assert_greater_than_or_equal(net_totals_after['totalbytesrecv'], peers_recv) - assert_greater_than_or_equal(peers_sent, net_totals_before['totalbytessent']) - assert_greater_than_or_equal(net_totals_after['totalbytessent'], peers_sent) - - # test getnettotals and getpeerinfo by doing a ping - # the bytes sent/received should change - # note ping and pong are 32 bytes each + peer_info_before = self.nodes[0].getpeerinfo() + self.nodes[0].ping() - self.wait_until(lambda: (self.nodes[0].getnettotals()['totalbytessent'] >= net_totals_after['totalbytessent'] + 32 * 2), timeout=1) - self.wait_until(lambda: (self.nodes[0].getnettotals()['totalbytesrecv'] >= net_totals_after['totalbytesrecv'] + 32 * 2), timeout=1) + self.wait_until(lambda: (self.nodes[0].getnettotals()['totalbytessent'] >= net_totals_before['totalbytessent'] + 32 * 2), timeout=1) + self.wait_until(lambda: (self.nodes[0].getnettotals()['totalbytesrecv'] >= net_totals_before['totalbytesrecv'] + 32 * 2), timeout=1) - peer_info_after_ping = self.nodes[0].getpeerinfo() - for before, after in zip(peer_info, peer_info_after_ping): - assert_greater_than_or_equal(after['bytesrecv_per_msg'].get('pong', 0), before['bytesrecv_per_msg'].get('pong', 0) + 32) - assert_greater_than_or_equal(after['bytessent_per_msg'].get('ping', 0), before['bytessent_per_msg'].get('ping', 0) + 32) + for peer_before in peer_info_before: + peer_after = lambda: next(p for p in self.nodes[0].getpeerinfo() if p['id'] == peer_before['id']) + self.wait_until(lambda: peer_after()['bytesrecv_per_msg'].get('pong', 0) >= peer_before['bytesrecv_per_msg'].get('pong', 0) + 32, timeout=1) + self.wait_until(lambda: peer_after()['bytessent_per_msg'].get('ping', 0) >= peer_before['bytessent_per_msg'].get('ping', 0) + 32, timeout=1) def test_getnetworkinfo(self): self.log.info("Test getnetworkinfo") @@ -151,36 +169,6 @@ class NetTest(BitcoinTestFramework): # check that a non-existent node returns an error assert_raises_rpc_error(-24, "Node has not been added", self.nodes[0].getaddednodeinfo, '1.1.1.1') - def test_getpeerinfo(self): - self.log.info("Test getpeerinfo") - # Create a few getpeerinfo last_block/last_transaction values. - if self.is_wallet_compiled(): - self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 1) - self.nodes[1].generate(1) - self.sync_all() - time_now = int(time.time()) - peer_info = [x.getpeerinfo() for x in self.nodes] - # Verify last_block and last_transaction keys/values. - for node, peer, field in product(range(self.num_nodes), range(2), ['last_block', 'last_transaction']): - assert field in peer_info[node][peer].keys() - if peer_info[node][peer][field] != 0: - assert_approx(peer_info[node][peer][field], time_now, vspan=60) - # check both sides of bidirectional connection between nodes - # the address bound to on one side will be the source address for the other node - assert_equal(peer_info[0][0]['addrbind'], peer_info[1][0]['addr']) - assert_equal(peer_info[1][0]['addrbind'], peer_info[0][0]['addr']) - assert_equal(peer_info[0][0]['minfeefilter'], Decimal("0.00000500")) - assert_equal(peer_info[1][0]['minfeefilter'], Decimal("0.00001000")) - # check the `servicesnames` field - for info in peer_info: - assert_net_servicesnames(int(info[0]["services"], 0x10), info[0]["servicesnames"]) - - assert_equal(peer_info[0][0]['connection_type'], 'inbound') - assert_equal(peer_info[0][1]['connection_type'], 'manual') - - assert_equal(peer_info[1][0]['connection_type'], 'manual') - assert_equal(peer_info[1][1]['connection_type'], 'inbound') - def test_service_flags(self): self.log.info("Test service flags") self.nodes[0].add_p2p_connection(P2PInterface(), services=(1 << 4) | (1 << 63)) diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py index 32dc2f8644..ca75bcb9bb 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -172,8 +172,11 @@ class PSBTTest(BitcoinTestFramework): elif out['scriptPubKey']['addresses'][0] == p2pkh: p2pkh_pos = out['n'] + inputs = [{"txid": txid, "vout": p2wpkh_pos}, {"txid": txid, "vout": p2sh_p2wpkh_pos}, {"txid": txid, "vout": p2pkh_pos}] + outputs = [{self.nodes[1].getnewaddress(): 29.99}] + # spend single key from node 1 - created_psbt = self.nodes[1].walletcreatefundedpsbt([{"txid":txid,"vout":p2wpkh_pos},{"txid":txid,"vout":p2sh_p2wpkh_pos},{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():29.99}) + created_psbt = self.nodes[1].walletcreatefundedpsbt(inputs, outputs) walletprocesspsbt_out = self.nodes[1].walletprocesspsbt(created_psbt['psbt']) # Make sure it has both types of UTXOs decoded = self.nodes[1].decodepsbt(walletprocesspsbt_out['psbt']) @@ -184,15 +187,62 @@ class PSBTTest(BitcoinTestFramework): assert_equal(walletprocesspsbt_out['complete'], True) self.nodes[1].sendrawtransaction(self.nodes[1].finalizepsbt(walletprocesspsbt_out['psbt'])['hex']) - # feeRate of 0.1 BTC / KB produces a total fee slightly below -maxtxfee (~0.05280000): - res = self.nodes[1].walletcreatefundedpsbt([{"txid":txid,"vout":p2wpkh_pos},{"txid":txid,"vout":p2sh_p2wpkh_pos},{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():29.99}, 0, {"feeRate": 0.1, "add_inputs": True}) + self.log.info("Test walletcreatefundedpsbt feeRate of 0.1 BTC/kB produces a total fee at or slightly below -maxtxfee (~0.05290000)") + res = self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"feeRate": 0.1, "add_inputs": True}) assert_approx(res["fee"], 0.055, 0.005) - # feeRate of 10 BTC / KB produces a total fee well above -maxtxfee + self.log.info("Test walletcreatefundedpsbt explicit fee rate with conf_target and estimate_mode") + for unit, fee_rate in {"btc/kb": 0.1, "sat/b": 10000}.items(): + fee = self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"conf_target": fee_rate, "estimate_mode": unit, "add_inputs": True})["fee"] + self.log.info("- conf_target {}, estimate_mode {} produces fee {} at or slightly below -maxtxfee (~0.05290000)".format(fee_rate, unit, fee)) + assert_approx(fee, vexp=0.055, vspan=0.005) + + for field, fee_rate in {"conf_target": 0.1, "estimate_mode": "sat/b"}.items(): + self.log.info("- raises RPC error if both feeRate and {} are passed".format(field)) + assert_raises_rpc_error(-8, "Cannot specify both {} and feeRate".format(field), + lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"feeRate": 0.1, field: fee_rate, "add_inputs": True})) + + self.log.info("- raises RPC error with invalid estimate_mode settings") + for k, v in {"number": 42, "object": {"foo": "bar"}}.items(): + assert_raises_rpc_error(-3, "Expected type string for estimate_mode, got {}".format(k), + lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"estimate_mode": v, "conf_target": 0.1, "add_inputs": True})) + for mode in ["foo", Decimal("3.141592")]: + assert_raises_rpc_error(-8, "Invalid estimate_mode parameter", + lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"estimate_mode": mode, "conf_target": 0.1, "add_inputs": True})) + + self.log.info("- raises RPC error if estimate_mode is passed without a conf_target") + for unit in ["SAT/B", "BTC/KB"]: + assert_raises_rpc_error(-8, "Selected estimate_mode {} requires a fee rate to be specified in conf_target".format(unit), + lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"estimate_mode": unit})) + + self.log.info("- raises RPC error with invalid conf_target settings") + for mode in ["unset", "economical", "conservative", "btc/kb", "sat/b"]: + self.log.debug("{}".format(mode)) + for k, v in {"string": "", "object": {"foo": "bar"}}.items(): + assert_raises_rpc_error(-3, "Expected type number for conf_target, got {}".format(k), + lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"estimate_mode": mode, "conf_target": v, "add_inputs": True})) + if mode in ["btc/kb", "sat/b"]: + assert_raises_rpc_error(-3, "Amount out of range", + lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"estimate_mode": mode, "conf_target": -1, "add_inputs": True})) + assert_raises_rpc_error(-4, "Fee rate (0.00000000 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)", + lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"estimate_mode": mode, "conf_target": 0, "add_inputs": True})) + else: + for n in [-1, 0, 1009]: + assert_raises_rpc_error(-8, "Invalid conf_target, must be between 1 and 1008", + lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"estimate_mode": mode, "conf_target": n, "add_inputs": True})) + + for unit, fee_rate in {"SAT/B": 0.99999999, "BTC/KB": 0.00000999}.items(): + self.log.info("- raises RPC error 'fee rate too low' if conf_target {} and estimate_mode {} are passed".format(fee_rate, unit)) + assert_raises_rpc_error(-4, "Fee rate (0.00000999 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)", + lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"estimate_mode": unit, "conf_target": fee_rate, "add_inputs": True})) + + self.log.info("Test walletcreatefundedpsbt feeRate of 10 BTC/kB produces total fee well above -maxtxfee and raises RPC error") # previously this was silently capped at -maxtxfee - assert_raises_rpc_error(-4, "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)", self.nodes[1].walletcreatefundedpsbt, [{"txid":txid,"vout":p2wpkh_pos},{"txid":txid,"vout":p2sh_p2wpkh_pos},{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():29.99}, 0, {"feeRate": 10, "add_inputs": True}) - assert_raises_rpc_error(-4, "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)", self.nodes[1].walletcreatefundedpsbt, [{"txid":txid,"vout":p2wpkh_pos},{"txid":txid,"vout":p2sh_p2wpkh_pos},{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():1}, 0, {"feeRate": 10, "add_inputs": False}) + for bool_add, outputs_array in {True: outputs, False: [{self.nodes[1].getnewaddress(): 1}]}.items(): + assert_raises_rpc_error(-4, "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)", + self.nodes[1].walletcreatefundedpsbt, inputs, outputs_array, 0, {"feeRate": 10, "add_inputs": bool_add}) + self.log.info("Test various PSBT operations") # partially sign multisig things with node 1 psbtx = wmulti.walletcreatefundedpsbt(inputs=[{"txid":txid,"vout":p2wsh_pos},{"txid":txid,"vout":p2sh_pos},{"txid":txid,"vout":p2sh_p2wsh_pos}], outputs={self.nodes[1].getnewaddress():29.99}, options={'changeAddress': self.nodes[1].getrawchangeaddress()})['psbt'] walletprocesspsbt_out = self.nodes[1].walletprocesspsbt(psbtx) diff --git a/test/functional/rpc_rawtransaction.py b/test/functional/rpc_rawtransaction.py index 326495843f..554c30c0d2 100755 --- a/test/functional/rpc_rawtransaction.py +++ b/test/functional/rpc_rawtransaction.py @@ -20,6 +20,7 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_raises_rpc_error, + find_vout_for_address, hex_str_to_bytes, ) @@ -242,121 +243,124 @@ class RawTransactionsTest(BitcoinTestFramework): self.nodes[0].reconsiderblock(block1) assert_equal(self.nodes[0].getbestblockhash(), block2) - ######################### - # RAW TX MULTISIG TESTS # - ######################### - # 2of2 test - addr1 = self.nodes[2].getnewaddress() - addr2 = self.nodes[2].getnewaddress() - - addr1Obj = self.nodes[2].getaddressinfo(addr1) - addr2Obj = self.nodes[2].getaddressinfo(addr2) - - # Tests for createmultisig and addmultisigaddress - assert_raises_rpc_error(-5, "Invalid public key", self.nodes[0].createmultisig, 1, ["01020304"]) - self.nodes[0].createmultisig(2, [addr1Obj['pubkey'], addr2Obj['pubkey']]) # createmultisig can only take public keys - assert_raises_rpc_error(-5, "Invalid public key", self.nodes[0].createmultisig, 2, [addr1Obj['pubkey'], addr1]) # addmultisigaddress can take both pubkeys and addresses so long as they are in the wallet, which is tested here. - - mSigObj = self.nodes[2].addmultisigaddress(2, [addr1Obj['pubkey'], addr1])['address'] - - #use balance deltas instead of absolute values - bal = self.nodes[2].getbalance() - - # send 1.2 BTC to msig adr - txId = self.nodes[0].sendtoaddress(mSigObj, 1.2) - self.sync_all() - self.nodes[0].generate(1) - self.sync_all() - assert_equal(self.nodes[2].getbalance(), bal+Decimal('1.20000000')) #node2 has both keys of the 2of2 ms addr., tx should affect the balance - - - # 2of3 test from different nodes - bal = self.nodes[2].getbalance() - addr1 = self.nodes[1].getnewaddress() - addr2 = self.nodes[2].getnewaddress() - addr3 = self.nodes[2].getnewaddress() - - addr1Obj = self.nodes[1].getaddressinfo(addr1) - addr2Obj = self.nodes[2].getaddressinfo(addr2) - addr3Obj = self.nodes[2].getaddressinfo(addr3) - - mSigObj = self.nodes[2].addmultisigaddress(2, [addr1Obj['pubkey'], addr2Obj['pubkey'], addr3Obj['pubkey']])['address'] - - txId = self.nodes[0].sendtoaddress(mSigObj, 2.2) - decTx = self.nodes[0].gettransaction(txId) - rawTx = self.nodes[0].decoderawtransaction(decTx['hex']) - self.sync_all() - self.nodes[0].generate(1) - self.sync_all() - - #THIS IS AN INCOMPLETE FEATURE - #NODE2 HAS TWO OF THREE KEY AND THE FUNDS SHOULD BE SPENDABLE AND COUNT AT BALANCE CALCULATION - assert_equal(self.nodes[2].getbalance(), bal) #for now, assume the funds of a 2of3 multisig tx are not marked as spendable - - txDetails = self.nodes[0].gettransaction(txId, True) - rawTx = self.nodes[0].decoderawtransaction(txDetails['hex']) - vout = next(o for o in rawTx['vout'] if o['value'] == Decimal('2.20000000')) - - bal = self.nodes[0].getbalance() - inputs = [{ "txid" : txId, "vout" : vout['n'], "scriptPubKey" : vout['scriptPubKey']['hex'], "amount" : vout['value']}] - outputs = { self.nodes[0].getnewaddress() : 2.19 } - rawTx = self.nodes[2].createrawtransaction(inputs, outputs) - rawTxPartialSigned = self.nodes[1].signrawtransactionwithwallet(rawTx, inputs) - assert_equal(rawTxPartialSigned['complete'], False) #node1 only has one key, can't comp. sign the tx - - rawTxSigned = self.nodes[2].signrawtransactionwithwallet(rawTx, inputs) - assert_equal(rawTxSigned['complete'], True) #node2 can sign the tx compl., own two of three keys - self.nodes[2].sendrawtransaction(rawTxSigned['hex']) - rawTx = self.nodes[0].decoderawtransaction(rawTxSigned['hex']) - self.sync_all() - self.nodes[0].generate(1) - self.sync_all() - assert_equal(self.nodes[0].getbalance(), bal+Decimal('50.00000000')+Decimal('2.19000000')) #block reward + tx - - # 2of2 test for combining transactions - bal = self.nodes[2].getbalance() - addr1 = self.nodes[1].getnewaddress() - addr2 = self.nodes[2].getnewaddress() - - addr1Obj = self.nodes[1].getaddressinfo(addr1) - addr2Obj = self.nodes[2].getaddressinfo(addr2) - - self.nodes[1].addmultisigaddress(2, [addr1Obj['pubkey'], addr2Obj['pubkey']])['address'] - mSigObj = self.nodes[2].addmultisigaddress(2, [addr1Obj['pubkey'], addr2Obj['pubkey']])['address'] - mSigObjValid = self.nodes[2].getaddressinfo(mSigObj) - - txId = self.nodes[0].sendtoaddress(mSigObj, 2.2) - decTx = self.nodes[0].gettransaction(txId) - rawTx2 = self.nodes[0].decoderawtransaction(decTx['hex']) - self.sync_all() - self.nodes[0].generate(1) - self.sync_all() - - assert_equal(self.nodes[2].getbalance(), bal) # the funds of a 2of2 multisig tx should not be marked as spendable - - txDetails = self.nodes[0].gettransaction(txId, True) - rawTx2 = self.nodes[0].decoderawtransaction(txDetails['hex']) - vout = next(o for o in rawTx2['vout'] if o['value'] == Decimal('2.20000000')) - - bal = self.nodes[0].getbalance() - inputs = [{ "txid" : txId, "vout" : vout['n'], "scriptPubKey" : vout['scriptPubKey']['hex'], "redeemScript" : mSigObjValid['hex'], "amount" : vout['value']}] - outputs = { self.nodes[0].getnewaddress() : 2.19 } - rawTx2 = self.nodes[2].createrawtransaction(inputs, outputs) - rawTxPartialSigned1 = self.nodes[1].signrawtransactionwithwallet(rawTx2, inputs) - self.log.debug(rawTxPartialSigned1) - assert_equal(rawTxPartialSigned1['complete'], False) #node1 only has one key, can't comp. sign the tx - - rawTxPartialSigned2 = self.nodes[2].signrawtransactionwithwallet(rawTx2, inputs) - self.log.debug(rawTxPartialSigned2) - assert_equal(rawTxPartialSigned2['complete'], False) #node2 only has one key, can't comp. sign the tx - rawTxComb = self.nodes[2].combinerawtransaction([rawTxPartialSigned1['hex'], rawTxPartialSigned2['hex']]) - self.log.debug(rawTxComb) - self.nodes[2].sendrawtransaction(rawTxComb) - rawTx2 = self.nodes[0].decoderawtransaction(rawTxComb) - self.sync_all() - self.nodes[0].generate(1) - self.sync_all() - assert_equal(self.nodes[0].getbalance(), bal+Decimal('50.00000000')+Decimal('2.19000000')) #block reward + tx + if not self.options.descriptors: + # The traditional multisig workflow does not work with descriptor wallets so these are legacy only. + # The multisig workflow with descriptor wallets uses PSBTs and is tested elsewhere, no need to do them here. + ######################### + # RAW TX MULTISIG TESTS # + ######################### + # 2of2 test + addr1 = self.nodes[2].getnewaddress() + addr2 = self.nodes[2].getnewaddress() + + addr1Obj = self.nodes[2].getaddressinfo(addr1) + addr2Obj = self.nodes[2].getaddressinfo(addr2) + + # Tests for createmultisig and addmultisigaddress + assert_raises_rpc_error(-5, "Invalid public key", self.nodes[0].createmultisig, 1, ["01020304"]) + self.nodes[0].createmultisig(2, [addr1Obj['pubkey'], addr2Obj['pubkey']]) # createmultisig can only take public keys + assert_raises_rpc_error(-5, "Invalid public key", self.nodes[0].createmultisig, 2, [addr1Obj['pubkey'], addr1]) # addmultisigaddress can take both pubkeys and addresses so long as they are in the wallet, which is tested here. + + mSigObj = self.nodes[2].addmultisigaddress(2, [addr1Obj['pubkey'], addr1])['address'] + + #use balance deltas instead of absolute values + bal = self.nodes[2].getbalance() + + # send 1.2 BTC to msig adr + txId = self.nodes[0].sendtoaddress(mSigObj, 1.2) + self.sync_all() + self.nodes[0].generate(1) + self.sync_all() + assert_equal(self.nodes[2].getbalance(), bal+Decimal('1.20000000')) #node2 has both keys of the 2of2 ms addr., tx should affect the balance + + + # 2of3 test from different nodes + bal = self.nodes[2].getbalance() + addr1 = self.nodes[1].getnewaddress() + addr2 = self.nodes[2].getnewaddress() + addr3 = self.nodes[2].getnewaddress() + + addr1Obj = self.nodes[1].getaddressinfo(addr1) + addr2Obj = self.nodes[2].getaddressinfo(addr2) + addr3Obj = self.nodes[2].getaddressinfo(addr3) + + mSigObj = self.nodes[2].addmultisigaddress(2, [addr1Obj['pubkey'], addr2Obj['pubkey'], addr3Obj['pubkey']])['address'] + + txId = self.nodes[0].sendtoaddress(mSigObj, 2.2) + decTx = self.nodes[0].gettransaction(txId) + rawTx = self.nodes[0].decoderawtransaction(decTx['hex']) + self.sync_all() + self.nodes[0].generate(1) + self.sync_all() + + #THIS IS AN INCOMPLETE FEATURE + #NODE2 HAS TWO OF THREE KEY AND THE FUNDS SHOULD BE SPENDABLE AND COUNT AT BALANCE CALCULATION + assert_equal(self.nodes[2].getbalance(), bal) #for now, assume the funds of a 2of3 multisig tx are not marked as spendable + + txDetails = self.nodes[0].gettransaction(txId, True) + rawTx = self.nodes[0].decoderawtransaction(txDetails['hex']) + vout = next(o for o in rawTx['vout'] if o['value'] == Decimal('2.20000000')) + + bal = self.nodes[0].getbalance() + inputs = [{ "txid" : txId, "vout" : vout['n'], "scriptPubKey" : vout['scriptPubKey']['hex'], "amount" : vout['value']}] + outputs = { self.nodes[0].getnewaddress() : 2.19 } + rawTx = self.nodes[2].createrawtransaction(inputs, outputs) + rawTxPartialSigned = self.nodes[1].signrawtransactionwithwallet(rawTx, inputs) + assert_equal(rawTxPartialSigned['complete'], False) #node1 only has one key, can't comp. sign the tx + + rawTxSigned = self.nodes[2].signrawtransactionwithwallet(rawTx, inputs) + assert_equal(rawTxSigned['complete'], True) #node2 can sign the tx compl., own two of three keys + self.nodes[2].sendrawtransaction(rawTxSigned['hex']) + rawTx = self.nodes[0].decoderawtransaction(rawTxSigned['hex']) + self.sync_all() + self.nodes[0].generate(1) + self.sync_all() + assert_equal(self.nodes[0].getbalance(), bal+Decimal('50.00000000')+Decimal('2.19000000')) #block reward + tx + + # 2of2 test for combining transactions + bal = self.nodes[2].getbalance() + addr1 = self.nodes[1].getnewaddress() + addr2 = self.nodes[2].getnewaddress() + + addr1Obj = self.nodes[1].getaddressinfo(addr1) + addr2Obj = self.nodes[2].getaddressinfo(addr2) + + self.nodes[1].addmultisigaddress(2, [addr1Obj['pubkey'], addr2Obj['pubkey']])['address'] + mSigObj = self.nodes[2].addmultisigaddress(2, [addr1Obj['pubkey'], addr2Obj['pubkey']])['address'] + mSigObjValid = self.nodes[2].getaddressinfo(mSigObj) + + txId = self.nodes[0].sendtoaddress(mSigObj, 2.2) + decTx = self.nodes[0].gettransaction(txId) + rawTx2 = self.nodes[0].decoderawtransaction(decTx['hex']) + self.sync_all() + self.nodes[0].generate(1) + self.sync_all() + + assert_equal(self.nodes[2].getbalance(), bal) # the funds of a 2of2 multisig tx should not be marked as spendable + + txDetails = self.nodes[0].gettransaction(txId, True) + rawTx2 = self.nodes[0].decoderawtransaction(txDetails['hex']) + vout = next(o for o in rawTx2['vout'] if o['value'] == Decimal('2.20000000')) + + bal = self.nodes[0].getbalance() + inputs = [{ "txid" : txId, "vout" : vout['n'], "scriptPubKey" : vout['scriptPubKey']['hex'], "redeemScript" : mSigObjValid['hex'], "amount" : vout['value']}] + outputs = { self.nodes[0].getnewaddress() : 2.19 } + rawTx2 = self.nodes[2].createrawtransaction(inputs, outputs) + rawTxPartialSigned1 = self.nodes[1].signrawtransactionwithwallet(rawTx2, inputs) + self.log.debug(rawTxPartialSigned1) + assert_equal(rawTxPartialSigned1['complete'], False) #node1 only has one key, can't comp. sign the tx + + rawTxPartialSigned2 = self.nodes[2].signrawtransactionwithwallet(rawTx2, inputs) + self.log.debug(rawTxPartialSigned2) + assert_equal(rawTxPartialSigned2['complete'], False) #node2 only has one key, can't comp. sign the tx + rawTxComb = self.nodes[2].combinerawtransaction([rawTxPartialSigned1['hex'], rawTxPartialSigned2['hex']]) + self.log.debug(rawTxComb) + self.nodes[2].sendrawtransaction(rawTxComb) + rawTx2 = self.nodes[0].decoderawtransaction(rawTxComb) + self.sync_all() + self.nodes[0].generate(1) + self.sync_all() + assert_equal(self.nodes[0].getbalance(), bal+Decimal('50.00000000')+Decimal('2.19000000')) #block reward + tx # decoderawtransaction tests # witness transaction @@ -369,9 +373,20 @@ class RawTransactionsTest(BitcoinTestFramework): decrawtx = self.nodes[0].decoderawtransaction(encrawtx, False) # decode as non-witness transaction assert_equal(decrawtx['vout'][0]['value'], Decimal('1.00000000')) + # Basic signrawtransaction test + addr = self.nodes[1].getnewaddress() + txid = self.nodes[0].sendtoaddress(addr, 10) + self.nodes[0].generate(1) + self.sync_all() + vout = find_vout_for_address(self.nodes[1], txid, addr) + rawTx = self.nodes[1].createrawtransaction([{'txid': txid, 'vout': vout}], {self.nodes[1].getnewaddress(): 9.999}) + rawTxSigned = self.nodes[1].signrawtransactionwithwallet(rawTx) + txId = self.nodes[1].sendrawtransaction(rawTxSigned['hex']) + self.nodes[0].generate(1) + self.sync_all() + # getrawtransaction tests # 1. valid parameters - only supply txid - txId = rawTx["txid"] assert_equal(self.nodes[0].getrawtransaction(txId), rawTxSigned['hex']) # 2. valid parameters - supply txid and 0 for non-verbose diff --git a/test/functional/rpc_scantxoutset.py b/test/functional/rpc_scantxoutset.py index 861b394e70..070f59d314 100755 --- a/test/functional/rpc_scantxoutset.py +++ b/test/functional/rpc_scantxoutset.py @@ -55,7 +55,8 @@ class ScantxoutsetTest(BitcoinTestFramework): self.log.info("Stop node, remove wallet, mine again some blocks...") self.stop_node(0) shutil.rmtree(os.path.join(self.nodes[0].datadir, self.chain, 'wallets')) - self.start_node(0) + self.start_node(0, ['-nowallet']) + self.import_deterministic_coinbase_privkeys() self.nodes[0].generate(110) scan = self.nodes[0].scantxoutset("start", []) diff --git a/test/functional/rpc_signrawtransaction.py b/test/functional/rpc_signrawtransaction.py index 704b65c060..b962e1c3a5 100755 --- a/test/functional/rpc_signrawtransaction.py +++ b/test/functional/rpc_signrawtransaction.py @@ -5,10 +5,13 @@ """Test transaction signing using the signrawtransaction* RPCs.""" from test_framework.address import check_script, script_to_p2sh +from test_framework.key import ECKey from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error, find_vout_for_address, hex_str_to_bytes from test_framework.messages import sha256 from test_framework.script import CScript, OP_0, OP_CHECKSIG +from test_framework.script_util import key_to_p2pkh_script, script_to_p2sh_p2wsh_script, script_to_p2wsh_script +from test_framework.wallet_util import bytes_to_wif from decimal import Decimal @@ -151,21 +154,24 @@ class SignRawTransactionsTest(BitcoinTestFramework): def witness_script_test(self): self.log.info("Test signing transaction to P2SH-P2WSH addresses without wallet") # Create a new P2SH-P2WSH 1-of-1 multisig address: - embedded_address = self.nodes[1].getaddressinfo(self.nodes[1].getnewaddress()) - embedded_privkey = self.nodes[1].dumpprivkey(embedded_address["address"]) - p2sh_p2wsh_address = self.nodes[1].addmultisigaddress(1, [embedded_address["pubkey"]], "", "p2sh-segwit") + eckey = ECKey() + eckey.generate() + embedded_privkey = bytes_to_wif(eckey.get_bytes()) + embedded_pubkey = eckey.get_pubkey().get_bytes().hex() + p2sh_p2wsh_address = self.nodes[1].createmultisig(1, [embedded_pubkey], "p2sh-segwit") # send transaction to P2SH-P2WSH 1-of-1 multisig address self.nodes[0].generate(101) self.nodes[0].sendtoaddress(p2sh_p2wsh_address["address"], 49.999) self.nodes[0].generate(1) self.sync_all() - # Find the UTXO for the transaction node[1] should have received, check witnessScript matches - unspent_output = self.nodes[1].listunspent(0, 999999, [p2sh_p2wsh_address["address"]])[0] - assert_equal(unspent_output["witnessScript"], p2sh_p2wsh_address["redeemScript"]) - p2sh_redeemScript = CScript([OP_0, sha256(hex_str_to_bytes(p2sh_p2wsh_address["redeemScript"]))]) - assert_equal(unspent_output["redeemScript"], p2sh_redeemScript.hex()) + # Get the UTXO info from scantxoutset + unspent_output = self.nodes[1].scantxoutset('start', [p2sh_p2wsh_address['descriptor']])['unspents'][0] + spk = script_to_p2sh_p2wsh_script(p2sh_p2wsh_address['redeemScript']).hex() + unspent_output['witnessScript'] = p2sh_p2wsh_address['redeemScript'] + unspent_output['redeemScript'] = script_to_p2wsh_script(unspent_output['witnessScript']).hex() + assert_equal(spk, unspent_output['scriptPubKey']) # Now create and sign a transaction spending that output on node[0], which doesn't know the scripts or keys - spending_tx = self.nodes[0].createrawtransaction([unspent_output], {self.nodes[1].getnewaddress(): Decimal("49.998")}) + spending_tx = self.nodes[0].createrawtransaction([unspent_output], {self.nodes[1].get_wallet_rpc(self.default_wallet_name).getnewaddress(): Decimal("49.998")}) spending_tx_signed = self.nodes[0].signrawtransactionwithkey(spending_tx, [embedded_privkey], [unspent_output]) # Check the signing completed successfully assert 'complete' in spending_tx_signed @@ -177,11 +183,13 @@ class SignRawTransactionsTest(BitcoinTestFramework): def verify_txn_with_witness_script(self, tx_type): self.log.info("Test with a {} script as the witnessScript".format(tx_type)) - embedded_addr_info = self.nodes[1].getaddressinfo(self.nodes[1].getnewaddress('', 'legacy')) - embedded_privkey = self.nodes[1].dumpprivkey(embedded_addr_info['address']) + eckey = ECKey() + eckey.generate() + embedded_privkey = bytes_to_wif(eckey.get_bytes()) + embedded_pubkey = eckey.get_pubkey().get_bytes().hex() witness_script = { - 'P2PKH': embedded_addr_info['scriptPubKey'], - 'P2PK': CScript([hex_str_to_bytes(embedded_addr_info['pubkey']), OP_CHECKSIG]).hex() + 'P2PKH': key_to_p2pkh_script(embedded_pubkey).hex(), + 'P2PK': CScript([hex_str_to_bytes(embedded_pubkey), OP_CHECKSIG]).hex() }.get(tx_type, "Invalid tx_type") redeem_script = CScript([OP_0, sha256(check_script(witness_script))]).hex() addr = script_to_p2sh(redeem_script) diff --git a/test/functional/test_framework/blocktools.py b/test/functional/test_framework/blocktools.py index 0859380d06..6b7214f03a 100644 --- a/test/functional/test_framework/blocktools.py +++ b/test/functional/test_framework/blocktools.py @@ -5,7 +5,6 @@ """Utilities for manipulating blocks and transactions.""" from binascii import a2b_hex -import io import struct import time import unittest @@ -45,7 +44,6 @@ from .script import ( hash160, ) from .util import assert_equal -from io import BytesIO WITNESS_SCALE_FACTOR = 4 MAX_BLOCK_SIGOPS = 20000 @@ -78,9 +76,7 @@ def create_block(hashprev=None, coinbase=None, ntime=None, *, version=None, tmpl if txlist: for tx in txlist: if not hasattr(tx, 'calc_sha256'): - txo = CTransaction() - txo.deserialize(io.BytesIO(tx)) - tx = txo + tx = FromHex(CTransaction(), tx) block.vtx.append(tx) block.hashMerkleRoot = block.calc_merkle_root() block.calc_sha256() @@ -162,25 +158,27 @@ def create_tx_with_script(prevtx, n, script_sig=b"", *, amount, script_pub_key=C def create_transaction(node, txid, to_address, *, amount): """ Return signed transaction spending the first output of the - input txid. Note that the node must be able to sign for the - output that is being spent, and the node must not be running - multiple wallets. + input txid. Note that the node must have a wallet that can + sign for the output that is being spent. """ raw_tx = create_raw_transaction(node, txid, to_address, amount=amount) - tx = CTransaction() - tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx))) + tx = FromHex(CTransaction(), raw_tx) return tx def create_raw_transaction(node, txid, to_address, *, amount): """ Return raw signed transaction spending the first output of the - input txid. Note that the node must be able to sign for the - output that is being spent, and the node must not be running - multiple wallets. + input txid. Note that the node must have a wallet that can sign + for the output that is being spent. """ - rawtx = node.createrawtransaction(inputs=[{"txid": txid, "vout": 0}], outputs={to_address: amount}) - signresult = node.signrawtransactionwithwallet(rawtx) - assert_equal(signresult["complete"], True) - return signresult['hex'] + psbt = node.createpsbt(inputs=[{"txid": txid, "vout": 0}], outputs={to_address: amount}) + for _ in range(2): + for w in node.listwallets(): + wrpc = node.get_wallet_rpc(w) + signed_psbt = wrpc.walletprocesspsbt(psbt) + psbt = signed_psbt['psbt'] + final_psbt = node.finalizepsbt(psbt) + assert_equal(final_psbt["complete"], True) + return final_psbt['hex'] def get_legacy_sigopcount_block(block, accurate=True): count = 0 diff --git a/test/functional/test_framework/key.py b/test/functional/test_framework/key.py index a6bc187985..f3d13c049b 100644 --- a/test/functional/test_framework/key.py +++ b/test/functional/test_framework/key.py @@ -10,7 +10,6 @@ import csv import hashlib import os import random -import sys import unittest from .util import modinv @@ -22,6 +21,7 @@ def TaggedHash(tag, data): return hashlib.sha256(ss).digest() def xor_bytes(b0, b1): + assert len(b0) == len(b1) return bytes(x ^ y for (x, y) in zip(b0, b1)) def jacobi_symbol(n, k): @@ -322,7 +322,7 @@ class ECPubKey(): u1 = z*w % SECP256K1_ORDER u2 = r*w % SECP256K1_ORDER R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, u1), (self.p, u2)])) - if R is None or R[0] != r: + if R is None or (R[0] % SECP256K1_ORDER) != r: return False return True @@ -523,7 +523,8 @@ class TestFrameworkKey(unittest.TestCase): def test_schnorr_testvectors(self): """Implement the BIP340 test vectors (read from bip340_test_vectors.csv).""" num_tests = 0 - with open(os.path.join(sys.path[0], 'test_framework', 'bip340_test_vectors.csv'), newline='', encoding='utf8') as csvfile: + vectors_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'bip340_test_vectors.csv') + with open(vectors_file, newline='', encoding='utf8') as csvfile: reader = csv.reader(csvfile) next(reader) for row in reader: diff --git a/test/functional/test_framework/script_util.py b/test/functional/test_framework/script_util.py index 80fbae70bf..318a438705 100755 --- a/test/functional/test_framework/script_util.py +++ b/test/functional/test_framework/script_util.py @@ -3,7 +3,8 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Useful Script constants and utils.""" -from test_framework.script import CScript +from test_framework.script import CScript, hash160, sha256, OP_0, OP_DUP, OP_HASH160, OP_CHECKSIG, OP_EQUAL, OP_EQUALVERIFY +from test_framework.util import hex_str_to_bytes # To prevent a "tx-size-small" policy rule error, a transaction has to have a # non-witness size of at least 82 bytes (MIN_STANDARD_TX_NONWITNESS_SIZE in @@ -24,3 +25,59 @@ from test_framework.script import CScript # met. DUMMY_P2WPKH_SCRIPT = CScript([b'a' * 21]) DUMMY_2_P2WPKH_SCRIPT = CScript([b'b' * 21]) + +def keyhash_to_p2pkh_script(hash, main = False): + assert len(hash) == 20 + return CScript([OP_DUP, OP_HASH160, hash, OP_EQUALVERIFY, OP_CHECKSIG]) + +def scripthash_to_p2sh_script(hash, main = False): + assert len(hash) == 20 + return CScript([OP_HASH160, hash, OP_EQUAL]) + +def key_to_p2pkh_script(key, main = False): + key = check_key(key) + return keyhash_to_p2pkh_script(hash160(key), main) + +def script_to_p2sh_script(script, main = False): + script = check_script(script) + return scripthash_to_p2sh_script(hash160(script), main) + +def key_to_p2sh_p2wpkh_script(key, main = False): + key = check_key(key) + p2shscript = CScript([OP_0, hash160(key)]) + return script_to_p2sh_script(p2shscript, main) + +def program_to_witness_script(version, program, main = False): + if isinstance(program, str): + program = hex_str_to_bytes(program) + assert 0 <= version <= 16 + assert 2 <= len(program) <= 40 + assert version > 0 or len(program) in [20, 32] + return CScript([version, program]) + +def script_to_p2wsh_script(script, main = False): + script = check_script(script) + return program_to_witness_script(0, sha256(script), main) + +def key_to_p2wpkh_script(key, main = False): + key = check_key(key) + return program_to_witness_script(0, hash160(key), main) + +def script_to_p2sh_p2wsh_script(script, main = False): + script = check_script(script) + p2shscript = CScript([OP_0, sha256(script)]) + return script_to_p2sh_script(p2shscript, main) + +def check_key(key): + if isinstance(key, str): + key = hex_str_to_bytes(key) # Assuming this is hex string + if isinstance(key, bytes) and (len(key) == 33 or len(key) == 65): + return key + assert False + +def check_script(script): + if isinstance(script, str): + script = hex_str_to_bytes(script) # Assuming this is hex string + if isinstance(script, bytes) or isinstance(script, CScript): + return script + assert False diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index 20f608a9cf..831599913d 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -111,6 +111,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): # are not imported. self.wallet_names = None self.set_test_params() + assert self.wallet_names is None or len(self.wallet_names) <= self.num_nodes if self.options.timeout_factor == 0 : self.options.timeout_factor = 99999 self.rpc_timeout = int(self.rpc_timeout * self.options.timeout_factor) # optionally, increase timeout by a factor @@ -182,9 +183,14 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): help="run nodes under the valgrind memory error detector: expect at least a ~10x slowdown, valgrind 3.14 or later required") parser.add_argument("--randomseed", type=int, help="set a random seed for deterministically reproducing a previous test run") - parser.add_argument("--descriptors", default=False, action="store_true", - help="Run test using a descriptor wallet") parser.add_argument('--timeout-factor', dest="timeout_factor", type=float, default=1.0, help='adjust test timeouts by a factor. Setting it to 0 disables all timeouts') + + group = parser.add_mutually_exclusive_group() + group.add_argument("--descriptors", default=False, action="store_true", + help="Run test using a descriptor wallet", dest='descriptors') + group.add_argument("--legacy-wallet", default=False, action="store_false", + help="Run test using legacy wallets", dest='descriptors') + self.add_options(parser) self.options = parser.parse_args() self.options.previous_releases_path = previous_releases_path @@ -390,9 +396,13 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): assert_equal(chain_info["initialblockdownload"], False) def import_deterministic_coinbase_privkeys(self): - wallet_names = [self.default_wallet_name] * len(self.nodes) if self.wallet_names is None else self.wallet_names - assert len(wallet_names) <= len(self.nodes) - for wallet_name, n in zip(wallet_names, self.nodes): + for i in range(self.num_nodes): + self.init_wallet(i) + + def init_wallet(self, i): + wallet_name = self.default_wallet_name if self.wallet_names is None else self.wallet_names[i] if i < len(self.wallet_names) else False + if wallet_name is not False: + n = self.nodes[i] if wallet_name is not None: n.createwallet(wallet_name=wallet_name, descriptors=self.options.descriptors, load_on_startup=True) n.importprivkey(privkey=n.get_deterministic_priv_key().key, label='coinbase') @@ -764,6 +774,13 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): """Skip the running test if wallet has not been compiled.""" if not self.is_wallet_compiled(): raise SkipTest("wallet has not been compiled.") + if self.options.descriptors: + self.skip_if_no_sqlite() + + def skip_if_no_sqlite(self): + """Skip the running test if sqlite has not been compiled.""" + if not self.is_sqlite_compiled(): + raise SkipTest("sqlite has not been compiled.") def skip_if_no_wallet_tool(self): """Skip the running test if bitcoin-wallet has not been compiled.""" @@ -803,3 +820,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): def is_zmq_compiled(self): """Checks whether the zmq module was compiled.""" return self.config["components"].getboolean("ENABLE_ZMQ") + + def is_sqlite_compiled(self): + """Checks whether the wallet module was compiled.""" + return self.config["components"].getboolean("USE_SQLITE") diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 8cd82649b6..5b3db282e1 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -95,8 +95,9 @@ BASE_SCRIPTS = [ 'feature_maxuploadtarget.py', 'feature_block.py', 'rpc_fundrawtransaction.py', + 'rpc_fundrawtransaction.py --descriptors', 'p2p_compactblocks.py', - 'feature_segwit.py', + 'feature_segwit.py --legacy-wallet', # vv Tests less than 2m vv 'wallet_basic.py', 'wallet_basic.py --descriptors', @@ -106,19 +107,24 @@ BASE_SCRIPTS = [ 'p2p_timeouts.py', 'p2p_tx_download.py', 'mempool_updatefromblock.py', - 'wallet_dump.py', + 'wallet_dump.py --legacy-wallet', 'wallet_listtransactions.py', + 'wallet_listtransactions.py --descriptors', 'feature_taproot.py', # vv Tests less than 60s vv 'p2p_sendheaders.py', - 'wallet_importmulti.py', + 'wallet_importmulti.py --legacy-wallet', 'mempool_limit.py', 'rpc_txoutproof.py', 'wallet_listreceivedby.py', + 'wallet_listreceivedby.py --descriptors', 'wallet_abandonconflict.py', + 'wallet_abandonconflict.py --descriptors', 'feature_csv_activation.py', 'rpc_rawtransaction.py', + 'rpc_rawtransaction.py --descriptors', 'wallet_address_types.py', + 'wallet_address_types.py --descriptors', 'feature_bip68_sequence.py', 'p2p_feefilter.py', 'feature_reindex.py', @@ -132,6 +138,7 @@ BASE_SCRIPTS = [ 'mempool_resurrect.py', 'wallet_txn_doublespend.py --mineblock', 'tool_wallet.py', + 'tool_wallet.py --descriptors', 'wallet_txn_clone.py', 'wallet_txn_clone.py --segwit', 'rpc_getchaintips.py', @@ -147,8 +154,9 @@ BASE_SCRIPTS = [ 'wallet_multiwallet.py --usecli', 'wallet_createwallet.py', 'wallet_createwallet.py --usecli', - 'wallet_watchonly.py', - 'wallet_watchonly.py --usecli', + 'wallet_createwallet.py --descriptors', + 'wallet_watchonly.py --legacy-wallet', + 'wallet_watchonly.py --usecli --legacy-wallet', 'wallet_reorgsrestore.py', 'interface_http.py', 'interface_rpc.py', @@ -158,20 +166,23 @@ BASE_SCRIPTS = [ 'rpc_whitelist.py', 'feature_proxy.py', 'rpc_signrawtransaction.py', + 'rpc_signrawtransaction.py --descriptors', 'wallet_groups.py', 'p2p_addrv2_relay.py', + 'wallet_groups.py --descriptors', 'p2p_disconnect_ban.py', 'rpc_decodescript.py', 'rpc_blockchain.py', 'rpc_deprecated.py', 'wallet_disable.py', + 'wallet_disable.py --descriptors', 'p2p_addr_relay.py', 'p2p_getaddr_caching.py', 'p2p_getdata.py', 'rpc_net.py', 'wallet_keypool.py', 'wallet_keypool.py --descriptors', - 'wallet_descriptor.py', + 'wallet_descriptor.py --descriptors', 'p2p_nobloomfilter_messages.py', 'p2p_filter.py', 'rpc_setban.py', @@ -184,7 +195,9 @@ BASE_SCRIPTS = [ 'feature_assumevalid.py', 'example_test.py', 'wallet_txn_doublespend.py', + 'wallet_txn_doublespend.py --descriptors', 'feature_backwards_compatibility.py', + 'feature_backwards_compatibility.py --descriptors', 'wallet_txn_clone.py --mineblock', 'feature_notifications.py', 'rpc_getblockfilter.py', @@ -197,18 +210,21 @@ BASE_SCRIPTS = [ 'feature_versionbits_warning.py', 'rpc_preciousblock.py', 'wallet_importprunedfunds.py', + 'wallet_importprunedfunds.py --descriptors', 'p2p_leak_tx.py', 'p2p_eviction.py', 'rpc_signmessage.py', 'rpc_generateblock.py', 'rpc_generate.py', 'wallet_balance.py', + 'wallet_balance.py --descriptors', 'feature_nulldummy.py', + 'feature_nulldummy.py --descriptors', 'mempool_accept.py', 'mempool_expiry.py', - 'wallet_import_rescan.py', - 'wallet_import_with_label.py', - 'wallet_importdescriptors.py', + 'wallet_import_rescan.py --legacy-wallet', + 'wallet_import_with_label.py --legacy-wallet', + 'wallet_importdescriptors.py --descriptors', 'wallet_upgradewallet.py', 'rpc_bind.py --ipv4', 'rpc_bind.py --ipv6', @@ -216,9 +232,11 @@ BASE_SCRIPTS = [ 'mining_basic.py', 'feature_signet.py', 'wallet_bumpfee.py', - 'wallet_implicitsegwit.py', + 'wallet_bumpfee.py --descriptors', + 'wallet_implicitsegwit.py --legacy-wallet', 'rpc_named_arguments.py', 'wallet_listsinceblock.py', + 'wallet_listsinceblock.py --descriptors', 'p2p_leak.py', 'wallet_encryption.py', 'wallet_encryption.py --descriptors', @@ -226,16 +244,20 @@ BASE_SCRIPTS = [ 'feature_cltv.py', 'rpc_uptime.py', 'wallet_resendwallettransactions.py', + 'wallet_resendwallettransactions.py --descriptors', 'wallet_fallbackfee.py', + 'wallet_fallbackfee.py --descriptors', 'rpc_dumptxoutset.py', 'feature_minchainwork.py', 'rpc_estimatefee.py', 'rpc_getblockstats.py', 'wallet_create_tx.py', 'wallet_send.py', + 'wallet_create_tx.py --descriptors', 'p2p_fingerprint.py', 'feature_uacomment.py', 'wallet_coinbase_category.py', + 'wallet_coinbase_category.py --descriptors', 'feature_filelock.py', 'feature_loadblock.py', 'p2p_dos_header_tree.py', diff --git a/test/functional/tool_wallet.py b/test/functional/tool_wallet.py index 958341c691..615b772dc8 100755 --- a/test/functional/tool_wallet.py +++ b/test/functional/tool_wallet.py @@ -71,8 +71,11 @@ class ToolWalletTest(BitcoinTestFramework): self.assert_raises_tool_error('Error: two methods provided (info and create). Only one method should be provided.', 'info', 'create') self.assert_raises_tool_error('Error parsing command line arguments: Invalid parameter -foo', '-foo') locked_dir = os.path.join(self.options.tmpdir, "node0", "regtest", "wallets") + error = 'Error initializing wallet database environment "{}"!'.format(locked_dir) + if self.options.descriptors: + error = "SQLiteDatabase: Unable to obtain an exclusive lock on the database, is it being used by another bitcoind?" self.assert_raises_tool_error( - 'Error initializing wallet database environment "{}"!'.format(locked_dir), + error, '-wallet=' + self.default_wallet_name, 'info', ) @@ -95,19 +98,33 @@ 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 = textwrap.dedent('''\ - Wallet info - =========== - Name: \ + if self.options.descriptors: + out = textwrap.dedent('''\ + Wallet info + =========== + Name: default_wallet + Format: sqlite + Descriptors: yes + Encrypted: no + HD (hd seed available): yes + Keypool Size: 6 + Transactions: 0 + Address Book: 1 + ''') + else: + out = textwrap.dedent('''\ + Wallet info + =========== + Name: \ - Format: bdb - Descriptors: no - Encrypted: no - HD (hd seed available): yes - Keypool Size: 2 - Transactions: 0 - Address Book: 3 - ''') + Format: bdb + Descriptors: no + Encrypted: no + HD (hd seed available): yes + Keypool Size: 2 + Transactions: 0 + Address Book: 3 + ''') 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)) @@ -138,19 +155,33 @@ 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 = textwrap.dedent('''\ - Wallet info - =========== - Name: \ + if self.options.descriptors: + out = textwrap.dedent('''\ + Wallet info + =========== + Name: default_wallet + Format: sqlite + Descriptors: yes + Encrypted: no + HD (hd seed available): yes + Keypool Size: 6 + Transactions: 1 + Address Book: 1 + ''') + else: + out = textwrap.dedent('''\ + Wallet info + =========== + Name: \ - Format: bdb - Descriptors: no - Encrypted: no - HD (hd seed available): yes - Keypool Size: 2 - Transactions: 1 - Address Book: 3 - ''') + Format: bdb + Descriptors: no + Encrypted: no + HD (hd seed available): yes + Keypool Size: 2 + Transactions: 1 + Address Book: 3 + ''') self.assert_tool_output(out, '-wallet=' + self.default_wallet_name, 'info') shasum_after = self.wallet_shasum() timestamp_after = self.wallet_timestamp() @@ -218,7 +249,8 @@ class ToolWalletTest(BitcoinTestFramework): def test_salvage(self): # TODO: Check salvage actually salvages and doesn't break things. https://github.com/bitcoin/bitcoin/issues/7463 self.log.info('Check salvage') - self.start_node(0, ['-wallet=salvage']) + self.start_node(0) + self.nodes[0].createwallet("salvage") self.stop_node(0) self.assert_tool_output('', '-wallet=salvage', 'salvage') @@ -229,9 +261,12 @@ class ToolWalletTest(BitcoinTestFramework): # Warning: The following tests are order-dependent. self.test_tool_wallet_info() self.test_tool_wallet_info_after_transaction() - self.test_tool_wallet_create_on_existing_wallet() - self.test_getwalletinfo_on_different_wallet() - self.test_salvage() + if not self.options.descriptors: + # TODO: Wallet tool needs more create options at which point these can be enabled. + self.test_tool_wallet_create_on_existing_wallet() + self.test_getwalletinfo_on_different_wallet() + # Salvage is a legacy wallet only thing + self.test_salvage() if __name__ == '__main__': ToolWalletTest().main() diff --git a/test/functional/wallet_address_types.py b/test/functional/wallet_address_types.py index 6b3276e404..2db5eae33b 100755 --- a/test/functional/wallet_address_types.py +++ b/test/functional/wallet_address_types.py @@ -228,18 +228,25 @@ class AddressTypeTest(BitcoinTestFramework): compressed_1 = "0296b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52" compressed_2 = "037211a824f55b505228e4c3d5194c1fcfaa15a456abdf37f9b9d97a4040afc073" - # addmultisigaddress with at least 1 uncompressed key should return a legacy address. - for node in range(4): - self.test_address(node, self.nodes[node].addmultisigaddress(2, [uncompressed_1, uncompressed_2])['address'], True, 'legacy') - self.test_address(node, self.nodes[node].addmultisigaddress(2, [compressed_1, uncompressed_2])['address'], True, 'legacy') - self.test_address(node, self.nodes[node].addmultisigaddress(2, [uncompressed_1, compressed_2])['address'], True, 'legacy') - # addmultisigaddress with all compressed keys should return the appropriate address type (even when the keys are not ours). - self.test_address(0, self.nodes[0].addmultisigaddress(2, [compressed_1, compressed_2])['address'], True, 'legacy') - self.test_address(1, self.nodes[1].addmultisigaddress(2, [compressed_1, compressed_2])['address'], True, 'p2sh-segwit') - self.test_address(2, self.nodes[2].addmultisigaddress(2, [compressed_1, compressed_2])['address'], True, 'p2sh-segwit') - self.test_address(3, self.nodes[3].addmultisigaddress(2, [compressed_1, compressed_2])['address'], True, 'bech32') - - for explicit_type, multisig, from_node in itertools.product([False, True], [False, True], range(4)): + if not self.options.descriptors: + # Tests for addmultisigaddress's address type behavior is only for legacy wallets. + # Descriptor wallets do not have addmultsigaddress so these tests are not needed for those. + # addmultisigaddress with at least 1 uncompressed key should return a legacy address. + for node in range(4): + self.test_address(node, self.nodes[node].addmultisigaddress(2, [uncompressed_1, uncompressed_2])['address'], True, 'legacy') + self.test_address(node, self.nodes[node].addmultisigaddress(2, [compressed_1, uncompressed_2])['address'], True, 'legacy') + self.test_address(node, self.nodes[node].addmultisigaddress(2, [uncompressed_1, compressed_2])['address'], True, 'legacy') + # addmultisigaddress with all compressed keys should return the appropriate address type (even when the keys are not ours). + self.test_address(0, self.nodes[0].addmultisigaddress(2, [compressed_1, compressed_2])['address'], True, 'legacy') + self.test_address(1, self.nodes[1].addmultisigaddress(2, [compressed_1, compressed_2])['address'], True, 'p2sh-segwit') + self.test_address(2, self.nodes[2].addmultisigaddress(2, [compressed_1, compressed_2])['address'], True, 'p2sh-segwit') + self.test_address(3, self.nodes[3].addmultisigaddress(2, [compressed_1, compressed_2])['address'], True, 'bech32') + + do_multisigs = [False] + if not self.options.descriptors: + do_multisigs.append(True) + + for explicit_type, multisig, from_node in itertools.product([False, True], do_multisigs, range(4)): address_type = None if explicit_type and not multisig: if from_node == 1: diff --git a/test/functional/wallet_backup.py b/test/functional/wallet_backup.py index c7bd2ea02b..f34c1345e0 100755 --- a/test/functional/wallet_backup.py +++ b/test/functional/wallet_backup.py @@ -91,10 +91,10 @@ class WalletBackupTest(BitcoinTestFramework): self.sync_blocks() # As above, this mirrors the original bash test. - def start_three(self): - self.start_node(0) - self.start_node(1) - self.start_node(2) + def start_three(self, args=()): + self.start_node(0, self.extra_args[0] + list(args)) + self.start_node(1, self.extra_args[1] + list(args)) + self.start_node(2, self.extra_args[2] + list(args)) self.connect_nodes(0, 3) self.connect_nodes(1, 3) self.connect_nodes(2, 3) @@ -110,6 +110,11 @@ class WalletBackupTest(BitcoinTestFramework): os.remove(os.path.join(self.nodes[1].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename)) os.remove(os.path.join(self.nodes[2].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename)) + def init_three(self): + self.init_wallet(0) + self.init_wallet(1) + self.init_wallet(2) + def run_test(self): self.log.info("Generating initial blockchain") self.nodes[0].generate(1) @@ -193,7 +198,8 @@ class WalletBackupTest(BitcoinTestFramework): shutil.rmtree(os.path.join(self.nodes[2].datadir, self.chain, 'blocks')) shutil.rmtree(os.path.join(self.nodes[2].datadir, self.chain, 'chainstate')) - self.start_three() + self.start_three(["-nowallet"]) + self.init_three() assert_equal(self.nodes[0].getbalance(), 0) assert_equal(self.nodes[1].getbalance(), 0) diff --git a/test/functional/wallet_balance.py b/test/functional/wallet_balance.py index 589fab9992..0e786ca595 100755 --- a/test/functional/wallet_balance.py +++ b/test/functional/wallet_balance.py @@ -57,14 +57,16 @@ class WalletTest(BitcoinTestFramework): self.skip_if_no_wallet() def run_test(self): - self.nodes[0].importaddress(ADDRESS_WATCHONLY) - # Check that nodes don't own any UTXOs - assert_equal(len(self.nodes[0].listunspent()), 0) - assert_equal(len(self.nodes[1].listunspent()), 0) + if not self.options.descriptors: + # Tests legacy watchonly behavior which is not present (and does not need to be tested) in descriptor wallets + self.nodes[0].importaddress(ADDRESS_WATCHONLY) + # Check that nodes don't own any UTXOs + assert_equal(len(self.nodes[0].listunspent()), 0) + assert_equal(len(self.nodes[1].listunspent()), 0) - self.log.info("Check that only node 0 is watching an address") - assert 'watchonly' in self.nodes[0].getbalances() - assert 'watchonly' not in self.nodes[1].getbalances() + self.log.info("Check that only node 0 is watching an address") + assert 'watchonly' in self.nodes[0].getbalances() + assert 'watchonly' not in self.nodes[1].getbalances() self.log.info("Mining blocks ...") self.nodes[0].generate(1) @@ -73,22 +75,28 @@ class WalletTest(BitcoinTestFramework): self.nodes[1].generatetoaddress(101, ADDRESS_WATCHONLY) self.sync_all() - assert_equal(self.nodes[0].getbalances()['mine']['trusted'], 50) - assert_equal(self.nodes[0].getwalletinfo()['balance'], 50) - assert_equal(self.nodes[1].getbalances()['mine']['trusted'], 50) + if not self.options.descriptors: + # Tests legacy watchonly behavior which is not present (and does not need to be tested) in descriptor wallets + assert_equal(self.nodes[0].getbalances()['mine']['trusted'], 50) + assert_equal(self.nodes[0].getwalletinfo()['balance'], 50) + assert_equal(self.nodes[1].getbalances()['mine']['trusted'], 50) - assert_equal(self.nodes[0].getbalances()['watchonly']['immature'], 5000) - assert 'watchonly' not in self.nodes[1].getbalances() + assert_equal(self.nodes[0].getbalances()['watchonly']['immature'], 5000) + assert 'watchonly' not in self.nodes[1].getbalances() - assert_equal(self.nodes[0].getbalance(), 50) - assert_equal(self.nodes[1].getbalance(), 50) + assert_equal(self.nodes[0].getbalance(), 50) + assert_equal(self.nodes[1].getbalance(), 50) self.log.info("Test getbalance with different arguments") assert_equal(self.nodes[0].getbalance("*"), 50) assert_equal(self.nodes[0].getbalance("*", 1), 50) - assert_equal(self.nodes[0].getbalance("*", 1, True), 100) assert_equal(self.nodes[0].getbalance(minconf=1), 50) - assert_equal(self.nodes[0].getbalance(minconf=0, include_watchonly=True), 100) + if not self.options.descriptors: + assert_equal(self.nodes[0].getbalance(minconf=0, include_watchonly=True), 100) + assert_equal(self.nodes[0].getbalance("*", 1, True), 100) + else: + assert_equal(self.nodes[0].getbalance(minconf=0, include_watchonly=True), 50) + assert_equal(self.nodes[0].getbalance("*", 1, True), 50) assert_equal(self.nodes[1].getbalance(minconf=0, include_watchonly=True), 50) # Send 40 BTC from 0 to 1 and 60 BTC from 1 to 0. @@ -156,6 +164,8 @@ class WalletTest(BitcoinTestFramework): expected_balances_1 = {'mine': {'immature': Decimal('0E-8'), 'trusted': Decimal('0E-8'), # node 1's send had an unsafe input 'untrusted_pending': Decimal('30.0') - fee_node_1}} # Doesn't include output of node 0's send since it was spent + if self.options.descriptors: + del expected_balances_0["watchonly"] assert_equal(self.nodes[0].getbalances(), expected_balances_0) assert_equal(self.nodes[1].getbalances(), expected_balances_1) # getbalance without any arguments includes unconfirmed transactions, but not untrusted transactions diff --git a/test/functional/wallet_basic.py b/test/functional/wallet_basic.py index d386d94e0c..40daffd2c2 100755 --- a/test/functional/wallet_basic.py +++ b/test/functional/wallet_basic.py @@ -209,6 +209,8 @@ class WalletTest(BitcoinTestFramework): assert_equal(self.nodes[2].getbalance(), node_2_bal) node_0_bal = self.check_fee_amount(self.nodes[0].getbalance(), Decimal('20'), fee_per_byte, self.get_vsize(self.nodes[2].gettransaction(txid)['hex'])) + self.log.info("Test sendmany") + # Sendmany 10 BTC txid = self.nodes[2].sendmany('', {address: 10}, 0, "", []) self.nodes[2].generate(1) @@ -225,9 +227,9 @@ class WalletTest(BitcoinTestFramework): assert_equal(self.nodes[2].getbalance(), node_2_bal) node_0_bal = self.check_fee_amount(self.nodes[0].getbalance(), node_0_bal + Decimal('10'), fee_per_byte, self.get_vsize(self.nodes[2].gettransaction(txid)['hex'])) - # Sendmany with explicit fee (BTC/kB) + self.log.info("Test case-insensitive explicit fee rate (sendmany as BTC/kB)") # Throw if no conf_target provided - assert_raises_rpc_error(-8, "Selected estimate_mode requires a fee rate", + assert_raises_rpc_error(-8, "Selected estimate_mode bTc/kB requires a fee rate to be specified in conf_target", self.nodes[2].sendmany, amounts={ address: 10 }, estimate_mode='bTc/kB') @@ -251,9 +253,9 @@ class WalletTest(BitcoinTestFramework): node_0_bal += Decimal('10') assert_equal(self.nodes[0].getbalance(), node_0_bal) - # Sendmany with explicit fee (SAT/B) + self.log.info("Test case-insensitive explicit fee rate (sendmany as sat/B)") # Throw if no conf_target provided - assert_raises_rpc_error(-8, "Selected estimate_mode requires a fee rate", + assert_raises_rpc_error(-8, "Selected estimate_mode sat/b requires a fee rate to be specified in conf_target", self.nodes[2].sendmany, amounts={ address: 10 }, estimate_mode='sat/b') @@ -279,6 +281,12 @@ class WalletTest(BitcoinTestFramework): node_0_bal += Decimal('10') assert_equal(self.nodes[0].getbalance(), node_0_bal) + # Test setting explicit fee rate just below the minimum. + for unit, fee_rate in {"BTC/kB": 0.00000999, "sat/B": 0.99999999}.items(): + self.log.info("Test sendmany raises 'fee rate too low' if conf_target {} and estimate_mode {} are passed".format(fee_rate, unit)) + assert_raises_rpc_error(-6, "Fee rate (0.00000999 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)", + self.nodes[2].sendmany, amounts={address: 10}, estimate_mode=unit, conf_target=fee_rate) + self.start_node(3, self.nodes[3].extra_args) self.connect_nodes(0, 3) self.sync_all() @@ -412,15 +420,14 @@ class WalletTest(BitcoinTestFramework): self.nodes[0].generate(1) self.sync_all(self.nodes[0:3]) - # send with explicit btc/kb fee - self.log.info("test explicit fee (sendtoaddress as btc/kb)") + self.log.info("Test case-insensitive explicit fee rate (sendtoaddress as BTC/kB)") self.nodes[0].generate(1) self.sync_all(self.nodes[0:3]) prebalance = self.nodes[2].getbalance() assert prebalance > 2 address = self.nodes[1].getnewaddress() # Throw if no conf_target provided - assert_raises_rpc_error(-8, "Selected estimate_mode requires a fee rate", + assert_raises_rpc_error(-8, "Selected estimate_mode BTc/Kb requires a fee rate to be specified in conf_target", self.nodes[2].sendtoaddress, address=address, amount=1.0, @@ -446,15 +453,15 @@ class WalletTest(BitcoinTestFramework): fee = prebalance - postbalance - Decimal('1') assert_fee_amount(fee, tx_size, Decimal('0.00002500')) - # send with explicit sat/b fee self.sync_all(self.nodes[0:3]) - self.log.info("test explicit fee (sendtoaddress as sat/b)") + + self.log.info("Test case-insensitive explicit fee rate (sendtoaddress as sat/B)") self.nodes[0].generate(1) prebalance = self.nodes[2].getbalance() assert prebalance > 2 address = self.nodes[1].getnewaddress() # Throw if no conf_target provided - assert_raises_rpc_error(-8, "Selected estimate_mode requires a fee rate", + assert_raises_rpc_error(-8, "Selected estimate_mode SAT/b requires a fee rate to be specified in conf_target", self.nodes[2].sendtoaddress, address=address, amount=1.0, @@ -480,6 +487,12 @@ class WalletTest(BitcoinTestFramework): fee = prebalance - postbalance - Decimal('1') assert_fee_amount(fee, tx_size, Decimal('0.00002000')) + # Test setting explicit fee rate just below the minimum. + for unit, fee_rate in {"BTC/kB": 0.00000999, "sat/B": 0.99999999}.items(): + self.log.info("Test sendtoaddress raises 'fee rate too low' if conf_target {} and estimate_mode {} are passed".format(fee_rate, unit)) + assert_raises_rpc_error(-6, "Fee rate (0.00000999 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)", + self.nodes[2].sendtoaddress, address=address, amount=1, estimate_mode=unit, conf_target=fee_rate) + # 2. Import address from node2 to node1 self.nodes[1].importaddress(address_to_import) diff --git a/test/functional/wallet_bumpfee.py b/test/functional/wallet_bumpfee.py index 4b29e65b09..88a7778e19 100755 --- a/test/functional/wallet_bumpfee.py +++ b/test/functional/wallet_bumpfee.py @@ -17,7 +17,7 @@ from decimal import Decimal import io from test_framework.blocktools import add_witness_commitment, create_block, create_coinbase, send_to_witness -from test_framework.messages import BIP125_SEQUENCE_NUMBER, CTransaction +from test_framework.messages import BIP125_SEQUENCE_NUMBER, COIN, CTransaction from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, @@ -36,6 +36,8 @@ NORMAL = 0.00100000 HIGH = 0.00500000 TOO_HIGH = 1.00000000 +BTC_MODE = "BTC/kB" +SAT_MODE = "sat/B" class BumpFeeTest(BitcoinTestFramework): def set_test_params(self): @@ -76,10 +78,9 @@ class BumpFeeTest(BitcoinTestFramework): self.log.info("Running tests") dest_address = peer_node.getnewaddress() - self.test_invalid_parameters(rbf_node, dest_address) - test_simple_bumpfee_succeeds(self, "default", rbf_node, peer_node, dest_address) - test_simple_bumpfee_succeeds(self, "fee_rate", rbf_node, peer_node, dest_address) - test_feerate_args(self, rbf_node, peer_node, dest_address) + for mode in ["default", "fee_rate", BTC_MODE, SAT_MODE]: + test_simple_bumpfee_succeeds(self, mode, rbf_node, peer_node, dest_address) + self.test_invalid_parameters(rbf_node, peer_node, dest_address) test_segwit_bumpfee_succeeds(self, rbf_node, dest_address) test_nonrbf_bumpfee_fails(self, peer_node, dest_address) test_notmine_bumpfee_fails(self, rbf_node, peer_node, dest_address) @@ -98,28 +99,56 @@ class BumpFeeTest(BitcoinTestFramework): test_small_output_with_feerate_succeeds(self, rbf_node, dest_address) test_no_more_inputs_fails(self, rbf_node, dest_address) - def test_invalid_parameters(self, node, dest_address): - txid = spend_one_input(node, dest_address) - # invalid estimate mode - assert_raises_rpc_error(-8, "Invalid estimate_mode parameter", node.bumpfee, txid, { - "estimate_mode": "moo", - }) - assert_raises_rpc_error(-3, "Expected type string", node.bumpfee, txid, { - "estimate_mode": 38, - }) - assert_raises_rpc_error(-3, "Expected type string", node.bumpfee, txid, { - "estimate_mode": { - "foo": "bar", - }, - }) - assert_raises_rpc_error(-8, "Invalid estimate_mode parameter", node.bumpfee, txid, { - "estimate_mode": Decimal("3.141592"), - }) - # confTarget and conf_target - assert_raises_rpc_error(-8, "confTarget and conf_target options should not both be set", node.bumpfee, txid, { - "confTarget": 123, - "conf_target": 456, - }) + def test_invalid_parameters(self, rbf_node, peer_node, dest_address): + self.log.info('Test invalid parameters') + rbfid = spend_one_input(rbf_node, dest_address) + self.sync_mempools((rbf_node, peer_node)) + assert rbfid in rbf_node.getrawmempool() and rbfid in peer_node.getrawmempool() + + assert_raises_rpc_error(-3, "Unexpected key totalFee", rbf_node.bumpfee, rbfid, {"totalFee": NORMAL}) + assert_raises_rpc_error(-4, "is too high (cannot be higher than", rbf_node.bumpfee, rbfid, {"fee_rate": TOO_HIGH}) + + # For each fee mode, bumping to just above minrelay should fail to increase the total fee enough. + for options in [{"fee_rate": INSUFFICIENT}, {"conf_target": INSUFFICIENT, "estimate_mode": BTC_MODE}, {"conf_target": 1, "estimate_mode": SAT_MODE}]: + assert_raises_rpc_error(-8, "Insufficient total fee", rbf_node.bumpfee, rbfid, options) + + self.log.info("Test explicit fee rate raises RPC error if estimate_mode is passed without a conf_target") + for unit, fee_rate in {"SAT/B": 100, "BTC/KB": NORMAL}.items(): + assert_raises_rpc_error(-8, "Selected estimate_mode {} requires a fee rate to be specified in conf_target".format(unit), + rbf_node.bumpfee, rbfid, {"fee_rate": fee_rate, "estimate_mode": unit}) + + self.log.info("Test explicit fee rate raises RPC error if both fee_rate and conf_target are passed") + error_both = "Cannot specify both conf_target and fee_rate. Please provide either a confirmation " \ + "target in blocks for automatic fee estimation, or an explicit fee rate." + assert_raises_rpc_error(-8, error_both, rbf_node.bumpfee, rbfid, {"conf_target": NORMAL, "fee_rate": NORMAL}) + + self.log.info("Test invalid conf_target settings") + assert_raises_rpc_error(-8, "confTarget and conf_target options should not both be set", + rbf_node.bumpfee, rbfid, {"confTarget": 123, "conf_target": 456}) + for field in ["confTarget", "conf_target"]: + assert_raises_rpc_error(-4, "is too high (cannot be higher than -maxtxfee", + lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": BTC_MODE, "conf_target": 2009})) + assert_raises_rpc_error(-4, "is too high (cannot be higher than -maxtxfee", + lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": SAT_MODE, "conf_target": 2009 * 10000})) + + self.log.info("Test invalid estimate_mode settings") + for k, v in {"number": 42, "object": {"foo": "bar"}}.items(): + assert_raises_rpc_error(-3, "Expected type string for estimate_mode, got {}".format(k), + lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": v, "fee_rate": NORMAL})) + for mode in ["foo", Decimal("3.141592")]: + assert_raises_rpc_error(-8, "Invalid estimate_mode parameter", + lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": mode, "fee_rate": NORMAL})) + + self.log.info("Test invalid fee_rate settings") + for mode in ["unset", "economical", "conservative", BTC_MODE, SAT_MODE]: + self.log.debug("{}".format(mode)) + for k, v in {"string": "", "object": {"foo": "bar"}}.items(): + assert_raises_rpc_error(-3, "Expected type number for fee_rate, got {}".format(k), + lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": mode, "fee_rate": v})) + assert_raises_rpc_error(-3, "Amount out of range", + lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": mode, "fee_rate": -1})) + assert_raises_rpc_error(-8, "Invalid fee_rate 0.00000000 BTC/kB (must be greater than 0)", + lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": mode, "fee_rate": 0})) self.clear_mempool() @@ -132,6 +161,13 @@ def test_simple_bumpfee_succeeds(self, mode, rbf_node, peer_node, dest_address): if mode == "fee_rate": bumped_psbt = rbf_node.psbtbumpfee(rbfid, {"fee_rate": NORMAL}) bumped_tx = rbf_node.bumpfee(rbfid, {"fee_rate": NORMAL}) + elif mode == BTC_MODE: + bumped_psbt = rbf_node.psbtbumpfee(rbfid, {"conf_target": NORMAL, "estimate_mode": BTC_MODE}) + bumped_tx = rbf_node.bumpfee(rbfid, {"conf_target": NORMAL, "estimate_mode": BTC_MODE}) + elif mode == SAT_MODE: + sat_fee = NORMAL * COIN / 1000 # convert NORMAL from BTC/kB to sat/B + bumped_psbt = rbf_node.psbtbumpfee(rbfid, {"conf_target": sat_fee, "estimate_mode": SAT_MODE}) + bumped_tx = rbf_node.bumpfee(rbfid, {"conf_target": sat_fee, "estimate_mode": SAT_MODE}) else: bumped_psbt = rbf_node.psbtbumpfee(rbfid) bumped_tx = rbf_node.bumpfee(rbfid) @@ -158,33 +194,13 @@ def test_simple_bumpfee_succeeds(self, mode, rbf_node, peer_node, dest_address): self.clear_mempool() -def test_feerate_args(self, rbf_node, peer_node, dest_address): - self.log.info('Test fee_rate args') - rbfid = spend_one_input(rbf_node, dest_address) - self.sync_mempools((rbf_node, peer_node)) - assert rbfid in rbf_node.getrawmempool() and rbfid in peer_node.getrawmempool() - - assert_raises_rpc_error(-8, "conf_target can't be set with fee_rate. Please provide either a confirmation target in blocks for automatic fee estimation, or an explicit fee rate.", rbf_node.bumpfee, rbfid, {"fee_rate": NORMAL, "confTarget": 1}) - - assert_raises_rpc_error(-3, "Unexpected key totalFee", rbf_node.bumpfee, rbfid, {"totalFee": NORMAL}) - assert_raises_rpc_error(-8, "conf_target can't be set with fee_rate. Please provide either a confirmation target in blocks for automatic fee estimation, or an explicit fee rate.", rbf_node.bumpfee, rbfid, {"fee_rate":0.00001, "confTarget": 1}) - - # Bumping to just above minrelay should fail to increase total fee enough, at least - assert_raises_rpc_error(-8, "Insufficient total fee", rbf_node.bumpfee, rbfid, {"fee_rate": INSUFFICIENT}) - - assert_raises_rpc_error(-3, "Amount out of range", rbf_node.bumpfee, rbfid, {"fee_rate": -1}) - - assert_raises_rpc_error(-4, "is too high (cannot be higher than", rbf_node.bumpfee, rbfid, {"fee_rate": TOO_HIGH}) - self.clear_mempool() - - def test_segwit_bumpfee_succeeds(self, rbf_node, dest_address): self.log.info('Test that segwit-sourcing bumpfee works') # Create a transaction with segwit output, then create an RBF transaction # which spends it, and make sure bumpfee can be called on it. segwit_in = next(u for u in rbf_node.listunspent() if u["amount"] == Decimal("0.001")) - segwit_out = rbf_node.getaddressinfo(rbf_node.getnewaddress(address_type='p2sh-segwit')) + segwit_out = rbf_node.getaddressinfo(rbf_node.getnewaddress(address_type='bech32')) segwitid = send_to_witness( use_p2wsh=False, node=rbf_node, @@ -365,7 +381,7 @@ def test_watchonly_psbt(self, peer_node, rbf_node, dest_address): rbf_node.createwallet(wallet_name="signer", disable_private_keys=False, blank=True) signer = rbf_node.get_wallet_rpc("signer") assert signer.getwalletinfo()['private_keys_enabled'] - result = signer.importmulti([{ + reqs = [{ "desc": priv_rec_desc, "timestamp": 0, "range": [0,1], @@ -378,7 +394,11 @@ def test_watchonly_psbt(self, peer_node, rbf_node, dest_address): "range": [0, 0], "internal": True, "keypool": False - }]) + }] + if self.options.descriptors: + result = signer.importdescriptors(reqs) + else: + result = signer.importmulti(reqs) assert_equal(result, [{'success': True}, {'success': True}]) # Create another wallet with just the public keys, which creates PSBTs @@ -386,21 +406,27 @@ def test_watchonly_psbt(self, peer_node, rbf_node, dest_address): watcher = rbf_node.get_wallet_rpc("watcher") assert not watcher.getwalletinfo()['private_keys_enabled'] - result = watcher.importmulti([{ + reqs = [{ "desc": pub_rec_desc, "timestamp": 0, "range": [0, 10], "internal": False, "keypool": True, - "watchonly": True + "watchonly": True, + "active": True, }, { "desc": pub_change_desc, "timestamp": 0, "range": [0, 10], "internal": True, "keypool": True, - "watchonly": True - }]) + "watchonly": True, + "active": True, + }] + if self.options.descriptors: + result = watcher.importdescriptors(reqs) + else: + result = watcher.importmulti(reqs) assert_equal(result, [{'success': True}, {'success': True}]) funding_address1 = watcher.getnewaddress(address_type='bech32') diff --git a/test/functional/wallet_createwallet.py b/test/functional/wallet_createwallet.py index 48c0fcb731..cf3317121f 100755 --- a/test/functional/wallet_createwallet.py +++ b/test/functional/wallet_createwallet.py @@ -5,11 +5,15 @@ """Test createwallet arguments. """ +from test_framework.address import key_to_p2wpkh +from test_framework.descriptors import descsum_create +from test_framework.key import ECKey from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_raises_rpc_error, ) +from test_framework.wallet_util import bytes_to_wif, generate_wif_key class CreateWalletTest(BitcoinTestFramework): def set_test_params(self): @@ -35,10 +39,14 @@ class CreateWalletTest(BitcoinTestFramework): w1.importpubkey(w0.getaddressinfo(address1)['pubkey']) self.log.info('Test that private keys cannot be imported') - addr = w0.getnewaddress('', 'legacy') - privkey = w0.dumpprivkey(addr) + eckey = ECKey() + eckey.generate() + privkey = bytes_to_wif(eckey.get_bytes()) assert_raises_rpc_error(-4, 'Cannot import private keys to a wallet with private keys disabled', w1.importprivkey, privkey) - result = w1.importmulti([{'scriptPubKey': {'address': addr}, 'timestamp': 'now', 'keys': [privkey]}]) + if self.options.descriptors: + result = w1.importdescriptors([{'desc': descsum_create('wpkh(' + privkey + ')'), 'timestamp': 'now'}]) + else: + result = w1.importmulti([{'scriptPubKey': {'address': key_to_p2wpkh(eckey.get_pubkey().get_bytes())}, 'timestamp': 'now', 'keys': [privkey]}]) assert not result[0]['success'] assert 'warning' not in result[0] assert_equal(result[0]['error']['code'], -4) @@ -58,12 +66,25 @@ class CreateWalletTest(BitcoinTestFramework): assert_raises_rpc_error(-4, "Error: This wallet has no available keys", w3.getnewaddress) assert_raises_rpc_error(-4, "Error: This wallet has no available keys", w3.getrawchangeaddress) # Import private key - w3.importprivkey(w0.dumpprivkey(address1)) + w3.importprivkey(generate_wif_key()) # Imported private keys are currently ignored by the keypool assert_equal(w3.getwalletinfo()['keypoolsize'], 0) assert_raises_rpc_error(-4, "Error: This wallet has no available keys", w3.getnewaddress) # Set the seed - w3.sethdseed() + if self.options.descriptors: + w3.importdescriptors([{ + 'desc': descsum_create('wpkh(tprv8ZgxMBicQKsPcwuZGKp8TeWppSuLMiLe2d9PupB14QpPeQsqoj3LneJLhGHH13xESfvASyd4EFLJvLrG8b7DrLxEuV7hpF9uUc6XruKA1Wq/0h/*)'), + 'timestamp': 'now', + 'active': True + }, + { + 'desc': descsum_create('wpkh(tprv8ZgxMBicQKsPcwuZGKp8TeWppSuLMiLe2d9PupB14QpPeQsqoj3LneJLhGHH13xESfvASyd4EFLJvLrG8b7DrLxEuV7hpF9uUc6XruKA1Wq/1h/*)'), + 'timestamp': 'now', + 'active': True, + 'internal': True + }]) + else: + w3.sethdseed() assert_equal(w3.getwalletinfo()['keypoolsize'], 1) w3.getnewaddress() w3.getrawchangeaddress() @@ -80,7 +101,20 @@ class CreateWalletTest(BitcoinTestFramework): assert_raises_rpc_error(-4, "Error: This wallet has no available keys", w4.getrawchangeaddress) # Now set a seed and it should work. Wallet should also be encrypted w4.walletpassphrase('pass', 60) - w4.sethdseed() + if self.options.descriptors: + w4.importdescriptors([{ + 'desc': descsum_create('wpkh(tprv8ZgxMBicQKsPcwuZGKp8TeWppSuLMiLe2d9PupB14QpPeQsqoj3LneJLhGHH13xESfvASyd4EFLJvLrG8b7DrLxEuV7hpF9uUc6XruKA1Wq/0h/*)'), + 'timestamp': 'now', + 'active': True + }, + { + 'desc': descsum_create('wpkh(tprv8ZgxMBicQKsPcwuZGKp8TeWppSuLMiLe2d9PupB14QpPeQsqoj3LneJLhGHH13xESfvASyd4EFLJvLrG8b7DrLxEuV7hpF9uUc6XruKA1Wq/1h/*)'), + 'timestamp': 'now', + 'active': True, + 'internal': True + }]) + else: + w4.sethdseed() w4.getnewaddress() w4.getrawchangeaddress() @@ -111,13 +145,14 @@ class CreateWalletTest(BitcoinTestFramework): w6.walletpassphrase('thisisapassphrase', 60) w6.signmessage(w6.getnewaddress('', 'legacy'), "test") w6.keypoolrefill(1) - # There should only be 1 key + # There should only be 1 key for legacy, 3 for descriptors walletinfo = w6.getwalletinfo() - assert_equal(walletinfo['keypoolsize'], 1) - assert_equal(walletinfo['keypoolsize_hd_internal'], 1) + keys = 3 if self.options.descriptors else 1 + assert_equal(walletinfo['keypoolsize'], keys) + assert_equal(walletinfo['keypoolsize_hd_internal'], keys) # Allow empty passphrase, but there should be a warning resp = self.nodes[0].createwallet(wallet_name='w7', disable_private_keys=False, blank=False, passphrase='') - assert_equal(resp['warning'], 'Empty string given as passphrase, wallet will not be encrypted.') + assert 'Empty string given as passphrase, wallet will not be encrypted.' in resp['warning'] w7 = node.get_wallet_rpc('w7') assert_raises_rpc_error(-15, 'Error: running with an unencrypted wallet, but walletpassphrase was called.', w7.walletpassphrase, '', 60) diff --git a/test/functional/wallet_descriptor.py b/test/functional/wallet_descriptor.py index 51e9ec8f40..0fec8ea4a4 100755 --- a/test/functional/wallet_descriptor.py +++ b/test/functional/wallet_descriptor.py @@ -16,18 +16,22 @@ class WalletDescriptorTest(BitcoinTestFramework): self.setup_clean_chain = True self.num_nodes = 1 self.extra_args = [['-keypool=100']] + self.wallet_names = [] def skip_test_if_missing_module(self): self.skip_if_no_wallet() + self.skip_if_no_sqlite() def run_test(self): + # Make a legacy wallet and check it is BDB + self.nodes[0].createwallet(wallet_name="legacy1", descriptors=False) wallet_info = self.nodes[0].getwalletinfo() assert_equal(wallet_info['format'], 'bdb') + self.nodes[0].unloadwallet("legacy1") # Make a descriptor wallet self.log.info("Making a descriptor wallet") self.nodes[0].createwallet(wallet_name="desc1", descriptors=True) - self.nodes[0].unloadwallet(self.default_wallet_name) # A descriptor wallet should have 100 addresses * 3 types = 300 keys self.log.info("Checking wallet info") diff --git a/test/functional/wallet_dump.py b/test/functional/wallet_dump.py index 09581d864b..eb54da99f5 100755 --- a/test/functional/wallet_dump.py +++ b/test/functional/wallet_dump.py @@ -95,7 +95,7 @@ def read_dump(file_name, addrs, script_addrs, hd_master_addr_old): class WalletDumpTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 - self.extra_args = [["-keypool=90", "-addresstype=legacy", "-wallet=dump"]] + self.extra_args = [["-keypool=90", "-addresstype=legacy"]] self.rpc_timeout = 120 def skip_test_if_missing_module(self): @@ -106,6 +106,8 @@ class WalletDumpTest(BitcoinTestFramework): self.start_nodes() def run_test(self): + self.nodes[0].createwallet("dump") + wallet_unenc_dump = os.path.join(self.nodes[0].datadir, "wallet.unencrypted.dump") wallet_enc_dump = os.path.join(self.nodes[0].datadir, "wallet.encrypted.dump") @@ -190,7 +192,8 @@ class WalletDumpTest(BitcoinTestFramework): assert_raises_rpc_error(-8, "already exists", lambda: self.nodes[0].dumpwallet(wallet_enc_dump)) # Restart node with new wallet, and test importwallet - self.restart_node(0, ['-wallet=w2']) + self.restart_node(0) + self.nodes[0].createwallet("w2") # Make sure the address is not IsMine before import result = self.nodes[0].getaddressinfo(multisig_addr) diff --git a/test/functional/wallet_importdescriptors.py b/test/functional/wallet_importdescriptors.py index 2d982edef8..2903a84998 100755 --- a/test/functional/wallet_importdescriptors.py +++ b/test/functional/wallet_importdescriptors.py @@ -15,6 +15,7 @@ variants. - `test_address()` is called to call getaddressinfo for an address on node1 and test the values returned.""" +from test_framework.address import key_to_p2pkh from test_framework.test_framework import BitcoinTestFramework from test_framework.descriptors import descsum_create from test_framework.util import ( @@ -34,9 +35,11 @@ class ImportDescriptorsTest(BitcoinTestFramework): ["-addresstype=bech32", "-keypool=5"] ] self.setup_clean_chain = True + self.wallet_names = [] def skip_test_if_missing_module(self): self.skip_if_no_wallet() + self.skip_if_no_sqlite() def test_importdesc(self, req, success, error_code=None, error_message=None, warnings=None, wallet=None): """Run importdescriptors and assert success""" @@ -58,7 +61,7 @@ class ImportDescriptorsTest(BitcoinTestFramework): def run_test(self): self.log.info('Setting up wallets') - self.nodes[0].createwallet(wallet_name='w0', disable_private_keys=False) + self.nodes[0].createwallet(wallet_name='w0', disable_private_keys=False, descriptors=True) w0 = self.nodes[0].get_wallet_rpc('w0') self.nodes[1].createwallet(wallet_name='w1', disable_private_keys=True, blank=True, descriptors=True) @@ -105,6 +108,17 @@ class ImportDescriptorsTest(BitcoinTestFramework): error_code=-8, error_message="Internal addresses should not have a label") + self.log.info("Internal addresses should be detected as such") + key = get_generate_key() + addr = key_to_p2pkh(key.pubkey) + self.test_importdesc({"desc": descsum_create("pkh(" + key.pubkey + ")"), + "timestamp": "now", + "internal": True}, + success=True) + info = w1.getaddressinfo(addr) + assert_equal(info["ismine"], True) + assert_equal(info["ischange"], True) + # # Test importing of a P2SH-P2WPKH descriptor key = get_generate_key() self.log.info("Should not import a p2sh-p2wpkh descriptor without checksum") @@ -207,6 +221,15 @@ class ImportDescriptorsTest(BitcoinTestFramework): success=False, error_code=-4, error_message='Cannot import private keys to a wallet with private keys disabled') + + self.log.info("Should not import a descriptor with hardened derivations when private keys are disabled") + self.test_importdesc({"desc": descsum_create("wpkh(" + xpub + "/1h/*)"), + "timestamp": "now", + "range": 1}, + success=False, + error_code=-4, + error_message='Cannot expand descriptor. Probably because of hardened derivations without private keys provided') + for address in addresses: test_address(w1, address, diff --git a/test/functional/wallet_importprunedfunds.py b/test/functional/wallet_importprunedfunds.py index 78426018ef..d10d8ae1c8 100755 --- a/test/functional/wallet_importprunedfunds.py +++ b/test/functional/wallet_importprunedfunds.py @@ -5,11 +5,14 @@ """Test the importprunedfunds and removeprunedfunds RPCs.""" from decimal import Decimal +from test_framework.address import key_to_p2wpkh +from test_framework.key import ECKey from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_raises_rpc_error, ) +from test_framework.wallet_util import bytes_to_wif class ImportPrunedFundsTest(BitcoinTestFramework): def set_test_params(self): @@ -30,8 +33,11 @@ class ImportPrunedFundsTest(BitcoinTestFramework): # pubkey address2 = self.nodes[0].getnewaddress() # privkey - address3 = self.nodes[0].getnewaddress() - address3_privkey = self.nodes[0].dumpprivkey(address3) # Using privkey + eckey = ECKey() + eckey.generate() + address3_privkey = bytes_to_wif(eckey.get_bytes()) + address3 = key_to_p2wpkh(eckey.get_pubkey().get_bytes()) + self.nodes[0].importprivkey(address3_privkey) # Check only one address address_info = self.nodes[0].getaddressinfo(address1) @@ -80,37 +86,44 @@ class ImportPrunedFundsTest(BitcoinTestFramework): assert_equal(balance1, Decimal(0)) # Import with affiliated address with no rescan - self.nodes[1].importaddress(address=address2, rescan=False) - self.nodes[1].importprunedfunds(rawtransaction=rawtxn2, txoutproof=proof2) - assert [tx for tx in self.nodes[1].listtransactions(include_watchonly=True) if tx['txid'] == txnid2] + self.nodes[1].createwallet('wwatch', disable_private_keys=True) + wwatch = self.nodes[1].get_wallet_rpc('wwatch') + wwatch.importaddress(address=address2, rescan=False) + wwatch.importprunedfunds(rawtransaction=rawtxn2, txoutproof=proof2) + assert [tx for tx in wwatch.listtransactions(include_watchonly=True) if tx['txid'] == txnid2] # Import with private key with no rescan - self.nodes[1].importprivkey(privkey=address3_privkey, rescan=False) - self.nodes[1].importprunedfunds(rawtxn3, proof3) - assert [tx for tx in self.nodes[1].listtransactions() if tx['txid'] == txnid3] - balance3 = self.nodes[1].getbalance() + w1 = self.nodes[1].get_wallet_rpc(self.default_wallet_name) + w1.importprivkey(privkey=address3_privkey, rescan=False) + w1.importprunedfunds(rawtxn3, proof3) + assert [tx for tx in w1.listtransactions() if tx['txid'] == txnid3] + balance3 = w1.getbalance() assert_equal(balance3, Decimal('0.025')) # Addresses Test - after import - address_info = self.nodes[1].getaddressinfo(address1) + address_info = w1.getaddressinfo(address1) assert_equal(address_info['iswatchonly'], False) assert_equal(address_info['ismine'], False) - address_info = self.nodes[1].getaddressinfo(address2) - assert_equal(address_info['iswatchonly'], True) - assert_equal(address_info['ismine'], False) - address_info = self.nodes[1].getaddressinfo(address3) + address_info = wwatch.getaddressinfo(address2) + if self.options.descriptors: + assert_equal(address_info['iswatchonly'], False) + assert_equal(address_info['ismine'], True) + else: + assert_equal(address_info['iswatchonly'], True) + assert_equal(address_info['ismine'], False) + address_info = w1.getaddressinfo(address3) assert_equal(address_info['iswatchonly'], False) assert_equal(address_info['ismine'], True) # Remove transactions - assert_raises_rpc_error(-8, "Transaction does not exist in wallet.", self.nodes[1].removeprunedfunds, txnid1) - assert not [tx for tx in self.nodes[1].listtransactions(include_watchonly=True) if tx['txid'] == txnid1] + assert_raises_rpc_error(-8, "Transaction does not exist in wallet.", w1.removeprunedfunds, txnid1) + assert not [tx for tx in w1.listtransactions(include_watchonly=True) if tx['txid'] == txnid1] - self.nodes[1].removeprunedfunds(txnid2) - assert not [tx for tx in self.nodes[1].listtransactions(include_watchonly=True) if tx['txid'] == txnid2] + wwatch.removeprunedfunds(txnid2) + assert not [tx for tx in wwatch.listtransactions(include_watchonly=True) if tx['txid'] == txnid2] - self.nodes[1].removeprunedfunds(txnid3) - assert not [tx for tx in self.nodes[1].listtransactions(include_watchonly=True) if tx['txid'] == txnid3] + w1.removeprunedfunds(txnid3) + assert not [tx for tx in w1.listtransactions(include_watchonly=True) if tx['txid'] == txnid3] if __name__ == '__main__': ImportPrunedFundsTest().main() diff --git a/test/functional/wallet_labels.py b/test/functional/wallet_labels.py index cff59bd1c1..883b97561e 100755 --- a/test/functional/wallet_labels.py +++ b/test/functional/wallet_labels.py @@ -135,7 +135,7 @@ class WalletLabelsTest(BitcoinTestFramework): change_label(node, labels[2].addresses[0], labels[2], labels[2]) self.log.info('Check watchonly labels') - node.createwallet(wallet_name='watch_only', disable_private_keys=True, descriptors=False) + node.createwallet(wallet_name='watch_only', disable_private_keys=True) wallet_watch_only = node.get_wallet_rpc('watch_only') BECH32_VALID = { '✔️_VER15_PROG40': 'bcrt10qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqn2cjv3', @@ -156,7 +156,7 @@ class WalletLabelsTest(BitcoinTestFramework): ad = BECH32_INVALID[l] assert_raises_rpc_error( -5, - "Invalid Bitcoin address or script", + "Address is not valid" if self.options.descriptors else "Invalid Bitcoin address or script", lambda: wallet_watch_only.importaddress(label=l, rescan=False, address=ad), ) diff --git a/test/functional/wallet_listsinceblock.py b/test/functional/wallet_listsinceblock.py index 09a336b764..6a1b9097c5 100755 --- a/test/functional/wallet_listsinceblock.py +++ b/test/functional/wallet_listsinceblock.py @@ -4,6 +4,8 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the listsinceblock RPC.""" +from test_framework.address import key_to_p2wpkh +from test_framework.key import ECKey from test_framework.test_framework import BitcoinTestFramework from test_framework.messages import BIP125_SEQUENCE_NUMBER from test_framework.util import ( @@ -11,6 +13,7 @@ from test_framework.util import ( assert_equal, assert_raises_rpc_error, ) +from test_framework.wallet_util import bytes_to_wif from decimal import Decimal @@ -181,15 +184,22 @@ class ListSinceBlockTest(BitcoinTestFramework): self.sync_all() - # Split network into two - self.split_network() - # share utxo between nodes[1] and nodes[2] + eckey = ECKey() + eckey.generate() + privkey = bytes_to_wif(eckey.get_bytes()) + address = key_to_p2wpkh(eckey.get_pubkey().get_bytes()) + self.nodes[2].sendtoaddress(address, 10) + self.nodes[2].generate(6) + self.sync_all() + self.nodes[2].importprivkey(privkey) utxos = self.nodes[2].listunspent() - utxo = utxos[0] - privkey = self.nodes[2].dumpprivkey(utxo['address']) + utxo = [u for u in utxos if u["address"] == address][0] self.nodes[1].importprivkey(privkey) + # Split network into two + self.split_network() + # send from nodes[1] using utxo to nodes[0] change = '%.8f' % (float(utxo['amount']) - 1.0003) recipient_dict = { diff --git a/test/functional/wallet_listtransactions.py b/test/functional/wallet_listtransactions.py index 8ff663ccd2..3e98208431 100755 --- a/test/functional/wallet_listtransactions.py +++ b/test/functional/wallet_listtransactions.py @@ -91,18 +91,20 @@ class ListTransactionsTest(BitcoinTestFramework): {"category": "receive", "amount": Decimal("0.44")}, {"txid": txid}) - pubkey = self.nodes[1].getaddressinfo(self.nodes[1].getnewaddress())['pubkey'] - multisig = self.nodes[1].createmultisig(1, [pubkey]) - self.nodes[0].importaddress(multisig["redeemScript"], "watchonly", False, True) - txid = self.nodes[1].sendtoaddress(multisig["address"], 0.1) - self.nodes[1].generate(1) - self.sync_all() - assert_equal(len(self.nodes[0].listtransactions(label="watchonly", include_watchonly=True)), 1) - assert_equal(len(self.nodes[0].listtransactions(dummy="watchonly", include_watchonly=True)), 1) - assert len(self.nodes[0].listtransactions(label="watchonly", count=100, include_watchonly=False)) == 0 - assert_array_result(self.nodes[0].listtransactions(label="watchonly", count=100, include_watchonly=True), - {"category": "receive", "amount": Decimal("0.1")}, - {"txid": txid, "label": "watchonly"}) + if not self.options.descriptors: + # include_watchonly is a legacy wallet feature, so don't test it for descriptor wallets + pubkey = self.nodes[1].getaddressinfo(self.nodes[1].getnewaddress())['pubkey'] + multisig = self.nodes[1].createmultisig(1, [pubkey]) + self.nodes[0].importaddress(multisig["redeemScript"], "watchonly", False, True) + txid = self.nodes[1].sendtoaddress(multisig["address"], 0.1) + self.nodes[1].generate(1) + self.sync_all() + assert_equal(len(self.nodes[0].listtransactions(label="watchonly", include_watchonly=True)), 1) + assert_equal(len(self.nodes[0].listtransactions(dummy="watchonly", include_watchonly=True)), 1) + assert len(self.nodes[0].listtransactions(label="watchonly", count=100, include_watchonly=False)) == 0 + assert_array_result(self.nodes[0].listtransactions(label="watchonly", count=100, include_watchonly=True), + {"category": "receive", "amount": Decimal("0.1")}, + {"txid": txid, "label": "watchonly"}) self.run_rbf_opt_in_test() diff --git a/test/functional/wallet_multiwallet.py b/test/functional/wallet_multiwallet.py index 61791a756c..cf55b28afb 100755 --- a/test/functional/wallet_multiwallet.py +++ b/test/functional/wallet_multiwallet.py @@ -10,6 +10,7 @@ from decimal import Decimal from threading import Thread import os import shutil +import stat import time from test_framework.authproxy import JSONRPCException @@ -41,6 +42,7 @@ class MultiWalletTest(BitcoinTestFramework): self.setup_clean_chain = True self.num_nodes = 2 self.rpc_timeout = 120 + self.extra_args = [["-nowallet"], []] def skip_test_if_missing_module(self): self.skip_if_no_wallet() @@ -77,10 +79,17 @@ class MultiWalletTest(BitcoinTestFramework): os.mkdir(wallet_dir('w7')) os.symlink('w7', wallet_dir('w7_symlink')) + os.symlink('..', wallet_dir('recursive_dir_symlink')) + + os.mkdir(wallet_dir('self_walletdat_symlink')) + os.symlink('wallet.dat', wallet_dir('self_walletdat_symlink/wallet.dat')) + # rename wallet.dat to make sure plain wallet file paths (as opposed to # directory paths) can be loaded # create another dummy wallet for use in testing backups later - self.start_node(0, ["-nowallet", "-wallet=empty", "-wallet=plain"]) + self.start_node(0) + node.createwallet("empty") + node.createwallet("plain") node.createwallet("created") self.stop_nodes() empty_wallet = os.path.join(self.options.tmpdir, 'empty.dat') @@ -98,35 +107,58 @@ class MultiWalletTest(BitcoinTestFramework): # sub/w5 - to verify relative wallet path is created correctly # extern/w6 - to verify absolute wallet path is created correctly # w7_symlink - to verify symlinked wallet path is initialized correctly - # w8 - to verify existing wallet file is loaded correctly + # w8 - to verify existing wallet file is loaded correctly. Not tested for SQLite wallets as this is a deprecated BDB behavior. # '' - to verify default wallet file is created correctly - wallet_names = ['w1', 'w2', 'w3', 'w', 'sub/w5', os.path.join(self.options.tmpdir, 'extern/w6'), 'w7_symlink', 'w8', self.default_wallet_name] - extra_args = ['-nowallet'] + ['-wallet={}'.format(n) for n in wallet_names] - self.start_node(0, extra_args) - assert_equal(sorted(map(lambda w: w['name'], self.nodes[0].listwalletdir()['wallets'])), [self.default_wallet_name, os.path.join('sub', 'w5'), 'w', 'w1', 'w2', 'w3', 'w7', 'w7_symlink', 'w8']) + to_create = ['w1', 'w2', 'w3', 'w', 'sub/w5', 'w7_symlink'] + in_wallet_dir = [w.replace('/', os.path.sep) for w in to_create] # Wallets in the wallet dir + in_wallet_dir.append('w7') # w7 is not loaded or created, but will be listed by listwalletdir because w7_symlink + to_create.append(os.path.join(self.options.tmpdir, 'extern/w6')) # External, not in the wallet dir, so we need to avoid adding it to in_wallet_dir + to_load = [self.default_wallet_name] + if not self.options.descriptors: + to_load.append('w8') + wallet_names = to_create + to_load # Wallet names loaded in the wallet + in_wallet_dir += to_load # The loaded wallets are also in the wallet dir + self.start_node(0) + for wallet_name in to_create: + self.nodes[0].createwallet(wallet_name) + for wallet_name in to_load: + self.nodes[0].loadwallet(wallet_name) + + os.mkdir(wallet_dir('no_access')) + os.chmod(wallet_dir('no_access'), 0) + try: + with self.nodes[0].assert_debug_log(expected_msgs=['Too many levels of symbolic links', 'Error scanning']): + walletlist = self.nodes[0].listwalletdir()['wallets'] + finally: + # Need to ensure access is restored for cleanup + os.chmod(wallet_dir('no_access'), stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) + assert_equal(sorted(map(lambda w: w['name'], walletlist)), sorted(in_wallet_dir)) assert_equal(set(node.listwallets()), set(wallet_names)) + # should raise rpc error if wallet path can't be created + err_code = -4 if self.options.descriptors else -1 + assert_raises_rpc_error(err_code, "boost::filesystem::create_directory:", self.nodes[0].createwallet, "w8/bad") + # check that all requested wallets were created self.stop_node(0) for wallet_name in wallet_names: assert_equal(os.path.isfile(wallet_file(wallet_name)), True) - # should not initialize if wallet path can't be created - exp_stderr = "boost::filesystem::create_directory:" - self.nodes[0].assert_start_raises_init_error(['-wallet=w8/bad'], exp_stderr, match=ErrorMatch.PARTIAL_REGEX) - self.nodes[0].assert_start_raises_init_error(['-walletdir=wallets'], 'Error: Specified -walletdir "wallets" does not exist') self.nodes[0].assert_start_raises_init_error(['-walletdir=wallets'], 'Error: Specified -walletdir "wallets" is a relative path', cwd=data_dir()) self.nodes[0].assert_start_raises_init_error(['-walletdir=debug.log'], 'Error: Specified -walletdir "debug.log" is not a directory', cwd=data_dir()) - # should not initialize if there are duplicate wallets - self.nodes[0].assert_start_raises_init_error(['-wallet=w1', '-wallet=w1'], 'Error: Error loading wallet w1. Duplicate -wallet filename specified.') + self.start_node(0, ['-wallet=w1', '-wallet=w1']) + self.stop_node(0, 'Warning: Ignoring duplicate -wallet w1.') - # should not initialize if one wallet is a copy of another - shutil.copyfile(wallet_dir('w8'), wallet_dir('w8_copy')) - exp_stderr = r"BerkeleyDatabase: Can't open database w8_copy \(duplicates fileid \w+ from w8\)" - self.nodes[0].assert_start_raises_init_error(['-wallet=w8', '-wallet=w8_copy'], exp_stderr, match=ErrorMatch.PARTIAL_REGEX) + if not self.options.descriptors: + # Only BDB doesn't open duplicate wallet files. SQLite does not have this limitation. While this may be desired in the future, it is not necessary + # should not initialize if one wallet is a copy of another + shutil.copyfile(wallet_dir('w8'), wallet_dir('w8_copy')) + in_wallet_dir.append('w8_copy') + exp_stderr = r"BerkeleyDatabase: Can't open database w8_copy \(duplicates fileid \w+ from w8\)" + self.nodes[0].assert_start_raises_init_error(['-wallet=w8', '-wallet=w8_copy'], exp_stderr, match=ErrorMatch.PARTIAL_REGEX) # should not initialize if wallet file is a symlink os.symlink('w8', wallet_dir('w8_symlink')) @@ -142,14 +174,18 @@ class MultiWalletTest(BitcoinTestFramework): # if wallets/ doesn't exist, datadir should be the default wallet dir wallet_dir2 = data_dir('walletdir') os.rename(wallet_dir(), wallet_dir2) - self.start_node(0, ['-nowallet', '-wallet=w4', '-wallet=w5']) + self.start_node(0) + self.nodes[0].createwallet("w4") + self.nodes[0].createwallet("w5") assert_equal(set(node.listwallets()), {"w4", "w5"}) w5 = wallet("w5") node.generatetoaddress(nblocks=1, address=w5.getnewaddress()) # now if wallets/ exists again, but the rootdir is specified as the walletdir, w4 and w5 should still be loaded os.rename(wallet_dir2, wallet_dir()) - self.restart_node(0, ['-nowallet', '-wallet=w4', '-wallet=w5', '-walletdir=' + data_dir()]) + self.restart_node(0, ['-nowallet', '-walletdir=' + data_dir()]) + self.nodes[0].loadwallet("w4") + self.nodes[0].loadwallet("w5") assert_equal(set(node.listwallets()), {"w4", "w5"}) w5 = wallet("w5") w5_info = w5.getwalletinfo() @@ -157,13 +193,19 @@ class MultiWalletTest(BitcoinTestFramework): competing_wallet_dir = os.path.join(self.options.tmpdir, 'competing_walletdir') os.mkdir(competing_wallet_dir) - self.restart_node(0, ['-walletdir=' + competing_wallet_dir]) - exp_stderr = r"Error: Error initializing wallet database environment \"\S+competing_walletdir\S*\"!" + self.restart_node(0, ['-nowallet', '-walletdir=' + competing_wallet_dir]) + self.nodes[0].createwallet(self.default_wallet_name) + if self.options.descriptors: + exp_stderr = r"Error: SQLiteDatabase: Unable to obtain an exclusive lock on the database, is it being used by another bitcoind?" + else: + exp_stderr = r"Error: Error initializing wallet database environment \"\S+competing_walletdir\S*\"!" self.nodes[1].assert_start_raises_init_error(['-walletdir=' + competing_wallet_dir], exp_stderr, match=ErrorMatch.PARTIAL_REGEX) - self.restart_node(0, extra_args) + self.restart_node(0) + for wallet_name in wallet_names: + self.nodes[0].loadwallet(wallet_name) - assert_equal(sorted(map(lambda w: w['name'], self.nodes[0].listwalletdir()['wallets'])), [self.default_wallet_name, os.path.join('sub', 'w5'), 'w', 'w1', 'w2', 'w3', 'w7', 'w7_symlink', 'w8', 'w8_copy']) + assert_equal(sorted(map(lambda w: w['name'], self.nodes[0].listwalletdir()['wallets'])), sorted(in_wallet_dir)) wallets = [wallet(w) for w in wallet_names] wallet_bad = wallet("bad") @@ -254,19 +296,22 @@ class MultiWalletTest(BitcoinTestFramework): # Fail to load duplicate wallets path = os.path.join(self.options.tmpdir, "node0", "regtest", "wallets", "w1", "wallet.dat") - assert_raises_rpc_error(-4, "Wallet file verification failed. Refusing to load database. Data file '{}' is already loaded.".format(path), self.nodes[0].loadwallet, wallet_names[0]) + if self.options.descriptors: + assert_raises_rpc_error(-4, "Wallet file verification failed. SQLiteDatabase: Unable to obtain an exclusive lock on the database, is it being used by another bitcoind?", self.nodes[0].loadwallet, wallet_names[0]) + else: + assert_raises_rpc_error(-4, "Wallet file verification failed. Refusing to load database. Data file '{}' is already loaded.".format(path), self.nodes[0].loadwallet, wallet_names[0]) - # Fail to load duplicate wallets by different ways (directory and filepath) - if not self.options.descriptors: + # This tests the default wallet that BDB makes, so SQLite wallet doesn't need to test this + # Fail to load duplicate wallets by different ways (directory and filepath) path = os.path.join(self.options.tmpdir, "node0", "regtest", "wallets", "wallet.dat") assert_raises_rpc_error(-4, "Wallet file verification failed. Refusing to load database. Data file '{}' is already loaded.".format(path), self.nodes[0].loadwallet, 'wallet.dat') - # Fail to load if one wallet is a copy of another - assert_raises_rpc_error(-4, "BerkeleyDatabase: Can't open database w8_copy (duplicates fileid", self.nodes[0].loadwallet, 'w8_copy') - - # Fail to load if one wallet is a copy of another, test this twice to make sure that we don't re-introduce #14304 - assert_raises_rpc_error(-4, "BerkeleyDatabase: Can't open database w8_copy (duplicates fileid", self.nodes[0].loadwallet, 'w8_copy') + # Only BDB doesn't open duplicate wallet files. SQLite does not have this limitation. While this may be desired in the future, it is not necessary + # Fail to load if one wallet is a copy of another + assert_raises_rpc_error(-4, "BerkeleyDatabase: Can't open database w8_copy (duplicates fileid", self.nodes[0].loadwallet, 'w8_copy') + # Fail to load if one wallet is a copy of another, test this twice to make sure that we don't re-introduce #14304 + assert_raises_rpc_error(-4, "BerkeleyDatabase: Can't open database w8_copy (duplicates fileid", self.nodes[0].loadwallet, 'w8_copy') # Fail to load if wallet file is a symlink assert_raises_rpc_error(-4, "Wallet file verification failed. Invalid -wallet path 'w8_symlink'", self.nodes[0].loadwallet, 'w8_symlink') @@ -284,6 +329,7 @@ class MultiWalletTest(BitcoinTestFramework): # Successfully create a wallet with a new name loadwallet_name = self.nodes[0].createwallet('w9') + in_wallet_dir.append('w9') assert_equal(loadwallet_name['name'], 'w9') w9 = node.get_wallet_rpc('w9') assert_equal(w9.getwalletinfo()['walletname'], 'w9') @@ -331,7 +377,7 @@ class MultiWalletTest(BitcoinTestFramework): assert_equal(self.nodes[0].listwallets(), ['w1']) assert_equal(w1.getwalletinfo()['walletname'], 'w1') - assert_equal(sorted(map(lambda w: w['name'], self.nodes[0].listwalletdir()['wallets'])), [self.default_wallet_name, os.path.join('sub', 'w5'), 'w', 'w1', 'w2', 'w3', 'w7', 'w7_symlink', 'w8', 'w8_copy', 'w9']) + assert_equal(sorted(map(lambda w: w['name'], self.nodes[0].listwalletdir()['wallets'])), sorted(in_wallet_dir)) # Test backing up and restoring wallets self.log.info("Test wallet backup") @@ -365,5 +411,6 @@ class MultiWalletTest(BitcoinTestFramework): self.nodes[0].unloadwallet(wallet) self.nodes[1].loadwallet(wallet) + if __name__ == '__main__': MultiWalletTest().main() diff --git a/test/functional/wallet_send.py b/test/functional/wallet_send.py index 60ab2d457e..921a726d4b 100755 --- a/test/functional/wallet_send.py +++ b/test/functional/wallet_send.py @@ -97,6 +97,8 @@ class WalletSendTest(BitcoinTestFramework): except AssertionError: # Provide debug info if the test fails self.log.error("Unexpected successful result:") + self.log.error(arg_conf_target) + self.log.error(arg_estimate_mode) self.log.error(options) res = from_wallet.send(outputs=outputs, conf_target=arg_conf_target, estimate_mode=arg_estimate_mode, options=options) self.log.error(res) @@ -224,8 +226,10 @@ class WalletSendTest(BitcoinTestFramework): assert_equal(self.nodes[1].decodepsbt(res1["psbt"])["fee"], self.nodes[1].decodepsbt(res2["psbt"])["fee"]) # but not at the same time - self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_conf_target=1, arg_estimate_mode="economical", - conf_target=1, estimate_mode="economical", add_to_wallet=False, expect_error=(-8,"Use either conf_target and estimate_mode or the options dictionary to control fee rate")) + for mode in ["unset", "economical", "conservative", "btc/kb", "sat/b"]: + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_conf_target=1, arg_estimate_mode="economical", + conf_target=1, estimate_mode=mode, add_to_wallet=False, + expect_error=(-8, "Use either conf_target and estimate_mode or the options dictionary to control fee rate")) self.log.info("Create PSBT from watch-only wallet w3, sign with w2...") res = self.test_send(from_wallet=w3, to_wallet=w1, amount=1) @@ -246,19 +250,61 @@ class WalletSendTest(BitcoinTestFramework): res = w2.walletprocesspsbt(res["psbt"]) assert res["complete"] - self.log.info("Set fee rate...") + self.log.info("Test setting explicit fee rate") + res1 = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_conf_target=1, arg_estimate_mode="economical", add_to_wallet=False) + res2 = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=1, estimate_mode="economical", add_to_wallet=False) + assert_equal(self.nodes[1].decodepsbt(res1["psbt"])["fee"], self.nodes[1].decodepsbt(res2["psbt"])["fee"]) + + res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=0.00007, estimate_mode="btc/kb", add_to_wallet=False) + fee = self.nodes[1].decodepsbt(res["psbt"])["fee"] + assert_fee_amount(fee, Decimal(len(res["hex"]) / 2), Decimal("0.00007")) + res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=2, estimate_mode="sat/b", add_to_wallet=False) fee = self.nodes[1].decodepsbt(res["psbt"])["fee"] assert_fee_amount(fee, Decimal(len(res["hex"]) / 2), Decimal("0.00002")) - self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=-1, estimate_mode="sat/b", - expect_error=(-3, "Amount out of range")) - # Fee rate of 0.1 satoshi per byte should throw an error - # TODO: error should use sat/b - self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=0.1, estimate_mode="sat/b", - expect_error=(-4, "Fee rate (0.00000100 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)")) - - self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=0.000001, estimate_mode="BTC/KB", - expect_error=(-4, "Fee rate (0.00000100 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)")) + + res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_conf_target=0.00004531, arg_estimate_mode="btc/kb", add_to_wallet=False) + fee = self.nodes[1].decodepsbt(res["psbt"])["fee"] + assert_fee_amount(fee, Decimal(len(res["hex"]) / 2), Decimal("0.00004531")) + + res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_conf_target=3, arg_estimate_mode="sat/b", add_to_wallet=False) + fee = self.nodes[1].decodepsbt(res["psbt"])["fee"] + assert_fee_amount(fee, Decimal(len(res["hex"]) / 2), Decimal("0.00003")) + + # TODO: This test should pass with all modes, e.g. with the next line uncommented, for consistency with the other explicit feerate RPCs. + # for mode in ["unset", "economical", "conservative", "btc/kb", "sat/b"]: + for mode in ["btc/kb", "sat/b"]: + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=-1, estimate_mode=mode, + expect_error=(-3, "Amount out of range")) + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=0, estimate_mode=mode, + expect_error=(-4, "Fee rate (0.00000000 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)")) + + for mode in ["foo", Decimal("3.141592")]: + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=0.1, estimate_mode=mode, + expect_error=(-8, "Invalid estimate_mode parameter")) + # TODO: these 2 equivalent sends with an invalid estimate_mode arg should both fail, but they do not...why? + # self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_conf_target=0.1, arg_estimate_mode=mode, + # expect_error=(-8, "Invalid estimate_mode parameter")) + # assert_raises_rpc_error(-8, "Invalid estimate_mode parameter", lambda: w0.send({w1.getnewaddress(): 1}, 0.1, mode)) + + # TODO: These tests should pass for consistency with the other explicit feerate RPCs, but they do not. + # for mode in ["unset", "economical", "conservative", "btc/kb", "sat/b"]: + # self.log.debug("{}".format(mode)) + # for k, v in {"string": "", "object": {"foo": "bar"}}.items(): + # self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=v, estimate_mode=mode, + # expect_error=(-3, "Expected type number for conf_target, got {}".format(k))) + + # TODO: error should use sat/B instead of BTC/kB if sat/B is selected. + # Test setting explicit fee rate just below the minimum. + for unit, fee_rate in {"sat/B": 0.99999999, "BTC/kB": 0.00000999}.items(): + self.log.info("Explicit fee rate raises RPC error 'fee rate too low' if conf_target {} and estimate_mode {} are passed".format(fee_rate, unit)) + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=fee_rate, estimate_mode=unit, + expect_error=(-4, "Fee rate (0.00000999 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)")) + + self.log.info("Explicit fee rate raises RPC error if estimate_mode is passed without a conf_target") + for unit, fee_rate in {"sat/B": 100, "BTC/kB": 0.001}.items(): + self.test_send(from_wallet=w0, to_wallet=w1, amount=1, estimate_mode=unit, + expect_error=(-8, "Selected estimate_mode {} requires a fee rate to be specified in conf_target".format(unit))) # TODO: Return hex if fee rate is below -maxmempool # res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=0.1, estimate_mode="sat/b", add_to_wallet=False) diff --git a/test/functional/wallet_upgradewallet.py b/test/functional/wallet_upgradewallet.py index 446a601aee..15d9b109c5 100755 --- a/test/functional/wallet_upgradewallet.py +++ b/test/functional/wallet_upgradewallet.py @@ -107,7 +107,7 @@ class UpgradeWalletTest(BitcoinTestFramework): # calling upgradewallet without version arguments # should return nothing if successful - assert_equal(wallet.upgradewallet(), "") + assert_equal(wallet.upgradewallet(), {}) new_version = wallet.getwalletinfo()["walletversion"] # upgraded wallet version should be greater than older one assert_greater_than(new_version, old_version) @@ -130,7 +130,7 @@ class UpgradeWalletTest(BitcoinTestFramework): assert_equal('hdseedid' in wallet.getwalletinfo(), False) # calling upgradewallet with explicit version number # should return nothing if successful - assert_equal(wallet.upgradewallet(169900), "") + assert_equal(wallet.upgradewallet(169900), {}) new_version = wallet.getwalletinfo()["walletversion"] # upgraded wallet should have version 169900 assert_equal(new_version, 169900) |