aboutsummaryrefslogtreecommitdiff
path: root/src/wallet/wallet.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/wallet/wallet.cpp')
-rw-r--r--src/wallet/wallet.cpp668
1 files changed, 593 insertions, 75 deletions
diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp
index 1994ac2171..e0f1655ab7 100644
--- a/src/wallet/wallet.cpp
+++ b/src/wallet/wallet.cpp
@@ -149,6 +149,13 @@ std::vector<std::shared_ptr<CWallet>> GetWallets(WalletContext& context)
return context.wallets;
}
+std::shared_ptr<CWallet> GetDefaultWallet(WalletContext& context, size_t& count)
+{
+ LOCK(context.wallets_mutex);
+ count = context.wallets.size();
+ return count == 1 ? context.wallets[0] : nullptr;
+}
+
std::shared_ptr<CWallet> GetWallet(WalletContext& context, const std::string& name)
{
LOCK(context.wallets_mutex);
@@ -541,6 +548,7 @@ void CWallet::SetMinVersion(enum WalletFeature nVersion, WalletBatch* batch_in)
LOCK(cs_wallet);
if (nWalletVersion >= nVersion)
return;
+ WalletLogPrintf("Setting minversion to %d\n", nVersion);
nWalletVersion = nVersion;
{
@@ -880,7 +888,7 @@ bool CWallet::MarkReplaced(const uint256& originalHash, const uint256& newHash)
wtx.mapValue["replaced_by_txid"] = newHash.ToString();
- // Refresh mempool status without waiting for transactionRemovedFromMempool
+ // Refresh mempool status without waiting for transactionRemovedFromMempool or transactionAddedToMempool
RefreshMempoolStatus(wtx, chain());
WalletBatch batch(GetDatabase());
@@ -1427,6 +1435,19 @@ bool CWallet::IsMine(const CTransaction& tx) const
return false;
}
+isminetype CWallet::IsMine(const COutPoint& outpoint) const
+{
+ AssertLockHeld(cs_wallet);
+ auto wtx = GetWalletTx(outpoint.hash);
+ if (!wtx) {
+ return ISMINE_NO;
+ }
+ if (outpoint.n >= wtx->tx->vout.size()) {
+ return ISMINE_NO;
+ }
+ return IsMine(wtx->tx->vout[outpoint.n]);
+}
+
bool CWallet::IsFromMe(const CTransaction& tx) const
{
return (GetDebit(tx, ISMINE_ALL) > 0);
@@ -1512,16 +1533,20 @@ bool CWallet::LoadWalletFlags(uint64_t flags)
return true;
}
-bool CWallet::AddWalletFlags(uint64_t flags)
+void CWallet::InitWalletFlags(uint64_t flags)
{
LOCK(cs_wallet);
+
// We should never be writing unknown non-tolerable wallet flags
assert(((flags & KNOWN_WALLET_FLAGS) >> 32) == (flags >> 32));
+ // This should only be used once, when creating a new wallet - so current flags are expected to be blank
+ assert(m_wallet_flags == 0);
+
if (!WalletBatch(GetDatabase()).WriteWalletFlags(flags)) {
throw std::runtime_error(std::string(__func__) + ": writing wallet flags failed");
}
- return LoadWalletFlags(flags);
+ if (!LoadWalletFlags(flags)) assert(false);
}
// Helper for producing a max-sized low-S low-R signature (eg 71 bytes)
@@ -1838,34 +1863,6 @@ CWallet::ScanResult CWallet::ScanForWalletTransactions(const uint256& start_bloc
return result;
}
-void CWallet::ReacceptWalletTransactions()
-{
- // If transactions aren't being broadcasted, don't let them into local mempool either
- if (!fBroadcastTransactions)
- return;
- std::map<int64_t, CWalletTx*> mapSorted;
-
- // Sort pending wallet transactions based on their initial wallet insertion order
- for (std::pair<const uint256, CWalletTx>& item : mapWallet) {
- const uint256& wtxid = item.first;
- CWalletTx& wtx = item.second;
- assert(wtx.GetHash() == wtxid);
-
- int nDepth = GetTxDepthInMainChain(wtx);
-
- if (!wtx.IsCoinBase() && (nDepth == 0 && !wtx.isAbandoned())) {
- mapSorted.insert(std::make_pair(wtx.nOrderPos, &wtx));
- }
- }
-
- // Try to add wallet transactions to memory pool
- for (const std::pair<const int64_t, CWalletTx*>& item : mapSorted) {
- CWalletTx& wtx = *(item.second);
- std::string unused_err_string;
- SubmitTxMemoryPoolAndRelay(wtx, unused_err_string, false);
- }
-}
-
bool CWallet::SubmitTxMemoryPoolAndRelay(CWalletTx& wtx, std::string& err_string, bool relay) const
{
AssertLockHeld(cs_wallet);
@@ -1906,43 +1903,69 @@ std::set<uint256> CWallet::GetTxConflicts(const CWalletTx& wtx) const
return result;
}
-// Rebroadcast transactions from the wallet. We do this on a random timer
-// to slightly obfuscate which transactions come from our wallet.
+// Resubmit transactions from the wallet to the mempool, optionally asking the
+// mempool to relay them. On startup, we will do this for all unconfirmed
+// transactions but will not ask the mempool to relay them. We do this on startup
+// to ensure that our own mempool is aware of our transactions, and to also
+// initialize nNextResend so that the actual rebroadcast is scheduled. There
+// is a privacy side effect here as not broadcasting on startup also means that we won't
+// inform the world of our wallet's state, particularly if the wallet (or node) is not
+// yet synced.
+//
+// Otherwise this function is called periodically in order to relay our unconfirmed txs.
+// We do this on a random timer to slightly obfuscate which transactions
+// come from our wallet.
//
-// Ideally, we'd only resend transactions that we think should have been
+// TODO: Ideally, we'd only resend transactions that we think should have been
// mined in the most recent block. Any transaction that wasn't in the top
// blockweight of transactions in the mempool shouldn't have been mined,
// and so is probably just sitting in the mempool waiting to be confirmed.
// Rebroadcasting does nothing to speed up confirmation and only damages
// privacy.
-void CWallet::ResendWalletTransactions()
+//
+// The `force` option results in all unconfirmed transactions being submitted to
+// the mempool. This does not necessarily result in those transactions being relayed,
+// that depends on the `relay` option. Periodic rebroadcast uses the pattern
+// relay=true force=false, while loading into the mempool
+// (on start, or after import) uses relay=false force=true.
+void CWallet::ResubmitWalletTransactions(bool relay, bool force)
{
+ // Don't attempt to resubmit if the wallet is configured to not broadcast,
+ // even if forcing.
+ if (!fBroadcastTransactions) return;
+
// During reindex, importing and IBD, old wallet transactions become
// unconfirmed. Don't resend them as that would spam other nodes.
- if (!chain().isReadyToBroadcast()) return;
+ // We only allow forcing mempool submission when not relaying to avoid this spam.
+ if (!force && relay && !chain().isReadyToBroadcast()) return;
// Do this infrequently and randomly to avoid giving away
// that these are our transactions.
- if (GetTime() < nNextResend || !fBroadcastTransactions) return;
- bool fFirst = (nNextResend == 0);
+ if (!force && GetTime() < nNextResend) return;
// resend 12-36 hours from now, ~1 day on average.
nNextResend = GetTime() + (12 * 60 * 60) + GetRand(24 * 60 * 60);
- if (fFirst) return;
int submitted_tx_count = 0;
{ // cs_wallet scope
LOCK(cs_wallet);
- // Relay transactions
- for (std::pair<const uint256, CWalletTx>& item : mapWallet) {
- CWalletTx& wtx = item.second;
- // Attempt to rebroadcast all txes more than 5 minutes older than
- // the last block. SubmitTxMemoryPoolAndRelay() will not rebroadcast
- // any confirmed or conflicting txs.
- if (wtx.nTimeReceived > m_best_block_time - 5 * 60) continue;
+ // First filter for the transactions we want to rebroadcast.
+ // We use a set with WalletTxOrderComparator so that rebroadcasting occurs in insertion order
+ std::set<CWalletTx*, WalletTxOrderComparator> to_submit;
+ for (auto& [txid, wtx] : mapWallet) {
+ // Only rebroadcast unconfirmed txs
+ if (!wtx.isUnconfirmed()) continue;
+
+ // attempt to rebroadcast all txes more than 5 minutes older than
+ // the last block, or all txs if forcing.
+ if (!force && wtx.nTimeReceived > m_best_block_time - 5 * 60) continue;
+ to_submit.insert(&wtx);
+ }
+ // Now try submitting the transactions to the memory pool and (optionally) relay them.
+ for (auto wtx : to_submit) {
std::string unused_err_string;
- if (SubmitTxMemoryPoolAndRelay(wtx, unused_err_string, true)) ++submitted_tx_count;
+ if (SubmitTxMemoryPoolAndRelay(*wtx, unused_err_string, relay)) ++submitted_tx_count;
}
} // cs_wallet
@@ -1956,7 +1979,7 @@ void CWallet::ResendWalletTransactions()
void MaybeResendWalletTxs(WalletContext& context)
{
for (const std::shared_ptr<CWallet>& pwallet : GetWallets(context)) {
- pwallet->ResendWalletTransactions();
+ pwallet->ResubmitWalletTransactions(/*relay=*/true, /*force=*/false);
}
}
@@ -2769,7 +2792,7 @@ std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::stri
ArgsManager& args = *Assert(context.args);
const std::string& walletFile = database->Filename();
- int64_t nStart = GetTimeMillis();
+ const auto start{SteadyClock::now()};
// TODO: Can't use std::make_shared because we need a custom deleter but
// should be possible to use std::allocate_shared.
const std::shared_ptr<CWallet> walletInstance(new CWallet(chain, name, args, std::move(database)), ReleaseWallet);
@@ -2802,8 +2825,12 @@ std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::stri
warnings.push_back(strprintf(_("Error reading %s! Transaction data may be missing or incorrect."
" Rescanning wallet."), walletFile));
rescan_required = true;
- }
- else {
+ } else if (nLoadWalletRet == DBErrors::UNKNOWN_DESCRIPTOR) {
+ error = strprintf(_("Unrecognized descriptor found. Loading wallet %s\n\n"
+ "The wallet might had been created on a newer version.\n"
+ "Please try running the latest software version.\n"), walletFile);
+ return nullptr;
+ } else {
error = strprintf(_("Error loading %s"), walletFile);
return nullptr;
}
@@ -2818,7 +2845,7 @@ std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::stri
// ensure this wallet.dat can only be opened by clients supporting HD with chain split and expects no default key
walletInstance->SetMinVersion(FEATURE_LATEST);
- walletInstance->AddWalletFlags(wallet_creation_flags);
+ walletInstance->InitWalletFlags(wallet_creation_flags);
// Only create LegacyScriptPubKeyMan when not descriptor wallet
if (!walletInstance->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) {
@@ -2986,7 +3013,7 @@ std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::stri
walletInstance->m_spend_zero_conf_change = args.GetBoolArg("-spendzeroconfchange", DEFAULT_SPEND_ZEROCONF_CHANGE);
walletInstance->m_signal_rbf = args.GetBoolArg("-walletrbf", DEFAULT_WALLET_RBF);
- walletInstance->WalletLogPrintf("Wallet completed loading in %15dms\n", GetTimeMillis() - nStart);
+ walletInstance->WalletLogPrintf("Wallet completed loading in %15dms\n", Ticks<std::chrono::milliseconds>(SteadyClock::now() - start));
// Try to top up keypool. No-op if the wallet is locked.
walletInstance->TopUpKeyPool();
@@ -3172,7 +3199,7 @@ void CWallet::postInitProcess()
// Add wallet transactions that aren't already in a block to mempool
// Do this here as mempool requires genesis block to be loaded
- ReacceptWalletTransactions();
+ ResubmitWalletTransactions(/*relay=*/false, /*force=*/true);
// Update wallet transactions with current mempool transactions.
chain().requestMempoolTransactions(*this);
@@ -3342,6 +3369,18 @@ std::unique_ptr<SigningProvider> CWallet::GetSolvingProvider(const CScript& scri
return nullptr;
}
+std::vector<WalletDescriptor> CWallet::GetWalletDescriptors(const CScript& script) const
+{
+ std::vector<WalletDescriptor> descs;
+ for (const auto spk_man: GetScriptPubKeyMans(script)) {
+ if (const auto desc_spk_man = dynamic_cast<DescriptorScriptPubKeyMan*>(spk_man)) {
+ LOCK(desc_spk_man->cs_desc_man);
+ descs.push_back(desc_spk_man->GetWalletDescriptor());
+ }
+ }
+ return descs;
+}
+
LegacyScriptPubKeyMan* CWallet::GetLegacyScriptPubKeyMan() const
{
if (IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) {
@@ -3403,6 +3442,29 @@ void CWallet::LoadDescriptorScriptPubKeyMan(uint256 id, WalletDescriptor& desc)
}
}
+void CWallet::SetupDescriptorScriptPubKeyMans(const CExtKey& master_key)
+{
+ AssertLockHeld(cs_wallet);
+
+ for (bool internal : {false, true}) {
+ for (OutputType t : OUTPUT_TYPES) {
+ auto spk_manager = std::unique_ptr<DescriptorScriptPubKeyMan>(new DescriptorScriptPubKeyMan(*this));
+ if (IsCrypted()) {
+ if (IsLocked()) {
+ throw std::runtime_error(std::string(__func__) + ": Wallet is locked, cannot setup new descriptors");
+ }
+ if (!spk_manager->CheckDecryptionKey(vMasterKey) && !spk_manager->Encrypt(vMasterKey, nullptr)) {
+ throw std::runtime_error(std::string(__func__) + ": Could not encrypt new descriptors");
+ }
+ }
+ spk_manager->SetupDescriptorGeneration(master_key, t, internal);
+ uint256 id = spk_manager->GetID();
+ m_spk_managers[id] = std::move(spk_manager);
+ AddActiveScriptPubKeyMan(id, t, internal);
+ }
+ }
+}
+
void CWallet::SetupDescriptorScriptPubKeyMans()
{
AssertLockHeld(cs_wallet);
@@ -3418,23 +3480,7 @@ void CWallet::SetupDescriptorScriptPubKeyMans()
CExtKey master_key;
master_key.SetSeed(seed_key);
- for (bool internal : {false, true}) {
- for (OutputType t : OUTPUT_TYPES) {
- auto spk_manager = std::unique_ptr<DescriptorScriptPubKeyMan>(new DescriptorScriptPubKeyMan(*this));
- if (IsCrypted()) {
- if (IsLocked()) {
- throw std::runtime_error(std::string(__func__) + ": Wallet is locked, cannot setup new descriptors");
- }
- if (!spk_manager->CheckDecryptionKey(vMasterKey) && !spk_manager->Encrypt(vMasterKey, nullptr)) {
- throw std::runtime_error(std::string(__func__) + ": Could not encrypt new descriptors");
- }
- }
- spk_manager->SetupDescriptorGeneration(master_key, t, internal);
- uint256 id = spk_manager->GetID();
- m_spk_managers[id] = std::move(spk_manager);
- AddActiveScriptPubKeyMan(id, t, internal);
- }
- }
+ SetupDescriptorScriptPubKeyMans(master_key);
} else {
ExternalSigner signer = ExternalSignerScriptPubKeyMan::GetExternalSigner();
@@ -3447,7 +3493,7 @@ void CWallet::SetupDescriptorScriptPubKeyMans()
const UniValue& descriptor_vals = find_value(signer_res, internal ? "internal" : "receive");
if (!descriptor_vals.isArray()) throw std::runtime_error(std::string(__func__) + ": Unexpected result");
for (const UniValue& desc_val : descriptor_vals.get_array().getValues()) {
- std::string desc_str = desc_val.getValStr();
+ const std::string& desc_str = desc_val.getValStr();
FlatSigningProvider keys;
std::string desc_error;
std::unique_ptr<Descriptor> desc = Parse(desc_str, keys, desc_error, false);
@@ -3602,9 +3648,13 @@ ScriptPubKeyMan* CWallet::AddWalletDescriptor(WalletDescriptor& desc, const Flat
return nullptr;
}
- CTxDestination dest;
- if (!internal && ExtractDestination(script_pub_keys.at(0), dest)) {
- SetAddressBook(dest, label, "receive");
+ if (!internal) {
+ for (const auto& script : script_pub_keys) {
+ CTxDestination dest;
+ if (ExtractDestination(script, dest)) {
+ SetAddressBook(dest, label, "receive");
+ }
+ }
}
}
@@ -3613,4 +3663,472 @@ ScriptPubKeyMan* CWallet::AddWalletDescriptor(WalletDescriptor& desc, const Flat
return spk_man;
}
+
+bool CWallet::MigrateToSQLite(bilingual_str& error)
+{
+ AssertLockHeld(cs_wallet);
+
+ WalletLogPrintf("Migrating wallet storage database from BerkeleyDB to SQLite.\n");
+
+ if (m_database->Format() == "sqlite") {
+ error = _("Error: This wallet already uses SQLite");
+ return false;
+ }
+
+ // Get all of the records for DB type migration
+ std::unique_ptr<DatabaseBatch> batch = m_database->MakeBatch();
+ std::vector<std::pair<SerializeData, SerializeData>> records;
+ if (!batch->StartCursor()) {
+ error = _("Error: Unable to begin reading all records in the database");
+ return false;
+ }
+ bool complete = false;
+ while (true) {
+ CDataStream ss_key(SER_DISK, CLIENT_VERSION);
+ CDataStream ss_value(SER_DISK, CLIENT_VERSION);
+ bool ret = batch->ReadAtCursor(ss_key, ss_value, complete);
+ if (!ret) {
+ break;
+ }
+ SerializeData key(ss_key.begin(), ss_key.end());
+ SerializeData value(ss_value.begin(), ss_value.end());
+ records.emplace_back(key, value);
+ }
+ batch->CloseCursor();
+ batch.reset();
+ if (!complete) {
+ error = _("Error: Unable to read all records in the database");
+ return false;
+ }
+
+ // Close this database and delete the file
+ fs::path db_path = fs::PathFromString(m_database->Filename());
+ fs::path db_dir = db_path.parent_path();
+ m_database->Close();
+ fs::remove(db_path);
+
+ // Make new DB
+ DatabaseOptions opts;
+ opts.require_create = true;
+ opts.require_format = DatabaseFormat::SQLITE;
+ DatabaseStatus db_status;
+ std::unique_ptr<WalletDatabase> new_db = MakeDatabase(db_dir, opts, db_status, error);
+ assert(new_db); // This is to prevent doing anything further with this wallet. The original file was deleted, but a backup exists.
+ m_database.reset();
+ m_database = std::move(new_db);
+
+ // Write existing records into the new DB
+ batch = m_database->MakeBatch();
+ bool began = batch->TxnBegin();
+ assert(began); // 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.
+ for (const auto& [key, value] : records) {
+ CDataStream ss_key(key, SER_DISK, CLIENT_VERSION);
+ CDataStream ss_value(value, SER_DISK, CLIENT_VERSION);
+ if (!batch->Write(ss_key, ss_value)) {
+ batch->TxnAbort();
+ m_database->Close();
+ fs::remove(m_database->Filename());
+ assert(false); // 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.
+ }
+ }
+ bool committed = batch->TxnCommit();
+ 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