Implement MigrateLegacyToDescriptor

This commit is contained in:
Andrew Chow 2020-07-13 16:38:39 -04:00 committed by Andrew Chow
parent e7b16f925a
commit 0bf7b38bff
2 changed files with 413 additions and 0 deletions

View file

@ -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

View file

@ -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