backport: batch 2 of migratewallet RPC#7277
Conversation
…lets 53e7ed0 doc: Release notes and other docs for migration (Andrew Chow) 9c44bfe Test migratewallet (Andrew Chow) 0b26e7c descriptors: addr() and raw() should return false for ToPrivateString (Andrew Chow) 31764c3 Add migratewallet RPC (Andrew Chow) 0bf7b38 Implement MigrateLegacyToDescriptor (Andrew Chow) e7b16f9 Implement MigrateToSQLite (Andrew Chow) 5b62f09 wallet: Refactor SetupDescSPKMs to take CExtKey (Andrew Chow) 22401f1 Implement LegacyScriptPubKeyMan::DeleteRecords (Andrew Chow) 35f428f Implement LegacyScriptPubKeyMan::MigrateToDescriptor (Andrew Chow) ea1ab39 scriptpubkeyman: Implement GetScriptPubKeys in Legacy (Andrew Chow) e664af2 Apply label to all scriptPubKeys of imported combo() (Andrew Chow) Pull request description: This PR adds a new `migratewallet` RPC which migrates a legacy wallet to a descriptor wallet. Migrated wallets will need a new backup. If a wallet has watchonly stuff in it, a new watchonly descriptor wallet will be created containing those watchonly things. The related transactions, labels, and descriptors for those watchonly things will be removed from the original wallet. Migrated wallets will not have any of the legacy things be available for fetching from `getnewaddress` or `getrawchangeaddress`. Wallets that have private keys enabled will have newly generated descriptors. Wallets with private keys disabled will not have any active `ScriptPubKeyMan`s. For the basic HD wallet case of just generated keys, in addition to the standard descriptor wallet descriptors using the master key derived from the pre-existing hd seed, the migration will also create 3 descriptors for each HD chain in: a ranged combo external, a ranged combo internal, and a single key combo for the seed (the seed is a valid key that we can receive coins at!). The migrated wallet will then have newly generated descriptors as the active `ScriptPubKeyMan`s. This is equivalent to creating a new descriptor wallet and importing the 3 descriptors for each HD chain. For wallets containing non-HD keys, each key will have its own combo descriptor. There are also tests. ACKs for top commit: Sjors: tACK 53e7ed0 w0xlt: reACK bitcoin@53e7ed0 Tree-SHA512: c0c003694ca2e17064922d08e8464278d314e970efb7df874b4fe04ec5d124c7206409ca701c65c099d17779ab2136ae63f1da2a9dba39b45f6d62cf93b5c60a
…data exist 6d31900 wallet: migrate wallet, exit early if no legacy data exist (furszy) Pull request description: The process first creates a backup file then return an error, without removing the recently created file, when notices that the db is already running sqlite. ACKs for top commit: john-moffett: ACK 6d31900 achow101: ACK 6d31900 ishaanam: crACK 6d31900 Tree-SHA512: 9fb52e80de96e129487ab91bef13647bc4570a782003b1e37940e2a00ca26283fd24ad39bdb63a984ae0a56140b518fd0d74aa2fc59ab04405b2c179b7d3c54a
…atchonly/solvable wallets 730e14a test: wallet: check that labels are migrated to watchonly wallet (Sebastian Falbesoner) d5f4ae7 wallet: fully migrate address book entries for watchonly/solvable wallets (Sebastian Falbesoner) Pull request description: Currently `migratewallet` migrates the address book (i.e. labels and purposes) for watchonly and solvable wallets only in RAM, but doesn't persist them on disk. Fix this by adding another loop for both of the special wallet types after which writes the corresponding NAME and PURPOSE entries to the database in a single batch. Also adds a corresponding test that checks if labels were migrated correctly for a watchonly wallet. ACKs for top commit: achow101: ACK 730e14a furszy: code ACK 730e14a, left a non-blocking nit. aureleoules: ACK 730e14a Tree-SHA512: 159487e11e858924ef762e0190ccaea185bdff239e3d2280c8d63c4ac2649ec71714dc4d53dec644f03488f91c3b4bbbbf3434dad23bc0fcecb6657f353ea766
…e cleanup 5e65a21 wallet: Explicitly say migratewallet on encrypted wallets is unsupported (Andrew Chow) 88afc73 tests: Test for migrating encrypted wallets (Andrew Chow) 86ef7b3 wallet: Avoid null pointer deref when cleaning up migratewallet (Andrew Chow) Pull request description: When `migratewallet` fails, we do an automatic cleanup in order to reset everything so that the user does not experience any interruptions. However, this apparently has a segfault in it, caused by the the pointers to the watchonly and solvables wallets being nullptr. If those wallets are not created (either not needed, or failed early on), we will accidentally attempt to dereference these nullptrs, which causes a segfault. This failure can be easily reached by trying to migrate an encrypted wallet. Currently, we can't migrate encrypted wallets because of how we unload wallets before migrating, and therefore forget the encryption key if the wallet was unlocked. So any encrypted wallets will fail, entering the cleanup, and because watchonly and solvables wallets don't exist yet, the segfault is reached. This PR fixes this by not putting those nullptrs in a place that we will end up dereferencing them later. It also adds a test that uses the encrypted wallet issue. ACKs for top commit: S3RK: reACK 5e65a21 stickies-v: ACK [5e65a21](bitcoin@5e65a21) furszy: diff ACK 5e65a21 Tree-SHA512: f75643797220d4232ad3ab8cb4b46d0f3667f00486e910ca748c9b6d174d446968f1ec4dd7f907da1be9566088849da7edcd8cd8f12de671c3241b513deb8e80
…sphrase to migratewallet 9486509 wallet, rpc: Update migratewallet help text for encrypted wallets (Andrew Chow) aaf02b5 tests: Tests for migrating wallets by name, and providing passphrase (Andrew Chow) 7fd125b wallet: Be able to unlock the wallet for migration (Andrew Chow) 6bdbc5f rpc: Allow users to specify wallet name for migratewallet (Andrew Chow) dbfa345 wallet: Allow MigrateLegacyToDescriptor to take a wallet name (Andrew Chow) Pull request description: `migratewallet` currently operates on wallets that are already loaded, however this is not necessarily required, and in the future, not possible once the legacy wallet is removed. So we need to also be able to give the wallet name to migrate. Additionally, the passphrase is required when migrating a wallet. Since a wallet may not be loaded when we migrate, and as we currently unload wallets when migrating, we need the passphrase to be given to `migratewallet` in order to migrate encrypted wallets. Fixes bitcoin#27048 ACKs for top commit: john-moffett: reACK 9486509 pinheadmz: ACK 9486509 furszy: ACK 9486509 Tree-SHA512: 35e2ba69a148e129a41e20d7fb99c4cab7947b1b7e7c362f4fd06ff8ac6e79e476e07207e063ba5b80e1a33e2343f4b4f1d72d7930ce80c34571c130d2f5cff4
…uccessfully restored 769f5b1 test: check backup from `migratewallet` can be successfully restored (brunoerg) Pull request description: `migratewallet` migrates the wallet to a descriptor one. During the process, it generates a backup file of the wallet in case of an incorrect migration. This PR adds test to check if the backup file can be successfully restored. ACKs for top commit: achow101: ACK 769f5b1 MarcoFalke: lgtm ACK 769f5b1 Tree-SHA512: 94c50b34fbd47c4d3cc34b94e9e7903bc233608c7f50f45c161669996fd5f5b7d8f9a4e6a3437b9151d66a76af833f3f1ca28e44ecb63b5a8f391f6d6be0e39f
17554ef test: prefer sqlite for wallet tests (S3RK) 8e0faba test: make wallet_migration.py pass with both wallet flags (S3RK) Pull request description: Fixes bitcoin#26511 ACKs for top commit: MarcoFalke: review ACK 17554ef achow101: ACK 17554ef Tree-SHA512: 97cae275998f07032feffe1b533d4747b8ff03c3c1fb830af69ee38cadb75fd58532956f66f79c0d275b00620ce53b0b5240f885e4f29b8bd4d0b6e6cbc683fa
7ecc29a test: wallet, add coverage for addressbook migration (furszy) a277f83 wallet: migration bugfix, persist empty labels (furszy) 1b64f64 wallet: migration bugfix, clone 'send' record label to all wallets (furszy) Pull request description: Addressing two specific bugs encountered during the wallet migration process, related to the address book, and improves the test coverage for it. Bug 1: Non-Cloning of External 'Send' Records The external 'send' records were not being correctly cloned to all wallets. Bug 2: Persistence of Empty Labels As address book entries without associated db label records can be treated as change (the `label` field inside the `CAddressBookData` class is optional, `nullopt` labels make `CAddressBookData ::IsChange()` return true), we must persist empty labels during the migration process. The user might have called `setlabel` with an "" string for an external address and that must be retained during migration. ACKs for top commit: achow101: ACK 7ecc29a Tree-SHA512: b8a8483a4178a37c49af11eb7ba8a82ca95e54a6cd799e155e33f9fbe7f37b259e28372c77d6944d46b6765f9eaca6b8ca8d1cdd9d223120a3653e4e41d0b6b7
This reverts commit ff7a5f4.
…fter migration 4814e40 test: Check tx metadata is migrated to watchonly (Andrew Chow) d616d30 wallet: Reload watchonly and solvables wallets after migration (Andrew Chow) 118f2d7 wallet: Copy all tx metadata to watchonly wallet (Andrew Chow) 9af87cf test: Check that a failed wallet migration is cleaned up (Andrew Chow) Pull request description: Some incomplete/incorrect state as a result of migration can be mitigated/cleaned up by simply restarting the migrated wallets. We already do this for a wallet when it is migrated, but we do not for the new watchonly and solvables wallets that may be created. This PR introduces this behavior, in addition to creating those wallets initially without an attached chain. While implementing this, I noticed that not all `CWalletTx` metadata was being copied over to the watchonly wallet and so some data, such as time received, was being lost. This PR fixes this as a side effect of not having a chain attached to the watchonly wallet. A test has also been added. ACKs for top commit: ishaanam: light code review ACK 4814e40 ryanofsky: Code review ACK 4814e40. Just implemented the suggested orderpos, copyfrom, and path set comments since last review furszy: ACK 4814e40 Tree-SHA512: 0b992430df9f452cb252c2212df8e876613f43564fcd1dc00c6c31fa497adb84dfff6b5ef597590f9b288c5f64cb455f108fcc9b6c9d1fe9eb2c39e7f2c12a89
… dir a1e6538 test: Add test for migrating default wallet and plain file wallet (Andrew Chow) bdbe3fd wallet: Generated migrated wallet's path from walletdir and name (Andrew Chow) Pull request description: This PR fixes an assertion error that is hit during the setup of the new database during migration of a wallet that was not contained in a wallet dir. Also added a test for this case as well as one for migrating the default wallet. ACKs for top commit: ryanofsky: Code review ACK a1e6538 furszy: ACK a1e6538 Tree-SHA512: 96b218c0de8567d8650ec96e1bf58b0f8ca4c4726f5efc6362453979b56b9d569baea0bb09befb3a5aed8d16d29bf75ed5cd8ffc432bbd4cbcad3ac5574bc479
…ave 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
|
Review GateCommit:
|
WalkthroughThis PR introduces a Sequence DiagramsequenceDiagram
participant RPC as RPC Layer<br/>(migratewallet)
participant Wallet as CWallet
participant LegacySPKM as LegacyScriptPubKeyMan
participant DescSPKM as DescriptorScriptPubKeyMan
participant DB as WalletDatabase
participant Context as WalletContext
RPC->>Wallet: MigrateLegacyToDescriptor(wallet_name, passphrase)
activate Wallet
Wallet->>Wallet: Backup legacy wallet
Wallet->>LegacySPKM: GetScriptPubKeys()
activate LegacySPKM
LegacySPKM-->>Wallet: unordered_set<CScript>
deactivate LegacySPKM
Wallet->>LegacySPKM: MigrateToDescriptor()
activate LegacySPKM
LegacySPKM->>LegacySPKM: Enumerate keys, HD chains,<br/>multisig-in-P2SH
LegacySPKM->>LegacySPKM: Create combo() descriptors<br/>for loose/HD keys
LegacySPKM->>LegacySPKM: InferDescriptor for<br/>remaining scripts
LegacySPKM-->>Wallet: MigrationData
deactivate LegacySPKM
Wallet->>DB: MigrateToSQLite()
activate DB
DB->>DB: Read all BDB records
DB->>DB: Create SQLite database
DB->>DB: Write records in transaction
DB-->>Wallet: success
deactivate DB
Wallet->>Wallet: ApplyMigrationData(data)
activate Wallet
Wallet->>DescSPKM: Add descriptors & keys
activate DescSPKM
DescSPKM->>DescSPKM: Setup descriptor handlers
DescSPKM-->>Wallet: ready
deactivate DescSPKM
Wallet->>LegacySPKM: DeleteRecords()
Wallet->>DB: EraseRecords(LEGACY_TYPES)
deactivate Wallet
Wallet->>Context: Optionally create<br/>watchonly/solvables wallets
Wallet-->>RPC: MigrationResult{wallet_name,<br/>watchonly_wallet,<br/>solvables_wallet,<br/>backup_path}
deactivate Wallet
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (2)
src/wallet/wallet.h (1)
1072-1073: Pass the mnemonic passphrase byconst SecureString&.Both overloads currently copy
SecureString, which needlessly duplicates secret material in memory. Changeconst SecureString mnemonic_passphrasetoconst SecureString& mnemonic_passphrasefor both function declarations.♻️ Proposed fix
- void SetupDescriptorScriptPubKeyMans(const CExtKey& master_key, const SecureString& mnemonic, const SecureString mnemonic_passphrase) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); - void SetupDescriptorScriptPubKeyMans(const SecureString& mnemonic, const SecureString mnemonic_passphrase) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + void SetupDescriptorScriptPubKeyMans(const CExtKey& master_key, const SecureString& mnemonic, const SecureString& mnemonic_passphrase) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + void SetupDescriptorScriptPubKeyMans(const SecureString& mnemonic, const SecureString& mnemonic_passphrase) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wallet/wallet.h` around lines 1072 - 1073, Change the two declarations of SetupDescriptorScriptPubKeyMans to take the mnemonic passphrase by reference instead of by value: replace the parameter type `const SecureString mnemonic_passphrase` with `const SecureString& mnemonic_passphrase` in both overloads (the functions declared as `void SetupDescriptorScriptPubKeyMans(const CExtKey& master_key, const SecureString& mnemonic, const SecureString mnemonic_passphrase) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);` and `void SetupDescriptorScriptPubKeyMans(const SecureString& mnemonic, const SecureString mnemonic_passphrase) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);`) so they become `const SecureString&` for the passphrase.src/wallet/scriptpubkeyman.cpp (1)
1930-1940: Consider adding a nullptr check afterParsefor defensive programming.While the descriptor string constructed from a valid key should always parse successfully, the HD chain path (line 2018-2021) includes explicit nullptr checking. Adding consistency here would guard against unexpected failures.
🔧 Optional: Add nullptr check for consistency
// Construct the combo descriptor std::string desc_str = "combo(" + origin_str + HexStr(key.GetPubKey()) + ")"; FlatSigningProvider keys; std::string error; std::unique_ptr<Descriptor> desc = Parse(desc_str, keys, error, false); + if (!desc) { + throw std::runtime_error(std::string(__func__) + ": failed to parse combo descriptor: " + error); + } WalletDescriptor w_desc(std::move(desc), creation_time, 0, 0, 0);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wallet/scriptpubkeyman.cpp` around lines 1930 - 1940, Add a nullptr check after calling Parse on desc_str to mirror the defensive checks used elsewhere: verify that the returned std::unique_ptr<Descriptor> desc is non-null before constructing WalletDescriptor and creating DescriptorScriptPubKeyMan; if desc is null, surface the parse error (from the existing error string) and handle it consistently (e.g., log and return or throw) instead of proceeding to call WalletDescriptor, DescriptorScriptPubKeyMan, AddDescriptorKey, and TopUp.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@doc/managing-wallets.md`:
- Around line 125-144: Confirm whether documentation updates were intended for
this backport: if the backport explicitly requested doc changes, add a note to
the PR description referencing that approval and keep the edits in the doc
(references: "Migrating Legacy Wallets to Descriptor Wallets", migratewallet,
restorewallet); otherwise revert the changes to doc/managing-wallets.md (lines
introducing migratewallet documentation) and move them to a separate
documentation-only follow-up PR so this backport contains only code changes.
In `@src/wallet/wallet.cpp`:
- Around line 4629-4630: UpdateWalletSetting is being called before helper
wallets (e.g. *_watchonly and *_solvables) are fully committed, causing
load_on_startup entries to remain if a later step fails and the wallet
directories are rolled back; move the UpdateWalletSetting calls (the ones that
set load_on_startup=true) so they execute only after the wallet commit/migration
has completed successfully (i.e., after all filesystem operations and rollback
points are passed) and do the same change for the other instance noted (the
second call around the other occurrence), ensuring settings are updated only on
successful commit of the new wallet directories.
- Around line 4794-4796: The backup copy operation in the wallet
migration/rollback code can attempt to copy a file onto itself for direct-file
wallets (where backup_path and temp_backup_location resolve to the same path),
causing copy_file to fail; update the logic around the uses of
temp_backup_location and backup_path (the copy_file calls around the code that
uses GetWalletDir(), backup_filename, and the rollback path) to guard against
this by checking if fs::equivalent(backup_path, temp_backup_location) or
backup_path == temp_backup_location and skipping the copy (or generating a
unique temp name) when they refer to the same file; apply the same guard to the
later copy_file occurrence(s) referenced near the other block (around the code
at the second copy_file use).
- Around line 4040-4041: The exception currently echoes the full seed phrase
when CMnemonic::Check(mnemonic) fails; remove the sensitive mnemonic from the
error text and throw a generic message instead (e.g., change the throw in the
CMnemonic::Check failure branch to use only __func__ and a non-sensitive string
like ": invalid mnemonic" without including the mnemonic variable or any
backticks). Ensure the change is applied where the throw is raised (the block
that calls CMnemonic::Check and constructs std::runtime_error).
In `@src/wallet/walletdb.cpp`:
- Around line 1182-1184: In EraseRecords(), the return value of
m_batch->Erase(key_data) is ignored causing EraseRecords() to always succeed;
update the loop that checks types.count(type) > 0 to capture the boolean result
of m_batch->Erase(key_data) and if it returns false, immediately return false
(or set an overall failure flag and return false at the end) so EraseRecords()
propagates delete failures back to the caller; ensure you reference the
EraseRecords() method and the m_batch->Erase(...) call when making this change.
In `@test/functional/wallet_migration.py`:
- Around line 141-143: The test currently mutates the dict returned by
basic0.getaddressinfo(new_addr) instead of checking it; replace the assignment
on basic0.getaddressinfo(new_addr)["hdkeypath"] = "m/44'/1'/0'/0/1" with a real
assertion: call info = basic0.getaddressinfo(new_addr) and assert
info["hdkeypath"] == "m/44'/1'/0'/0/1" (while keeping the existing assert not
new_addr == addr), so the test actually verifies the derived path returned for
new_addr.
---
Nitpick comments:
In `@src/wallet/scriptpubkeyman.cpp`:
- Around line 1930-1940: Add a nullptr check after calling Parse on desc_str to
mirror the defensive checks used elsewhere: verify that the returned
std::unique_ptr<Descriptor> desc is non-null before constructing
WalletDescriptor and creating DescriptorScriptPubKeyMan; if desc is null,
surface the parse error (from the existing error string) and handle it
consistently (e.g., log and return or throw) instead of proceeding to call
WalletDescriptor, DescriptorScriptPubKeyMan, AddDescriptorKey, and TopUp.
In `@src/wallet/wallet.h`:
- Around line 1072-1073: Change the two declarations of
SetupDescriptorScriptPubKeyMans to take the mnemonic passphrase by reference
instead of by value: replace the parameter type `const SecureString
mnemonic_passphrase` with `const SecureString& mnemonic_passphrase` in both
overloads (the functions declared as `void SetupDescriptorScriptPubKeyMans(const
CExtKey& master_key, const SecureString& mnemonic, const SecureString
mnemonic_passphrase) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);` and `void
SetupDescriptorScriptPubKeyMans(const SecureString& mnemonic, const SecureString
mnemonic_passphrase) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);`) so they become
`const SecureString&` for the passphrase.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: be43b772-e7a7-47b1-af64-43f1656f21c8
📒 Files selected for processing (18)
doc/managing-wallets.mddoc/release-notes-19602.mdsrc/script/descriptor.cppsrc/wallet/rpc/wallet.cppsrc/wallet/scriptpubkeyman.cppsrc/wallet/scriptpubkeyman.hsrc/wallet/test/ismine_tests.cppsrc/wallet/transaction.cppsrc/wallet/transaction.hsrc/wallet/wallet.cppsrc/wallet/wallet.hsrc/wallet/walletdb.cppsrc/wallet/walletdb.hsrc/wallet/walletutil.htest/functional/test_framework/test_node.pytest/functional/test_runner.pytest/functional/tool_wallet.pytest/functional/wallet_migration.py
| ## Migrating Legacy Wallets to Descriptor Wallets | ||
|
|
||
| Legacy wallets (traditional non-descriptor wallets) can be migrated to become Descriptor wallets | ||
| through the use of the `migratewallet` RPC. Migrated wallets will have all of their addresses and private keys added to | ||
| a newly created Descriptor wallet that has the same name as the original wallet. Because Descriptor | ||
| wallets do not support having private keys and watch-only scripts, there may be up to two | ||
| additional wallets created after migration. In addition to a descriptor wallet of the same name, | ||
| there may also be a wallet named `<name>_watchonly` and `<name>_solvables`. `<name>_watchonly` | ||
| contains all of the watchonly scripts. `<name>_solvables` contains any scripts which the wallet | ||
| knows but is not watching the corresponding P2SH scripts. | ||
|
|
||
| Given that there is an extremely large number of possible configurations for the scripts that | ||
| Legacy wallets can know about, be watching for, and be able to sign for, `migratewallet` only | ||
| makes a best effort attempt to capture all of these things into Descriptor wallets. There may be | ||
| unforeseen configurations which result in some scripts being excluded. If a migration fails | ||
| unexpectedly or otherwise misses any scripts, please create an issue on GitHub. A backup of the | ||
| original wallet can be found in the wallet directory with the name `<name>-<timestamp>.legacy.bak`. | ||
|
|
||
| The backup can be restored using the `restorewallet` command as discussed in the | ||
| [Restoring the Wallet From a Backup](#16-restoring-the-wallet-from-a-backup) section |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Check if the file exists and examine the lines in question
if [ -f "doc/managing-wallets.md" ]; then
echo "File exists. Checking lines 125-144:";
sed -n '125,144p' doc/managing-wallets.md
else
echo "File not found. Listing doc directory:";
ls -la doc/ 2>/dev/null | head -20;
fiRepository: dashpay/dash
Length of output: 1637
🏁 Script executed:
# Check git history for recent changes to this file to understand context
git log --oneline -10 -- doc/managing-wallets.md 2>/dev/null || echo "No git history available"Repository: dashpay/dash
Length of output: 183
🏁 Script executed:
# Search for any references to 'migratewallet' to understand scope of changes
rg "migratewallet" doc/ 2>/dev/null | head -20Repository: dashpay/dash
Length of output: 441
Confirm whether documentation changes were explicitly requested for this backport.
Per the coding guidelines, changes to doc/** should be avoided unless specifically prompted. This PR modifies doc/managing-wallets.md (lines 125-144) with documentation for the migratewallet feature. If this backport was requested to include documentation updates, that should be noted; otherwise, these changes should be split into a separate documentation follow-up.
🧰 Tools
🪛 LanguageTool
[style] ~133-~133: Consider removing “of” to be more concise
Context: ...solvables. _watchonlycontains all of the watchonly scripts._solvables` c...
(ALL_OF_THE)
[style] ~136-~136: As an alternative to the over-used intensifier ‘extremely’, consider replacing this phrase.
Context: ...ding P2SH scripts. Given that there is an extremely large number of possible configurations for t...
(EN_WEAK_ADJECTIVE)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@doc/managing-wallets.md` around lines 125 - 144, Confirm whether
documentation updates were intended for this backport: if the backport
explicitly requested doc changes, add a note to the PR description referencing
that approval and keep the edits in the doc (references: "Migrating Legacy
Wallets to Descriptor Wallets", migratewallet, restorewallet); otherwise revert
the changes to doc/managing-wallets.md (lines introducing migratewallet
documentation) and move them to a separate documentation-only follow-up PR so
this backport contains only code changes.
| if (!CMnemonic::Check(mnemonic)) { | ||
| throw std::runtime_error(std::string(__func__) + ": invalid mnemonic: `" + std::string(mnemonic) + "`"); |
There was a problem hiding this comment.
Don't echo the mnemonic in the exception.
If validation fails here, the full seed phrase is copied into the error text and can leak into logs or RPC output. Keep the message generic.
Suggested fix
const SecureString mnemonic = mnemonic_arg.empty() ? CMnemonic::Generate(m_args.GetIntArg("-mnemonicbits", CHDChain::DEFAULT_MNEMONIC_BITS)) : mnemonic_arg;
if (!CMnemonic::Check(mnemonic)) {
- throw std::runtime_error(std::string(__func__) + ": invalid mnemonic: `" + std::string(mnemonic) + "`");
+ throw std::runtime_error(std::string(__func__) + ": invalid mnemonic");
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (!CMnemonic::Check(mnemonic)) { | |
| throw std::runtime_error(std::string(__func__) + ": invalid mnemonic: `" + std::string(mnemonic) + "`"); | |
| if (!CMnemonic::Check(mnemonic)) { | |
| throw std::runtime_error(std::string(__func__) + ": invalid mnemonic"); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/wallet/wallet.cpp` around lines 4040 - 4041, The exception currently
echoes the full seed phrase when CMnemonic::Check(mnemonic) fails; remove the
sensitive mnemonic from the error text and throw a generic message instead
(e.g., change the throw in the CMnemonic::Check failure branch to use only
__func__ and a non-sensitive string like ": invalid mnemonic" without including
the mnemonic variable or any backticks). Ensure the change is applied where the
throw is raised (the block that calls CMnemonic::Check and constructs
std::runtime_error).
| // Add the wallet to settings | ||
| UpdateWalletSetting(*context.chain, wallet_name, /*load_on_startup=*/true, warnings); |
There was a problem hiding this comment.
Delay startup-setting writes until the helper wallets are fully committed.
*_watchonly and *_solvables are added to load_on_startup before migration finishes. If a later step fails, rollback deletes those wallet dirs but never removes the new settings, so the next startup will try to autoload wallets that no longer exist.
Also applies to: 4667-4668
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/wallet/wallet.cpp` around lines 4629 - 4630, UpdateWalletSetting is being
called before helper wallets (e.g. *_watchonly and *_solvables) are fully
committed, causing load_on_startup entries to remain if a later step fails and
the wallet directories are rolled back; move the UpdateWalletSetting calls (the
ones that set load_on_startup=true) so they execute only after the wallet
commit/migration has completed successfully (i.e., after all filesystem
operations and rollback points are passed) and do the same change for the other
instance noted (the second call around the other occurrence), ensuring settings
are updated only on successful commit of the new wallet directories.
| // If the wallet is still loaded, unload it so that nothing else tries to use it while we're changing it | ||
| bool was_loaded = false; | ||
| if (auto wallet = GetWallet(context, wallet_name)) { | ||
| if (!RemoveWallet(context, wallet, /*load_on_start=*/std::nullopt, warnings)) { | ||
| return util::Error{_("Unable to unload the wallet before migrating")}; | ||
| } | ||
| UnloadWallet(std::move(wallet)); | ||
| was_loaded = true; |
There was a problem hiding this comment.
Several failure paths leave an already-loaded wallet unexpectedly unloaded.
After the original wallet is removed from context, failures in MakeWalletDatabase, CWallet::Create, or MigrateToSQLite return immediately without restoring the previous loaded state. A failed migratewallet should not silently unload the caller's wallet.
Also applies to: 4702-4710, 4761-4764
| fs::path temp_backup_location = fsbridge::AbsPathJoin(GetWalletDir(), backup_filename); | ||
| fs::copy_file(backup_path, temp_backup_location, fs::copy_options::none); | ||
|
|
There was a problem hiding this comment.
Guard the backup copy for direct-file wallets.
For plain-file wallets under wallets/, backup_path and temp_backup_location resolve to the same file. Both copy_file(..., copy_options::none) calls then fail during rollback, which breaks recovery հենց for the direct-file migration path this PR supports.
Suggested fix
// Migration failed, cleanup
// Copy the backup to the actual wallet dir
fs::path temp_backup_location = fsbridge::AbsPathJoin(GetWalletDir(), backup_filename);
- fs::copy_file(backup_path, temp_backup_location, fs::copy_options::none);
+ if (temp_backup_location != backup_path) {
+ fs::copy_file(backup_path, temp_backup_location, fs::copy_options::none);
+ }
@@
// Move the backup to the wallet dir
- fs::copy_file(temp_backup_location, backup_path, fs::copy_options::none);
- fs::remove(temp_backup_location);
+ if (temp_backup_location != backup_path) {
+ fs::copy_file(temp_backup_location, backup_path, fs::copy_options::overwrite_existing);
+ fs::remove(temp_backup_location);
+ }Also applies to: 4837-4839
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/wallet/wallet.cpp` around lines 4794 - 4796, The backup copy operation in
the wallet migration/rollback code can attempt to copy a file onto itself for
direct-file wallets (where backup_path and temp_backup_location resolve to the
same path), causing copy_file to fail; update the logic around the uses of
temp_backup_location and backup_path (the copy_file calls around the code that
uses GetWalletDir(), backup_filename, and the rollback path) to guard against
this by checking if fs::equivalent(backup_path, temp_backup_location) or
backup_path == temp_backup_location and skipping the copy (or generating a
unique temp name) when they refer to the same file; apply the same guard to the
later copy_file occurrence(s) referenced near the other block (around the code
at the second copy_file use).
| if (types.count(type) > 0) { | ||
| m_batch->Erase(key_data); | ||
| } |
There was a problem hiding this comment.
Propagate delete failures from EraseRecords().
Line 1183 ignores the return value from m_batch->Erase(key_data). If one delete fails, this method still returns true, so migration can report success while legacy records remain in the wallet DB.
🩹 Proposed fix
- if (types.count(type) > 0) {
- m_batch->Erase(key_data);
- }
+ if (types.count(type) > 0) {
+ if (!m_batch->Erase(key_data)) {
+ m_batch->CloseCursor();
+ return false;
+ }
+ m_database.IncrementUpdateCounter();
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (types.count(type) > 0) { | |
| m_batch->Erase(key_data); | |
| } | |
| if (types.count(type) > 0) { | |
| if (!m_batch->Erase(key_data)) { | |
| m_batch->CloseCursor(); | |
| return false; | |
| } | |
| m_database.IncrementUpdateCounter(); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/wallet/walletdb.cpp` around lines 1182 - 1184, In EraseRecords(), the
return value of m_batch->Erase(key_data) is ignored causing EraseRecords() to
always succeed; update the loop that checks types.count(type) > 0 to capture the
boolean result of m_batch->Erase(key_data) and if it returns false, immediately
return false (or set an overall failure flag and return false at the end) so
EraseRecords() propagates delete failures back to the caller; ensure you
reference the EraseRecords() method and the m_batch->Erase(...) call when making
this change.
| new_addr = basic0.getnewaddress() | ||
| assert not new_addr == addr | ||
| basic0.getaddressinfo(new_addr)["hdkeypath"] = "m/44'/1'/0'/0/1" |
There was a problem hiding this comment.
This check never verifies the new derivation path.
Line 143 mutates the dict returned by getaddressinfo() instead of asserting on it, so this regression test passes even if migration hands back the wrong next index.
Suggested fix
self.log.info("Test for re-using old addresses after migration")
new_addr = basic0.getnewaddress()
assert not new_addr == addr
- basic0.getaddressinfo(new_addr)["hdkeypath"] = "m/44'/1'/0'/0/1"
+ assert_equal(basic0.getaddressinfo(new_addr)["hdkeypath"], "m/44'/1'/0'/0/1")📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| new_addr = basic0.getnewaddress() | |
| assert not new_addr == addr | |
| basic0.getaddressinfo(new_addr)["hdkeypath"] = "m/44'/1'/0'/0/1" | |
| new_addr = basic0.getnewaddress() | |
| assert not new_addr == addr | |
| assert_equal(basic0.getaddressinfo(new_addr)["hdkeypath"], "m/44'/1'/0'/0/1") |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@test/functional/wallet_migration.py` around lines 141 - 143, The test
currently mutates the dict returned by basic0.getaddressinfo(new_addr) instead
of checking it; replace the assignment on
basic0.getaddressinfo(new_addr)["hdkeypath"] = "m/44'/1'/0'/0/1" with a real
assertion: call info = basic0.getaddressinfo(new_addr) and assert
info["hdkeypath"] == "m/44'/1'/0'/0/1" (while keeping the existing assert not
new_addr == addr), so the test actually verifies the derived path returned for
new_addr.
Depends on #7275
Issue being fixed or feature implemented
Further improvements of
migratewalletRPC to migrate legacy wallets to descriptor wallets.What was done?
Scope to be defined
How Has This Been Tested?
Please describe in detail how you tested your changes.
Include details of your testing environment, and the tests you ran
to see how your change affects other areas of the code, etc.
Breaking Changes
Please describe any breaking changes your code introduces
Checklist:
Go over all the following points, and put an
xin all the boxes that apply.