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 ) 43 44 45 class WalletBackupTest(BitcoinTestFramework): 46 def set_test_params(self): 47 self.num_nodes = 4 48 self.setup_clean_chain = True 49 # whitelist peers to speed up tx relay / mempool sync 50 self.noban_tx_relay = True 51 # nodes 1, 2, 3 are spenders, let's give them a keypool=100 52 self.extra_args = [ 53 ["-keypool=100"], 54 ["-keypool=100"], 55 ["-keypool=100"], 56 [], 57 ] 58 self.rpc_timeout = 120 59 60 def skip_test_if_missing_module(self): 61 self.skip_if_no_wallet() 62 63 def setup_network(self): 64 self.setup_nodes() 65 self.connect_nodes(0, 3) 66 self.connect_nodes(1, 3) 67 self.connect_nodes(2, 3) 68 self.connect_nodes(2, 0) 69 self.sync_all() 70 71 def one_send(self, from_node, to_address): 72 if (randint(1,2) == 1): 73 amount = Decimal(randint(1,10)) / Decimal(10) 74 self.nodes[from_node].sendtoaddress(to_address, amount) 75 76 def do_one_round(self): 77 a0 = self.nodes[0].getnewaddress() 78 a1 = self.nodes[1].getnewaddress() 79 a2 = self.nodes[2].getnewaddress() 80 81 self.one_send(0, a1) 82 self.one_send(0, a2) 83 self.one_send(1, a0) 84 self.one_send(1, a2) 85 self.one_send(2, a0) 86 self.one_send(2, a1) 87 88 # Have the miner (node3) mine a block. 89 # Must sync mempools before mining. 90 self.sync_mempools() 91 self.generate(self.nodes[3], 1) 92 93 # As above, this mirrors the original bash test. 94 def start_three(self, args=()): 95 self.start_node(0, self.extra_args[0] + list(args)) 96 self.start_node(1, self.extra_args[1] + list(args)) 97 self.start_node(2, self.extra_args[2] + list(args)) 98 self.connect_nodes(0, 3) 99 self.connect_nodes(1, 3) 100 self.connect_nodes(2, 3) 101 self.connect_nodes(2, 0) 102 103 def stop_three(self): 104 self.stop_node(0) 105 self.stop_node(1) 106 self.stop_node(2) 107 108 def erase_three(self): 109 for node_num in range(3): 110 (self.nodes[node_num].wallets_path / self.default_wallet_name / self.wallet_data_filename).unlink() 111 112 def restore_invalid_wallet(self): 113 node = self.nodes[3] 114 invalid_wallet_file = self.nodes[0].datadir_path / 'invalid_wallet_file.bak' 115 open(invalid_wallet_file, "a").write("invalid_wallet_content") 116 wallet_name = "res0" 117 not_created_wallet_file = node.wallets_path / wallet_name 118 error_message = "Wallet file verification failed. Failed to load database path '{}'. Data is not in recognized format.".format(not_created_wallet_file) 119 assert_raises_rpc_error(-18, error_message, node.restorewallet, wallet_name, invalid_wallet_file) 120 assert not not_created_wallet_file.exists() 121 122 def restore_nonexistent_wallet(self): 123 node = self.nodes[3] 124 nonexistent_wallet_file = self.nodes[0].datadir_path / 'nonexistent_wallet.bak' 125 wallet_name = "res0" 126 assert_raises_rpc_error(-8, "Backup file does not exist", node.restorewallet, wallet_name, nonexistent_wallet_file) 127 not_created_wallet_file = node.wallets_path / wallet_name 128 assert not not_created_wallet_file.exists() 129 130 def restore_wallet_existent_name(self): 131 node = self.nodes[3] 132 backup_file = self.nodes[0].datadir_path / 'wallet.bak' 133 wallet_name = "res0" 134 wallet_file = node.wallets_path / wallet_name 135 error_message = "Failed to create database path '{}'. Database already exists.".format(wallet_file) 136 assert_raises_rpc_error(-36, error_message, node.restorewallet, wallet_name, backup_file) 137 assert wallet_file.exists() 138 139 def test_pruned_wallet_backup(self): 140 self.log.info("Test loading backup on a pruned node when the backup was created close to the prune height of the restoring node") 141 node = self.nodes[3] 142 self.restart_node(3, ["-prune=1", "-fastprune=1"]) 143 # Ensure the chain tip is at height 214, because this test assume it is. 144 assert_equal(node.getchaintips()[0]["height"], 214) 145 # We need a few more blocks so we can actually get above an realistic 146 # minimal prune height 147 self.generate(node, 50, sync_fun=self.no_op) 148 # Backup created at block height 264 149 node.backupwallet(node.datadir_path / 'wallet_pruned.bak') 150 # Generate more blocks so we can actually prune the older blocks 151 self.generate(node, 300, sync_fun=self.no_op) 152 # This gives us an actual prune height roughly in the range of 220 - 240 153 node.pruneblockchain(250) 154 # The backup should be updated with the latest height (locator) for 155 # the backup to load successfully this close to the prune height 156 node.restorewallet('pruned', node.datadir_path / 'wallet_pruned.bak') 157 158 def run_test(self): 159 self.log.info("Generating initial blockchain") 160 self.generate(self.nodes[0], 1) 161 self.generate(self.nodes[1], 1) 162 self.generate(self.nodes[2], 1) 163 self.generate(self.nodes[3], COINBASE_MATURITY) 164 165 assert_equal(self.nodes[0].getbalance(), 50) 166 assert_equal(self.nodes[1].getbalance(), 50) 167 assert_equal(self.nodes[2].getbalance(), 50) 168 assert_equal(self.nodes[3].getbalance(), 0) 169 170 self.log.info("Creating transactions") 171 # Five rounds of sending each other transactions. 172 for _ in range(5): 173 self.do_one_round() 174 175 self.log.info("Backing up") 176 177 for node_num in range(3): 178 self.nodes[node_num].backupwallet(self.nodes[node_num].datadir_path / 'wallet.bak') 179 180 self.log.info("More transactions") 181 for _ in range(5): 182 self.do_one_round() 183 184 # Generate 101 more blocks, so any fees paid mature 185 self.generate(self.nodes[3], COINBASE_MATURITY + 1) 186 187 balance0 = self.nodes[0].getbalance() 188 balance1 = self.nodes[1].getbalance() 189 balance2 = self.nodes[2].getbalance() 190 balance3 = self.nodes[3].getbalance() 191 total = balance0 + balance1 + balance2 + balance3 192 193 # At this point, there are 214 blocks (103 for setup, then 10 rounds, then 101.) 194 # 114 are mature, so the sum of all wallets should be 114 * 50 = 5700. 195 assert_equal(total, 5700) 196 197 ## 198 # Test restoring spender wallets from backups 199 ## 200 self.log.info("Restoring wallets on node 3 using backup files") 201 202 self.restore_invalid_wallet() 203 self.restore_nonexistent_wallet() 204 205 backup_files = [] 206 for node_num in range(3): 207 backup_files.append(self.nodes[node_num].datadir_path / 'wallet.bak') 208 209 for idx, backup_file in enumerate(backup_files): 210 self.nodes[3].restorewallet(f'res{idx}', backup_file) 211 assert (self.nodes[3].wallets_path / f'res{idx}').exists() 212 213 res0_rpc = self.nodes[3].get_wallet_rpc("res0") 214 res1_rpc = self.nodes[3].get_wallet_rpc("res1") 215 res2_rpc = self.nodes[3].get_wallet_rpc("res2") 216 217 assert_equal(res0_rpc.getbalance(), balance0) 218 assert_equal(res1_rpc.getbalance(), balance1) 219 assert_equal(res2_rpc.getbalance(), balance2) 220 221 self.restore_wallet_existent_name() 222 223 # Backup to source wallet file must fail 224 sourcePaths = [ 225 os.path.join(self.nodes[0].wallets_path, self.default_wallet_name, self.wallet_data_filename), 226 os.path.join(self.nodes[0].wallets_path, '.', self.default_wallet_name, self.wallet_data_filename), 227 os.path.join(self.nodes[0].wallets_path, self.default_wallet_name), 228 os.path.join(self.nodes[0].wallets_path)] 229 230 for sourcePath in sourcePaths: 231 assert_raises_rpc_error(-4, "backup failed", self.nodes[0].backupwallet, sourcePath) 232 233 self.test_pruned_wallet_backup() 234 235 236 if __name__ == '__main__': 237 WalletBackupTest(__file__).main()