aboutsummaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rwxr-xr-xtest/functional/test_runner.py2
-rwxr-xr-xtest/functional/wallet_abandonconflict.py6
-rwxr-xr-xtest/functional/wallet_backwards_compatibility.py19
-rwxr-xr-xtest/functional/wallet_basic.py2
-rwxr-xr-xtest/functional/wallet_conflicts.py301
-rwxr-xr-xtest/functional/wallet_createwalletdescriptor.py123
-rwxr-xr-xtest/functional/wallet_gethdkeys.py185
-rw-r--r--test/lint/README.md4
-rwxr-xr-xtest/lint/lint-include-guards.py8
-rwxr-xr-xtest/lint/lint-includes.py8
-rwxr-xr-xtest/lint/lint-spelling.py5
-rw-r--r--test/lint/lint_ignore_dirs.py5
12 files changed, 655 insertions, 13 deletions
diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py
index 1408854e02..3f6e47d410 100755
--- a/test/functional/test_runner.py
+++ b/test/functional/test_runner.py
@@ -181,6 +181,8 @@ BASE_SCRIPTS = [
'wallet_keypool_topup.py --legacy-wallet',
'wallet_keypool_topup.py --descriptors',
'wallet_fast_rescan.py --descriptors',
+ 'wallet_gethdkeys.py --descriptors',
+ 'wallet_createwalletdescriptor.py --descriptors',
'interface_zmq.py',
'rpc_invalid_address_message.py',
'rpc_validateaddress.py',
diff --git a/test/functional/wallet_abandonconflict.py b/test/functional/wallet_abandonconflict.py
index e69546bb82..dda48aae1b 100755
--- a/test/functional/wallet_abandonconflict.py
+++ b/test/functional/wallet_abandonconflict.py
@@ -231,7 +231,11 @@ class AbandonConflictTest(BitcoinTestFramework):
balance = newbalance
# Invalidate the block with the double spend. B & C's 10 BTC outputs should no longer be available
- self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash())
+ blk = self.nodes[0].getbestblockhash()
+ # mine 10 blocks so that when the blk is invalidated, the transactions are not
+ # returned to the mempool
+ self.generate(self.nodes[1], 10)
+ self.nodes[0].invalidateblock(blk)
assert_equal(alice.gettransaction(txAB1)["confirmations"], 0)
newbalance = alice.getbalance()
assert_equal(newbalance, balance - Decimal("20"))
diff --git a/test/functional/wallet_backwards_compatibility.py b/test/functional/wallet_backwards_compatibility.py
index 4d6e6024c5..ab008a40cd 100755
--- a/test/functional/wallet_backwards_compatibility.py
+++ b/test/functional/wallet_backwards_compatibility.py
@@ -355,6 +355,25 @@ class BackwardsCompatibilityTest(BitcoinTestFramework):
down_wallet_name = f"re_down_{node.version}"
down_backup_path = os.path.join(self.options.tmpdir, f"{down_wallet_name}.dat")
wallet.backupwallet(down_backup_path)
+
+ # Check that taproot descriptors can be added to 0.21 wallets
+ # This must be done after the backup is created so that 0.21 can still load
+ # the backup
+ if self.options.descriptors and self.major_version_equals(node, 21):
+ assert_raises_rpc_error(-12, "No bech32m addresses available", wallet.getnewaddress, address_type="bech32m")
+ xpubs = wallet.gethdkeys(active_only=True)
+ assert_equal(len(xpubs), 1)
+ assert_equal(len(xpubs[0]["descriptors"]), 6)
+ wallet.createwalletdescriptor("bech32m")
+ xpubs = wallet.gethdkeys(active_only=True)
+ assert_equal(len(xpubs), 1)
+ assert_equal(len(xpubs[0]["descriptors"]), 8)
+ tr_descs = [desc["desc"] for desc in xpubs[0]["descriptors"] if desc["desc"].startswith("tr(")]
+ assert_equal(len(tr_descs), 2)
+ for desc in tr_descs:
+ assert info["hdmasterfingerprint"] in desc
+ wallet.getnewaddress(address_type="bech32m")
+
wallet.unloadwallet()
# Check that no automatic upgrade broke the downgrading the wallet
diff --git a/test/functional/wallet_basic.py b/test/functional/wallet_basic.py
index 31d3c14e55..56228d2bad 100755
--- a/test/functional/wallet_basic.py
+++ b/test/functional/wallet_basic.py
@@ -681,7 +681,7 @@ class WalletTest(BitcoinTestFramework):
"category": baz["category"],
"vout": baz["vout"]}
expected_fields = frozenset({'amount', 'bip125-replaceable', 'confirmations', 'details', 'fee',
- 'hex', 'lastprocessedblock', 'time', 'timereceived', 'trusted', 'txid', 'wtxid', 'walletconflicts'})
+ 'hex', 'lastprocessedblock', 'time', 'timereceived', 'trusted', 'txid', 'wtxid', 'walletconflicts', 'mempoolconflicts'})
verbose_field = "decoded"
expected_verbose_fields = expected_fields | {verbose_field}
diff --git a/test/functional/wallet_conflicts.py b/test/functional/wallet_conflicts.py
index 802b718cd5..e5739a6a59 100755
--- a/test/functional/wallet_conflicts.py
+++ b/test/functional/wallet_conflicts.py
@@ -9,6 +9,7 @@ Test that wallet correctly tracks transactions that have been conflicted by bloc
from decimal import Decimal
+from test_framework.blocktools import COINBASE_MATURITY
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
@@ -28,6 +29,20 @@ class TxConflicts(BitcoinTestFramework):
return next(tx_out["vout"] for tx_out in self.nodes[0].gettransaction(from_tx_id)["details"] if tx_out["amount"] == Decimal(f"{search_value}"))
def run_test(self):
+ """
+ The following tests check the behavior of the wallet when
+ transaction conflicts are created. These conflicts are created
+ using raw transaction RPCs that double-spend UTXOs and have more
+ fees, replacing the original transaction.
+ """
+
+ self.test_block_conflicts()
+ self.generatetoaddress(self.nodes[0], COINBASE_MATURITY + 7, self.nodes[2].getnewaddress())
+ self.test_mempool_conflict()
+ self.test_mempool_and_block_conflicts()
+ self.test_descendants_with_mempool_conflicts()
+
+ def test_block_conflicts(self):
self.log.info("Send tx from which to conflict outputs later")
txid_conflict_from_1 = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), Decimal("10"))
txid_conflict_from_2 = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), Decimal("10"))
@@ -123,5 +138,291 @@ class TxConflicts(BitcoinTestFramework):
assert_equal(former_conflicted["confirmations"], 1)
assert_equal(former_conflicted["blockheight"], 217)
+ def test_mempool_conflict(self):
+ self.nodes[0].createwallet("alice")
+ alice = self.nodes[0].get_wallet_rpc("alice")
+
+ bob = self.nodes[1]
+
+ self.nodes[2].send(outputs=[{alice.getnewaddress() : 25} for _ in range(3)])
+ self.generate(self.nodes[2], 1)
+
+ self.log.info("Test a scenario where a transaction has a mempool conflict")
+
+ unspents = alice.listunspent()
+ assert_equal(len(unspents), 3)
+ assert all([tx["amount"] == 25 for tx in unspents])
+
+ # tx1 spends unspent[0] and unspent[1]
+ raw_tx = alice.createrawtransaction(inputs=[unspents[0], unspents[1]], outputs=[{bob.getnewaddress() : 49.9999}])
+ tx1 = alice.signrawtransactionwithwallet(raw_tx)['hex']
+
+ # tx2 spends unspent[1] and unspent[2], conflicts with tx1
+ raw_tx = alice.createrawtransaction(inputs=[unspents[1], unspents[2]], outputs=[{bob.getnewaddress() : 49.99}])
+ tx2 = alice.signrawtransactionwithwallet(raw_tx)['hex']
+
+ # tx3 spends unspent[2], conflicts with tx2
+ raw_tx = alice.createrawtransaction(inputs=[unspents[2]], outputs=[{bob.getnewaddress() : 24.9899}])
+ tx3 = alice.signrawtransactionwithwallet(raw_tx)['hex']
+
+ # broadcast tx1
+ tx1_txid = alice.sendrawtransaction(tx1)
+
+ assert_equal(alice.listunspent(), [unspents[2]])
+ assert_equal(alice.getbalance(), 25)
+
+ # broadcast tx2, replaces tx1 in mempool
+ tx2_txid = alice.sendrawtransaction(tx2)
+
+ # Check that unspent[0] is now available because the transaction spending it has been replaced in the mempool
+ assert_equal(alice.listunspent(), [unspents[0]])
+ assert_equal(alice.getbalance(), 25)
+
+ assert_equal(alice.gettransaction(tx1_txid)["mempoolconflicts"], [tx2_txid])
+
+ self.log.info("Test scenario where a mempool conflict is removed")
+
+ # broadcast tx3, replaces tx2 in mempool
+ # Now that tx1's conflict has been removed, tx1 is now
+ # not conflicted, and instead is inactive until it is
+ # rebroadcasted. Now unspent[0] is not available, because
+ # tx1 is no longer conflicted.
+ alice.sendrawtransaction(tx3)
+
+ assert_equal(alice.gettransaction(tx1_txid)["mempoolconflicts"], [])
+ assert tx1_txid not in self.nodes[0].getrawmempool()
+
+ # now all of alice's outputs should be considered spent
+ # unspent[0]: spent by inactive tx1
+ # unspent[1]: spent by inactive tx1
+ # unspent[2]: spent by active tx3
+ assert_equal(alice.listunspent(), [])
+ assert_equal(alice.getbalance(), 0)
+
+ # Clean up for next test
+ bob.sendall([self.nodes[2].getnewaddress()])
+ self.generate(self.nodes[2], 1)
+
+ alice.unloadwallet()
+
+ def test_mempool_and_block_conflicts(self):
+ self.nodes[0].createwallet("alice_2")
+ alice = self.nodes[0].get_wallet_rpc("alice_2")
+ bob = self.nodes[1]
+
+ self.nodes[2].send(outputs=[{alice.getnewaddress() : 25} for _ in range(3)])
+ self.generate(self.nodes[2], 1)
+
+ self.log.info("Test a scenario where a transaction has both a block conflict and a mempool conflict")
+ unspents = [{"txid" : element["txid"], "vout" : element["vout"]} for element in alice.listunspent()]
+
+ assert_equal(bob.getbalances()["mine"]["untrusted_pending"], 0)
+
+ # alice and bob nodes are disconnected so that transactions can be
+ # created by alice, but broadcasted from bob so that alice's wallet
+ # doesn't know about them
+ self.disconnect_nodes(0, 1)
+
+ # Sends funds to bob
+ raw_tx = alice.createrawtransaction(inputs=[unspents[0]], outputs=[{bob.getnewaddress() : 24.99999}])
+ raw_tx1 = alice.signrawtransactionwithwallet(raw_tx)['hex']
+ tx1_txid = bob.sendrawtransaction(raw_tx1) # broadcast original tx spending unspents[0] only to bob
+
+ # create a conflict to previous tx (also spends unspents[0]), but don't broadcast, sends funds back to alice
+ raw_tx = alice.createrawtransaction(inputs=[unspents[0], unspents[2]], outputs=[{alice.getnewaddress() : 49.999}])
+ tx1_conflict = alice.signrawtransactionwithwallet(raw_tx)['hex']
+
+ # Sends funds to bob
+ raw_tx = alice.createrawtransaction(inputs=[unspents[1]], outputs=[{bob.getnewaddress() : 24.9999}])
+ raw_tx2 = alice.signrawtransactionwithwallet(raw_tx)['hex']
+ tx2_txid = bob.sendrawtransaction(raw_tx2) # broadcast another original tx spending unspents[1] only to bob
+
+ # create a conflict to previous tx (also spends unspents[1]), but don't broadcast, sends funds to alice
+ raw_tx = alice.createrawtransaction(inputs=[unspents[1]], outputs=[{alice.getnewaddress() : 24.9999}])
+ tx2_conflict = alice.signrawtransactionwithwallet(raw_tx)['hex']
+
+ bob_unspents = [{"txid" : element, "vout" : 0} for element in [tx1_txid, tx2_txid]]
+
+ # tx1 and tx2 are now in bob's mempool, and they are unconflicted, so bob has these funds
+ assert_equal(bob.getbalances()["mine"]["untrusted_pending"], Decimal("49.99989000"))
+
+ # spend both of bob's unspents, child tx of tx1 and tx2
+ raw_tx = bob.createrawtransaction(inputs=[bob_unspents[0], bob_unspents[1]], outputs=[{bob.getnewaddress() : 49.999}])
+ raw_tx3 = bob.signrawtransactionwithwallet(raw_tx)['hex']
+ tx3_txid = bob.sendrawtransaction(raw_tx3) # broadcast tx only to bob
+
+ # alice knows about 0 txs, bob knows about 3
+ assert_equal(len(alice.getrawmempool()), 0)
+ assert_equal(len(bob.getrawmempool()), 3)
+
+ assert_equal(bob.getbalances()["mine"]["untrusted_pending"], Decimal("49.99900000"))
+
+ # bob broadcasts tx_1 conflict
+ tx1_conflict_txid = bob.sendrawtransaction(tx1_conflict)
+ assert_equal(len(alice.getrawmempool()), 0)
+ assert_equal(len(bob.getrawmempool()), 2) # tx1_conflict kicks out both tx1, and its child tx3
+
+ assert tx2_txid in bob.getrawmempool()
+ assert tx1_conflict_txid in bob.getrawmempool()
+
+ assert_equal(bob.gettransaction(tx1_txid)["mempoolconflicts"], [tx1_conflict_txid])
+ assert_equal(bob.gettransaction(tx2_txid)["mempoolconflicts"], [])
+ assert_equal(bob.gettransaction(tx3_txid)["mempoolconflicts"], [tx1_conflict_txid])
+
+ # check that tx3 is now conflicted, so the output from tx2 can now be spent
+ assert_equal(bob.getbalances()["mine"]["untrusted_pending"], Decimal("24.99990000"))
+
+ # we will be disconnecting this block in the future
+ alice.sendrawtransaction(tx2_conflict)
+ assert_equal(len(alice.getrawmempool()), 1) # currently alice's mempool is only aware of tx2_conflict
+ # 11 blocks are mined so that when they are invalidated, tx_2
+ # does not get put back into the mempool
+ blk = self.generate(self.nodes[0], 11, sync_fun=self.no_op)[0]
+ assert_equal(len(alice.getrawmempool()), 0) # tx2_conflict is now mined
+
+ self.connect_nodes(0, 1)
+ self.sync_blocks()
+ assert_equal(alice.getbestblockhash(), bob.getbestblockhash())
+
+ # now that tx2 has a block conflict, tx1_conflict should be the only tx in bob's mempool
+ assert tx1_conflict_txid in bob.getrawmempool()
+ assert_equal(len(bob.getrawmempool()), 1)
+
+ # tx3 should now also be block-conflicted by tx2_conflict
+ assert_equal(bob.gettransaction(tx3_txid)["confirmations"], -11)
+ # bob has no pending funds, since tx1, tx2, and tx3 are all conflicted
+ assert_equal(bob.getbalances()["mine"]["untrusted_pending"], 0)
+ bob.invalidateblock(blk) # remove tx2_conflict
+ # bob should still have no pending funds because tx1 and tx3 are still conflicted, and tx2 has not been re-broadcast
+ assert_equal(bob.getbalances()["mine"]["untrusted_pending"], 0)
+ assert_equal(len(bob.getrawmempool()), 1)
+ # check that tx3 is no longer block-conflicted
+ assert_equal(bob.gettransaction(tx3_txid)["confirmations"], 0)
+
+ bob.sendrawtransaction(raw_tx2)
+ assert_equal(bob.getbalances()["mine"]["untrusted_pending"], Decimal("24.99990000"))
+
+ # create a conflict to previous tx (also spends unspents[2]), but don't broadcast, sends funds back to alice
+ raw_tx = alice.createrawtransaction(inputs=[unspents[2]], outputs=[{alice.getnewaddress() : 24.99}])
+ tx1_conflict_conflict = alice.signrawtransactionwithwallet(raw_tx)['hex']
+
+ bob.sendrawtransaction(tx1_conflict_conflict) # kick tx1_conflict out of the mempool
+ bob.sendrawtransaction(raw_tx1) #re-broadcast tx1 because it is no longer conflicted
+
+ # Now bob has no pending funds because tx1 and tx2 are spent by tx3, which hasn't been re-broadcast yet
+ assert_equal(bob.getbalances()["mine"]["untrusted_pending"], 0)
+
+ bob.sendrawtransaction(raw_tx3)
+ assert_equal(len(bob.getrawmempool()), 4) # The mempool contains: tx1, tx2, tx1_conflict_conflict, tx3
+ assert_equal(bob.getbalances()["mine"]["untrusted_pending"], Decimal("49.99900000"))
+
+ # Clean up for next test
+ bob.reconsiderblock(blk)
+ assert_equal(alice.getbestblockhash(), bob.getbestblockhash())
+ self.sync_mempools()
+ self.generate(self.nodes[2], 1)
+
+ alice.unloadwallet()
+
+ def test_descendants_with_mempool_conflicts(self):
+ self.nodes[0].createwallet("alice_3")
+ alice = self.nodes[0].get_wallet_rpc("alice_3")
+
+ self.nodes[2].send(outputs=[{alice.getnewaddress() : 25} for _ in range(2)])
+ self.generate(self.nodes[2], 1)
+
+ self.nodes[1].createwallet("bob_1")
+ bob = self.nodes[1].get_wallet_rpc("bob_1")
+
+ self.nodes[2].createwallet("carol")
+ carol = self.nodes[2].get_wallet_rpc("carol")
+
+ self.log.info("Test a scenario where a transaction's parent has a mempool conflict")
+
+ unspents = alice.listunspent()
+ assert_equal(len(unspents), 2)
+ assert all([tx["amount"] == 25 for tx in unspents])
+
+ assert_equal(alice.getrawmempool(), [])
+
+ # Alice spends first utxo to bob in tx1
+ raw_tx = alice.createrawtransaction(inputs=[unspents[0]], outputs=[{bob.getnewaddress() : 24.9999}])
+ tx1 = alice.signrawtransactionwithwallet(raw_tx)['hex']
+ tx1_txid = alice.sendrawtransaction(tx1)
+
+ self.sync_mempools()
+
+ assert_equal(alice.getbalance(), 25)
+ assert_equal(bob.getbalances()["mine"]["untrusted_pending"], Decimal("24.99990000"))
+
+ assert_equal(bob.gettransaction(tx1_txid)["mempoolconflicts"], [])
+
+ raw_tx = bob.createrawtransaction(inputs=[bob.listunspent(minconf=0)[0]], outputs=[{carol.getnewaddress() : 24.999}])
+ # Bob creates a child to tx1
+ tx1_child = bob.signrawtransactionwithwallet(raw_tx)['hex']
+ tx1_child_txid = bob.sendrawtransaction(tx1_child)
+
+ self.sync_mempools()
+
+ # Currently neither tx1 nor tx1_child should have any conflicts
+ assert_equal(bob.gettransaction(tx1_txid)["mempoolconflicts"], [])
+ assert_equal(bob.gettransaction(tx1_child_txid)["mempoolconflicts"], [])
+ assert tx1_txid in bob.getrawmempool()
+ assert tx1_child_txid in bob.getrawmempool()
+ assert_equal(len(bob.getrawmempool()), 2)
+
+ assert_equal(bob.getbalances()["mine"]["untrusted_pending"], 0)
+ assert_equal(carol.getbalances()["mine"]["untrusted_pending"], Decimal("24.99900000"))
+
+ # Alice spends first unspent again, conflicting with tx1
+ raw_tx = alice.createrawtransaction(inputs=[unspents[0], unspents[1]], outputs=[{carol.getnewaddress() : 49.99}])
+ tx1_conflict = alice.signrawtransactionwithwallet(raw_tx)['hex']
+ tx1_conflict_txid = alice.sendrawtransaction(tx1_conflict)
+
+ self.sync_mempools()
+
+ assert_equal(bob.getbalances()["mine"]["untrusted_pending"], 0)
+ assert_equal(carol.getbalances()["mine"]["untrusted_pending"], Decimal("49.99000000"))
+
+ assert tx1_txid not in bob.getrawmempool()
+ assert tx1_child_txid not in bob.getrawmempool()
+ assert tx1_conflict_txid in bob.getrawmempool()
+ assert_equal(len(bob.getrawmempool()), 1)
+
+ # Now both tx1 and tx1_child are conflicted by tx1_conflict
+ assert_equal(bob.gettransaction(tx1_txid)["mempoolconflicts"], [tx1_conflict_txid])
+ assert_equal(bob.gettransaction(tx1_child_txid)["mempoolconflicts"], [tx1_conflict_txid])
+
+ # Now create a conflict to tx1_conflict, so that it gets kicked out of the mempool
+ raw_tx = alice.createrawtransaction(inputs=[unspents[1]], outputs=[{carol.getnewaddress() : 24.9895}])
+ tx1_conflict_conflict = alice.signrawtransactionwithwallet(raw_tx)['hex']
+ tx1_conflict_conflict_txid = alice.sendrawtransaction(tx1_conflict_conflict)
+
+ self.sync_mempools()
+
+ # Now that tx1_conflict has been removed, both tx1 and tx1_child
+ assert_equal(bob.gettransaction(tx1_txid)["mempoolconflicts"], [])
+ assert_equal(bob.gettransaction(tx1_child_txid)["mempoolconflicts"], [])
+
+ # Both tx1 and tx1_child are still not in the mempool because they have not be re-broadcasted
+ assert tx1_txid not in bob.getrawmempool()
+ assert tx1_child_txid not in bob.getrawmempool()
+ assert tx1_conflict_txid not in bob.getrawmempool()
+ assert tx1_conflict_conflict_txid in bob.getrawmempool()
+ assert_equal(len(bob.getrawmempool()), 1)
+
+ assert_equal(alice.getbalance(), 0)
+ assert_equal(bob.getbalances()["mine"]["untrusted_pending"], 0)
+ assert_equal(carol.getbalances()["mine"]["untrusted_pending"], Decimal("24.98950000"))
+
+ # Both tx1 and tx1_child can now be re-broadcasted
+ bob.sendrawtransaction(tx1)
+ bob.sendrawtransaction(tx1_child)
+ assert_equal(len(bob.getrawmempool()), 3)
+
+ alice.unloadwallet()
+ bob.unloadwallet()
+ carol.unloadwallet()
+
if __name__ == '__main__':
TxConflicts().main()
diff --git a/test/functional/wallet_createwalletdescriptor.py b/test/functional/wallet_createwalletdescriptor.py
new file mode 100755
index 0000000000..18e1703da3
--- /dev/null
+++ b/test/functional/wallet_createwalletdescriptor.py
@@ -0,0 +1,123 @@
+#!/usr/bin/env python3
+# Copyright (c) 2023 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+"""Test wallet createwalletdescriptor RPC."""
+
+from test_framework.descriptors import descsum_create
+from test_framework.test_framework import BitcoinTestFramework
+from test_framework.util import (
+ assert_equal,
+ assert_raises_rpc_error,
+)
+from test_framework.wallet_util import WalletUnlock
+
+
+class WalletCreateDescriptorTest(BitcoinTestFramework):
+ def add_options(self, parser):
+ self.add_wallet_options(parser, descriptors=True, legacy=False)
+
+ def set_test_params(self):
+ self.setup_clean_chain = True
+ self.num_nodes = 1
+
+ def skip_test_if_missing_module(self):
+ self.skip_if_no_wallet()
+
+ def run_test(self):
+ self.test_basic()
+ self.test_imported_other_keys()
+ self.test_encrypted()
+
+ def test_basic(self):
+ def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
+ self.nodes[0].createwallet("blank", blank=True)
+ wallet = self.nodes[0].get_wallet_rpc("blank")
+
+ xpub_info = def_wallet.gethdkeys(private=True)
+ xpub = xpub_info[0]["xpub"]
+ xprv = xpub_info[0]["xprv"]
+ expected_descs = []
+ for desc in def_wallet.listdescriptors()["descriptors"]:
+ if desc["desc"].startswith("wpkh("):
+ expected_descs.append(desc["desc"])
+
+ assert_raises_rpc_error(-5, "Unable to determine which HD key to use from active descriptors. Please specify with 'hdkey'", wallet.createwalletdescriptor, "bech32")
+ assert_raises_rpc_error(-5, f"Private key for {xpub} is not known", wallet.createwalletdescriptor, type="bech32", hdkey=xpub)
+
+ self.log.info("Test createwalletdescriptor after importing active descriptor to blank wallet")
+ # Import one active descriptor
+ assert_equal(wallet.importdescriptors([{"desc": descsum_create(f"pkh({xprv}/44h/2h/0h/0/0/*)"), "timestamp": "now", "active": True}])[0]["success"], True)
+ assert_equal(len(wallet.listdescriptors()["descriptors"]), 1)
+ assert_equal(len(wallet.gethdkeys()), 1)
+
+ new_descs = wallet.createwalletdescriptor("bech32")["descs"]
+ assert_equal(len(new_descs), 2)
+ assert_equal(len(wallet.gethdkeys()), 1)
+ assert_equal(new_descs, expected_descs)
+
+ self.log.info("Test descriptor creation options")
+ old_descs = set([(d["desc"], d["active"], d["internal"]) for d in wallet.listdescriptors(private=True)["descriptors"]])
+ wallet.createwalletdescriptor(type="bech32m", internal=False)
+ curr_descs = set([(d["desc"], d["active"], d["internal"]) for d in wallet.listdescriptors(private=True)["descriptors"]])
+ new_descs = list(curr_descs - old_descs)
+ assert_equal(len(new_descs), 1)
+ assert_equal(len(wallet.gethdkeys()), 1)
+ assert_equal(new_descs[0][0], descsum_create(f"tr({xprv}/86h/1h/0h/0/*)"))
+ assert_equal(new_descs[0][1], True)
+ assert_equal(new_descs[0][2], False)
+
+ old_descs = curr_descs
+ wallet.createwalletdescriptor(type="bech32m", internal=True)
+ curr_descs = set([(d["desc"], d["active"], d["internal"]) for d in wallet.listdescriptors(private=True)["descriptors"]])
+ new_descs = list(curr_descs - old_descs)
+ assert_equal(len(new_descs), 1)
+ assert_equal(len(wallet.gethdkeys()), 1)
+ assert_equal(new_descs[0][0], descsum_create(f"tr({xprv}/86h/1h/0h/1/*)"))
+ assert_equal(new_descs[0][1], True)
+ assert_equal(new_descs[0][2], True)
+
+ def test_imported_other_keys(self):
+ self.log.info("Test createwalletdescriptor with multiple keys in active descriptors")
+ def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
+ self.nodes[0].createwallet("multiple_keys")
+ wallet = self.nodes[0].get_wallet_rpc("multiple_keys")
+
+ wallet_xpub = wallet.gethdkeys()[0]["xpub"]
+
+ xpub_info = def_wallet.gethdkeys(private=True)
+ xpub = xpub_info[0]["xpub"]
+ xprv = xpub_info[0]["xprv"]
+
+ assert_equal(wallet.importdescriptors([{"desc": descsum_create(f"wpkh({xprv}/0/0/*)"), "timestamp": "now", "active": True}])[0]["success"], True)
+ assert_equal(len(wallet.gethdkeys()), 2)
+
+ assert_raises_rpc_error(-5, "Unable to determine which HD key to use from active descriptors. Please specify with 'hdkey'", wallet.createwalletdescriptor, "bech32")
+ assert_raises_rpc_error(-4, "Descriptor already exists", wallet.createwalletdescriptor, type="bech32m", hdkey=wallet_xpub)
+ assert_raises_rpc_error(-5, "Unable to parse HD key. Please provide a valid xpub", wallet.createwalletdescriptor, type="bech32m", hdkey=xprv)
+
+ # Able to replace tr() descriptor with other hd key
+ wallet.createwalletdescriptor(type="bech32m", hdkey=xpub)
+
+ def test_encrypted(self):
+ self.log.info("Test createwalletdescriptor with encrypted wallets")
+ def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
+ self.nodes[0].createwallet("encrypted", blank=True, passphrase="pass")
+ wallet = self.nodes[0].get_wallet_rpc("encrypted")
+
+ xpub_info = def_wallet.gethdkeys(private=True)
+ xprv = xpub_info[0]["xprv"]
+
+ with WalletUnlock(wallet, "pass"):
+ assert_equal(wallet.importdescriptors([{"desc": descsum_create(f"wpkh({xprv}/0/0/*)"), "timestamp": "now", "active": True}])[0]["success"], True)
+ assert_equal(len(wallet.gethdkeys()), 1)
+
+ assert_raises_rpc_error(-13, "Error: Please enter the wallet passphrase with walletpassphrase first.", wallet.createwalletdescriptor, type="bech32m")
+
+ with WalletUnlock(wallet, "pass"):
+ wallet.createwalletdescriptor(type="bech32m")
+
+
+
+if __name__ == '__main__':
+ WalletCreateDescriptorTest().main()
diff --git a/test/functional/wallet_gethdkeys.py b/test/functional/wallet_gethdkeys.py
new file mode 100755
index 0000000000..f09b8c875a
--- /dev/null
+++ b/test/functional/wallet_gethdkeys.py
@@ -0,0 +1,185 @@
+#!/usr/bin/env python3
+# Copyright (c) 2023 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+"""Test wallet gethdkeys RPC."""
+
+from test_framework.descriptors import descsum_create
+from test_framework.test_framework import BitcoinTestFramework
+from test_framework.util import (
+ assert_equal,
+ assert_raises_rpc_error,
+)
+from test_framework.wallet_util import WalletUnlock
+
+
+class WalletGetHDKeyTest(BitcoinTestFramework):
+ def add_options(self, parser):
+ self.add_wallet_options(parser, descriptors=True, legacy=False)
+
+ def set_test_params(self):
+ self.setup_clean_chain = True
+ self.num_nodes = 1
+
+ def skip_test_if_missing_module(self):
+ self.skip_if_no_wallet()
+
+ def run_test(self):
+ self.test_basic_gethdkeys()
+ self.test_ranged_imports()
+ self.test_lone_key_imports()
+ self.test_ranged_multisig()
+ self.test_mixed_multisig()
+
+ def test_basic_gethdkeys(self):
+ self.log.info("Test gethdkeys basics")
+ self.nodes[0].createwallet("basic")
+ wallet = self.nodes[0].get_wallet_rpc("basic")
+ xpub_info = wallet.gethdkeys()
+ assert_equal(len(xpub_info), 1)
+ assert_equal(xpub_info[0]["has_private"], True)
+
+ assert "xprv" not in xpub_info[0]
+ xpub = xpub_info[0]["xpub"]
+
+ xpub_info = wallet.gethdkeys(private=True)
+ xprv = xpub_info[0]["xprv"]
+ assert_equal(xpub_info[0]["xpub"], xpub)
+ assert_equal(xpub_info[0]["has_private"], True)
+
+ descs = wallet.listdescriptors(True)
+ for desc in descs["descriptors"]:
+ assert xprv in desc["desc"]
+
+ self.log.info("HD pubkey can be retrieved from encrypted wallets")
+ prev_xprv = xprv
+ wallet.encryptwallet("pass")
+ # HD key is rotated on encryption, there should now be 2 HD keys
+ assert_equal(len(wallet.gethdkeys()), 2)
+ # New key is active, should be able to get only that one and its descriptors
+ xpub_info = wallet.gethdkeys(active_only=True)
+ assert_equal(len(xpub_info), 1)
+ assert xpub_info[0]["xpub"] != xpub
+ assert "xprv" not in xpub_info[0]
+ assert_equal(xpub_info[0]["has_private"], True)
+
+ self.log.info("HD privkey can be retrieved from encrypted wallets")
+ assert_raises_rpc_error(-13, "Error: Please enter the wallet passphrase with walletpassphrase first", wallet.gethdkeys, private=True)
+ with WalletUnlock(wallet, "pass"):
+ xpub_info = wallet.gethdkeys(active_only=True, private=True)[0]
+ assert xpub_info["xprv"] != xprv
+ for desc in wallet.listdescriptors(True)["descriptors"]:
+ if desc["active"]:
+ # After encrypting, HD key was rotated and should appear in all active descriptors
+ assert xpub_info["xprv"] in desc["desc"]
+ else:
+ # Inactive descriptors should have the previous HD key
+ assert prev_xprv in desc["desc"]
+
+ def test_ranged_imports(self):
+ self.log.info("Keys of imported ranged descriptors appear in gethdkeys")
+ def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
+ self.nodes[0].createwallet("imports")
+ wallet = self.nodes[0].get_wallet_rpc("imports")
+
+ xpub_info = wallet.gethdkeys()
+ assert_equal(len(xpub_info), 1)
+ active_xpub = xpub_info[0]["xpub"]
+
+ import_xpub = def_wallet.gethdkeys(active_only=True)[0]["xpub"]
+ desc_import = def_wallet.listdescriptors(True)["descriptors"]
+ for desc in desc_import:
+ desc["active"] = False
+ wallet.importdescriptors(desc_import)
+ assert_equal(wallet.gethdkeys(active_only=True), xpub_info)
+
+ xpub_info = wallet.gethdkeys()
+ assert_equal(len(xpub_info), 2)
+ for x in xpub_info:
+ if x["xpub"] == active_xpub:
+ for desc in x["descriptors"]:
+ assert_equal(desc["active"], True)
+ elif x["xpub"] == import_xpub:
+ for desc in x["descriptors"]:
+ assert_equal(desc["active"], False)
+ else:
+ assert False
+
+
+ def test_lone_key_imports(self):
+ self.log.info("Non-HD keys do not appear in gethdkeys")
+ self.nodes[0].createwallet("lonekey", blank=True)
+ wallet = self.nodes[0].get_wallet_rpc("lonekey")
+
+ assert_equal(wallet.gethdkeys(), [])
+ wallet.importdescriptors([{"desc": descsum_create("wpkh(cTe1f5rdT8A8DFgVWTjyPwACsDPJM9ff4QngFxUixCSvvbg1x6sh)"), "timestamp": "now"}])
+ assert_equal(wallet.gethdkeys(), [])
+
+ self.log.info("HD keys of non-ranged descriptors should appear in gethdkeys")
+ def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
+ xpub_info = def_wallet.gethdkeys(private=True)
+ xpub = xpub_info[0]["xpub"]
+ xprv = xpub_info[0]["xprv"]
+ prv_desc = descsum_create(f"wpkh({xprv})")
+ pub_desc = descsum_create(f"wpkh({xpub})")
+ assert_equal(wallet.importdescriptors([{"desc": prv_desc, "timestamp": "now"}])[0]["success"], True)
+ xpub_info = wallet.gethdkeys()
+ assert_equal(len(xpub_info), 1)
+ assert_equal(xpub_info[0]["xpub"], xpub)
+ assert_equal(len(xpub_info[0]["descriptors"]), 1)
+ assert_equal(xpub_info[0]["descriptors"][0]["desc"], pub_desc)
+ assert_equal(xpub_info[0]["descriptors"][0]["active"], False)
+
+ def test_ranged_multisig(self):
+ self.log.info("HD keys of a multisig appear in gethdkeys")
+ def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
+ self.nodes[0].createwallet("ranged_multisig")
+ wallet = self.nodes[0].get_wallet_rpc("ranged_multisig")
+
+ xpub1 = wallet.gethdkeys()[0]["xpub"]
+ xprv1 = wallet.gethdkeys(private=True)[0]["xprv"]
+ xpub2 = def_wallet.gethdkeys()[0]["xpub"]
+
+ prv_multi_desc = descsum_create(f"wsh(multi(2,{xprv1}/*,{xpub2}/*))")
+ pub_multi_desc = descsum_create(f"wsh(multi(2,{xpub1}/*,{xpub2}/*))")
+ assert_equal(wallet.importdescriptors([{"desc": prv_multi_desc, "timestamp": "now"}])[0]["success"], True)
+
+ xpub_info = wallet.gethdkeys()
+ assert_equal(len(xpub_info), 2)
+ for x in xpub_info:
+ if x["xpub"] == xpub1:
+ found_desc = next((d for d in xpub_info[0]["descriptors"] if d["desc"] == pub_multi_desc), None)
+ assert found_desc is not None
+ assert_equal(found_desc["active"], False)
+ elif x["xpub"] == xpub2:
+ assert_equal(len(x["descriptors"]), 1)
+ assert_equal(x["descriptors"][0]["desc"], pub_multi_desc)
+ assert_equal(x["descriptors"][0]["active"], False)
+ else:
+ assert False
+
+ def test_mixed_multisig(self):
+ self.log.info("Non-HD keys of a multisig do not appear in gethdkeys")
+ def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
+ self.nodes[0].createwallet("single_multisig")
+ wallet = self.nodes[0].get_wallet_rpc("single_multisig")
+
+ xpub = wallet.gethdkeys()[0]["xpub"]
+ xprv = wallet.gethdkeys(private=True)[0]["xprv"]
+ pub = def_wallet.getaddressinfo(def_wallet.getnewaddress())["pubkey"]
+
+ prv_multi_desc = descsum_create(f"wsh(multi(2,{xprv},{pub}))")
+ pub_multi_desc = descsum_create(f"wsh(multi(2,{xpub},{pub}))")
+ import_res = wallet.importdescriptors([{"desc": prv_multi_desc, "timestamp": "now"}])
+ assert_equal(import_res[0]["success"], True)
+
+ xpub_info = wallet.gethdkeys()
+ assert_equal(len(xpub_info), 1)
+ assert_equal(xpub_info[0]["xpub"], xpub)
+ found_desc = next((d for d in xpub_info[0]["descriptors"] if d["desc"] == pub_multi_desc), None)
+ assert found_desc is not None
+ assert_equal(found_desc["active"], False)
+
+
+if __name__ == '__main__':
+ WalletGetHDKeyTest().main()
diff --git a/test/lint/README.md b/test/lint/README.md
index 83264de06e..9d167bac72 100644
--- a/test/lint/README.md
+++ b/test/lint/README.md
@@ -87,3 +87,7 @@ To do so, add the upstream repository as remote:
```
git remote add --fetch secp256k1 https://github.com/bitcoin-core/secp256k1.git
```
+
+lint_ignore_dirs.py
+===================
+Add list of common directories to ignore when running tests
diff --git a/test/lint/lint-include-guards.py b/test/lint/lint-include-guards.py
index 291e528c1d..77af05c1c2 100755
--- a/test/lint/lint-include-guards.py
+++ b/test/lint/lint-include-guards.py
@@ -12,19 +12,17 @@ import re
import sys
from subprocess import check_output
+from lint_ignore_dirs import SHARED_EXCLUDED_SUBTREES
+
HEADER_ID_PREFIX = 'BITCOIN_'
HEADER_ID_SUFFIX = '_H'
EXCLUDE_FILES_WITH_PREFIX = ['contrib/devtools/bitcoin-tidy',
'src/crypto/ctaes',
- 'src/leveldb',
- 'src/crc32c',
- 'src/secp256k1',
- 'src/minisketch',
'src/tinyformat.h',
'src/bench/nanobench.h',
- 'src/test/fuzz/FuzzedDataProvider.h']
+ 'src/test/fuzz/FuzzedDataProvider.h'] + SHARED_EXCLUDED_SUBTREES
def _get_header_file_lst() -> list[str]:
diff --git a/test/lint/lint-includes.py b/test/lint/lint-includes.py
index 6386a92c0f..81ed4c0840 100755
--- a/test/lint/lint-includes.py
+++ b/test/lint/lint-includes.py
@@ -14,13 +14,11 @@ import sys
from subprocess import check_output, CalledProcessError
+from lint_ignore_dirs import SHARED_EXCLUDED_SUBTREES
+
EXCLUDED_DIRS = ["contrib/devtools/bitcoin-tidy/",
- "src/leveldb/",
- "src/crc32c/",
- "src/secp256k1/",
- "src/minisketch/",
- ]
+ ] + SHARED_EXCLUDED_SUBTREES
EXPECTED_BOOST_INCLUDES = ["boost/date_time/posix_time/posix_time.hpp",
"boost/multi_index/detail/hash_index_iterator.hpp",
diff --git a/test/lint/lint-spelling.py b/test/lint/lint-spelling.py
index ac0bddeaa6..3e578b218f 100755
--- a/test/lint/lint-spelling.py
+++ b/test/lint/lint-spelling.py
@@ -11,8 +11,11 @@ Note: Will exit successfully regardless of spelling errors.
from subprocess import check_output, STDOUT, CalledProcessError
+from lint_ignore_dirs import SHARED_EXCLUDED_SUBTREES
+
IGNORE_WORDS_FILE = 'test/lint/spelling.ignore-words.txt'
-FILES_ARGS = ['git', 'ls-files', '--', ":(exclude)build-aux/m4/", ":(exclude)contrib/seeds/*.txt", ":(exclude)depends/", ":(exclude)doc/release-notes/", ":(exclude)src/leveldb/", ":(exclude)src/crc32c/", ":(exclude)src/qt/locale/", ":(exclude)src/qt/*.qrc", ":(exclude)src/secp256k1/", ":(exclude)src/minisketch/", ":(exclude)contrib/guix/patches"]
+FILES_ARGS = ['git', 'ls-files', '--', ":(exclude)build-aux/m4/", ":(exclude)contrib/seeds/*.txt", ":(exclude)depends/", ":(exclude)doc/release-notes/", ":(exclude)src/qt/locale/", ":(exclude)src/qt/*.qrc", ":(exclude)contrib/guix/patches"]
+FILES_ARGS += [f":(exclude){dir}" for dir in SHARED_EXCLUDED_SUBTREES]
def check_codespell_install():
diff --git a/test/lint/lint_ignore_dirs.py b/test/lint/lint_ignore_dirs.py
new file mode 100644
index 0000000000..af9ee7ef6b
--- /dev/null
+++ b/test/lint/lint_ignore_dirs.py
@@ -0,0 +1,5 @@
+SHARED_EXCLUDED_SUBTREES = ["src/leveldb/",
+ "src/crc32c/",
+ "src/secp256k1/",
+ "src/minisketch/",
+ ]