Skip to content

Commit 7921026

Browse files
committed
Merge bitcoin/bitcoin#19602: wallet: Migrate legacy wallets to descriptor wallets
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/bitcoin@53e7ed0 Tree-SHA512: c0c003694ca2e17064922d08e8464278d314e970efb7df874b4fe04ec5d124c7206409ca701c65c099d17779ab2136ae63f1da2a9dba39b45f6d62cf93b5c60a
2 parents 36e1b52 + 53e7ed0 commit 7921026

15 files changed

+1458
-34
lines changed

doc/managing-wallets.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,29 @@ After that, `getwalletinfo` can be used to check if the wallet has been fully re
120120
$ bitcoin-cli -rpcwallet="restored-wallet" getwalletinfo
121121
```
122122

123-
The restored wallet can also be loaded in the GUI via `File` ->`Open wallet`.
123+
The restored wallet can also be loaded in the GUI via `File` ->`Open wallet`.
124+
125+
## Migrating Legacy Wallets to Descriptor Wallets
126+
127+
Legacy wallets (traditional non-descriptor wallets) can be migrated to become Descriptor wallets
128+
through the use of the `migratewallet` RPC. Migrated wallets will have all of their addresses and private keys added to
129+
a newly created Descriptor wallet that has the same name as the original wallet. Because Descriptor
130+
wallets do not support having private keys and watch-only scripts, there may be up to two
131+
additional wallets created after migration. In addition to a descriptor wallet of the same name,
132+
there may also be a wallet named `<name>_watchonly` and `<name>_solvables`. `<name>_watchonly`
133+
contains all of the watchonly scripts. `<name>_solvables` contains any scripts which the wallet
134+
knows but is not watching the corresponding P2(W)SH scripts.
135+
136+
Migrated wallets will also generate new addresses differently. While the same BIP 32 seed will be
137+
used, the BIP 44, 49, 84, and 86 standard derivation paths will be used. After migrating, a new
138+
backup of the wallet(s) will need to be created.
139+
140+
Given that there is an extremely large number of possible configurations for the scripts that
141+
Legacy wallets can know about, be watching for, and be able to sign for, `migratewallet` only
142+
makes a best effort attempt to capture all of these things into Descriptor wallets. There may be
143+
unforeseen configurations which result in some scripts being excluded. If a migration fails
144+
unexpectedly or otherwise misses any scripts, please create an issue on GitHub. A backup of the
145+
original wallet can be found in the wallet directory with the name `<name>-<timestamp>.legacy.bak`.
146+
147+
The backup can be restored using the `restorewallet` command as discussed in the
148+
[Restoring the Wallet From a Backup](#16-restoring-the-wallet-from-a-backup) section

doc/release-notes-19602.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Wallet
2+
======
3+
4+
Migrating Legacy Wallets to Descriptor Wallets
5+
---------------------------------------------
6+
7+
An experimental RPC `migratewallet` has been added to migrate Legacy (non-descriptor) wallets to
8+
Descriptor wallets. More information about the migration process is available in the
9+
[documentation](https://github.com/bitcoin/bitcoin/blob/master/doc/managing-wallets.md#migrating-legacy-wallets-to-descriptor-wallets).

src/script/descriptor.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -612,7 +612,7 @@ class DescriptorImpl : public Descriptor
612612
return AddChecksum(ret);
613613
}
614614

615-
bool ToPrivateString(const SigningProvider& arg, std::string& out) const final
615+
bool ToPrivateString(const SigningProvider& arg, std::string& out) const override
616616
{
617617
bool ret = ToStringHelper(&arg, out, StringType::PRIVATE);
618618
out = AddChecksum(out);
@@ -698,6 +698,7 @@ class AddressDescriptor final : public DescriptorImpl
698698
return OutputTypeFromDestination(m_destination);
699699
}
700700
bool IsSingleType() const final { return true; }
701+
bool ToPrivateString(const SigningProvider& arg, std::string& out) const final { return false; }
701702
};
702703

703704
/** A parsed raw(H) descriptor. */
@@ -718,6 +719,7 @@ class RawDescriptor final : public DescriptorImpl
718719
return OutputTypeFromDestination(dest);
719720
}
720721
bool IsSingleType() const final { return true; }
722+
bool ToPrivateString(const SigningProvider& arg, std::string& out) const final { return false; }
721723
};
722724

723725
/** A parsed pk(P) descriptor. */

src/wallet/rpc/wallet.cpp

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,59 @@ RPCHelpMan simulaterawtransaction()
701701
};
702702
}
703703

704+
static RPCHelpMan migratewallet()
705+
{
706+
return RPCHelpMan{"migratewallet",
707+
"EXPERIMENTAL warning: This call may not work as expected and may be changed in future releases\n"
708+
"\nMigrate the wallet to a descriptor wallet.\n"
709+
"A new wallet backup will need to be made.\n"
710+
"\nThe migration process will create a backup of the wallet before migrating. This backup\n"
711+
"file will be named <wallet name>-<timestamp>.legacy.bak and can be found in the directory\n"
712+
"for this wallet. In the event of an incorrect migration, the backup can be restored using restorewallet." +
713+
HELP_REQUIRING_PASSPHRASE,
714+
{},
715+
RPCResult{
716+
RPCResult::Type::OBJ, "", "",
717+
{
718+
{RPCResult::Type::STR, "wallet_name", "The name of the primary migrated wallet"},
719+
{RPCResult::Type::STR, "watchonly_name", /*optional=*/true, "The name of the migrated wallet containing the watchonly scripts"},
720+
{RPCResult::Type::STR, "solvables_name", /*optional=*/true, "The name of the migrated wallet containing solvable but not watched scripts"},
721+
{RPCResult::Type::STR, "backup_path", "The location of the backup of the original wallet"},
722+
}
723+
},
724+
RPCExamples{
725+
HelpExampleCli("migratewallet", "")
726+
+ HelpExampleRpc("migratewallet", "")
727+
},
728+
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
729+
{
730+
std::shared_ptr<CWallet> wallet = GetWalletForJSONRPCRequest(request);
731+
if (!wallet) return NullUniValue;
732+
733+
EnsureWalletIsUnlocked(*wallet);
734+
735+
WalletContext& context = EnsureWalletContext(request.context);
736+
737+
util::Result<MigrationResult> res = MigrateLegacyToDescriptor(std::move(wallet), context);
738+
if (!res) {
739+
throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(res).original);
740+
}
741+
742+
UniValue r{UniValue::VOBJ};
743+
r.pushKV("wallet_name", res->wallet_name);
744+
if (res->watchonly_wallet) {
745+
r.pushKV("watchonly_name", res->watchonly_wallet->GetName());
746+
}
747+
if (res->solvables_wallet) {
748+
r.pushKV("solvables_name", res->solvables_wallet->GetName());
749+
}
750+
r.pushKV("backup_path", res->backup_path.u8string());
751+
752+
return r;
753+
},
754+
};
755+
}
756+
704757
// addresses
705758
RPCHelpMan getaddressinfo();
706759
RPCHelpMan getnewaddress();
@@ -820,6 +873,7 @@ Span<const CRPCCommand> GetWalletRPCCommands()
820873
{"wallet", &listwallets},
821874
{"wallet", &loadwallet},
822875
{"wallet", &lockunspent},
876+
{"wallet", &migratewallet},
823877
{"wallet", &newkeypool},
824878
{"wallet", &removeprunedfunds},
825879
{"wallet", &rescanblockchain},

0 commit comments

Comments
 (0)