Skip to content

Commit b5fa231

Browse files
committed
Merge #15687: test: tool wallet test coverage for unexpected writes to wallet
7195fa7 test: Tool wallet test coverage for unexpected writes to wallet (Jon Atack) 3bf2b3a test: Split tool_wallet.py test into subtests (Jon Atack) 1eb13f0 test: Add log messages to test/functional/tool_wallet.py (Jon Atack) Pull request description: This pull request adds test coverage in `test/functional/tool_wallet.py` to reproduce unexpected writes to the wallet as described in bitcoin/bitcoin#15608 and serve as a benchmark for fixing the issue: - Wallet tool `info` unexpectedly writes to the wallet file if the wallet file permissions are read/write. - Wallet tool `info` raises with "Error loading . Is wallet being used by another process?" if the wallet file permissions are read-only. Goals: 1. Reproduce the reported issue, define the current unexpected behavior, and add test coverage to guide a future fix. Add debug-level logging for sanity checking and commented-out assertions to be uncommented when fixing the issue. Add the same coverage to the wallet tool create test and the getwalletinfo test as regression tests while fixing the issue. 2. Add info log messages as there are currently none in the test file. 3. Split the tests out to separate functions as per review feedback. Thanks to Marco Falke for pointing me in the right direction. ACKs for top commit: laanwj: code review ACK 7195fa7 Tree-SHA512: 16a41cce989c8f819cf5b02c6cf8ea84653ede2738fb402f6c36cf4dc075b424dff3e2c73a1cfa1ec9c75f614675baecc71e588845a2596db06ba0957db2df7b
2 parents 983c848 + 7195fa7 commit b5fa231

File tree

1 file changed

+118
-10
lines changed

1 file changed

+118
-10
lines changed

test/functional/tool_wallet.py

Lines changed: 118 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
#!/usr/bin/env python3
2-
# Copyright (c) 2018 The Bitcoin Core developers
2+
# Copyright (c) 2018-2019 The Bitcoin Core developers
33
# Distributed under the MIT software license, see the accompanying
44
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
55
"""Test bitcoin-wallet."""
6+
7+
import hashlib
8+
import os
9+
import stat
610
import subprocess
711
import textwrap
812

913
from test_framework.test_framework import BitcoinTestFramework
1014
from test_framework.util import assert_equal
1115

16+
BUFFER_SIZE = 16 * 1024
17+
1218
class ToolWalletTest(BitcoinTestFramework):
1319
def set_test_params(self):
1420
self.num_nodes = 1
@@ -32,23 +38,54 @@ def assert_raises_tool_error(self, error, *args):
3238
def assert_tool_output(self, output, *args):
3339
p = self.bitcoin_wallet_process(*args)
3440
stdout, stderr = p.communicate()
35-
assert_equal(p.poll(), 0)
3641
assert_equal(stderr, '')
3742
assert_equal(stdout, output)
43+
assert_equal(p.poll(), 0)
3844

39-
def run_test(self):
45+
def wallet_shasum(self):
46+
h = hashlib.sha1()
47+
mv = memoryview(bytearray(BUFFER_SIZE))
48+
with open(self.wallet_path, 'rb', buffering=0) as f:
49+
for n in iter(lambda : f.readinto(mv), 0):
50+
h.update(mv[:n])
51+
return h.hexdigest()
4052

53+
def wallet_timestamp(self):
54+
return os.path.getmtime(self.wallet_path)
55+
56+
def wallet_permissions(self):
57+
return oct(os.lstat(self.wallet_path).st_mode)[-3:]
58+
59+
def log_wallet_timestamp_comparison(self, old, new):
60+
result = 'unchanged' if new == old else 'increased!'
61+
self.log.debug('Wallet file timestamp {}'.format(result))
62+
63+
def test_invalid_tool_commands_and_args(self):
64+
self.log.info('Testing that various invalid commands raise with specific error messages')
4165
self.assert_raises_tool_error('Invalid command: foo', 'foo')
42-
# `bitcoin-wallet help` is an error. Use `bitcoin-wallet -help`
66+
# `bitcoin-wallet help` raises an error. Use `bitcoin-wallet -help`.
4367
self.assert_raises_tool_error('Invalid command: help', 'help')
4468
self.assert_raises_tool_error('Error: two methods provided (info and create). Only one method should be provided.', 'info', 'create')
4569
self.assert_raises_tool_error('Error parsing command line arguments: Invalid parameter -foo', '-foo')
4670
self.assert_raises_tool_error('Error loading wallet.dat. Is wallet being used by other process?', '-wallet=wallet.dat', 'info')
4771
self.assert_raises_tool_error('Error: no wallet file at nonexistent.dat', '-wallet=nonexistent.dat', 'info')
4872

49-
# stop the node to close the wallet to call info command
73+
def test_tool_wallet_info(self):
74+
# Stop the node to close the wallet to call the info command.
5075
self.stop_node(0)
51-
76+
self.log.info('Calling wallet tool info, testing output')
77+
#
78+
# TODO: Wallet tool info should work with wallet file permissions set to
79+
# read-only without raising:
80+
# "Error loading wallet.dat. Is wallet being used by another process?"
81+
# The following lines should be uncommented and the tests still succeed:
82+
#
83+
# self.log.debug('Setting wallet file permissions to 400 (read-only)')
84+
# os.chmod(self.wallet_path, stat.S_IRUSR)
85+
# assert(self.wallet_permissions() in ['400', '666']) # Sanity check. 666 because Appveyor.
86+
# shasum_before = self.wallet_shasum()
87+
timestamp_before = self.wallet_timestamp()
88+
self.log.debug('Wallet file timestamp before calling info: {}'.format(timestamp_before))
5289
out = textwrap.dedent('''\
5390
Wallet info
5491
===========
@@ -59,12 +96,35 @@ def run_test(self):
5996
Address Book: 3
6097
''')
6198
self.assert_tool_output(out, '-wallet=wallet.dat', 'info')
62-
63-
# mutate the wallet to check the info command output changes accordingly
99+
timestamp_after = self.wallet_timestamp()
100+
self.log.debug('Wallet file timestamp after calling info: {}'.format(timestamp_after))
101+
self.log_wallet_timestamp_comparison(timestamp_before, timestamp_after)
102+
self.log.debug('Setting wallet file permissions back to 600 (read/write)')
103+
os.chmod(self.wallet_path, stat.S_IRUSR | stat.S_IWUSR)
104+
assert(self.wallet_permissions() in ['600', '666']) # Sanity check. 666 because Appveyor.
105+
#
106+
# TODO: Wallet tool info should not write to the wallet file.
107+
# The following lines should be uncommented and the tests still succeed:
108+
#
109+
# assert_equal(timestamp_before, timestamp_after)
110+
# shasum_after = self.wallet_shasum()
111+
# assert_equal(shasum_before, shasum_after)
112+
# self.log.debug('Wallet file shasum unchanged\n')
113+
114+
def test_tool_wallet_info_after_transaction(self):
115+
"""
116+
Mutate the wallet with a transaction to verify that the info command
117+
output changes accordingly.
118+
"""
64119
self.start_node(0)
120+
self.log.info('Generating transaction to mutate wallet')
65121
self.nodes[0].generate(1)
66122
self.stop_node(0)
67123

124+
self.log.info('Calling wallet tool info after generating a transaction, testing output')
125+
shasum_before = self.wallet_shasum()
126+
timestamp_before = self.wallet_timestamp()
127+
self.log.debug('Wallet file timestamp before calling info: {}'.format(timestamp_before))
68128
out = textwrap.dedent('''\
69129
Wallet info
70130
===========
@@ -75,7 +135,22 @@ def run_test(self):
75135
Address Book: 3
76136
''')
77137
self.assert_tool_output(out, '-wallet=wallet.dat', 'info')
78-
138+
shasum_after = self.wallet_shasum()
139+
timestamp_after = self.wallet_timestamp()
140+
self.log.debug('Wallet file timestamp after calling info: {}'.format(timestamp_after))
141+
self.log_wallet_timestamp_comparison(timestamp_before, timestamp_after)
142+
#
143+
# TODO: Wallet tool info should not write to the wallet file.
144+
# This assertion should be uncommented and succeed:
145+
# assert_equal(timestamp_before, timestamp_after)
146+
assert_equal(shasum_before, shasum_after)
147+
self.log.debug('Wallet file shasum unchanged\n')
148+
149+
def test_tool_wallet_create_on_existing_wallet(self):
150+
self.log.info('Calling wallet tool create on an existing wallet, testing output')
151+
shasum_before = self.wallet_shasum()
152+
timestamp_before = self.wallet_timestamp()
153+
self.log.debug('Wallet file timestamp before calling create: {}'.format(timestamp_before))
79154
out = textwrap.dedent('''\
80155
Topping up keypool...
81156
Wallet info
@@ -87,15 +162,48 @@ def run_test(self):
87162
Address Book: 0
88163
''')
89164
self.assert_tool_output(out, '-wallet=foo', 'create')
90-
165+
shasum_after = self.wallet_shasum()
166+
timestamp_after = self.wallet_timestamp()
167+
self.log.debug('Wallet file timestamp after calling create: {}'.format(timestamp_after))
168+
self.log_wallet_timestamp_comparison(timestamp_before, timestamp_after)
169+
assert_equal(timestamp_before, timestamp_after)
170+
assert_equal(shasum_before, shasum_after)
171+
self.log.debug('Wallet file shasum unchanged\n')
172+
173+
def test_getwalletinfo_on_different_wallet(self):
174+
self.log.info('Starting node with arg -wallet=foo')
91175
self.start_node(0, ['-wallet=foo'])
176+
177+
self.log.info('Calling getwalletinfo on a different wallet ("foo"), testing output')
178+
shasum_before = self.wallet_shasum()
179+
timestamp_before = self.wallet_timestamp()
180+
self.log.debug('Wallet file timestamp before calling getwalletinfo: {}'.format(timestamp_before))
92181
out = self.nodes[0].getwalletinfo()
93182
self.stop_node(0)
94183

184+
shasum_after = self.wallet_shasum()
185+
timestamp_after = self.wallet_timestamp()
186+
self.log.debug('Wallet file timestamp after calling getwalletinfo: {}'.format(timestamp_after))
187+
95188
assert_equal(0, out['txcount'])
96189
assert_equal(1000, out['keypoolsize'])
97190
assert_equal(1000, out['keypoolsize_hd_internal'])
98191
assert_equal(True, 'hdseedid' in out)
99192

193+
self.log_wallet_timestamp_comparison(timestamp_before, timestamp_after)
194+
assert_equal(timestamp_before, timestamp_after)
195+
assert_equal(shasum_after, shasum_before)
196+
self.log.debug('Wallet file shasum unchanged\n')
197+
198+
def run_test(self):
199+
self.wallet_path = os.path.join(self.nodes[0].datadir, 'regtest', 'wallets', 'wallet.dat')
200+
self.test_invalid_tool_commands_and_args()
201+
# Warning: The following tests are order-dependent.
202+
self.test_tool_wallet_info()
203+
self.test_tool_wallet_info_after_transaction()
204+
self.test_tool_wallet_create_on_existing_wallet()
205+
self.test_getwalletinfo_on_different_wallet()
206+
207+
100208
if __name__ == '__main__':
101209
ToolWalletTest().main()

0 commit comments

Comments
 (0)