wallet_backup.py
1 #!/usr/bin/env python3 2 # Copyright (c) 2014-present The Bitcoin Core developers 3 # Distributed under the MIT software license, see the accompanying 4 # file COPYING or http://www.opensource.org/licenses/mit-license.php. 5 """Test the wallet backup features. 6 7 Test case is: 8 4 nodes. 1 2 and 3 send transactions between each other, 9 fourth node is a miner. 10 1 2 3 each mine a block to start, then 11 Miner creates 100 blocks so 1 2 3 each have 50 mature 12 coins to spend. 13 Then 5 iterations of 1/2/3 sending coins amongst 14 themselves to get transactions in the wallets, 15 and the miner mining one block. 16 17 Wallets are backed up using dumpwallet/backupwallet. 18 Then 5 more iterations of transactions and mining a block. 19 20 Miner then generates 101 more blocks, so any 21 transaction fees paid mature. 22 23 Sanity check: 24 Sum(1,2,3,4 balances) == 114*50 25 26 1/2/3 are shutdown, and their wallets erased. 27 Then restore using wallet.dat backup. And 28 confirm 1/2/3/4 balances are same as before. 29 30 Shutdown again, restore using importwallet, 31 and confirm again balances are correct. 32 """ 33 from decimal import Decimal 34 import os 35 from random import randint 36 37 from test_framework.blocktools import COINBASE_MATURITY 38 from test_framework.test_framework import BitcoinTestFramework 39 from test_framework.util import ( 40 assert_equal, 41 assert_raises_rpc_error, 42 sha256sum_file, 43 ) 44 45 46 class WalletBackupTest(BitcoinTestFramework): 47 def set_test_params(self): 48 self.num_nodes = 4 49 self.setup_clean_chain = True 50 # whitelist peers to speed up tx relay / mempool sync 51 self.noban_tx_relay = True 52 # nodes 1, 2, 3 are spenders, let's give them a keypool=100 53 self.extra_args = [ 54 ["-keypool=100"], 55 ["-keypool=100"], 56 ["-keypool=100"], 57 [], 58 ] 59 self.rpc_timeout = 120 60 61 def skip_test_if_missing_module(self): 62 self.skip_if_no_wallet() 63 64 def setup_network(self): 65 self.setup_nodes() 66 self.connect_nodes(0, 3) 67 self.connect_nodes(1, 3) 68 self.connect_nodes(2, 3) 69 self.connect_nodes(2, 0) 70 self.sync_all() 71 72 def one_send(self, from_node, to_address): 73 if (randint(1,2) == 1): 74 amount = Decimal(randint(1,10)) / Decimal(10) 75 self.nodes[from_node].sendtoaddress(to_address, amount) 76 77 def do_one_round(self): 78 a0 = self.nodes[0].getnewaddress() 79 a1 = self.nodes[1].getnewaddress() 80 a2 = self.nodes[2].getnewaddress() 81 82 self.one_send(0, a1) 83 self.one_send(0, a2) 84 self.one_send(1, a0) 85 self.one_send(1, a2) 86 self.one_send(2, a0) 87 self.one_send(2, a1) 88 89 # Have the miner (node3) mine a block. 90 # Must sync mempools before mining. 91 self.sync_mempools() 92 self.generate(self.nodes[3], 1) 93 94 def restore_invalid_wallet(self): 95 node = self.nodes[3] 96 invalid_wallet_file = self.nodes[0].datadir_path / 'invalid_wallet_file.bak' 97 open(invalid_wallet_file, "a").write("invalid_wallet_content") 98 wallet_name = "res0" 99 not_created_wallet_file = node.wallets_path / wallet_name 100 error_message = "Wallet file verification failed. Failed to load database path '{}'. Data is not in recognized format.".format(not_created_wallet_file) 101 assert_raises_rpc_error(-18, error_message, node.restorewallet, wallet_name, invalid_wallet_file) 102 assert not not_created_wallet_file.exists() 103 104 def restore_nonexistent_wallet(self): 105 node = self.nodes[3] 106 nonexistent_wallet_file = self.nodes[0].datadir_path / 'nonexistent_wallet.bak' 107 wallet_name = "res0" 108 assert_raises_rpc_error(-8, "Backup file does not exist", node.restorewallet, wallet_name, nonexistent_wallet_file) 109 not_created_wallet_file = node.wallets_path / wallet_name 110 assert not not_created_wallet_file.exists() 111 112 def restore_wallet_existent_name(self): 113 node = self.nodes[3] 114 backup_file = self.nodes[0].datadir_path / 'wallet.bak' 115 wallet_name = "res0" 116 wallet_file = node.wallets_path / wallet_name 117 error_message = "Failed to restore wallet. Database file exists in '{}'.".format(wallet_file / "wallet.dat") 118 assert_raises_rpc_error(-36, error_message, node.restorewallet, wallet_name, backup_file) 119 assert wallet_file.exists() 120 121 def test_restore_existent_dir(self): 122 self.log.info("Test restore on an existent empty directory") 123 node = self.nodes[3] 124 backup_file = self.nodes[0].datadir_path / 'wallet.bak' 125 wallet_name = "restored_wallet" 126 wallet_dir = node.wallets_path / wallet_name 127 os.mkdir(wallet_dir) 128 res = node.restorewallet(wallet_name, backup_file) 129 assert_equal(res['name'], wallet_name) 130 node.unloadwallet(wallet_name) 131 132 self.log.info("Test restore succeeds when the target directory contains non-wallet files") 133 wallet_file = node.wallets_path / wallet_name / "wallet.dat" 134 os.remove(wallet_file) 135 extra_file = node.wallets_path / wallet_name / "not_a_wallet.txt" 136 extra_file.touch() 137 res = node.restorewallet(wallet_name, backup_file) 138 assert_equal(res['name'], wallet_name) 139 assert extra_file.exists() # extra file was not removed by mistake 140 node.unloadwallet(wallet_name) 141 142 self.log.info("Test restore failure due to existing db file in the destination directory") 143 original_shasum = sha256sum_file(wallet_file) 144 error_message = "Failed to restore wallet. Database file exists in '{}'.".format(wallet_dir / "wallet.dat") 145 assert_raises_rpc_error(-36, error_message, node.restorewallet, wallet_name, backup_file) 146 # Ensure the wallet file remains untouched 147 assert wallet_dir.exists() 148 assert_equal(original_shasum, sha256sum_file(wallet_file)) 149 150 self.log.info("Test restore succeeds when the .dat file in the destination has a different name") 151 second_wallet = wallet_dir / "hidden_storage.dat" 152 os.rename(wallet_dir / "wallet.dat", second_wallet) 153 original_shasum = sha256sum_file(second_wallet) 154 res = node.restorewallet(wallet_name, backup_file) 155 assert_equal(res['name'], wallet_name) 156 assert (wallet_dir / "hidden_storage.dat").exists() 157 assert_equal(original_shasum, sha256sum_file(second_wallet)) 158 node.unloadwallet(wallet_name) 159 160 # Clean for follow-up tests 161 os.remove(wallet_file) 162 163 def test_restore_into_unnamed_wallet(self): 164 self.log.info("Test restore into a default unnamed wallet") 165 # This is also useful to test the migration recovery after failure logic 166 node = self.nodes[3] 167 backup_file = self.nodes[0].datadir_path / 'wallet.bak' 168 assert_raises_rpc_error(-8, "Wallet name cannot be empty", node.restorewallet, "", backup_file) 169 assert not (node.wallets_path / "wallet.dat").exists() 170 171 def test_pruned_wallet_backup(self): 172 self.log.info("Test loading backup on a pruned node when the backup was created close to the prune height of the restoring node") 173 node = self.nodes[3] 174 self.restart_node(3, ["-prune=1", "-fastprune=1"]) 175 # Ensure the chain tip is at height 214, because this test assumes it is. 176 assert_equal(node.getchaintips()[0]["height"], 214) 177 # We need a few more blocks so we can actually get above an realistic 178 # minimal prune height 179 self.generate(node, 50, sync_fun=self.no_op) 180 # Backup created at block height 264 181 node.backupwallet(node.datadir_path / 'wallet_pruned.bak') 182 # Generate more blocks so we can actually prune the older blocks 183 self.generate(node, 300, sync_fun=self.no_op) 184 # This gives us an actual prune height roughly in the range of 220 - 240 185 node.pruneblockchain(250) 186 # The backup should be updated with the latest height (locator) for 187 # the backup to load successfully this close to the prune height 188 node.restorewallet('pruned', node.datadir_path / 'wallet_pruned.bak') 189 190 self.log.info("Test restore on a pruned node when the backup was beyond the pruning point") 191 backup_file = self.nodes[0].datadir_path / 'wallet.bak' 192 error_message = "Wallet loading failed. Prune: last wallet synchronisation goes beyond pruned data. You need to -reindex (download the whole blockchain again in case of a pruned node)" 193 assert_raises_rpc_error(-4, error_message, node.restorewallet, "restore_pruned", backup_file) 194 assert node.wallets_path.exists() # ensure the wallets dir exists 195 196 def run_test(self): 197 self.log.info("Generating initial blockchain") 198 self.generate(self.nodes[0], 1) 199 self.generate(self.nodes[1], 1) 200 self.generate(self.nodes[2], 1) 201 self.generate(self.nodes[3], COINBASE_MATURITY) 202 203 assert_equal(self.nodes[0].getbalance(), 50) 204 assert_equal(self.nodes[1].getbalance(), 50) 205 assert_equal(self.nodes[2].getbalance(), 50) 206 assert_equal(self.nodes[3].getbalance(), 0) 207 208 self.log.info("Creating transactions") 209 # Five rounds of sending each other transactions. 210 for _ in range(5): 211 self.do_one_round() 212 213 self.log.info("Backing up") 214 215 for node_num in range(3): 216 self.nodes[node_num].backupwallet(self.nodes[node_num].datadir_path / 'wallet.bak') 217 218 self.log.info("More transactions") 219 for _ in range(5): 220 self.do_one_round() 221 222 # Generate 101 more blocks, so any fees paid mature 223 self.generate(self.nodes[3], COINBASE_MATURITY + 1) 224 225 balance0 = self.nodes[0].getbalance() 226 balance1 = self.nodes[1].getbalance() 227 balance2 = self.nodes[2].getbalance() 228 balance3 = self.nodes[3].getbalance() 229 total = balance0 + balance1 + balance2 + balance3 230 231 # At this point, there are 214 blocks (103 for setup, then 10 rounds, then 101.) 232 # 114 are mature, so the sum of all wallets should be 114 * 50 = 5700. 233 assert_equal(total, 5700) 234 235 ## 236 # Test restoring spender wallets from backups 237 ## 238 self.log.info("Restoring wallets on node 3 using backup files") 239 240 self.restore_invalid_wallet() 241 self.restore_nonexistent_wallet() 242 243 backup_files = [] 244 for node_num in range(3): 245 backup_files.append(self.nodes[node_num].datadir_path / 'wallet.bak') 246 247 for idx, backup_file in enumerate(backup_files): 248 self.nodes[3].restorewallet(f'res{idx}', backup_file) 249 assert (self.nodes[3].wallets_path / f'res{idx}').exists() 250 251 res0_rpc = self.nodes[3].get_wallet_rpc("res0") 252 res1_rpc = self.nodes[3].get_wallet_rpc("res1") 253 res2_rpc = self.nodes[3].get_wallet_rpc("res2") 254 255 assert_equal(res0_rpc.getbalance(), balance0) 256 assert_equal(res1_rpc.getbalance(), balance1) 257 assert_equal(res2_rpc.getbalance(), balance2) 258 259 self.restore_wallet_existent_name() 260 self.test_restore_existent_dir() 261 self.test_restore_into_unnamed_wallet() 262 263 # Backup to source wallet file must fail 264 sourcePaths = [ 265 os.path.join(self.nodes[0].wallets_path, self.default_wallet_name, self.wallet_data_filename), 266 os.path.join(self.nodes[0].wallets_path, '.', self.default_wallet_name, self.wallet_data_filename), 267 os.path.join(self.nodes[0].wallets_path, self.default_wallet_name), 268 os.path.join(self.nodes[0].wallets_path)] 269 270 for sourcePath in sourcePaths: 271 assert_raises_rpc_error(-4, "backup failed", self.nodes[0].backupwallet, sourcePath) 272 273 self.test_pruned_wallet_backup() 274 275 276 if __name__ == '__main__': 277 WalletBackupTest(__file__).main()