Skip to content

Commit a115856

Browse files
committed
Merge bitcoin/bitcoin#28868: wallet: Fix migration of wallets with txs that have both spendable and watchonly outputs
4da76ca test: Test migration of tx with both spendable and watchonly (Ava Chow) c62a8d0 wallet: Keep txs that belong to both watchonly and migrated wallets (Ava Chow) 71cb28e test: Make sure that migration test does not rescan on reloading (Ava Chow) 78ba0e6 wallet: Reload the wallet if migration exited early (Ava Chow) 9332c7e wallet: Write bestblock to watchonly and solvable wallets (Ava Chow) Pull request description: A transaction does not necessarily have to belong to either the migrated wallet (with the private keys) and the watchonly wallet (with watchonly things), it could have multiple outputs with each isminetype. So we should be putting such transactions in one or the other wallet, but rather putting it in both. I've added a test for this behavior, however the test also revealed a few other issues. Notably, it revealed that `migratewallet` would have the watchonly wallet rescan from genesis when it is reloaded at the end of migration. This could be a cause for migration appearing to be very slow. This is resolved by first writing best block records to the watchonly and solvable wallets, as well as updating the test to make sure that rescans don't happen. The change to avoid rescans also found an issue where some of our early exits would result in unloading the wallet even though nothing happened. So there is also a commit to reload the wallet for such early exits. ACKs for top commit: ryanofsky: Code review ACK 4da76ca. This looks great. The code is actually cleaner than before, two bugs are fixed, and the test checking for rescanning is pretty clever and broadens test coverage. furszy: Code review ACK 4da76ca Tree-SHA512: 5fc210cff16ca6720d7b2d0616d7e3f295c974147854abc704cf99a3bfaad17572ada084859e7a1b1ca94da647ad130303219678f429b7995f85e040236db35c
2 parents 93e10ca + 4da76ca commit a115856

File tree

2 files changed

+116
-62
lines changed

2 files changed

+116
-62
lines changed

src/wallet/wallet.cpp

Lines changed: 79 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3927,6 +3927,13 @@ bool CWallet::ApplyMigrationData(MigrationData& data, bilingual_str& error)
39273927
}
39283928
}
39293929

3930+
// Get best block locator so that we can copy it to the watchonly and solvables
3931+
CBlockLocator best_block_locator;
3932+
if (!WalletBatch(GetDatabase()).ReadBestBlock(best_block_locator)) {
3933+
error = _("Error: Unable to read wallet's best block locator record");
3934+
return false;
3935+
}
3936+
39303937
// Check if the transactions in the wallet are still ours. Either they belong here, or they belong in the watchonly wallet.
39313938
// We need to go through these in the tx insertion order so that lookups to spends works.
39323939
std::vector<uint256> txids_to_delete;
@@ -3937,32 +3944,47 @@ bool CWallet::ApplyMigrationData(MigrationData& data, bilingual_str& error)
39373944
LOCK(data.watchonly_wallet->cs_wallet);
39383945
data.watchonly_wallet->nOrderPosNext = nOrderPosNext;
39393946
watchonly_batch->WriteOrderPosNext(data.watchonly_wallet->nOrderPosNext);
3947+
// Write the best block locator to avoid rescanning on reload
3948+
if (!watchonly_batch->WriteBestBlock(best_block_locator)) {
3949+
error = _("Error: Unable to write watchonly wallet best block locator record");
3950+
return false;
3951+
}
3952+
}
3953+
if (data.solvable_wallet) {
3954+
// Write the best block locator to avoid rescanning on reload
3955+
if (!WalletBatch(data.solvable_wallet->GetDatabase()).WriteBestBlock(best_block_locator)) {
3956+
error = _("Error: Unable to write solvable wallet best block locator record");
3957+
return false;
3958+
}
39403959
}
39413960
for (const auto& [_pos, wtx] : wtxOrdered) {
3942-
if (!IsMine(*wtx->tx) && !IsFromMe(*wtx->tx)) {
3943-
// Check it is the watchonly wallet's
3944-
// solvable_wallet doesn't need to be checked because transactions for those scripts weren't being watched for
3945-
if (data.watchonly_wallet) {
3946-
LOCK(data.watchonly_wallet->cs_wallet);
3947-
if (data.watchonly_wallet->IsMine(*wtx->tx) || data.watchonly_wallet->IsFromMe(*wtx->tx)) {
3948-
// Add to watchonly wallet
3949-
const uint256& hash = wtx->GetHash();
3950-
const CWalletTx& to_copy_wtx = *wtx;
3951-
if (!data.watchonly_wallet->LoadToWallet(hash, [&](CWalletTx& ins_wtx, bool new_tx) EXCLUSIVE_LOCKS_REQUIRED(data.watchonly_wallet->cs_wallet) {
3952-
if (!new_tx) return false;
3953-
ins_wtx.SetTx(to_copy_wtx.tx);
3954-
ins_wtx.CopyFrom(to_copy_wtx);
3955-
return true;
3956-
})) {
3957-
error = strprintf(_("Error: Could not add watchonly tx %s to watchonly wallet"), wtx->GetHash().GetHex());
3958-
return false;
3959-
}
3960-
watchonly_batch->WriteTx(data.watchonly_wallet->mapWallet.at(hash));
3961-
// Mark as to remove from this wallet
3961+
// Check it is the watchonly wallet's
3962+
// solvable_wallet doesn't need to be checked because transactions for those scripts weren't being watched for
3963+
bool is_mine = IsMine(*wtx->tx) || IsFromMe(*wtx->tx);
3964+
if (data.watchonly_wallet) {
3965+
LOCK(data.watchonly_wallet->cs_wallet);
3966+
if (data.watchonly_wallet->IsMine(*wtx->tx) || data.watchonly_wallet->IsFromMe(*wtx->tx)) {
3967+
// Add to watchonly wallet
3968+
const uint256& hash = wtx->GetHash();
3969+
const CWalletTx& to_copy_wtx = *wtx;
3970+
if (!data.watchonly_wallet->LoadToWallet(hash, [&](CWalletTx& ins_wtx, bool new_tx) EXCLUSIVE_LOCKS_REQUIRED(data.watchonly_wallet->cs_wallet) {
3971+
if (!new_tx) return false;
3972+
ins_wtx.SetTx(to_copy_wtx.tx);
3973+
ins_wtx.CopyFrom(to_copy_wtx);
3974+
return true;
3975+
})) {
3976+
error = strprintf(_("Error: Could not add watchonly tx %s to watchonly wallet"), wtx->GetHash().GetHex());
3977+
return false;
3978+
}
3979+
watchonly_batch->WriteTx(data.watchonly_wallet->mapWallet.at(hash));
3980+
// Mark as to remove from the migrated wallet only if it does not also belong to it
3981+
if (!is_mine) {
39623982
txids_to_delete.push_back(hash);
3963-
continue;
39643983
}
3984+
continue;
39653985
}
3986+
}
3987+
if (!is_mine) {
39663988
// Both not ours and not in the watchonly wallet
39673989
error = strprintf(_("Error: Transaction %s in wallet cannot be identified to belong to migrated wallets"), wtx->GetHash().GetHex());
39683990
return false;
@@ -4194,11 +4216,13 @@ util::Result<MigrationResult> MigrateLegacyToDescriptor(const std::string& walle
41944216
std::vector<bilingual_str> warnings;
41954217

41964218
// If the wallet is still loaded, unload it so that nothing else tries to use it while we're changing it
4219+
bool was_loaded = false;
41974220
if (auto wallet = GetWallet(context, wallet_name)) {
41984221
if (!RemoveWallet(context, wallet, /*load_on_start=*/std::nullopt, warnings)) {
41994222
return util::Error{_("Unable to unload the wallet before migrating")};
42004223
}
42014224
UnloadWallet(std::move(wallet));
4225+
was_loaded = true;
42024226
}
42034227

42044228
// Load the wallet but only in the context of this function.
@@ -4219,8 +4243,20 @@ util::Result<MigrationResult> MigrateLegacyToDescriptor(const std::string& walle
42194243
return util::Error{Untranslated("Wallet loading failed.") + Untranslated(" ") + error};
42204244
}
42214245

4246+
// Helper to reload as normal for some of our exit scenarios
4247+
const auto& reload_wallet = [&](std::shared_ptr<CWallet>& to_reload) {
4248+
assert(to_reload.use_count() == 1);
4249+
std::string name = to_reload->GetName();
4250+
to_reload.reset();
4251+
to_reload = LoadWallet(context, name, /*load_on_start=*/std::nullopt, options, status, error, warnings);
4252+
return to_reload != nullptr;
4253+
};
4254+
42224255
// Before anything else, check if there is something to migrate.
42234256
if (local_wallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) {
4257+
if (was_loaded) {
4258+
reload_wallet(local_wallet);
4259+
}
42244260
return util::Error{_("Error: This wallet is already a descriptor wallet")};
42254261
}
42264262

@@ -4229,27 +4265,33 @@ util::Result<MigrationResult> MigrateLegacyToDescriptor(const std::string& walle
42294265
fs::path backup_filename = fs::PathFromString(strprintf("%s-%d.legacy.bak", wallet_name, GetTime()));
42304266
fs::path backup_path = this_wallet_dir / backup_filename;
42314267
if (!local_wallet->BackupWallet(fs::PathToString(backup_path))) {
4268+
if (was_loaded) {
4269+
reload_wallet(local_wallet);
4270+
}
42324271
return util::Error{_("Error: Unable to make a backup of your wallet")};
42334272
}
42344273
res.backup_path = backup_path;
42354274

42364275
bool success = false;
4237-
{
4238-
LOCK(local_wallet->cs_wallet);
42394276

4240-
// Unlock the wallet if needed
4241-
if (local_wallet->IsLocked() && !local_wallet->Unlock(passphrase)) {
4242-
if (passphrase.find('\0') == std::string::npos) {
4243-
return util::Error{Untranslated("Error: Wallet decryption failed, the wallet passphrase was not provided or was incorrect.")};
4244-
} else {
4245-
return util::Error{Untranslated("Error: Wallet decryption failed, the wallet passphrase entered was incorrect. "
4246-
"The passphrase contains a null character (ie - a zero byte). "
4247-
"If this passphrase was set with a version of this software prior to 25.0, "
4248-
"please try again with only the characters up to — but not including — "
4249-
"the first null character.")};
4250-
}
4277+
// Unlock the wallet if needed
4278+
if (local_wallet->IsLocked() && !local_wallet->Unlock(passphrase)) {
4279+
if (was_loaded) {
4280+
reload_wallet(local_wallet);
4281+
}
4282+
if (passphrase.find('\0') == std::string::npos) {
4283+
return util::Error{Untranslated("Error: Wallet decryption failed, the wallet passphrase was not provided or was incorrect.")};
4284+
} else {
4285+
return util::Error{Untranslated("Error: Wallet decryption failed, the wallet passphrase entered was incorrect. "
4286+
"The passphrase contains a null character (ie - a zero byte). "
4287+
"If this passphrase was set with a version of this software prior to 25.0, "
4288+
"please try again with only the characters up to — but not including — "
4289+
"the first null character.")};
42514290
}
4291+
}
42524292

4293+
{
4294+
LOCK(local_wallet->cs_wallet);
42534295
// First change to using SQLite
42544296
if (!local_wallet->MigrateToSQLite(error)) return util::Error{error};
42554297

@@ -4270,24 +4312,19 @@ util::Result<MigrationResult> MigrateLegacyToDescriptor(const std::string& walle
42704312
std::set<fs::path> wallet_dirs;
42714313
if (success) {
42724314
// Migration successful, unload all wallets locally, then reload them.
4273-
const auto& reload_wallet = [&](std::shared_ptr<CWallet>& to_reload) {
4274-
assert(to_reload.use_count() == 1);
4275-
std::string name = to_reload->GetName();
4276-
wallet_dirs.insert(fs::PathFromString(to_reload->GetDatabase().Filename()).parent_path());
4277-
to_reload.reset();
4278-
to_reload = LoadWallet(context, name, /*load_on_start=*/std::nullopt, options, status, error, warnings);
4279-
return to_reload != nullptr;
4280-
};
42814315
// Reload the main wallet
4316+
wallet_dirs.insert(fs::PathFromString(local_wallet->GetDatabase().Filename()).parent_path());
42824317
success = reload_wallet(local_wallet);
42834318
res.wallet = local_wallet;
42844319
res.wallet_name = wallet_name;
42854320
if (success && res.watchonly_wallet) {
42864321
// Reload watchonly
4322+
wallet_dirs.insert(fs::PathFromString(res.watchonly_wallet->GetDatabase().Filename()).parent_path());
42874323
success = reload_wallet(res.watchonly_wallet);
42884324
}
42894325
if (success && res.solvables_wallet) {
42904326
// Reload solvables
4327+
wallet_dirs.insert(fs::PathFromString(res.solvables_wallet->GetDatabase().Filename()).parent_path());
42914328
success = reload_wallet(res.solvables_wallet);
42924329
}
42934330
}

0 commit comments

Comments
 (0)