wallet: fix migration of watch-only wallets with transactions #36583

pull yusufyildiz34 wants to merge 2 commits into bitcoin:master from yusufyildiz34:wallet-migration-36483 changing 5 files +64 −3
  1. yusufyildiz34 commented at 12:08 PM on October 20, 2026: none

    Migrating a legacy wallet that holds no spendable material fails whenever the wallet contains transactions. migratewallet returns std::bad_function_call (-1) and the wallet is left unmigrated.

    Two defects are involved, and the first one hides the second.

    1. The migration check reads in-memory state that has not been updated yet

    When the wallet being migrated has no spendable material, CWallet::ApplyMigrationData() asserts that nothing is left behind in it:

    if (!m_address_book.empty()) return util::Error{_("Error: Not all address book records were migrated")};
    if (!mapWallet.empty()) return util::Error{_("Error: Not all transaction records were migrated")};
    

    The two checks look symmetric but are not. DelAddressBookWithDB() erases from m_address_book straight away, so the first check sees current state. RemoveTxs() only erases the transaction records from disk; it defers the mapWallet removal to the db txn's on_commit listener, which does not run until the txn is committed — after ApplyMigrationData() has returned.

    mapWallet is therefore never empty at that point and the check always fails.

    2. Aborting the txn calls a callback that was never set

    DbTxnListener declares on_commit and on_abort as plain std::function members and callers may set only the one they need. RemoveTxs() does exactly that: on_commit is set, on_abort is left empty.

    TxnCommit() and TxnAbort() each invoked their listener's callback unconditionally, so the abort triggered by the failed check above threw std::bad_function_call. The exception escaped the wallet entirely, replacing the real error with an unhelpful one and skipping the caller's cleanup path.

    Fix

    The first commit skips callbacks that are not set, and documents that they are optional. The second discounts the transactions already queued for deletion instead of requiring mapWallet to be empty.

    They are kept separate because the second defect masks the first: with only the listener fix applied, the migration surfaces the real error Error: Not all transaction records were migrated (-4) instead of std::bad_function_call (-1).

    Testing

    src/wallet/test/db_tests.cpp gains a regression test covering a listener with only one callback set, for both commit and abort. It fails on master with std::bad_function_call and passes with the fix.

    test/functional/wallet_migration.py gains a case migrating a blank legacy wallet with a watch-only address that has received a transaction. The test is taken from #36483.

    To reproduce on master, apply the functional test alone and run wallet_migration.py.

    Fixes #36483

  2. wallet: skip unset db txn listener callbacks
    DbTxnListener declares on_commit and on_abort as plain std::function
    members and callers may set only the one they need. RemoveTxs() does
    exactly that: it registers a listener with on_commit set and on_abort
    left empty.
    
    TxnCommit() and TxnAbort() invoked both unconditionally, so aborting a
    db txn in which RemoveTxs() had run threw std::bad_function_call. The
    exception escaped the wallet, replacing the error that caused the abort
    with an unhelpful "std::bad_function_call" RPC error and skipping the
    caller's cleanup path.
    
    Skip callbacks that are not set, and document that they are optional.
    72889f7e1e
  3. wallet: fix migration of watch-only wallets
    When the wallet being migrated has no spendable material,
    ApplyMigrationData() asserts that no records are left behind in it. The
    address book half of that check works because DelAddressBookWithDB()
    erases from m_address_book straight away. The transaction half cannot:
    RemoveTxs() only erases the records from disk and defers the mapWallet
    removal to the db txn's on_commit listener, which runs after
    ApplyMigrationData() has returned.
    
    mapWallet is therefore never empty at that point, so migrating a
    watch-only wallet that has transactions always failed with
    "Error: Not all transaction records were migrated".
    
    Discount the transactions queued for deletion instead.
    50f254e53a
  4. DrahtBot added the label Wallet on Oct 20, 2026
  5. DrahtBot commented at 12:08 PM on October 20, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

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

    <!--006a51241073e994b41acfe9ec718e94-->

    External sites

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    ACK Naomi-Gift

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

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #35990 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/35990.svg"></sub> (wallet: harden the read-only BDB parser against crafted files by shuv-amp)

    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.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  6. Naomi-Gift commented at 6:47 AM on October 22, 2026: none

    utACK 50f254e53ac357e5a8315062c634e36071e1aa19


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-10-26 06:51 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me