Skip to content
Bitcoin Roots

v29.4-roots.4 · Commit 23: [PATCH 23/37] feat(wallet): add transient private-key sweep RPC

test/functional/wallet_sweepprivkeys.py

Before: /dev/null · After: test/functional/wallet_sweepprivkeys.py

File metadata
new file mode 100644
index 0000000000000000000000000000000000000000..eab6638441788085b28aef9c917b8d580db24383

Download original patch · Release notes

Diff rows 1–145 of 145. Line numbers refer to the original before/after files.

+ Added− RemovedUnmarked lines are context.

Scroll the code area horizontally to read long lines. The two line-number columns refer to before and after.

test/functional/wallet_sweepprivkeys.py ADDED
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env python3
2
+ """Exercise transient-key sweep preview and broadcast without import."""
3
+ from decimal import Decimal
4
+ from pathlib import Path
5
+
6
+ from test_framework.address import key_to_p2pkh, key_to_p2sh_p2wpkh, key_to_p2wpkh
7
+ from test_framework.test_framework import BitcoinTestFramework
8
+ from test_framework.util import assert_equal, assert_raises_rpc_error
9
+ from test_framework.wallet_util import generate_keypair
10
+
11
+ COIN = Decimal(100_000_000)
12
+
13
+ class SweepPrivKeysTest(BitcoinTestFramework):
14
+ def add_options(self, parser):
15
+ self.add_wallet_options(parser)
16
+
17
+ def set_test_params(self):
18
+ self.num_nodes = 2
19
+
20
+ def skip_test_if_missing_module(self):
21
+ self.skip_if_no_wallet()
22
+
23
+ def run_test(self):
24
+ node, miner = self.nodes
25
+ wallet = node.get_wallet_rpc(node.listwallets()[0])
26
+ destination = wallet.getnewaddress()
27
+ wif, pubkey = generate_keypair(wif=True)
28
+ uncompressed_wif, uncompressed_pubkey = generate_keypair(compressed=False, wif=True)
29
+ race_wif, race_pubkey = generate_keypair(wif=True)
30
+ empty_wif, _ = generate_keypair(wif=True)
31
+ self.generate(miner, 101)
32
+ assert_raises_rpc_error(-6, 'No spendable UTXOs found', wallet.sweepprivkeys, [empty_wif], destination)
33
+ for address in (key_to_p2pkh(pubkey), key_to_p2wpkh(pubkey), key_to_p2sh_p2wpkh(pubkey), key_to_p2pkh(uncompressed_pubkey)):
34
+ miner.sendtoaddress(address, 1)
35
+ race_address = key_to_p2pkh(race_pubkey)
36
+ race_txid = miner.sendtoaddress(race_address, 1)
37
+ self.generate(miner, 1)
38
+ race_tx = miner.decoderawtransaction(miner.gettransaction(race_txid)['hex'])
39
+ race_vout = next(vout for vout in race_tx['vout'] if vout['scriptPubKey'].get('address') == race_address)
40
+ race_prevout = {'txid': race_txid, 'vout': race_vout['n'], 'scriptPubKey': race_vout['scriptPubKey']['hex'], 'amount': race_vout['value']}
41
+ race_raw = miner.createrawtransaction([race_prevout], {miner.getnewaddress(): Decimal('0.99990000')})
42
+ race_signed = miner.signrawtransactionwithkey(race_raw, [race_wif], [race_prevout])
43
+ assert_equal(race_signed['complete'], True)
44
+ miner.sendrawtransaction(race_signed['hex'])
45
+ race_block = self.generate(miner, 1)[0]
46
+ self.sync_blocks()
47
+ assert_raises_rpc_error(-6, 'No spendable UTXOs found', wallet.sweepprivkeys, [race_wif], destination)
48
+ node.invalidateblock(race_block)
49
+ mempool_before_preview = wallet.getrawmempool()
50
+ stale_preview = wallet.sweepprivkeys([race_wif], destination)
51
+ assert_equal(stale_preview['broadcast'], False)
52
+ assert_equal(stale_preview['inputs'], 1)
53
+ assert_equal(wallet.getrawmempool(), mempool_before_preview)
54
+ node.reconsiderblock(race_block)
55
+ assert_raises_rpc_error(-6, 'No spendable UTXOs found', wallet.sweepprivkeys, [race_wif], destination)
56
+ conflict_wif, conflict_pubkey = generate_keypair(wif=True)
57
+ conflict_address = key_to_p2pkh(conflict_pubkey)
58
+ conflict_txid = miner.sendtoaddress(conflict_address, 1)
59
+ self.generate(miner, 1)
60
+ self.sync_blocks()
61
+ conflict_tx = miner.decoderawtransaction(miner.gettransaction(conflict_txid)['hex'])
62
+ conflict_vout = next(v for v in conflict_tx['vout'] if v['scriptPubKey'].get('address') == conflict_address)
63
+ prevout = {'txid': conflict_txid, 'vout': conflict_vout['n'], 'scriptPubKey': conflict_vout['scriptPubKey']['hex'], 'amount': conflict_vout['value']}
64
+ raw = miner.createrawtransaction([prevout], {miner.getnewaddress(): Decimal('0.99990000')})
65
+ competing = miner.signrawtransactionwithkey(raw, [conflict_wif], [prevout])
66
+ conflict_id = miner.sendrawtransaction(competing['hex'])
67
+ self.sync_mempools()
68
+ rejected_state = wallet.listdescriptors(), wallet.listlabels(), wallet.getwalletinfo()['keypoolsize']
69
+ assert_raises_rpc_error(-4, 'Sweep transaction broadcast failed: insufficient fee, rejecting replacement', wallet.sweepprivkeys, [conflict_wif], destination, True)
70
+ assert_equal(wallet.getrawmempool(), [conflict_id])
71
+ assert_equal((wallet.listdescriptors(), wallet.listlabels(), wallet.getwalletinfo()['keypoolsize']), rejected_state)
72
+ assert_equal(wallet.getaddressinfo(conflict_address)['ismine'], False)
73
+ descriptors = wallet.listdescriptors()
74
+ labels = wallet.listlabels()
75
+ keypoolsize = wallet.getwalletinfo()['keypoolsize']
76
+
77
+ assert_raises_rpc_error(-5, 'Invalid private key', wallet.sweepprivkeys, ['invalid'], destination)
78
+ assert_raises_rpc_error(-5, 'Invalid private key', wallet.sweepprivkeys, ['KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn'], destination)
79
+ assert_raises_rpc_error(-8, 'Duplicate private key', wallet.sweepprivkeys, [wif, wif], destination)
80
+ assert_raises_rpc_error(-4, 'Destination is not controlled', wallet.sweepprivkeys, [wif], miner.getnewaddress())
81
+ node.createwallet(wallet_name='other')
82
+ other = node.get_wallet_rpc('other')
83
+ assert_raises_rpc_error(-4, 'Destination is not controlled', wallet.sweepprivkeys, [wif], other.getnewaddress())
84
+ node.createwallet(wallet_name='watch', disable_private_keys=True)
85
+ watch = node.get_wallet_rpc('watch')
86
+ watch.importpubkey(wallet.getaddressinfo(destination)['pubkey'])
87
+ assert_raises_rpc_error(-4, 'Destination is not controlled', watch.sweepprivkeys, [wif], destination)
88
+
89
+ wallet.settxfee(Decimal('0.00001000'))
90
+ preview = wallet.sweepprivkeys(privkeys=[wif, uncompressed_wif], destination=destination)
91
+ assert_equal(preview['broadcast'], False)
92
+ assert_equal(preview['inputs'], 4)
93
+ decoded = node.decoderawtransaction(preview['hex'])
94
+ required_fee = Decimal(decoded['vsize']) / COIN
95
+ assert preview['fee'] >= required_fee
96
+ assert preview['fee'] - required_fee <= Decimal('0.00000001')
97
+ assert_equal(preview['amount'] - sum(output['value'] for output in decoded['vout']), preview['fee'])
98
+ assert_equal(wallet.listdescriptors(), descriptors)
99
+ assert_equal(wallet.listlabels(), labels)
100
+ assert_equal(wallet.getwalletinfo()['keypoolsize'], keypoolsize)
101
+ assert_equal(wallet.getaddressinfo(key_to_p2pkh(pubkey))['ismine'], False)
102
+ assert_equal(wallet.getrawmempool(), [conflict_id])
103
+
104
+ wallet.encryptwallet('pass')
105
+ sent = wallet.sweepprivkeys([wif, uncompressed_wif], destination, True)
106
+ assert_equal(sent['broadcast'], True)
107
+ assert_equal(sent['txid'], preview['txid'])
108
+ assert sent['txid'] in wallet.getrawmempool()
109
+ rejected_descriptors = wallet.listdescriptors()
110
+ rejected_labels = wallet.listlabels()
111
+ rejected_keypoolsize = wallet.getwalletinfo()['keypoolsize']
112
+ duplicate = wallet.sweepprivkeys([wif, uncompressed_wif], destination, True)
113
+ assert_equal(duplicate['txid'], sent['txid'])
114
+ assert_equal(set(wallet.getrawmempool()), {conflict_id, sent['txid']})
115
+ assert_equal(wallet.listdescriptors(), rejected_descriptors)
116
+ assert_equal(wallet.listlabels(), rejected_labels)
117
+ assert_equal(wallet.getwalletinfo()['keypoolsize'], rejected_keypoolsize)
118
+ assert_equal(wallet.getaddressinfo(key_to_p2pkh(pubkey))['ismine'], False)
119
+ self.sync_mempools()
120
+ self.generate(miner, 1)
121
+ self.sync_blocks()
122
+ assert_raises_rpc_error(-6, 'No spendable UTXOs found', wallet.sweepprivkeys, [wif, uncompressed_wif], destination)
123
+
124
+ small_wif, small_pubkey = generate_keypair(wif=True)
125
+ miner.sendtoaddress(key_to_p2pkh(small_pubkey), Decimal('0.00010000'))
126
+ self.generate(miner, 1)
127
+ self.sync_blocks()
128
+ dust_destination = wallet.getnewaddress(address_type='legacy')
129
+ wallet.settxfee(Decimal('0.00052000'))
130
+ assert_raises_rpc_error(-4, 'Swept output would be dust', wallet.sweepprivkeys, [small_wif], dust_destination)
131
+ wallet.settxfee(Decimal('0.10000000'))
132
+ assert_raises_rpc_error(-6, 'Fee exceeds swept value', wallet.sweepprivkeys, [small_wif], destination)
133
+ assert wif not in Path(self.options.tmpdir, 'node0', 'regtest', 'debug.log').read_text()
134
+ wallet_name = wallet.getwalletinfo()['walletname']
135
+ self.restart_node(0)
136
+ wallet = self.nodes[0].get_wallet_rpc(wallet_name)
137
+ assert_equal(wallet.getaddressinfo(key_to_p2pkh(pubkey))['ismine'], False)
138
+ assert wif not in Path(self.options.tmpdir, 'node0', 'regtest', 'debug.log').read_text()
139
+ self.nodes[0].unloadwallet(wallet_name)
140
+ assert wallet_name not in self.nodes[0].listwallets()
141
+ self.nodes[0].loadwallet(wallet_name)
142
+ wallet = self.nodes[0].get_wallet_rpc(wallet_name)
143
+ assert_equal(wallet.getaddressinfo(key_to_p2pkh(pubkey))['ismine'], False)
144
+
145
+ if __name__ == '__main__': SweepPrivKeysTest(__file__).main()