Skip to content

Commit 90a5786

Browse files
committed
Merge bitcoin/bitcoin#30678: wallet: Write best block to disk before backup
f20fe33 test: Add basic balance coverage to wallet_assumeutxo.py (Fabian Jahr) 037b101 test: Add coverage for best block locator write in wallet_backup (Fabian Jahr) 31c0df0 wallet: migration, write best locator before unloading wallet (furszy) 7e3dbe4 wallet: Write best block to disk before backup (Fabian Jahr) Pull request description: I discovered that we don't write the best block to disk when trying to explain the behavior described here: bitcoin/bitcoin#30455 (comment) In the context of that test, the behavior is confusing and I think it also shows that one of the already existing tests in `wallet_assumeutxo.py` doesn't actually test what it says. It only fails because the best block isn't written and actually, the height of the backup that is loaded is at the snapshot height during backup. So it really shouldn't fail since it's past the background sync blocks already. I'm not sure if this is super relevant in practice though so I am first looking for concept ACKs on the `BackupWallet` code change. Either way, I think this behavior should be documented better if it is left as is and the test should be changed. ACKs for top commit: achow101: ACK f20fe33 furszy: ACK f20fe33 Tree-SHA512: bb384a940df5c942fffe2eb06314ade4fc5d9b924012bfef3b1c456c4182a30825d1e137d8ae561d93d3a8a2f4d1c1ffe568132d20fa7d04844f1e289ab4a28b
2 parents dabc74e + f20fe33 commit 90a5786

File tree

3 files changed

+69
-5
lines changed

3 files changed

+69
-5
lines changed

src/wallet/wallet.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3410,6 +3410,14 @@ void CWallet::postInitProcess()
34103410

34113411
bool CWallet::BackupWallet(const std::string& strDest) const
34123412
{
3413+
if (m_chain) {
3414+
CBlockLocator loc;
3415+
WITH_LOCK(cs_wallet, chain().findBlock(m_last_block_processed, FoundBlock().locator(loc)));
3416+
if (!loc.IsNull()) {
3417+
WalletBatch batch(GetDatabase());
3418+
batch.WriteBestBlock(loc);
3419+
}
3420+
}
34133421
return GetDatabase().Backup(strDest);
34143422
}
34153423

@@ -4390,6 +4398,11 @@ util::Result<MigrationResult> MigrateLegacyToDescriptor(const std::string& walle
43904398
return util::Error{_("Error: This wallet is already a descriptor wallet")};
43914399
}
43924400

4401+
// Flush chain state before unloading wallet
4402+
CBlockLocator locator;
4403+
WITH_LOCK(wallet->cs_wallet, context.chain->findBlock(wallet->GetLastBlockHash(), FoundBlock().locator(locator)));
4404+
if (!locator.IsNull()) wallet->chainStateFlushed(ChainstateRole::NORMAL, locator);
4405+
43934406
if (!RemoveWallet(context, wallet, /*load_on_start=*/std::nullopt, warnings)) {
43944407
return util::Error{_("Unable to unload the wallet before migrating")};
43954408
}

test/functional/wallet_assumeutxo.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
- TODO: test loading a wallet (backup) on a pruned node
1212
1313
"""
14+
from test_framework.address import address_to_scriptpubkey
1415
from test_framework.test_framework import BitcoinTestFramework
16+
from test_framework.messages import COIN
1517
from test_framework.util import (
1618
assert_equal,
1719
assert_raises_rpc_error,
@@ -62,8 +64,16 @@ def run_test(self):
6264
for n in self.nodes:
6365
n.setmocktime(n.getblockheader(n.getbestblockhash())['time'])
6466

67+
# Create a wallet that we will create a backup for later (at snapshot height)
6568
n0.createwallet('w')
6669
w = n0.get_wallet_rpc("w")
70+
w_address = w.getnewaddress()
71+
72+
# Create another wallet and backup now (before snapshot height)
73+
n0.createwallet('w2')
74+
w2 = n0.get_wallet_rpc("w2")
75+
w2_address = w2.getnewaddress()
76+
w2.backupwallet("backup_w2.dat")
6777

6878
# Generate a series of blocks that `n0` will have in the snapshot,
6979
# but that n1 doesn't yet see. In order for the snapshot to activate,
@@ -84,6 +94,8 @@ def run_test(self):
8494
assert_equal(n.getblockchaininfo()[
8595
"headers"], SNAPSHOT_BASE_HEIGHT)
8696

97+
# This backup is created at the snapshot height, so it's
98+
# not part of the background sync anymore
8799
w.backupwallet("backup_w.dat")
88100

89101
self.log.info("-- Testing assumeutxo")
@@ -103,7 +115,13 @@ def run_test(self):
103115

104116
# Mine more blocks on top of the snapshot that n1 hasn't yet seen. This
105117
# will allow us to test n1's sync-to-tip on top of a snapshot.
106-
self.generate(n0, nblocks=100, sync_fun=self.no_op)
118+
w_skp = address_to_scriptpubkey(w_address)
119+
w2_skp = address_to_scriptpubkey(w2_address)
120+
for i in range(100):
121+
if i % 3 == 0:
122+
self.mini_wallet.send_to(from_node=n0, scriptPubKey=w_skp, amount=1 * COIN)
123+
self.mini_wallet.send_to(from_node=n0, scriptPubKey=w2_skp, amount=10 * COIN)
124+
self.generate(n0, nblocks=1, sync_fun=self.no_op)
107125

108126
assert_equal(n0.getblockcount(), FINAL_HEIGHT)
109127
assert_equal(n1.getblockcount(), START_HEIGHT)
@@ -126,8 +144,13 @@ def run_test(self):
126144

127145
assert_equal(n1.getblockchaininfo()["blocks"], SNAPSHOT_BASE_HEIGHT)
128146

129-
self.log.info("Backup can't be loaded during background sync")
130-
assert_raises_rpc_error(-4, "Wallet loading failed. Error loading wallet. Wallet requires blocks to be downloaded, and software does not currently support loading wallets while blocks are being downloaded out of order when using assumeutxo snapshots. Wallet should be able to load successfully after node sync reaches height 299", n1.restorewallet, "w", "backup_w.dat")
147+
self.log.info("Backup from the snapshot height can be loaded during background sync")
148+
n1.restorewallet("w", "backup_w.dat")
149+
# Balance of w wallet is still still 0 because n1 has not synced yet
150+
assert_equal(n1.getbalance(), 0)
151+
152+
self.log.info("Backup from before the snapshot height can't be loaded during background sync")
153+
assert_raises_rpc_error(-4, "Wallet loading failed. Error loading wallet. Wallet requires blocks to be downloaded, and software does not currently support loading wallets while blocks are being downloaded out of order when using assumeutxo snapshots. Wallet should be able to load successfully after node sync reaches height 299", n1.restorewallet, "w2", "backup_w2.dat")
131154

132155
PAUSE_HEIGHT = FINAL_HEIGHT - 40
133156

@@ -159,8 +182,15 @@ def run_test(self):
159182
self.log.info("Ensuring background validation completes")
160183
self.wait_until(lambda: len(n1.getchainstates()['chainstates']) == 1)
161184

162-
self.log.info("Ensuring wallet can be restored from backup")
163-
n1.restorewallet("w", "backup_w.dat")
185+
self.log.info("Ensuring wallet can be restored from a backup that was created before the snapshot height")
186+
n1.restorewallet("w2", "backup_w2.dat")
187+
# Check balance of w2 wallet
188+
assert_equal(n1.getbalance(), 340)
189+
190+
# Check balance of w wallet after node is synced
191+
n1.loadwallet("w")
192+
w = n1.get_wallet_rpc("w")
193+
assert_equal(w.getbalance(), 34)
164194

165195

166196
if __name__ == '__main__':

test/functional/wallet_backup.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,25 @@ def restore_wallet_existent_name(self):
140140
assert_raises_rpc_error(-36, error_message, node.restorewallet, wallet_name, backup_file)
141141
assert wallet_file.exists()
142142

143+
def test_pruned_wallet_backup(self):
144+
self.log.info("Test loading backup on a pruned node when the backup was created close to the prune height of the restoring node")
145+
node = self.nodes[3]
146+
self.restart_node(3, ["-prune=1", "-fastprune=1"])
147+
# Ensure the chain tip is at height 214, because this test assume it is.
148+
assert_equal(node.getchaintips()[0]["height"], 214)
149+
# We need a few more blocks so we can actually get above an realistic
150+
# minimal prune height
151+
self.generate(node, 50, sync_fun=self.no_op)
152+
# Backup created at block height 264
153+
node.backupwallet(node.datadir_path / 'wallet_pruned.bak')
154+
# Generate more blocks so we can actually prune the older blocks
155+
self.generate(node, 300, sync_fun=self.no_op)
156+
# This gives us an actual prune height roughly in the range of 220 - 240
157+
node.pruneblockchain(250)
158+
# The backup should be updated with the latest height (locator) for
159+
# the backup to load successfully this close to the prune height
160+
node.restorewallet(f'pruned', node.datadir_path / 'wallet_pruned.bak')
161+
143162
def run_test(self):
144163
self.log.info("Generating initial blockchain")
145164
self.generate(self.nodes[0], 1)
@@ -242,6 +261,8 @@ def run_test(self):
242261
for sourcePath in sourcePaths:
243262
assert_raises_rpc_error(-4, "backup failed", self.nodes[0].backupwallet, sourcePath)
244263

264+
self.test_pruned_wallet_backup()
265+
245266

246267
if __name__ == '__main__':
247268
WalletBackupTest(__file__).main()

0 commit comments

Comments
 (0)