Skip to content

Conversation

@furszy
Copy link
Member

@furszy furszy commented Dec 27, 2025

Minimal fix for #34128.

The issue occurs during the migration of a legacy unnamed wallet
(the legacy "default" wallet). When the migration fails, the cleanup
logic is triggered to roll back the state, which involves erasing the
newly created descriptor wallets directories. Normally, this only
affects the parent directories of named wallets, since they each
reside in their own directories. However, because the unnamed
wallet resides directly in the top-level /wallets/ folder, this
logic accidentally deletes the main directory.

The fix ensures that only the wallet.dat file of the unnamed wallet
is touched and restored, preserving the wallet in BDB format and
leaving the main /wallets/ directory intact.

Story Line:

#32273 fixed a different set of issues and, in doing so, uncovered
this one.
Before the mentioned PR, backups were stored in the same directory
as the wallet.dat file. On a migration failure, the backup was then
copied to the top-level /wallets/ directory. For the unnamed legacy
wallet, the wallet directory is the /wallets/ directory, so the source
and destination paths were identical. As a result, we threw early in the
fs::copy_file call (here) because the file already existed, as we
were trying to copy the file onto itself. This caused the cleanup logic
to abort early on and never reach the removal line.

Testing Notes:

Cherry-pick the test commit on top of master and run it. You will
see the failure and realize the reason by reading the test code.

@DrahtBot
Copy link
Contributor

DrahtBot commented Dec 27, 2025

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Code Coverage & Benchmarks

For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/34156.

Reviews

See the guideline for information on the review process.

Type Reviewers
ACK achow101, w0xlt, davidgumberg, willcl-ark
Concept ACK pablomartin4btc, rkrux

If your review is incorrectly listed, please copy-paste <!--meta-tag:bot-skip--> into the comment that the bot should ignore.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #34198 (wallet: fix ancient wallets migration by furszy)
  • #34193 (wallet: make migration more robust against failures by furszy)

If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

@achow101 achow101 added this to the 31.0 milestone Dec 27, 2025
@achow101 achow101 self-requested a review December 27, 2025 04:39
@achow101
Copy link
Member

achow101 commented Dec 27, 2025

In #34128, the logs say that migration was successful, which is confusing as to how they ended up in the failure case. It looks like that log line is a little misleading since we can hit a failure after it is logged if the migrated wallet(s) cannot be reloaded, which will result in the failure cleanup being hit.

So a followup question is what happened that migration thinks it was successful, but subsequent loading failed.

Anyways, deleting user wallets is super bad and we should fix that ASAP.

@achow101
Copy link
Member

This appears to only affect 30.x.

29.x and earlier are unable to deal with default wallet migration failures in the first place as they have an issue with copying the legacy wallet backup.

@furszy furszy force-pushed the 2025_wallet_migration_jinglewreck branch from 39b456e to cad7eb7 Compare December 27, 2025 17:36
@furszy
Copy link
Member Author

furszy commented Dec 27, 2025

In #34128, the logs say that migration was successful, which is confusing as to how they ended up in the failure case. It looks like that log line is a little misleading since we can hit a failure after it is logged if the migrated wallet(s) cannot be reloaded, which will result in the failure cleanup being hit.

So a followup question is what happened that migration thinks it was successful, but subsequent loading failed.

Anyways, deleting user wallets is super bad and we should fix that ASAP.

Yeah, the failure must be related to reloading the wallet after migration, still I didn't want to delay this fix any longer.
Any other "fail to migrate" bug would be very minor and harmless compared to removing the /wallets/ directory.

This appears to only affect 30.x.

Thanks for checking, so we just need to back port it there.

@furszy furszy force-pushed the 2025_wallet_migration_jinglewreck branch from cad7eb7 to fe7ec95 Compare December 27, 2025 18:02
@furszy furszy force-pushed the 2025_wallet_migration_jinglewreck branch 2 times, most recently from a9e8167 to 631541d Compare December 27, 2025 23:26
Copy link
Member Author

@furszy furszy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added two more commits.

First one improves logging for post-migration failures, making it clearer to users (and us) if a migration is rolled back during the wallets reload process.

Second one fixes a bug where migrated watch-only and solvables wallets were incorrectly named when created from an unnamed default wallet.

@furszy furszy force-pushed the 2025_wallet_migration_jinglewreck branch from 631541d to 6ecfbc7 Compare December 28, 2025 00:40
Copy link
Member

@pablomartin4btc pablomartin4btc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept ACK

Good fix! Light code review, I'll test and re-review soon...

@maflcko maflcko removed the CI failed label Jan 1, 2026
@achow101
Copy link
Member

achow101 commented Jan 2, 2026

ACK 6ecfbc7

@furszy
Copy link
Member Author

furszy commented Jan 4, 2026

Just for completeness, added an extra test commit checking migration fails gracefully when the wallet last sync is beyond the node's prune height. Per #34128 (comment).

Nothing else was changed. Sorry for invalidating your ACK achow; took me a few minutes to add and doesn’t hurt to have extra tests.

Ready to go.

@furszy furszy force-pushed the 2025_wallet_migration_jinglewreck branch from 8d4ae2a to c17bceb Compare January 4, 2026 18:25
achow101 pushed a commit to achow101/bitcoin that referenced this pull request Jan 9, 2026
achow101 pushed a commit to achow101/bitcoin that referenced this pull request Jan 9, 2026
Track what RestoreWallet creates so only those files and directories
are removed during a failure and nothing else. Preexisting paths
must be left untouched.

Note:
Using fs::remove_all() instead of fs::remove() in RestoreWallet does
not cause any problems currently, but the change is necessary for the
next commit which extends RestoreWallet to work with existing directories,
which may contain files that must not be deleted.

Github-Pull: bitcoin#34156
Rebased-From: 4ed0693
achow101 pushed a commit to achow101/bitcoin that referenced this pull request Jan 9, 2026
When migrating any legacy unnamed wallet, a failed migration would
cause the cleanup logic to remove its parent directory. Since this
type of legacy wallet lives directly in the main '/wallets/' folder,
this resulted in unintentionally erasing all wallets, including the
backup file.

To be fully safe, we will no longer call `fs::remove_all`. Instead,
we only erase the individual db files we have created, leaving
everything else intact. The created wallets parent directories are
erased only if they are empty.
As part of this last change, `RestoreWallet` was modified to allow
an existing directory as the destination, since we no longer remove
the original wallet directory (we only remove the files we created
inside it). This also fixes the restore of top-level default wallets
during failures, which were failing due to the directory existence
check that always returns true for the /wallets/ directory.

This bug started after:
bitcoin@f6ee59b
Previously, the `fs::copy_file` call was failing for top-level wallets,
which prevented the `fs::remove_all` call from being reached.

Github-Pull: bitcoin#34156
Rebased-From: f4c7e28
achow101 pushed a commit to achow101/bitcoin that referenced this pull request Jan 9, 2026
Verifies that a failed migration of the unnamed (default) wallet
does not erase the main /wallets/ directory, and also that the
backup file exists.

Github-Pull: bitcoin#34156
Rebased-From: 36093bd
achow101 pushed a commit to achow101/bitcoin that referenced this pull request Jan 9, 2026
…rune failure

The first test verifies that restoring into an existing empty directory
or a directory with no .dat db files succeeds, while restoring into a
dir with a .dat file fails.

The second test covers restoring into the default unnamed wallet
(wallet.dat), which also implicitly exercises the recovery path used
after a failed migration.

The third test covers failure during restore on a prune node. When
the wallet last sync was beyond the pruning height.

Github-Pull: bitcoin#34156
Rebased-From: f011e0f
achow101 pushed a commit to achow101/bitcoin that referenced this pull request Jan 9, 2026
Right now, after migration the last message users see is "migration completed",
but the migration isn't actually finished yet. We still need to load the new wallets
to ensure consistency, and if that fails, the migration will be rolled back. This
can be confusing for users.

This change logs the post-migration loading step and if a wallet fails to load and
the migration will be rolled back.

Github-Pull: bitcoin#34156
Rebased-From: d70b159
achow101 pushed a commit to achow101/bitcoin that referenced this pull request Jan 9, 2026
Because the default wallet has no name, the watch-only and solvables
wallets created during migration end up having no name either.

This fixes it by applying the same prefix name we use for the backup
file for an unnamed default wallet.

Before: watch-only wallet named "_watchonly"
After:  watch-only wallet named "default_wallet_watchonly"

Github-Pull: bitcoin#34156
Rebased-From: 82caa81
achow101 pushed a commit to achow101/bitcoin that referenced this pull request Jan 9, 2026
fanquake added a commit that referenced this pull request Jan 9, 2026
eeaf28d wallet: test: Failed migration cleanup (David Gumberg)

Pull request description:

  Prior to #34156, an issue existed where if migration of a wallet with a relative pathname failed, the relatively specified path where the legacy wallet is would be deleted. This issue predates #32273, because the relative pathnames get stacked together, e.g. "../../", the copy conflict bug that caused migration to abort early instead of getting far enough to attempt clean-up that was fixed in #32273 is avoided.

  This is a functional test demonstrating that we handle failed migration clean-up correctly for relatively-named wallets. To see the issue, you can backport this test onto 29.x: https://github.com/davidgumberg/bitcoin/tree/2026-01-07-rel-migration-test-backport

  I've also added an absolute path failed migration cleanup test. WRT this and #34156, absolute paths exhibit similar behavior to unnamed wallets. Because of the name-conflict bug prior to #32273 an absolute-path migration would fail no matter what because migration would attempt to copy a file to a destination that already exists. But after #32273, absolute-path migration gets past there, and if it fails for some other reason, the same behavior that's fixed in #34156 occurs where the directory containing the wallet file is deleted.

ACKs for top commit:
  achow101:
    ACK eeaf28d
  furszy:
    ACK eeaf28d
  rkrux:
    lgtm ACK eeaf28d

Tree-SHA512: ee366fe526d0328654a86c2e9e6f228ca81554c8f8a78c259fa7aab90f024f9e5694ecf3f1d188938355f4e6d351c5a6a8ad236701bdd0ce63005e5d42c15e15
achow101 pushed a commit to achow101/bitcoin that referenced this pull request Jan 9, 2026
When migrating any legacy unnamed wallet, a failed migration would
cause the cleanup logic to remove its parent directory. Since this
type of legacy wallet lives directly in the main '/wallets/' folder,
this resulted in unintentionally erasing all wallets, including the
backup file.

To be fully safe, we will no longer call `fs::remove_all`. Instead,
we only erase the individual db files we have created, leaving
everything else intact. The created wallets parent directories are
erased only if they are empty.
As part of this last change, `RestoreWallet` was modified to allow
an existing directory as the destination, since we no longer remove
the original wallet directory (we only remove the files we created
inside it). This also fixes the restore of top-level default wallets
during failures, which were failing due to the directory existence
check that always returns true for the /wallets/ directory.

This bug started after:
bitcoin@f6ee59b
Previously, the `fs::copy_file` call was failing for top-level wallets,
which prevented the `fs::remove_all` call from being reached.

Github-Pull: bitcoin#34156
Rebased-From: f4c7e28
achow101 pushed a commit to achow101/bitcoin that referenced this pull request Jan 9, 2026
Verifies that a failed migration of the unnamed (default) wallet
does not erase the main /wallets/ directory, and also that the
backup file exists.

Github-Pull: bitcoin#34156
Rebased-From: 36093bd
achow101 pushed a commit to achow101/bitcoin that referenced this pull request Jan 9, 2026
…rune failure

The first test verifies that restoring into an existing empty directory
or a directory with no .dat db files succeeds, while restoring into a
dir with a .dat file fails.

The second test covers restoring into the default unnamed wallet
(wallet.dat), which also implicitly exercises the recovery path used
after a failed migration.

The third test covers failure during restore on a prune node. When
the wallet last sync was beyond the pruning height.

Github-Pull: bitcoin#34156
Rebased-From: f011e0f
achow101 pushed a commit to achow101/bitcoin that referenced this pull request Jan 9, 2026
Right now, after migration the last message users see is "migration completed",
but the migration isn't actually finished yet. We still need to load the new wallets
to ensure consistency, and if that fails, the migration will be rolled back. This
can be confusing for users.

This change logs the post-migration loading step and if a wallet fails to load and
the migration will be rolled back.

Github-Pull: bitcoin#34156
Rebased-From: d70b159
achow101 pushed a commit to achow101/bitcoin that referenced this pull request Jan 9, 2026
Because the default wallet has no name, the watch-only and solvables
wallets created during migration end up having no name either.

This fixes it by applying the same prefix name we use for the backup
file for an unnamed default wallet.

Before: watch-only wallet named "_watchonly"
After:  watch-only wallet named "default_wallet_watchonly"

Github-Pull: bitcoin#34156
Rebased-From: 82caa81
achow101 pushed a commit to achow101/bitcoin that referenced this pull request Jan 9, 2026
achow101 pushed a commit to achow101/bitcoin that referenced this pull request Jan 9, 2026
Verifies that a failed migration of the unnamed (default) wallet
does not erase the main /wallets/ directory, and also that the
backup file exists.

Github-Pull: bitcoin#34156
Rebased-From: 36093bd
achow101 pushed a commit to achow101/bitcoin that referenced this pull request Jan 9, 2026
…rune failure

The first test verifies that restoring into an existing empty directory
or a directory with no .dat db files succeeds, while restoring into a
dir with a .dat file fails.

The second test covers restoring into the default unnamed wallet
(wallet.dat), which also implicitly exercises the recovery path used
after a failed migration.

The third test covers failure during restore on a prune node. When
the wallet last sync was beyond the pruning height.

Github-Pull: bitcoin#34156
Rebased-From: f011e0f
achow101 pushed a commit to achow101/bitcoin that referenced this pull request Jan 9, 2026
Right now, after migration the last message users see is "migration completed",
but the migration isn't actually finished yet. We still need to load the new wallets
to ensure consistency, and if that fails, the migration will be rolled back. This
can be confusing for users.

This change logs the post-migration loading step and if a wallet fails to load and
the migration will be rolled back.

Github-Pull: bitcoin#34156
Rebased-From: d70b159
achow101 pushed a commit to achow101/bitcoin that referenced this pull request Jan 9, 2026
Because the default wallet has no name, the watch-only and solvables
wallets created during migration end up having no name either.

This fixes it by applying the same prefix name we use for the backup
file for an unnamed default wallet.

Before: watch-only wallet named "_watchonly"
After:  watch-only wallet named "default_wallet_watchonly"

Github-Pull: bitcoin#34156
Rebased-From: 82caa81
achow101 pushed a commit to achow101/bitcoin that referenced this pull request Jan 9, 2026
achow101 pushed a commit to achow101/bitcoin that referenced this pull request Jan 10, 2026
When migrating any legacy unnamed wallet, a failed migration would
cause the cleanup logic to remove its parent directory. Since this
type of legacy wallet lives directly in the main '/wallets/' folder,
this resulted in unintentionally erasing all wallets, including the
backup file.

To be fully safe, we will no longer call `fs::remove_all`. Instead,
we only erase the individual db files we have created, leaving
everything else intact. The created wallets parent directories are
erased only if they are empty.
As part of this last change, `RestoreWallet` was modified to allow
an existing directory as the destination, since we no longer remove
the original wallet directory (we only remove the files we created
inside it). This also fixes the restore of top-level default wallets
during failures, which were failing due to the directory existence
check that always returns true for the /wallets/ directory.

This bug started after:
bitcoin@f6ee59b
Previously, the `fs::copy_file` call was failing for top-level wallets,
which prevented the `fs::remove_all` call from being reached.

Github-Pull: bitcoin#34156
Rebased-From: f4c7e28
achow101 pushed a commit to achow101/bitcoin that referenced this pull request Jan 10, 2026
Verifies that a failed migration of the unnamed (default) wallet
does not erase the main /wallets/ directory, and also that the
backup file exists.

Github-Pull: bitcoin#34156
Rebased-From: 36093bd
achow101 pushed a commit to achow101/bitcoin that referenced this pull request Jan 10, 2026
…rune failure

The first test verifies that restoring into an existing empty directory
or a directory with no .dat db files succeeds, while restoring into a
dir with a .dat file fails.

The second test covers restoring into the default unnamed wallet
(wallet.dat), which also implicitly exercises the recovery path used
after a failed migration.

The third test covers failure during restore on a prune node. When
the wallet last sync was beyond the pruning height.

Github-Pull: bitcoin#34156
Rebased-From: f011e0f
achow101 pushed a commit to achow101/bitcoin that referenced this pull request Jan 10, 2026
Right now, after migration the last message users see is "migration completed",
but the migration isn't actually finished yet. We still need to load the new wallets
to ensure consistency, and if that fails, the migration will be rolled back. This
can be confusing for users.

This change logs the post-migration loading step and if a wallet fails to load and
the migration will be rolled back.

Github-Pull: bitcoin#34156
Rebased-From: d70b159
achow101 pushed a commit to achow101/bitcoin that referenced this pull request Jan 10, 2026
Because the default wallet has no name, the watch-only and solvables
wallets created during migration end up having no name either.

This fixes it by applying the same prefix name we use for the backup
file for an unnamed default wallet.

Before: watch-only wallet named "_watchonly"
After:  watch-only wallet named "default_wallet_watchonly"

Github-Pull: bitcoin#34156
Rebased-From: 82caa81
achow101 pushed a commit to achow101/bitcoin that referenced this pull request Jan 10, 2026
glozow added a commit that referenced this pull request Jan 12, 2026
76cdeb7 wallet: test: Failed migration cleanup (David Gumberg)
9405e91 test: coverage for migration failure when last sync is beyond prune height (furszy)
5e8ad98 wallet: migration, fix watch-only and solvables wallets names (furszy)
a7e2d10 wallet: improve post-migration logging (furszy)
9ea84c0 test: restorewallet, coverage for existing dirs, unnamed wallet and prune failure (furszy)
833848e test: add coverage for unnamed wallet migration failure (furszy)
a074d36 wallet: fix unnamed wallet migration failure (furszy)
d91f56e wallet: RestoreWallet failure, erase only what was created (furszy)
cc324aa wallettool: do not use fs::remove_all in createfromdump cleanup (Ava Chow)
01c04d3 wallet: introduce method to return all db created files (furszy)
abaf1e3 refactor: remove sqlite dir path back-and-forth conversion (furszy)

Pull request description:

  Backports:
  * #34215
  * #34156
  * #34226
  * 2 required commits from #31423

  Note that this backport is unclean and several changes have to be made to most commits to accommodate BDB and the differences in migration cleanup behavior.

ACKs for top commit:
  furszy:
    Code review ACK 76cdeb7
  brunoerg:
    light code review ACK 76cdeb7 + backported the functional tests without the fixes and all of them failed accordingly.
  glozow:
    light review ACK 76cdeb7.

Tree-SHA512: 432268117783fc9a221d895a6f6601b6a2a5031c76d1443cf804cc1d486b40fcded982409d548acd1c01a13c7b378b840fcc3fbe823d6ba5ffc4ebe017d4e13c
achow101 pushed a commit to achow101/bitcoin that referenced this pull request Jan 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.