Repository navigation
backport: partial bitcoin#28868 (wallet: reload wallet if migration exited early, keep mixed watchonly txs) - #7793
Conversation
Backport of bitcoin/bitcoin@78ba0e6748d2 from bitcoin#28868. Migration will unload loaded wallets prior to beginning. It will then perform some checks which may exit early. Such unloaded wallets should be reloaded prior to exiting. Adapted to Dash: the "already a descriptor wallet" check keeps using GetLegacyScriptPubKeyMan(), the Dash backup filename logic is kept, and the successful-migration path still reloads the wallet directly because bitcoin#28609 (which introduced the reload_wallet helper there) has not been backported, so the upstream hunk moving the helper out of that branch does not apply. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Backport of bitcoin/bitcoin@71cb28ea8cb5 from bitcoin#28868. We want to make sure that all of the transactions are being copied to the watchonly and solvable wallets as expected. The automatic rescanning behavior can cause us to pass a test by finding the transaction on loading rather than having it be copied as expected. The helper also reloads the wallet before each migration, so it fails if an earlier failed migratewallet call left the wallet unloaded. The Dash-only test_wallet_name_with_slashes migration goes through the helper as well. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Backport of bitcoin/bitcoin@c62a8d03a862 from bitcoin#28868. It is possible for a transaction that has an output that belongs to the migrated wallet, and another output that belongs to the watchonly wallet. Such transaction should appear in both wallets during migration. Adapted to Dash: the watchonly copy still goes through AddToWallet() because bitcoin#28125 (LoadToWallet()/CopyFrom() and the shared watchonly WalletBatch) has not been backported. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Backport of bitcoin/bitcoin@4da76ca24725 from bitcoin#28868. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 25 minutes. View limit detailsLimit details: You’ve used all 4 included reviews currently available. Review configuration: ⚙️ Run configuration
📒 Files selected for processing (2)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
✅ Final review complete — no blockers (commit 17b0589) |
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Phase 1 + Phase 2
At the exact reviewed head, the wallet fixes preserve the intended upstream behavior without an identified production correctness defect. One non-blocking upstream test transformation is missing and is not documented among the declared omissions. Verification was static: the supplied CI snapshot shows successful formatting, merge, and amd64 container checks, queued arm64 container checks, and a description-validation failure confirmed by its job log to be caused by upstream commit-reference syntax.
🟡 1 suggestion(s)
Review provenance
Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: backport-reviewer); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: dash-core-commit-history); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
normalbygpt-6.1-sol(effort low) — The diff changes wallet migration error recovery and transaction assignment between migrated wallets, but its contained logic does not meet the large-or-intricate threshold for critical. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity below 15% reserve: weekly 15% left, 5h 100% left),glm-5.3-flash(not used above high effort; tier asks max) - Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— backport-reviewer (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— dash-core-commit-history (completed, effort high); agentphase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `test/functional/wallet_migration.py`:
- [SUGGESTION] test/functional/wallet_migration.py:54-64: Missing test prerequisite: bitcoin#28038 addressbook migration scenario
Upstream commit 71cb28ea8cb5 also routes the migration in `test_addressbook` through `self.migrate_wallet(wallet)`, adding the no-Rescanning assertion to a scenario that creates both auxiliary wallets and checks migrated addressbook data. That scenario was introduced by 7ecc29a0b7a23d8f5d3c1e6a0dad29b3ad839eb9 in bitcoin#28038 and is absent from both the Dash base and this head, so this upstream test hunk has not been carried over. The existing multisig scenario exercises both auxiliary wallets, but it does not reproduce the addressbook assertions. This does not establish a prerequisite for either production fix, and the PR explicitly advertises a partial backport; however, its omission list does not identify this additional dropped test hunk. Add the adapted scenario and use this helper, or explicitly document the omitted transformation and its deferred test prerequisite.
| def migrate_wallet(self, wallet_rpc, *args, **kwargs): | ||
| # Helper to ensure that only migration happens | ||
| # Since we may rescan on loading of a wallet, make sure that the best block | ||
| # is written before beginning migration | ||
| # Reload to force write that record | ||
| wallet_name = wallet_rpc.getwalletinfo()["walletname"] | ||
| wallet_rpc.unloadwallet() | ||
| self.nodes[0].loadwallet(wallet_name) | ||
| # Migrate, checking that rescan does not occur | ||
| with self.nodes[0].assert_debug_log(expected_msgs=[], unexpected_msgs=["Rescanning"]): | ||
| return wallet_rpc.migratewallet(*args, **kwargs) |
There was a problem hiding this comment.
⏭️ Deferred (maintainer decision; not blocking) at
17b05897; see the replies below.
🟡 Suggestion: Missing test prerequisite: bitcoin#28038 addressbook migration scenario
Upstream commit 71cb28e also routes the migration in test_addressbook through self.migrate_wallet(wallet), adding the no-Rescanning assertion to a scenario that creates both auxiliary wallets and checks migrated addressbook data. That scenario was introduced by 7ecc29a in bitcoin#28038 and is absent from both the Dash base and this head, so this upstream test hunk has not been carried over. The existing multisig scenario exercises both auxiliary wallets, but it does not reproduce the addressbook assertions. This does not establish a prerequisite for either production fix, and the PR explicitly advertises a partial backport; however, its omission list does not identify this additional dropped test hunk. Add the adapted scenario and use this helper, or explicitly document the omitted transformation and its deferred test prerequisite.
source: gpt-6.1-sol (phase2-reviewer: general, backport-reviewer, dash-core-commit-history)
There was a problem hiding this comment.
Agreed that it was undocumented. The hunk cannot be carried over because test_addressbook comes from bitcoin#28038, which is not backported, so there is nothing for it to change here. The omission is now listed under "Dash adaptations and omitted hunks" in the PR description.
🤖 Posted autonomously by Claude on behalf of pasta.
There was a problem hiding this comment.
I confirmed that test_addressbook is absent from both the Dash base and this head, and the updated PR description now explicitly identifies the omitted helper transformation and its bitcoin#28038 prerequisite; that addresses my documentation concern, and I’m satisfied to leave the scenario outside this backport.
Noted as intentionally deferred; I will not press it further here.
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review after discussion — commit 17b0589
Standing review was COMMENTED; this re-review is APPROVE: no blocking findings remain.
Approved: the discussion withdrew or deferred every finding the full review of 17b05897 (run 3281) raised, and the approval rests on that review.
Same commit as the standing review; the inline threads above carry the per-finding outcome. This follow-up exists only to correct the verdict.
Review provenance
Source: conversation lane gpt-6.1-sol (agent: conversation); no reviewer or verifier lanes ran for this follow-up
- Verdict moved because the inline discussion withdrew or deferred findings of the standing review of this commit; the code was not re-reviewed
…f migration exited early, keep mixed watchonly txs) 17b0589 test: test migration of tx with both spendable and watchonly (pasta) 03821b7 fix(wallet): keep txs that belong to both watchonly and migrated wallets (pasta) c305aab test: make sure that migration test does not rescan on reloading (pasta) 1f9bf08 fix(wallet): reload the wallet if migration exited early (pasta) Pull request description: ## Issue being fixed or feature implemented `migratewallet` (and the GUI "Migrate wallet" action, which calls the same `MigrateLegacyToDescriptor()`) has two bugs that upstream fixed in bitcoin#28868. That PR is listed as outstanding in the "next batch" section of #7277. 1. **A failed migration leaves the wallet unloaded.** If the wallet is loaded, `MigrateLegacyToDescriptor()` unloads it first and then runs its checks. Several of those checks return early without loading it again: the wallet is already a descriptor wallet, the backup could not be written, or the passphrase is missing or wrong. After any of these errors the wallet is gone from `listwallets`, and RPC calls to it fail with `Requested wallet does not exist or is not loaded` until the user runs `loadwallet` manually. Mistyping the passphrase once on an encrypted legacy wallet is enough to trigger this. 2. **A transaction with outputs in both resulting wallets only stays in the migrated wallet.** During migration, `ApplyMigrationData()` only offers a transaction to the new `<name>_watchonly` wallet when the migrated wallet does *not* claim it. If one output pays a spendable address and another pays an imported watch-only address, the transaction stays in the migrated wallet and is never copied to `<name>_watchonly`. That wallet is created at the chain tip, so it does not rescan, and its history and balance silently omit that transaction. This is the watch-only bug mentioned in #7275. ### Why this is a real problem Code path at c14104b: - The loaded wallet is unloaded before any validation: [wallet.cpp#L5006-L5012](https://github.com/dashpay/dash/blob/c14104b172a2/src/wallet/wallet.cpp#L5006-L5012) - These early returns do not reload it: "already a descriptor wallet" [#L5033-L5035](https://github.com/dashpay/dash/blob/c14104b172a2/src/wallet/wallet.cpp#L5033-L5035), backup failure [#L5055-L5057](https://github.com/dashpay/dash/blob/c14104b172a2/src/wallet/wallet.cpp#L5055-L5057), missing or wrong passphrase [#L5062-L5075](https://github.com/dashpay/dash/blob/c14104b172a2/src/wallet/wallet.cpp#L5062-L5075) - Watch-only copying only runs when `!IsMine(tx) && !IsFromMe(tx)`: [wallet.cpp#L4759-L4781](https://github.com/dashpay/dash/blob/c14104b172a2/src/wallet/wallet.cpp#L4759-L4781) I reproduced all three cases against an unfixed `dashd` built from c14104b, using a functional-test-style script on regtest: <details><summary>Reproduction on c14104b (unfixed)</summary> Steps: 1. `createwallet desc` (descriptor), then `migratewallet desc`, then `listwallets` 2. `createwallet enc descriptors=false`, then `encryptwallet pass`, then `migratewallet enc badpass`, then `listwallets` and `getwalletinfo` on `enc` 3. `createwallet imports descriptors=false`, then `importaddress <addr from default wallet>`. From the default wallet, `send` one output to an `imports` address and one to the imported address, then mine a block. Then `migratewallet` and `gettransaction <txid>` on `imports_watchonly` ``` TestFramework (INFO): Case 1: migratewallet on a loaded descriptor wallet TestFramework (INFO): listwallets before: ['default_wallet', 'desc'] TestFramework (INFO): listwallets after: ['default_wallet'] TestFramework (INFO): Case 2: migratewallet on a loaded encrypted legacy wallet with a wrong passphrase TestFramework (INFO): listwallets before: ['default_wallet', 'enc'] TestFramework (INFO): listwallets after: ['default_wallet'] TestFramework (INFO): getwalletinfo on 'enc' -> RPC error -18 (wallet not loaded) TestFramework (INFO): Case 3: tx with a spendable output and a watch-only output TestFramework (INFO): imports_watchonly.gettransaction(cfbc39814ffc474a35bae511c660de7a759653f3a362639f08e38fe2bd6382ef) -> Invalid or non-wallet transaction id (-5) TestFramework (INFO): RESULT descriptor wallet still loaded: False TestFramework (INFO): RESULT encrypted wallet still loaded: False TestFramework (INFO): RESULT mixed tx present in watchonly wallet: False ``` With this branch, the same script reports `listwallets after: ['default_wallet', 'desc']` and `['default_wallet', 'desc', 'enc']`, and all three results are `True`. </details> The upstream tests backported here also fail on the unfixed code (see "How Has This Been Tested?"). ### Why it matters Neither bug loses funds or keys. The first one is a usability trap in a v24 feature: a typo in the passphrase makes the wallet disappear from the node, the GUI and RPC clients, with no hint that a `loadwallet` is needed. The second one leaves the post-migration watch-only wallet with incomplete history and balance, and nothing reports it. Users migrating real wallets have already hit it (see #7275). ## What was done? Backport of bitcoin#28868, minus one commit: - bitcoin/bitcoin commit `78ba0e6748d2` "wallet: Reload the wallet if migration exited early": remember whether the wallet was loaded (`was_loaded`), define the `reload_wallet` helper before the early exits, and reload the wallet on the descriptor-wallet, backup-failure and passphrase-failure exits. As upstream does, the passphrase unlock moves out of the `LOCK(cs_wallet)` scope so the reload does not happen while the wallet lock is held. - bitcoin/bitcoin commit `71cb28ea8cb5` "test: Make sure that migration test does not rescan on reloading": adds the `migrate_wallet()` helper, which reloads the wallet before migrating and checks that migration does not log `Rescanning`. Because the helper calls `getwalletinfo` on the wallet first, it fails if an earlier failed `migratewallet` left the wallet unloaded. The Dash-only `test_wallet_name_with_slashes` case also goes through the helper. - bitcoin/bitcoin commit `c62a8d03a862` "wallet: Keep txs that belong to both watchonly and migrated wallets": every transaction is offered to the watch-only wallet. It is removed from the migrated wallet only if the migrated wallet does not also own it. - bitcoin/bitcoin commit `4da76ca24725` "test: Test migration of tx with both spendable and watchonly" Dash adaptations and omitted hunks: - **Omitted bitcoin/bitcoin commit `9332c7edda79`** "wallet: Write bestblock to watchonly and solvable wallets". Upstream needs it because, after bitcoin#28609, the watch-only and solvable wallets are created in an empty context and reloaded at the end of migration, and without a best block record they rescan on that reload. Dash has not backported bitcoin#28609. Here those wallets are created with `CreateWallet(context, ...)`, which already writes the chain tip as their best block on first run, and they are not reloaded during migration. The commit fixes nothing on Dash today and belongs with a bitcoin#28609 backport. - 78ba0e6: Dash's "already a descriptor wallet" check (`!GetLegacyScriptPubKeyMan()`) and Dash's backup filename logic are kept as they are. The upstream hunk that moves `reload_wallet` out of the success branch does not apply, because that helper came from bitcoin#28609. The success path keeps its existing direct reload. - c62a8d0: the watch-only copy still uses `AddToWallet()`, because bitcoin#28125 (`LoadToWallet()`/`CopyFrom()` and the shared watch-only `WalletBatch`) is not backported. Only the ownership logic changes, exactly as upstream. - 71cb28e: the upstream hunk that routes `test_addressbook` through `self.migrate_wallet()` is not carried over. That scenario was added by bitcoin#28038, which Dash has not backported, so there is no `test_addressbook` to change. Every other scenario that upstream routes through the helper does so here as well. - 4da76ca: applied to Dash's existing `test_other_watchonly`. The upstream context lines that check copied tx metadata come from bitcoin#28125 and are not part of this change. ### Why this is the correct minimal fix The wallet goes missing because the unload happens before the checks, while only the success path reloads it. Reloading on each early exit is the upstream fix, and every exit taken after the unload but before any database change is covered. Moving the checks ahead of the unload would mean running the backup and the passphrase check against the loaded instance, which is a larger restructuring than this fix needs. As upstream does, no reload is attempted when `MakeWalletDatabase()`/`CWallet::Create()` fail (the same open would fail again) or when `MigrateToSQLite()` fails (by then the original BDB file may already have been removed, and the failed-migration restore path does not run). The `.legacy.bak` file written before a wrong-passphrase exit is also kept, as upstream does. The watch-only change is the upstream one-condition fix, and the rest of bitcoin#28609/bitcoin#28125 is not needed for either bug. The `migratewallet` RPC first shipped in v24.0.0-rc.1 (#7275/#7277 are on v24.0.x), so this is a candidate for v24.0.x. ## How Has This Been Tested? macOS arm64, `--enable-debug --enable-werror`, built `dashd`/`dash-cli`/`dash-wallet` at each step. - Reproduction script above: on c14104b it fails with all three bugs present. With this branch it passes. - `test/functional/wallet_migration.py` with only the 71cb28e test change, against the unfixed c14104b binary: **fails** in `test_encrypted`. After the expected wrong-passphrase errors, `migrate_wallet()` → `getwalletinfo` raises `Requested wallet does not exist or is not loaded (-18)`. All earlier migrations pass the no-`Rescanning` check. - Same test after 78ba0e6: passes. - `wallet_migration.py` with the 4da76ca test change, against a binary that has 78ba0e6 but not c62a8d0: **fails** at `watchonly.gettransaction(watchonly_spendable_txid)` with `Invalid or non-wallet transaction id (-5)`. The new listtransactions counts before migration (6) and in the migrated wallet (2) already pass at that point. - Full branch: `test/functional/test_runner.py wallet_migration.py` passes. - `test/lint/lint-python.py` and `test/lint/lint-whitespace.py`: clean. `clang-format-diff` only suggests re-wrapping the passphrase error strings. They keep upstream's layout to stay 1:1, and `wallet.cpp` is not in `non-backported.txt`. ## Breaking Changes None. After a failed `migratewallet`, a wallet that was loaded beforehand is now loaded again, as it was before the call. ## Checklist: - [x] I have performed a self-review of my own code - [ ] I have made corresponding changes to the documentation - [ ] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) ACKs for top commit: knst: utACK 17b0589 Tree-SHA512: c245f96c7a9a5922ae1ffad9e5c0a3a6eab3494949239cc1d94fab5cdab090239cc13fa4ea3d0a4cd017ef36657aac834d6dd0f84d2bbfd482430ed639e8b1c0 (cherry picked from commit 233aefd) Conflict in test/functional/wallet_migration.py: test_conflict_txs() comes from bitcoin#28542 (#7762), which v24.0.x does not have, so it is left out along with its migratewallet() -> migrate_wallet() change.
fbe6526 Merge #7807: feat(consensus): hold EvoNode shares and multiple payouts behind a new evo_shares deployment (pasta) 57fd2d9 Merge #7776: fix: keep a pending ProUpServTx across a registrar update that keeps the operator key (pasta) ca69801 Merge #7803: fix: return non-zero amount of accounts after sethdseed (pasta) 57a8f07 Merge #7796: backport: bitcoin#26720 (remaining), partial bitcoin#29523: weight-check the knapsack exact-match selection (pasta) 4eb82f6 Merge #7786: fix(net): keep a dedicated onion listener when -bind is given and correct the bind release note (pasta) 70626a0 Merge #7793: backport: partial bitcoin#28868 (wallet: reload wallet if migration exited early, keep mixed watchonly txs) (pasta) 250a3cd Merge #7790: fix: guard cached governance object flags with the object lock (pasta) 700c195 Merge #7787: fix: only sign EHF signals inside the deployment's start/timeout window (pasta) a3b0f3c Merge #7785: fix: restore the txindex requirement when governance validation is enabled (pasta) 2d1735d Merge #7794: fix(wallet): floor the denomination gap threshold so rebalancing cannot oscillate (pasta) d9f6592 Merge #7783: fix: stop fish completion from re-running the typed dash-cli command (pasta) 89d44c8 Merge #7795: backport: bitcoin#34272, partial bitcoin#31650 (pass PSBT by const reference in PSBTInputSignedAndVerified) (pasta) a1d7411 Merge #7779: fix!: report legacy evonode Platform addresses when listdiff changes its address (pasta) 73f6ea2 Merge #7789: fix(qt): report malformed lang/font settings instead of aborting at startup (pasta) 40de493 Merge #7781: fix(rpc): use a consistent tip height in quorum dkgstatus (pasta) 7d1a90d Merge #7791: fix(net): request object votes from nPeersPerHashMax peers (pasta) 51b145c Merge #7778: test: use single-member in functional tests: feature_mnehf, feature_notifications, p2p_instantsend (pasta) Pull request description: ## Issue being fixed or feature implemented Backports for v24.0.0-rc.3. `v24.0.x` was cut at the v24.0.0-rc.2 tag (1e239d4), and develop has since moved to 24.1.0 (#7775), so rc.3 is built from this branch instead of being tagged on develop like rc.1 and rc.2. ## What was done? Cherry-picked (`git cherry-pick -m1 -x`) every develop merge since rc.2 that carries `backport-candidate-24.0.x`, plus #7778, which #7807's test needs. They are in develop merge order: - #7778 test: use single-member in functional tests: feature_mnehf, feature_notifications, p2p_instantsend (test-only. Needed because #7807's `feature_evo_shares_activation.py` depends on its `mine_quorum_single_member()` fix and fails without it.) - #7791 fix(net): request object votes from nPeersPerHashMax peers - #7781 fix(rpc): use a consistent tip height in quorum dkgstatus - #7789 fix(qt): report malformed lang/font settings instead of aborting at startup - #7779 fix!: report legacy evonode Platform addresses when listdiff changes its address - #7795 backport: bitcoin#34272, partial bitcoin#31650 (pass PSBT by const reference in PSBTInputSignedAndVerified) - #7783 fix: stop fish completion from re-running the typed dash-cli command - #7794 fix(wallet): floor the denomination gap threshold so rebalancing cannot oscillate - #7785 fix: restore the txindex requirement when governance validation is enabled - #7787 fix: only sign EHF signals inside the deployment's start/timeout window - #7790 fix: guard cached governance object flags with the object lock - #7793 backport: partial bitcoin#28868 (wallet: reload wallet if migration exited early, keep mixed watchonly txs) - #7786 fix(net): keep a dedicated onion listener when -bind is given and correct the bind release note. Includes partial bitcoin#36170 (5f40d56). Upstream's dedicated-onion-bind changes to `feature_torcontrol.py` and `p2p_private_broadcast.py` are not included, because those tests depend on upstream commits (5693833, e74d54e) that Dash does not have. - #7796 backport: bitcoin#26720 (remaining), partial bitcoin#29523: weight-check the knapsack exact-match selection - #7803 fix: return non-zero amount of accounts after sethdseed - #7776 fix: keep a pending ProUpServTx across a registrar update that keeps the operator key - #7807 feat(consensus): hold EvoNode shares and multiple payouts behind a new evo_shares deployment One pick needed conflict resolution, in a test only, described in its commit message: #7793, `test/functional/wallet_migration.py`. `test_conflict_txs()` comes from bitcoin#28542 (#7762), which is not on this branch, so it is left out. The C++ part of #7793 is identical to develop. Merged since rc.2 but intentionally not included: #7775 (24.1 version bump), #7260 and #7804 (new Qt feature and its CI fix), #7584 (refactor), routine upstream backports #7257, #7759, #7762, #7768, #7788, #7801, and test/CI-only changes #7777 and #7797. No version change is needed. `configure.ac` is already 24.0.0 with `CLIENT_VERSION_IS_RELEASE` false, so the build takes its version string from the `v24.0.0-rc.3` tag, as it did for rc.2. ## How Has This Been Tested? Built on macOS arm64 against depends with `--enable-debug --enable-werror` (dashd, dash-qt, tests). - `make check`: all unit test suites pass, including `test_dash-qt`. - Functional tests, all passing: `feature_evo_shares_activation.py`, `feature_masternode_shares.py`, `feature_mnehf.py`, `feature_governance.py --descriptors`, `feature_governance_objects.py`, `feature_governance_txindex_devnet.py`, `feature_proxy.py`, `feature_notifications.py`, `feature_llmq_connections.py`, `feature_llmq_chainlocks_automatic.py`, `feature_llmq_singlenode.py`, `p2p_instantsend.py`, `rpc_blockchain.py` (v1 and v2 transport), `rpc_netinfo.py`, `rpc_quorum.py`, `rpc_verifychainlock.py`, `wallet_hd.py` (legacy and descriptors), `wallet_migration.py`. - `feature_bind_extra.py` (from #7786) only runs on Linux and was skipped locally, so CI covers it. Every touched file was diffed against develop. The remaining differences come only from develop PRs that are not part of this backport. ## Breaking Changes Carried over from the picked PRs: - #7807: consensus change behind the new `evo_shares` deployment (bit 14). Mainnet and testnet are unaffected until it activates. Devnets with `v24` already active and an EvoNode carrying several owner payouts need to check or reset. New RPC `protx shared_register_prepare_evo`. - #7785: outside regtest, a node with `-txindex=0` and without `-disablegovernance` refuses to start again, as before v23.1.0. - #7786: carries a partial bitcoin#36170. A node started with `-bind` but no specific `-bind=<addr:port>=onion` now refuses to start while `-listenonion` is enabled. That is the default when listening, even without Tor configured. A wildcard onion bind is also refused. Such nodes need `-bind=127.0.0.1:<port>=onion` or `-listenonion=0`. Nodes without `-bind`, including those using only `-whitebind`, keep the default `127.0.0.1:9996` (`19996` testnet) onion target. See `doc/release-notes-7300.md`. - #7779: `protx listdiff` now includes `platform_p2p` and `platform_https` for a legacy-address evonode whose core P2P address changes. ## Checklist: - [x] I have performed a self-review of my own code - [ ] I have made corresponding changes to the documentation - [ ] I have assigned this pull request to a milestone 🤖 Generated with [Claude Code](https://claude.com/claude-code) Top commit has no ACKs. Tree-SHA512: 8cb01bdb0234f31b5dd2c105b69673de148922991a7523627395c0e0ccfa22b94be5ffbecc842398d443bc90fd9baf2eee6a9cf6fcdbbd1626eb00b93726a55a
Issue being fixed or feature implemented
migratewallet(and the GUI "Migrate wallet" action, which calls the sameMigrateLegacyToDescriptor()) has two bugs that upstream fixed in bitcoin#28868. That PR is listed as outstanding in the "next batch" section of #7277.MigrateLegacyToDescriptor()unloads it first and then runs its checks. Several of those checks return early without loading it again: the wallet is already a descriptor wallet, the backup could not be written, or the passphrase is missing or wrong. After any of these errors the wallet is gone fromlistwallets, and RPC calls to it fail withRequested wallet does not exist or is not loadeduntil the user runsloadwalletmanually. Mistyping the passphrase once on an encrypted legacy wallet is enough to trigger this.ApplyMigrationData()only offers a transaction to the new<name>_watchonlywallet when the migrated wallet does not claim it. If one output pays a spendable address and another pays an imported watch-only address, the transaction stays in the migrated wallet and is never copied to<name>_watchonly. That wallet is created at the chain tip, so it does not rescan, and its history and balance silently omit that transaction. This is the watch-only bug mentioned in feat: new RPC migratewallet to migrate legacy wallets to backport wallets #7275.Why this is a real problem
Code path at c14104b:
!IsMine(tx) && !IsFromMe(tx): wallet.cpp#L4759-L4781I reproduced all three cases against an unfixed
dashdbuilt from c14104b, using a functional-test-style script on regtest:Reproduction on c14104b (unfixed)
Steps:
createwallet desc(descriptor), thenmigratewallet desc, thenlistwalletscreatewallet enc descriptors=false, thenencryptwallet pass, thenmigratewallet enc badpass, thenlistwalletsandgetwalletinfoonenccreatewallet imports descriptors=false, thenimportaddress <addr from default wallet>. From the default wallet,sendone output to animportsaddress and one to the imported address, then mine a block. Thenmigratewalletandgettransaction <txid>onimports_watchonlyWith this branch, the same script reports
listwallets after: ['default_wallet', 'desc']and['default_wallet', 'desc', 'enc'], and all three results areTrue.The upstream tests backported here also fail on the unfixed code (see "How Has This Been Tested?").
Why it matters
Neither bug loses funds or keys. The first one is a usability trap in a v24 feature: a typo in the passphrase makes the wallet disappear from the node, the GUI and RPC clients, with no hint that a
loadwalletis needed. The second one leaves the post-migration watch-only wallet with incomplete history and balance, and nothing reports it. Users migrating real wallets have already hit it (see #7275).What was done?
Backport of bitcoin#28868, minus one commit:
78ba0e6748d2"wallet: Reload the wallet if migration exited early": remember whether the wallet was loaded (was_loaded), define thereload_wallethelper before the early exits, and reload the wallet on the descriptor-wallet, backup-failure and passphrase-failure exits. As upstream does, the passphrase unlock moves out of theLOCK(cs_wallet)scope so the reload does not happen while the wallet lock is held.71cb28ea8cb5"test: Make sure that migration test does not rescan on reloading": adds themigrate_wallet()helper, which reloads the wallet before migrating and checks that migration does not logRescanning. Because the helper callsgetwalletinfoon the wallet first, it fails if an earlier failedmigratewalletleft the wallet unloaded. The Dash-onlytest_wallet_name_with_slashescase also goes through the helper.c62a8d03a862"wallet: Keep txs that belong to both watchonly and migrated wallets": every transaction is offered to the watch-only wallet. It is removed from the migrated wallet only if the migrated wallet does not also own it.4da76ca24725"test: Test migration of tx with both spendable and watchonly"Dash adaptations and omitted hunks:
9332c7edda79"wallet: Write bestblock to watchonly and solvable wallets". Upstream needs it because, after wallet: Reload watchonly and solvables wallets after migration bitcoin/bitcoin#28609, the watch-only and solvable wallets are created in an empty context and reloaded at the end of migration, and without a best block record they rescan on that reload. Dash has not backported wallet: Reload watchonly and solvables wallets after migration bitcoin/bitcoin#28609. Here those wallets are created withCreateWallet(context, ...), which already writes the chain tip as their best block on first run, and they are not reloaded during migration. The commit fixes nothing on Dash today and belongs with a wallet: Reload watchonly and solvables wallets after migration bitcoin/bitcoin#28609 backport.!GetLegacyScriptPubKeyMan()) and Dash's backup filename logic are kept as they are. The upstream hunk that movesreload_walletout of the success branch does not apply, because that helper came from wallet: Reload watchonly and solvables wallets after migration bitcoin/bitcoin#28609. The success path keeps its existing direct reload.AddToWallet(), because wallet: bugfix, disallow migration of invalid scripts bitcoin/bitcoin#28125 (LoadToWallet()/CopyFrom()and the shared watch-onlyWalletBatch) is not backported. Only the ownership logic changes, exactly as upstream.test_addressbookthroughself.migrate_wallet()is not carried over. That scenario was added by wallet: address book migration bug fixes bitcoin/bitcoin#28038, which Dash has not backported, so there is notest_addressbookto change. Every other scenario that upstream routes through the helper does so here as well.test_other_watchonly. The upstream context lines that check copied tx metadata come from wallet: bugfix, disallow migration of invalid scripts bitcoin/bitcoin#28125 and are not part of this change.Why this is the correct minimal fix
The wallet goes missing because the unload happens before the checks, while only the success path reloads it. Reloading on each early exit is the upstream fix, and every exit taken after the unload but before any database change is covered. Moving the checks ahead of the unload would mean running the backup and the passphrase check against the loaded instance, which is a larger restructuring than this fix needs. As upstream does, no reload is attempted when
MakeWalletDatabase()/CWallet::Create()fail (the same open would fail again) or whenMigrateToSQLite()fails (by then the original BDB file may already have been removed, and the failed-migration restore path does not run). The.legacy.bakfile written before a wrong-passphrase exit is also kept, as upstream does. The watch-only change is the upstream one-condition fix, and the rest of bitcoin#28609/bitcoin#28125 is not needed for either bug.The
migratewalletRPC first shipped in v24.0.0-rc.1 (#7275/#7277 are on v24.0.x), so this is a candidate for v24.0.x.How Has This Been Tested?
macOS arm64,
--enable-debug --enable-werror, builtdashd/dash-cli/dash-walletat each step.test/functional/wallet_migration.pywith only the 71cb28e test change, against the unfixed c14104b binary: fails intest_encrypted. After the expected wrong-passphrase errors,migrate_wallet()→getwalletinforaisesRequested wallet does not exist or is not loaded (-18). All earlier migrations pass the no-Rescanningcheck.wallet_migration.pywith the 4da76ca test change, against a binary that has 78ba0e6 but not c62a8d0: fails atwatchonly.gettransaction(watchonly_spendable_txid)withInvalid or non-wallet transaction id (-5). The new listtransactions counts before migration (6) and in the migrated wallet (2) already pass at that point.test/functional/test_runner.py wallet_migration.pypasses.test/lint/lint-python.pyandtest/lint/lint-whitespace.py: clean.clang-format-diffonly suggests re-wrapping the passphrase error strings. They keep upstream's layout to stay 1:1, andwallet.cppis not innon-backported.txt.Breaking Changes
None. After a failed
migratewallet, a wallet that was loaded beforehand is now loaded again, as it was before the call.Checklist:
🤖 Generated with Claude Code