Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
bcac7a3
Merge bitcoin/bitcoin#19602: wallet: Migrate legacy wallets to descri…
achow101 Sep 1, 2022
33a3162
Merge bitcoin/bitcoin#26910: wallet: migrate wallet, exit early if no…
achow101 Feb 1, 2023
bbba780
Merge bitcoin/bitcoin#26761: wallet: fully migrate address book entri…
achow101 Jan 5, 2023
1e37185
Merge bitcoin/bitcoin#26594: wallet: Avoid a segfault in migratewalle…
fanquake Dec 1, 2022
1c10b34
Merge bitcoin/bitcoin#26595: wallet: be able to specify a wallet name…
fanquake Feb 22, 2023
e8b3ce7
Merge bitcoin/bitcoin#28257: test: check backup from `migratewallet` …
fanquake Aug 16, 2023
970d28c
fix: advance active pkh() descriptor past the legacy counters
knst Apr 6, 2026
6f4d676
fix: protection belt for more than 1 account for legacy wallets
knst Apr 6, 2026
c6214e0
test: check mnemonic is set correctly for migrated wallet
knst Apr 7, 2026
67b60e2
fix: loosen condition about P2PK / P2PKH scripts for non-hd imported …
knst Apr 7, 2026
ff7a5f4
Merge bitcoin/bitcoin#26638: test: prefer sqlite for wallet tests
achow101 Dec 20, 2022
038bb89
Merge bitcoin/bitcoin#28038: wallet: address book migration bug fixes
fanquake Jul 7, 2023
4db0fbe
Revert "Merge bitcoin/bitcoin#26638: test: prefer sqlite for wallet t…
knst Apr 9, 2026
ce31c8f
Merge bitcoin/bitcoin#28609: wallet: Reload watchonly and solvables w…
ryanofsky Oct 23, 2023
ed84342
fix: follow-up backporting bitcoin#17261 - missing break
knst Apr 9, 2026
f19d250
Merge bitcoin/bitcoin#26740: wallet: Migrate wallets that are not in …
achow101 Dec 21, 2022
8b96a2b
Merge bitcoin/bitcoin#28868: wallet: Fix migration of wallets with tx…
ryanofsky Feb 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions doc/managing-wallets.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,24 @@ $ dash-cli -rpcwallet="restored-wallet" getwalletinfo
```

The restored wallet can also be loaded in the GUI via `File` ->`Open wallet`.

## 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
Comment on lines +125 to +144
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 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;
fi

Repository: 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 -20

Repository: 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.

9 changes: 9 additions & 0 deletions doc/release-notes-19602.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Wallet
======

Migrating Legacy Wallets to Descriptor Wallets
---------------------------------------------

An experimental RPC `migratewallet` has been added to migrate Legacy (non-descriptor) wallets to
Descriptor wallets. More information about the migration process is available in the
[documentation](https://github.com/dashpay/dash/blob/master/doc/managing-wallets.md#migrating-legacy-wallets-to-descriptor-wallets).
4 changes: 3 additions & 1 deletion src/script/descriptor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -587,7 +587,7 @@ class DescriptorImpl : public Descriptor
return AddChecksum(ret);
}

bool ToPrivateString(const SigningProvider& arg, std::string& out) const override final
bool ToPrivateString(const SigningProvider& arg, std::string& out) const override
{
bool ret = ToStringHelper(&arg, out, StringType::PRIVATE);
out = AddChecksum(out);
Expand Down Expand Up @@ -677,6 +677,7 @@ class AddressDescriptor final : public DescriptorImpl
}
}
bool IsSingleType() const final { return true; }
bool ToPrivateString(const SigningProvider& arg, std::string& out) const final { return false; }
};

/** A parsed raw(H) descriptor. */
Expand All @@ -702,6 +703,7 @@ class RawDescriptor final : public DescriptorImpl
}
}
bool IsSingleType() const final { return true; }
bool ToPrivateString(const SigningProvider& arg, std::string& out) const final { return false; }
};

/** A parsed pk(P) descriptor. */
Expand Down
69 changes: 69 additions & 0 deletions src/wallet/rpc/wallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1103,6 +1103,74 @@ RPCHelpMan simulaterawtransaction()
};
}

static RPCHelpMan migratewallet()
{
return RPCHelpMan{"migratewallet",
"EXPERIMENTAL warning: This call may not work as expected and may be changed in future releases\n"
"\nMigrate the wallet to a descriptor wallet.\n"
"A new wallet backup will need to be made.\n"
"\nThe migration process will create a backup of the wallet before migrating. This backup\n"
"file will be named <wallet name>-<timestamp>.legacy.bak and can be found in the directory\n"
"for this wallet. In the event of an incorrect migration, the backup can be restored using restorewallet."
"\nEncrypted wallets must have the passphrase provided as an argument to this call.",
{
{"wallet_name", RPCArg::Type::STR, RPCArg::DefaultHint{"the wallet name from the RPC endpoint"}, "The name of the wallet to migrate. If provided both here and in the RPC endpoint, the two must be identical."},
{"passphrase", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "The wallet passphrase"},
},
RPCResult{
RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::STR, "wallet_name", "The name of the primary migrated wallet"},
{RPCResult::Type::STR, "watchonly_name", /*optional=*/true, "The name of the migrated wallet containing the watchonly scripts"},
{RPCResult::Type::STR, "solvables_name", /*optional=*/true, "The name of the migrated wallet containing solvable but not watched scripts"},
{RPCResult::Type::STR, "backup_path", "The location of the backup of the original wallet"},
}
},
RPCExamples{
HelpExampleCli("migratewallet", "")
+ HelpExampleRpc("migratewallet", "")
},
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{
std::string wallet_name;
if (GetWalletNameFromJSONRPCRequest(request, wallet_name)) {
if (!(request.params[0].isNull() || request.params[0].get_str() == wallet_name)) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "RPC endpoint wallet and wallet_name parameter specify different wallets");
}
} else {
if (request.params[0].isNull()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Either RPC endpoint wallet or wallet_name parameter must be provided");
}
wallet_name = request.params[0].get_str();
}

SecureString wallet_pass;
wallet_pass.reserve(100);
if (!request.params[1].isNull()) {
wallet_pass = std::string_view{request.params[1].get_str()};
}

WalletContext& context = EnsureWalletContext(request.context);
util::Result<MigrationResult> res = MigrateLegacyToDescriptor(wallet_name, wallet_pass, context);
if (!res) {
throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(res).original);
}

UniValue r{UniValue::VOBJ};
r.pushKV("wallet_name", res->wallet_name);
if (res->watchonly_wallet) {
r.pushKV("watchonly_name", res->watchonly_wallet->GetName());
}
if (res->solvables_wallet) {
r.pushKV("solvables_name", res->solvables_wallet->GetName());
}
r.pushKV("backup_path", fs::PathToString(res->backup_path));

return r;
},
};
}

// addresses
RPCHelpMan getaddressinfo();
RPCHelpMan getnewaddress();
Expand Down Expand Up @@ -1223,6 +1291,7 @@ Span<const CRPCCommand> GetWalletRPCCommands()
{"wallet", &listwallets},
{"wallet", &loadwallet},
{"wallet", &lockunspent},
{"wallet", &migratewallet},
{"wallet", &newkeypool},
{"wallet", &removeprunedfunds},
{"wallet", &rescanblockchain},
Expand Down
Loading
Loading