mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-01-25 10:43:19 -03:00
Implement MigrateLegacyToDescriptor
This commit is contained in:
parent
e7b16f925a
commit
0bf7b38bff
2 changed files with 413 additions and 0 deletions
|
@ -3726,4 +3726,400 @@ bool CWallet::MigrateToSQLite(bilingual_str& error)
|
|||
assert(committed); // This is a critical error, the new db could not be written to. The original db exists as a backup, but we should not continue execution.
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<MigrationData> CWallet::GetDescriptorsForLegacy(bilingual_str& error) const
|
||||
{
|
||||
AssertLockHeld(cs_wallet);
|
||||
|
||||
LegacyScriptPubKeyMan* legacy_spkm = GetLegacyScriptPubKeyMan();
|
||||
if (!legacy_spkm) {
|
||||
error = _("Error: This wallet is already a descriptor wallet");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<MigrationData> res = legacy_spkm->MigrateToDescriptor();
|
||||
if (res == std::nullopt) {
|
||||
error = _("Error: Unable to produce descriptors for this legacy wallet. Make sure the wallet is unlocked first");
|
||||
return std::nullopt;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
bool CWallet::ApplyMigrationData(MigrationData& data, bilingual_str& error)
|
||||
{
|
||||
AssertLockHeld(cs_wallet);
|
||||
|
||||
LegacyScriptPubKeyMan* legacy_spkm = GetLegacyScriptPubKeyMan();
|
||||
if (!legacy_spkm) {
|
||||
error = _("Error: This wallet is already a descriptor wallet");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto& desc_spkm : data.desc_spkms) {
|
||||
if (m_spk_managers.count(desc_spkm->GetID()) > 0) {
|
||||
error = _("Error: Duplicate descriptors created during migration. Your wallet may be corrupted.");
|
||||
return false;
|
||||
}
|
||||
m_spk_managers[desc_spkm->GetID()] = std::move(desc_spkm);
|
||||
}
|
||||
|
||||
// Remove the LegacyScriptPubKeyMan from disk
|
||||
if (!legacy_spkm->DeleteRecords()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove the LegacyScriptPubKeyMan from memory
|
||||
m_spk_managers.erase(legacy_spkm->GetID());
|
||||
m_external_spk_managers.clear();
|
||||
m_internal_spk_managers.clear();
|
||||
|
||||
// Setup new descriptors
|
||||
SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
|
||||
if (!IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
|
||||
// Use the existing master key if we have it
|
||||
if (data.master_key.key.IsValid()) {
|
||||
SetupDescriptorScriptPubKeyMans(data.master_key);
|
||||
} else {
|
||||
// Setup with a new seed if we don't.
|
||||
SetupDescriptorScriptPubKeyMans();
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the transactions in the wallet are still ours. Either they belong here, or they belong in the watchonly wallet.
|
||||
// We need to go through these in the tx insertion order so that lookups to spends works.
|
||||
std::vector<uint256> txids_to_delete;
|
||||
for (const auto& [_pos, wtx] : wtxOrdered) {
|
||||
if (!IsMine(*wtx->tx) && !IsFromMe(*wtx->tx)) {
|
||||
// Check it is the watchonly wallet's
|
||||
// solvable_wallet doesn't need to be checked because transactions for those scripts weren't being watched for
|
||||
if (data.watchonly_wallet) {
|
||||
LOCK(data.watchonly_wallet->cs_wallet);
|
||||
if (data.watchonly_wallet->IsMine(*wtx->tx) || data.watchonly_wallet->IsFromMe(*wtx->tx)) {
|
||||
// Add to watchonly wallet
|
||||
if (!data.watchonly_wallet->AddToWallet(wtx->tx, wtx->m_state)) {
|
||||
error = _("Error: Could not add watchonly tx to watchonly wallet");
|
||||
return false;
|
||||
}
|
||||
// Mark as to remove from this wallet
|
||||
txids_to_delete.push_back(wtx->GetHash());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Both not ours and not in the watchonly wallet
|
||||
error = strprintf(_("Error: Transaction %s in wallet cannot be identified to belong to migrated wallets"), wtx->GetHash().GetHex());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Do the removes
|
||||
if (txids_to_delete.size() > 0) {
|
||||
std::vector<uint256> deleted_txids;
|
||||
if (ZapSelectTx(txids_to_delete, deleted_txids) != DBErrors::LOAD_OK) {
|
||||
error = _("Error: Could not delete watchonly transactions");
|
||||
return false;
|
||||
}
|
||||
if (deleted_txids != txids_to_delete) {
|
||||
error = _("Error: Not all watchonly txs could be deleted");
|
||||
return false;
|
||||
}
|
||||
// Tell the GUI of each tx
|
||||
for (const uint256& txid : deleted_txids) {
|
||||
NotifyTransactionChanged(txid, CT_UPDATED);
|
||||
}
|
||||
}
|
||||
|
||||
// Check the address book data in the same way we did for transactions
|
||||
std::vector<CTxDestination> dests_to_delete;
|
||||
for (const auto& addr_pair : m_address_book) {
|
||||
// Labels applied to receiving addresses should go based on IsMine
|
||||
if (addr_pair.second.purpose == "receive") {
|
||||
if (!IsMine(addr_pair.first)) {
|
||||
// Check the address book data is the watchonly wallet's
|
||||
if (data.watchonly_wallet) {
|
||||
LOCK(data.watchonly_wallet->cs_wallet);
|
||||
if (data.watchonly_wallet->IsMine(addr_pair.first)) {
|
||||
// Add to the watchonly. Preserve the labels, purpose, and change-ness
|
||||
std::string label = addr_pair.second.GetLabel();
|
||||
std::string purpose = addr_pair.second.purpose;
|
||||
if (!purpose.empty()) {
|
||||
data.watchonly_wallet->m_address_book[addr_pair.first].purpose = purpose;
|
||||
}
|
||||
if (!addr_pair.second.IsChange()) {
|
||||
data.watchonly_wallet->m_address_book[addr_pair.first].SetLabel(label);
|
||||
}
|
||||
dests_to_delete.push_back(addr_pair.first);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (data.solvable_wallet) {
|
||||
LOCK(data.solvable_wallet->cs_wallet);
|
||||
if (data.solvable_wallet->IsMine(addr_pair.first)) {
|
||||
// Add to the solvable. Preserve the labels, purpose, and change-ness
|
||||
std::string label = addr_pair.second.GetLabel();
|
||||
std::string purpose = addr_pair.second.purpose;
|
||||
if (!purpose.empty()) {
|
||||
data.solvable_wallet->m_address_book[addr_pair.first].purpose = purpose;
|
||||
}
|
||||
if (!addr_pair.second.IsChange()) {
|
||||
data.solvable_wallet->m_address_book[addr_pair.first].SetLabel(label);
|
||||
}
|
||||
dests_to_delete.push_back(addr_pair.first);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Not ours, not in watchonly wallet, and not in solvable
|
||||
error = _("Error: Address book data in wallet cannot be identified to belong to migrated wallets");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// Labels for everything else (send) should be cloned to all
|
||||
if (data.watchonly_wallet) {
|
||||
LOCK(data.watchonly_wallet->cs_wallet);
|
||||
// Add to the watchonly. Preserve the labels, purpose, and change-ness
|
||||
std::string label = addr_pair.second.GetLabel();
|
||||
std::string purpose = addr_pair.second.purpose;
|
||||
if (!purpose.empty()) {
|
||||
data.watchonly_wallet->m_address_book[addr_pair.first].purpose = purpose;
|
||||
}
|
||||
if (!addr_pair.second.IsChange()) {
|
||||
data.watchonly_wallet->m_address_book[addr_pair.first].SetLabel(label);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (data.solvable_wallet) {
|
||||
LOCK(data.solvable_wallet->cs_wallet);
|
||||
// Add to the solvable. Preserve the labels, purpose, and change-ness
|
||||
std::string label = addr_pair.second.GetLabel();
|
||||
std::string purpose = addr_pair.second.purpose;
|
||||
if (!purpose.empty()) {
|
||||
data.solvable_wallet->m_address_book[addr_pair.first].purpose = purpose;
|
||||
}
|
||||
if (!addr_pair.second.IsChange()) {
|
||||
data.solvable_wallet->m_address_book[addr_pair.first].SetLabel(label);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Remove the things to delete
|
||||
if (dests_to_delete.size() > 0) {
|
||||
for (const auto& dest : dests_to_delete) {
|
||||
if (!DelAddressBook(dest)) {
|
||||
error = _("Error: Unable to remove watchonly address book data");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Connect the SPKM signals
|
||||
ConnectScriptPubKeyManNotifiers();
|
||||
NotifyCanGetAddressesChanged();
|
||||
|
||||
WalletLogPrintf("Wallet migration complete.\n");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DoMigration(CWallet& wallet, WalletContext& context, bilingual_str& error, MigrationResult& res) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
|
||||
{
|
||||
AssertLockHeld(wallet.cs_wallet);
|
||||
|
||||
// Get all of the descriptors from the legacy wallet
|
||||
std::optional<MigrationData> data = wallet.GetDescriptorsForLegacy(error);
|
||||
if (data == std::nullopt) return false;
|
||||
|
||||
// Create the watchonly and solvable wallets if necessary
|
||||
if (data->watch_descs.size() > 0 || data->solvable_descs.size() > 0) {
|
||||
DatabaseOptions options;
|
||||
options.require_existing = false;
|
||||
options.require_create = true;
|
||||
|
||||
// Make the wallets
|
||||
options.create_flags = WALLET_FLAG_DISABLE_PRIVATE_KEYS | WALLET_FLAG_BLANK_WALLET | WALLET_FLAG_DESCRIPTORS;
|
||||
if (wallet.IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE)) {
|
||||
options.create_flags |= WALLET_FLAG_AVOID_REUSE;
|
||||
}
|
||||
if (wallet.IsWalletFlagSet(WALLET_FLAG_KEY_ORIGIN_METADATA)) {
|
||||
options.create_flags |= WALLET_FLAG_KEY_ORIGIN_METADATA;
|
||||
}
|
||||
if (data->watch_descs.size() > 0) {
|
||||
wallet.WalletLogPrintf("Making a new watchonly wallet containing the watched scripts\n");
|
||||
|
||||
DatabaseStatus status;
|
||||
std::vector<bilingual_str> warnings;
|
||||
std::string wallet_name = wallet.GetName() + "_watchonly";
|
||||
data->watchonly_wallet = CreateWallet(context, wallet_name, std::nullopt, options, status, error, warnings);
|
||||
if (status != DatabaseStatus::SUCCESS) {
|
||||
error = _("Error: Failed to create new watchonly wallet");
|
||||
return false;
|
||||
}
|
||||
res.watchonly_wallet = data->watchonly_wallet;
|
||||
LOCK(data->watchonly_wallet->cs_wallet);
|
||||
|
||||
// Parse the descriptors and add them to the new wallet
|
||||
for (const auto& [desc_str, creation_time] : data->watch_descs) {
|
||||
// Parse the descriptor
|
||||
FlatSigningProvider keys;
|
||||
std::string parse_err;
|
||||
std::unique_ptr<Descriptor> desc = Parse(desc_str, keys, parse_err, /* require_checksum */ true);
|
||||
assert(desc); // It shouldn't be possible to have the LegacyScriptPubKeyMan make an invalid descriptor
|
||||
assert(!desc->IsRange()); // It shouldn't be possible to have LegacyScriptPubKeyMan make a ranged watchonly descriptor
|
||||
|
||||
// Add to the wallet
|
||||
WalletDescriptor w_desc(std::move(desc), creation_time, 0, 0, 0);
|
||||
data->watchonly_wallet->AddWalletDescriptor(w_desc, keys, "", false);
|
||||
}
|
||||
|
||||
// Add the wallet to settings
|
||||
UpdateWalletSetting(*context.chain, wallet_name, /*load_on_startup=*/true, warnings);
|
||||
}
|
||||
if (data->solvable_descs.size() > 0) {
|
||||
wallet.WalletLogPrintf("Making a new watchonly wallet containing the unwatched solvable scripts\n");
|
||||
|
||||
DatabaseStatus status;
|
||||
std::vector<bilingual_str> warnings;
|
||||
std::string wallet_name = wallet.GetName() + "_solvables";
|
||||
data->solvable_wallet = CreateWallet(context, wallet_name, std::nullopt, options, status, error, warnings);
|
||||
if (status != DatabaseStatus::SUCCESS) {
|
||||
error = _("Error: Failed to create new watchonly wallet");
|
||||
return false;
|
||||
}
|
||||
res.solvables_wallet = data->solvable_wallet;
|
||||
LOCK(data->solvable_wallet->cs_wallet);
|
||||
|
||||
// Parse the descriptors and add them to the new wallet
|
||||
for (const auto& [desc_str, creation_time] : data->solvable_descs) {
|
||||
// Parse the descriptor
|
||||
FlatSigningProvider keys;
|
||||
std::string parse_err;
|
||||
std::unique_ptr<Descriptor> desc = Parse(desc_str, keys, parse_err, /* require_checksum */ true);
|
||||
assert(desc); // It shouldn't be possible to have the LegacyScriptPubKeyMan make an invalid descriptor
|
||||
assert(!desc->IsRange()); // It shouldn't be possible to have LegacyScriptPubKeyMan make a ranged watchonly descriptor
|
||||
|
||||
// Add to the wallet
|
||||
WalletDescriptor w_desc(std::move(desc), creation_time, 0, 0, 0);
|
||||
data->solvable_wallet->AddWalletDescriptor(w_desc, keys, "", false);
|
||||
}
|
||||
|
||||
// Add the wallet to settings
|
||||
UpdateWalletSetting(*context.chain, wallet_name, /*load_on_startup=*/true, warnings);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the descriptors to wallet, remove LegacyScriptPubKeyMan, and cleanup txs and address book data
|
||||
if (!wallet.ApplyMigrationData(*data, error)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
util::Result<MigrationResult> MigrateLegacyToDescriptor(std::shared_ptr<CWallet>&& wallet, WalletContext& context)
|
||||
{
|
||||
MigrationResult res;
|
||||
bilingual_str error;
|
||||
std::vector<bilingual_str> warnings;
|
||||
|
||||
// Make a backup of the DB
|
||||
std::string wallet_name = wallet->GetName();
|
||||
fs::path this_wallet_dir = fs::absolute(fs::PathFromString(wallet->GetDatabase().Filename())).parent_path();
|
||||
fs::path backup_filename = fs::PathFromString(strprintf("%s-%d.legacy.bak", wallet_name, GetTime()));
|
||||
fs::path backup_path = this_wallet_dir / backup_filename;
|
||||
if (!wallet->BackupWallet(fs::PathToString(backup_path))) {
|
||||
return util::Error{_("Error: Unable to make a backup of your wallet")};
|
||||
}
|
||||
res.backup_path = backup_path;
|
||||
|
||||
// Unload the wallet so that nothing else tries to use it while we're changing it
|
||||
if (!RemoveWallet(context, wallet, /*load_on_start=*/std::nullopt, warnings)) {
|
||||
return util::Error{_("Unable to unload the wallet before migrating")};
|
||||
}
|
||||
UnloadWallet(std::move(wallet));
|
||||
|
||||
// Load the wallet but only in the context of this function.
|
||||
// No signals should be connected nor should anything else be aware of this wallet
|
||||
WalletContext empty_context;
|
||||
empty_context.args = context.args;
|
||||
DatabaseOptions options;
|
||||
options.require_existing = true;
|
||||
DatabaseStatus status;
|
||||
std::unique_ptr<WalletDatabase> database = MakeWalletDatabase(wallet_name, options, status, error);
|
||||
if (!database) {
|
||||
return util::Error{Untranslated("Wallet file verification failed.") + Untranslated(" ") + error};
|
||||
}
|
||||
|
||||
std::shared_ptr<CWallet> local_wallet = CWallet::Create(empty_context, wallet_name, std::move(database), options.create_flags, error, warnings);
|
||||
if (!local_wallet) {
|
||||
return util::Error{Untranslated("Wallet loading failed.") + Untranslated(" ") + error};
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
{
|
||||
LOCK(local_wallet->cs_wallet);
|
||||
|
||||
// First change to using SQLite
|
||||
if (!local_wallet->MigrateToSQLite(error)) return util::Error{error};
|
||||
|
||||
// Do the migration, and cleanup if it fails
|
||||
success = DoMigration(*local_wallet, context, error, res);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
// Migration successful, unload the wallet locally, then reload it.
|
||||
assert(local_wallet.use_count() == 1);
|
||||
local_wallet.reset();
|
||||
LoadWallet(context, wallet_name, /*load_on_start=*/std::nullopt, options, status, error, warnings);
|
||||
res.wallet_name = wallet_name;
|
||||
} else {
|
||||
// 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);
|
||||
|
||||
// Remember this wallet's walletdir to remove after unloading
|
||||
std::vector<fs::path> wallet_dirs;
|
||||
wallet_dirs.push_back(fs::PathFromString(local_wallet->GetDatabase().Filename()).parent_path());
|
||||
|
||||
// Unload the wallet locally
|
||||
assert(local_wallet.use_count() == 1);
|
||||
local_wallet.reset();
|
||||
|
||||
// Make list of wallets to cleanup
|
||||
std::vector<std::shared_ptr<CWallet>> created_wallets;
|
||||
created_wallets.push_back(std::move(res.watchonly_wallet));
|
||||
created_wallets.push_back(std::move(res.solvables_wallet));
|
||||
|
||||
// Get the directories to remove after unloading
|
||||
for (std::shared_ptr<CWallet>& w : created_wallets) {
|
||||
wallet_dirs.push_back(fs::PathFromString(w->GetDatabase().Filename()).parent_path());
|
||||
}
|
||||
|
||||
// Unload the wallets
|
||||
for (std::shared_ptr<CWallet>& w : created_wallets) {
|
||||
if (!RemoveWallet(context, w, /*load_on_start=*/false)) {
|
||||
error += _("\nUnable to cleanup failed migration");
|
||||
return util::Error{error};
|
||||
}
|
||||
UnloadWallet(std::move(w));
|
||||
}
|
||||
|
||||
// Delete the wallet directories
|
||||
for (fs::path& dir : wallet_dirs) {
|
||||
fs::remove_all(dir);
|
||||
}
|
||||
|
||||
// Restore the backup
|
||||
DatabaseStatus status;
|
||||
std::vector<bilingual_str> warnings;
|
||||
if (!RestoreWallet(context, temp_backup_location, wallet_name, /*load_on_start=*/std::nullopt, status, error, warnings)) {
|
||||
error += _("\nUnable to restore backup of wallet.");
|
||||
return util::Error{error};
|
||||
}
|
||||
|
||||
// Move the backup to the wallet dir
|
||||
fs::copy_file(temp_backup_location, backup_path, fs::copy_options::none);
|
||||
fs::remove(temp_backup_location);
|
||||
|
||||
return util::Error{error};
|
||||
}
|
||||
return res;
|
||||
}
|
||||
} // namespace wallet
|
||||
|
|
|
@ -927,6 +927,13 @@ public:
|
|||
* May crash if something unexpected happens in the filesystem.
|
||||
*/
|
||||
bool MigrateToSQLite(bilingual_str& error) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
//! Get all of the descriptors from a legacy wallet
|
||||
std::optional<MigrationData> GetDescriptorsForLegacy(bilingual_str& error) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
//! Adds the ScriptPubKeyMans given in MigrationData to this wallet, removes LegacyScriptPubKeyMan,
|
||||
//! and where needed, moves tx and address book entries to watchonly_wallet or solvable_wallet
|
||||
bool ApplyMigrationData(MigrationData& data, bilingual_str& error) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -985,6 +992,16 @@ bool RemoveWalletSetting(interfaces::Chain& chain, const std::string& wallet_nam
|
|||
bool DummySignInput(const SigningProvider& provider, CTxIn &tx_in, const CTxOut &txout, const CCoinControl* coin_control = nullptr);
|
||||
|
||||
bool FillInputToWeight(CTxIn& txin, int64_t target_weight);
|
||||
|
||||
struct MigrationResult {
|
||||
std::string wallet_name;
|
||||
std::shared_ptr<CWallet> watchonly_wallet;
|
||||
std::shared_ptr<CWallet> solvables_wallet;
|
||||
fs::path backup_path;
|
||||
};
|
||||
|
||||
//! Do all steps to migrate a legacy wallet to a descriptor wallet
|
||||
util::Result<MigrationResult> MigrateLegacyToDescriptor(std::shared_ptr<CWallet>&& wallet, WalletContext& context);
|
||||
} // namespace wallet
|
||||
|
||||
#endif // BITCOIN_WALLET_WALLET_H
|
||||
|
|
Loading…
Add table
Reference in a new issue