Skip to content

Commit 091d29c

Browse files
committed
Merge bitcoin#28617: test: Add Wallet Unlock Context Manager
004903e test: Add Wallet Unlock Context Manager (Brandon Odiwuor) Pull request description: Fixes bitcoin#28601, see bitcoin#28403 (comment) Add Context Manager to manage the locking and unlocking of locked wallets with a passphrase during testing. ACKs for top commit: kevkevinpal: lgtm ACK [004903e](bitcoin@004903e) maflcko: lgtm ACK 004903e Tree-SHA512: ab234c167e71531df0d974ff9a31d444f7ce2a1d05aba5ea868cc9452f139845eeb24ca058d88f058bc02482b762adf2d99e63a6640b872cc71a57a0068abfe8
2 parents 5eb82d5 + 004903e commit 091d29c

File tree

7 files changed

+143
-128
lines changed

7 files changed

+143
-128
lines changed

test/functional/test_framework/wallet_util.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,22 @@ def generate_keypair(compressed=True, wif=False):
122122
if wif:
123123
privkey = bytes_to_wif(privkey.get_bytes(), compressed)
124124
return privkey, pubkey
125+
126+
class WalletUnlock():
127+
"""
128+
A context manager for unlocking a wallet with a passphrase and automatically locking it afterward.
129+
"""
130+
131+
MAXIMUM_TIMEOUT = 999000
132+
133+
def __init__(self, wallet, passphrase, timeout=MAXIMUM_TIMEOUT):
134+
self.wallet = wallet
135+
self.passphrase = passphrase
136+
self.timeout = timeout
137+
138+
def __enter__(self):
139+
self.wallet.walletpassphrase(self.passphrase, self.timeout)
140+
141+
def __exit__(self, *args):
142+
_ = args
143+
self.wallet.walletlock()

test/functional/wallet_createwallet.py

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
assert_equal,
1313
assert_raises_rpc_error,
1414
)
15-
from test_framework.wallet_util import generate_keypair
15+
from test_framework.wallet_util import generate_keypair, WalletUnlock
1616

1717

1818
EMPTY_PASSPHRASE_MSG = "Empty string given as passphrase, wallet will not be encrypted."
@@ -108,24 +108,24 @@ def run_test(self):
108108
w4.encryptwallet('pass')
109109
assert_raises_rpc_error(-4, "Error: This wallet has no available keys", w4.getnewaddress)
110110
assert_raises_rpc_error(-4, "Error: This wallet has no available keys", w4.getrawchangeaddress)
111-
# Now set a seed and it should work. Wallet should also be encrypted
112-
w4.walletpassphrase("pass", 999000)
113-
if self.options.descriptors:
114-
w4.importdescriptors([{
115-
'desc': descsum_create('wpkh(tprv8ZgxMBicQKsPcwuZGKp8TeWppSuLMiLe2d9PupB14QpPeQsqoj3LneJLhGHH13xESfvASyd4EFLJvLrG8b7DrLxEuV7hpF9uUc6XruKA1Wq/0h/*)'),
116-
'timestamp': 'now',
117-
'active': True
118-
},
119-
{
120-
'desc': descsum_create('wpkh(tprv8ZgxMBicQKsPcwuZGKp8TeWppSuLMiLe2d9PupB14QpPeQsqoj3LneJLhGHH13xESfvASyd4EFLJvLrG8b7DrLxEuV7hpF9uUc6XruKA1Wq/1h/*)'),
121-
'timestamp': 'now',
122-
'active': True,
123-
'internal': True
124-
}])
125-
else:
126-
w4.sethdseed()
127-
w4.getnewaddress()
128-
w4.getrawchangeaddress()
111+
with WalletUnlock(w4, "pass"):
112+
# Now set a seed and it should work. Wallet should also be encrypted
113+
if self.options.descriptors:
114+
w4.importdescriptors([{
115+
'desc': descsum_create('wpkh(tprv8ZgxMBicQKsPcwuZGKp8TeWppSuLMiLe2d9PupB14QpPeQsqoj3LneJLhGHH13xESfvASyd4EFLJvLrG8b7DrLxEuV7hpF9uUc6XruKA1Wq/0h/*)'),
116+
'timestamp': 'now',
117+
'active': True
118+
},
119+
{
120+
'desc': descsum_create('wpkh(tprv8ZgxMBicQKsPcwuZGKp8TeWppSuLMiLe2d9PupB14QpPeQsqoj3LneJLhGHH13xESfvASyd4EFLJvLrG8b7DrLxEuV7hpF9uUc6XruKA1Wq/1h/*)'),
121+
'timestamp': 'now',
122+
'active': True,
123+
'internal': True
124+
}])
125+
else:
126+
w4.sethdseed()
127+
w4.getnewaddress()
128+
w4.getrawchangeaddress()
129129

130130
self.log.info("Test blank creation with privkeys disabled and then encryption")
131131
self.nodes[0].createwallet(wallet_name='w5', disable_private_keys=True, blank=True)
@@ -142,23 +142,23 @@ def run_test(self):
142142
self.nodes[0].createwallet(wallet_name='wblank', disable_private_keys=False, blank=True, passphrase='thisisapassphrase')
143143
wblank = node.get_wallet_rpc('wblank')
144144
assert_raises_rpc_error(-13, "Error: Please enter the wallet passphrase with walletpassphrase first.", wblank.signmessage, "needanargument", "test")
145-
wblank.walletpassphrase("thisisapassphrase", 999000)
146-
assert_raises_rpc_error(-4, "Error: This wallet has no available keys", wblank.getnewaddress)
147-
assert_raises_rpc_error(-4, "Error: This wallet has no available keys", wblank.getrawchangeaddress)
145+
with WalletUnlock(wblank, "thisisapassphrase"):
146+
assert_raises_rpc_error(-4, "Error: This wallet has no available keys", wblank.getnewaddress)
147+
assert_raises_rpc_error(-4, "Error: This wallet has no available keys", wblank.getrawchangeaddress)
148148

149149
self.log.info('Test creating a new encrypted wallet.')
150150
# Born encrypted wallet is created (has keys)
151151
self.nodes[0].createwallet(wallet_name='w6', disable_private_keys=False, blank=False, passphrase='thisisapassphrase')
152152
w6 = node.get_wallet_rpc('w6')
153153
assert_raises_rpc_error(-13, "Error: Please enter the wallet passphrase with walletpassphrase first.", w6.signmessage, "needanargument", "test")
154-
w6.walletpassphrase("thisisapassphrase", 999000)
155-
w6.signmessage(w6.getnewaddress('', 'legacy'), "test")
156-
w6.keypoolrefill(1)
157-
# There should only be 1 key for legacy, 3 for descriptors
158-
walletinfo = w6.getwalletinfo()
159-
keys = 4 if self.options.descriptors else 1
160-
assert_equal(walletinfo['keypoolsize'], keys)
161-
assert_equal(walletinfo['keypoolsize_hd_internal'], keys)
154+
with WalletUnlock(w6, "thisisapassphrase"):
155+
w6.signmessage(w6.getnewaddress('', 'legacy'), "test")
156+
w6.keypoolrefill(1)
157+
# There should only be 1 key for legacy, 3 for descriptors
158+
walletinfo = w6.getwalletinfo()
159+
keys = 4 if self.options.descriptors else 1
160+
assert_equal(walletinfo['keypoolsize'], keys)
161+
assert_equal(walletinfo['keypoolsize_hd_internal'], keys)
162162
# Allow empty passphrase, but there should be a warning
163163
resp = self.nodes[0].createwallet(wallet_name='w7', disable_private_keys=False, blank=False, passphrase='')
164164
assert_equal(resp["warnings"], [EMPTY_PASSPHRASE_MSG] if self.options.descriptors else [EMPTY_PASSPHRASE_MSG, LEGACY_WALLET_MSG])

test/functional/wallet_descriptor.py

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
assert_equal,
1616
assert_raises_rpc_error
1717
)
18+
from test_framework.wallet_util import WalletUnlock
1819

1920

2021
class WalletDescriptorTest(BitcoinTestFramework):
@@ -128,11 +129,10 @@ def run_test(self):
128129

129130
# Encrypt wallet 0
130131
send_wrpc.encryptwallet('pass')
131-
send_wrpc.walletpassphrase("pass", 999000)
132-
addr = send_wrpc.getnewaddress()
133-
info2 = send_wrpc.getaddressinfo(addr)
134-
assert info1['hdmasterfingerprint'] != info2['hdmasterfingerprint']
135-
send_wrpc.walletlock()
132+
with WalletUnlock(send_wrpc, "pass"):
133+
addr = send_wrpc.getnewaddress()
134+
info2 = send_wrpc.getaddressinfo(addr)
135+
assert info1['hdmasterfingerprint'] != info2['hdmasterfingerprint']
136136
assert 'hdmasterfingerprint' in send_wrpc.getaddressinfo(send_wrpc.getnewaddress())
137137
info3 = send_wrpc.getaddressinfo(addr)
138138
assert_equal(info2['desc'], info3['desc'])
@@ -142,14 +142,13 @@ def run_test(self):
142142
send_wrpc.getnewaddress()
143143

144144
self.log.info("Test that unlock is needed when deriving only hardened keys in an encrypted wallet")
145-
send_wrpc.walletpassphrase("pass", 999000)
146-
send_wrpc.importdescriptors([{
147-
"desc": "wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/*h)#y4dfsj7n",
148-
"timestamp": "now",
149-
"range": [0,10],
150-
"active": True
151-
}])
152-
send_wrpc.walletlock()
145+
with WalletUnlock(send_wrpc, "pass"):
146+
send_wrpc.importdescriptors([{
147+
"desc": "wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/*h)#y4dfsj7n",
148+
"timestamp": "now",
149+
"range": [0,10],
150+
"active": True
151+
}])
153152
# Exhaust keypool of 100
154153
for _ in range(100):
155154
send_wrpc.getnewaddress(address_type='bech32')

test/functional/wallet_dump.py

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
assert_equal,
1212
assert_raises_rpc_error,
1313
)
14+
from test_framework.wallet_util import WalletUnlock
1415

1516

1617
def read_dump(file_name, addrs, script_addrs, hd_master_addr_old):
@@ -172,26 +173,26 @@ def run_test(self):
172173

173174
# encrypt wallet, restart, unlock and dump
174175
self.nodes[0].encryptwallet('test')
175-
self.nodes[0].walletpassphrase("test", 999000)
176-
# Should be a no-op:
177-
self.nodes[0].keypoolrefill()
178-
self.nodes[0].dumpwallet(wallet_enc_dump)
179-
180-
found_comments, found_legacy_addr, found_p2sh_segwit_addr, found_bech32_addr, found_script_addr, found_addr_chg, found_addr_rsv, _ = \
181-
read_dump(wallet_enc_dump, addrs, [multisig_addr], hd_master_addr_unenc)
182-
assert '# End of dump' in found_comments # Check that file is not corrupt
183-
assert_equal(dump_time_str, next(c for c in found_comments if c.startswith('# * Created on')))
184-
assert_equal(dump_best_block_1, next(c for c in found_comments if c.startswith('# * Best block')))
185-
assert_equal(dump_best_block_2, next(c for c in found_comments if c.startswith('# mined on')))
186-
assert_equal(found_legacy_addr, test_addr_count) # all keys must be in the dump
187-
assert_equal(found_p2sh_segwit_addr, test_addr_count) # all keys must be in the dump
188-
assert_equal(found_bech32_addr, test_addr_count) # all keys must be in the dump
189-
assert_equal(found_script_addr, 1)
190-
assert_equal(found_addr_chg, 90 * 2) # old reserve keys are marked as change now
191-
assert_equal(found_addr_rsv, 90 * 2)
192-
193-
# Overwriting should fail
194-
assert_raises_rpc_error(-8, "already exists", lambda: self.nodes[0].dumpwallet(wallet_enc_dump))
176+
with WalletUnlock(self.nodes[0], "test"):
177+
# Should be a no-op:
178+
self.nodes[0].keypoolrefill()
179+
self.nodes[0].dumpwallet(wallet_enc_dump)
180+
181+
found_comments, found_legacy_addr, found_p2sh_segwit_addr, found_bech32_addr, found_script_addr, found_addr_chg, found_addr_rsv, _ = \
182+
read_dump(wallet_enc_dump, addrs, [multisig_addr], hd_master_addr_unenc)
183+
assert '# End of dump' in found_comments # Check that file is not corrupt
184+
assert_equal(dump_time_str, next(c for c in found_comments if c.startswith('# * Created on')))
185+
assert_equal(dump_best_block_1, next(c for c in found_comments if c.startswith('# * Best block')))
186+
assert_equal(dump_best_block_2, next(c for c in found_comments if c.startswith('# mined on')))
187+
assert_equal(found_legacy_addr, test_addr_count) # all keys must be in the dump
188+
assert_equal(found_p2sh_segwit_addr, test_addr_count) # all keys must be in the dump
189+
assert_equal(found_bech32_addr, test_addr_count) # all keys must be in the dump
190+
assert_equal(found_script_addr, 1)
191+
assert_equal(found_addr_chg, 90 * 2) # old reserve keys are marked as change now
192+
assert_equal(found_addr_rsv, 90 * 2)
193+
194+
# Overwriting should fail
195+
assert_raises_rpc_error(-8, "already exists", lambda: self.nodes[0].dumpwallet(wallet_enc_dump))
195196

196197
# Restart node with new wallet, and test importwallet
197198
self.restart_node(0)

test/functional/wallet_encryption.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
assert_raises_rpc_error,
1212
assert_equal,
1313
)
14+
from test_framework.wallet_util import WalletUnlock
1415

1516

1617
class WalletEncryptionTest(BitcoinTestFramework):
@@ -59,19 +60,17 @@ def run_test(self):
5960
assert_raises_rpc_error(-14, "wallet passphrase entered was incorrect", self.nodes[0].walletpassphrase, passphrase + "wrong", 10)
6061

6162
# Test walletlock
62-
self.nodes[0].walletpassphrase(passphrase, 999000)
63-
sig = self.nodes[0].signmessage(address, msg)
64-
assert self.nodes[0].verifymessage(address, sig, msg)
65-
self.nodes[0].walletlock()
63+
with WalletUnlock(self.nodes[0], passphrase):
64+
sig = self.nodes[0].signmessage(address, msg)
65+
assert self.nodes[0].verifymessage(address, sig, msg)
6666
assert_raises_rpc_error(-13, "Please enter the wallet passphrase with walletpassphrase first", self.nodes[0].signmessage, address, msg)
6767

6868
# Test passphrase changes
6969
self.nodes[0].walletpassphrasechange(passphrase, passphrase2)
7070
assert_raises_rpc_error(-14, "wallet passphrase entered was incorrect", self.nodes[0].walletpassphrase, passphrase, 10)
71-
self.nodes[0].walletpassphrase(passphrase2, 999000)
72-
sig = self.nodes[0].signmessage(address, msg)
73-
assert self.nodes[0].verifymessage(address, sig, msg)
74-
self.nodes[0].walletlock()
71+
with WalletUnlock(self.nodes[0], passphrase2):
72+
sig = self.nodes[0].signmessage(address, msg)
73+
assert self.nodes[0].verifymessage(address, sig, msg)
7574

7675
# Test timeout bounds
7776
assert_raises_rpc_error(-8, "Timeout cannot be negative.", self.nodes[0].walletpassphrase, passphrase2, -10)
@@ -97,10 +96,9 @@ def run_test(self):
9796
self.nodes[0].walletpassphrasechange(passphrase2, passphrase_with_nulls)
9897
# walletpassphrasechange should not stop at null characters
9998
assert_raises_rpc_error(-14, "wallet passphrase entered was incorrect", self.nodes[0].walletpassphrase, passphrase_with_nulls.partition("\0")[0], 10)
100-
self.nodes[0].walletpassphrase(passphrase_with_nulls, 999000)
101-
sig = self.nodes[0].signmessage(address, msg)
102-
assert self.nodes[0].verifymessage(address, sig, msg)
103-
self.nodes[0].walletlock()
99+
with WalletUnlock(self.nodes[0], passphrase_with_nulls):
100+
sig = self.nodes[0].signmessage(address, msg)
101+
assert self.nodes[0].verifymessage(address, sig, msg)
104102

105103

106104
if __name__ == '__main__':

test/functional/wallet_fundrawtransaction.py

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
find_vout_for_address,
2626
get_fee,
2727
)
28-
from test_framework.wallet_util import generate_keypair
28+
from test_framework.wallet_util import generate_keypair, WalletUnlock
2929

3030
ERR_NOT_ENOUGH_PRESET_INPUTS = "The preselected coins total amount does not cover the transaction target. " \
3131
"Please allow other inputs to be automatically selected or include more coins manually"
@@ -581,19 +581,18 @@ def test_locked_wallet(self):
581581
wallet.encryptwallet("test")
582582

583583
if self.options.descriptors:
584-
wallet.walletpassphrase("test", 999000)
585-
wallet.importdescriptors([{
586-
'desc': descsum_create('wpkh(tprv8ZgxMBicQKsPdYeeZbPSKd2KYLmeVKtcFA7kqCxDvDR13MQ6us8HopUR2wLcS2ZKPhLyKsqpDL2FtL73LMHcgoCL7DXsciA8eX8nbjCR2eG/0h/*h)'),
587-
'timestamp': 'now',
588-
'active': True
589-
},
590-
{
591-
'desc': descsum_create('wpkh(tprv8ZgxMBicQKsPdYeeZbPSKd2KYLmeVKtcFA7kqCxDvDR13MQ6us8HopUR2wLcS2ZKPhLyKsqpDL2FtL73LMHcgoCL7DXsciA8eX8nbjCR2eG/1h/*h)'),
592-
'timestamp': 'now',
593-
'active': True,
594-
'internal': True
595-
}])
596-
wallet.walletlock()
584+
with WalletUnlock(wallet, "test"):
585+
wallet.importdescriptors([{
586+
'desc': descsum_create('wpkh(tprv8ZgxMBicQKsPdYeeZbPSKd2KYLmeVKtcFA7kqCxDvDR13MQ6us8HopUR2wLcS2ZKPhLyKsqpDL2FtL73LMHcgoCL7DXsciA8eX8nbjCR2eG/0h/*h)'),
587+
'timestamp': 'now',
588+
'active': True
589+
},
590+
{
591+
'desc': descsum_create('wpkh(tprv8ZgxMBicQKsPdYeeZbPSKd2KYLmeVKtcFA7kqCxDvDR13MQ6us8HopUR2wLcS2ZKPhLyKsqpDL2FtL73LMHcgoCL7DXsciA8eX8nbjCR2eG/1h/*h)'),
592+
'timestamp': 'now',
593+
'active': True,
594+
'internal': True
595+
}])
597596

598597
# Drain the keypool.
599598
wallet.getnewaddress()
@@ -619,9 +618,8 @@ def test_locked_wallet(self):
619618
assert_raises_rpc_error(-4, "Transaction needs a change address, but we can't generate it.", wallet.fundrawtransaction, rawtx)
620619

621620
# Refill the keypool.
622-
wallet.walletpassphrase("test", 999000)
623-
wallet.keypoolrefill(8) #need to refill the keypool to get an internal change address
624-
wallet.walletlock()
621+
with WalletUnlock(wallet, "test"):
622+
wallet.keypoolrefill(8) #need to refill the keypool to get an internal change address
625623

626624
assert_raises_rpc_error(-13, "walletpassphrase", wallet.sendtoaddress, self.nodes[0].getnewaddress(), 1.2)
627625

@@ -634,16 +632,16 @@ def test_locked_wallet(self):
634632
assert fundedTx["changepos"] != -1
635633

636634
# Now we need to unlock.
637-
wallet.walletpassphrase("test", 999000)
638-
signedTx = wallet.signrawtransactionwithwallet(fundedTx['hex'])
639-
wallet.sendrawtransaction(signedTx['hex'])
640-
self.generate(self.nodes[1], 1)
635+
with WalletUnlock(wallet, "test"):
636+
signedTx = wallet.signrawtransactionwithwallet(fundedTx['hex'])
637+
wallet.sendrawtransaction(signedTx['hex'])
638+
self.generate(self.nodes[1], 1)
641639

642-
# Make sure funds are received at node1.
643-
assert_equal(oldBalance+Decimal('51.10000000'), self.nodes[0].getbalance())
640+
# Make sure funds are received at node1.
641+
assert_equal(oldBalance+Decimal('51.10000000'), self.nodes[0].getbalance())
644642

645-
# Restore pre-test wallet state
646-
wallet.sendall(recipients=[df_wallet.getnewaddress(), df_wallet.getnewaddress(), df_wallet.getnewaddress()])
643+
# Restore pre-test wallet state
644+
wallet.sendall(recipients=[df_wallet.getnewaddress(), df_wallet.getnewaddress(), df_wallet.getnewaddress()])
647645
wallet.unloadwallet()
648646
self.generate(self.nodes[1], 1)
649647

0 commit comments

Comments
 (0)