wallet: Add CWallet::ExportWatchOnly

ExportWatchOnly produces a watchonly wallet file from a CWallet. This
can be restored onto another instance of Bitcoin Core to allow that
instance to watch the same descriptors, and also have all of the same
initial address book and transactions.
This commit is contained in:
Ava Chow
2025-05-12 13:39:26 -07:00
parent f9273f01db
commit 444878efef
3 changed files with 188 additions and 1 deletions

View File

@@ -4,10 +4,15 @@
#include <wallet/export.h>
#include <key_io.h>
#include <util/fs.h>
#include <util/expected.h>
#include <wallet/scriptpubkeyman.h>
#include <wallet/context.h>
#include <wallet/wallet.h>
#include <fstream>
namespace wallet {
util::Expected<std::vector<WalletDescInfo>, std::string> ExportDescriptors(const CWallet& wallet, bool export_private)
{
@@ -36,4 +41,182 @@ util::Expected<std::vector<WalletDescInfo>, std::string> ExportDescriptors(const
}
return wallet_descriptors;
}
util::Result<std::string> ExportWatchOnlyWallet(const CWallet& wallet, const fs::path& destination, WalletContext& context)
{
AssertLockHeld(wallet.cs_wallet);
if (destination.empty()) {
return util::Error{_("Error: Export destination cannot be empty")};
}
if (fs::exists(destination)) {
return util::Error{strprintf(_("Error: Export destination '%s' already exists"), fs::PathToString(destination))};
}
if (!std::ofstream{fs::PathToString(destination)}) {
return util::Error{strprintf(_("Error: Could not create file '%s'"), fs::PathToString(destination))};
}
bool success = false;
auto cleanup_destination = interfaces::MakeCleanupHandler([&success, &destination] {
if (!success) fs::remove(destination);
});
// Get the descriptors from this wallet
util::Expected<std::vector<WalletDescInfo>, std::string> exported = ExportDescriptors(wallet, /*export_private=*/false);
if (!exported) {
return util::Error{Untranslated(exported.error())};
}
if (exported->empty()) {
return util::Error{_("Error: Wallet has no descriptors to export")};
}
// Setup DatabaseOptions to create a new sqlite database
DatabaseOptions options;
options.require_existing = false;
options.require_create = true;
options.require_format = DatabaseFormat::SQLITE;
// Make the wallet with the same flags as this wallet, but without private keys
options.create_flags = wallet.GetWalletFlags() | WALLET_FLAG_DISABLE_PRIVATE_KEYS;
// Make the watchonly wallet
DatabaseStatus status;
std::vector<bilingual_str> warnings;
std::string wallet_name = wallet.GetName() + "_watchonly_temp";
bilingual_str error;
std::unique_ptr<WalletDatabase> database = MakeWalletDatabase(wallet_name, options, status, error);
if (!database) {
return util::Error{strprintf(_("Wallet file creation failed: %s"), error)};
}
// Always remove the temporary wallet files, even when returning early on error.
std::shared_ptr<CWallet> watchonly_wallet;
fs::path wallet_path = fs::PathFromString(database->Filename()).parent_path();
std::vector<fs::path> cleanup_files = database->Files();
auto cleanup_watchonly_wallet = interfaces::MakeCleanupHandler([&watchonly_wallet, &wallet_path, &cleanup_files] {
if (watchonly_wallet) watchonly_wallet.reset();
for (const auto& file : cleanup_files) {
fs::remove(file);
}
fs::remove(wallet_path);
});
WalletContext empty_context;
empty_context.args = context.args;
watchonly_wallet = CWallet::CreateNew(empty_context, wallet_name, std::move(database), options.create_flags, /*born_encrypted=*/false, error, warnings);
if (!watchonly_wallet) {
return util::Error{strprintf(_("Error: Failed to create new watchonly wallet. %s"), error)};
}
{
LOCK(watchonly_wallet->cs_wallet);
// Parse the descriptors and add them to the new wallet
for (const WalletDescInfo& desc_info : *Assert(exported)) {
// Parse the descriptor
FlatSigningProvider dummy_keys;
std::string dummy_err;
std::vector<std::unique_ptr<Descriptor>> descs = Parse(desc_info.descriptor, dummy_keys, dummy_err, /*require_checksum=*/true);
CHECK_NONFATAL(descs.size() == 1); // All of our descriptors should be valid, and not multipath
CHECK_NONFATAL(dummy_keys.keys.size() == 0); // No private keys should be present in our exported descriptors
// Get the range if there is one
int32_t range_start = 0;
int32_t range_end = 0;
if (desc_info.range) {
range_start = desc_info.range->first;
range_end = desc_info.range->second;
}
WalletDescriptor w_desc(std::move(descs.at(0)), desc_info.creation_time, range_start, range_end, desc_info.next_index);
// For descriptors that cannot self expand (i.e. needs private keys or cache), retrieve the cache
uint256 desc_id = w_desc.id;
if (!w_desc.descriptor->CanSelfExpand()) {
DescriptorScriptPubKeyMan* desc_spkm = dynamic_cast<DescriptorScriptPubKeyMan*>(wallet.GetScriptPubKeyMan(desc_id));
w_desc.cache = WITH_LOCK(desc_spkm->cs_desc_man, return desc_spkm->GetWalletDescriptor().cache);
}
// Add to the watchonly wallet
if (auto spkm_res = watchonly_wallet->AddWalletDescriptor(w_desc, dummy_keys, /*label=*/"", /*internal=*/false); !spkm_res) {
return util::Error{util::ErrorString(spkm_res)};
}
// Set active spkms as active
if (desc_info.active) {
// Determine whether this descriptor is internal
// This is only set for active spkms
bool internal = false;
if (desc_info.internal) {
internal = *desc_info.internal;
}
watchonly_wallet->AddActiveScriptPubKeyMan(desc_id, *Assert(w_desc.descriptor->GetOutputType()), internal);
}
}
// Copy locked coins that are persisted
for (const auto& [coin, persisted] : wallet.m_locked_coins) {
if (!persisted) continue;
watchonly_wallet->LockCoin(coin, persisted);
}
{
// Make a WalletBatch for the watchonly wallet so that everything else can be written atomically
WalletBatch watchonly_batch(watchonly_wallet->GetDatabase());
if (!watchonly_batch.TxnBegin()) {
return util::Error{strprintf(_("Error: database transaction cannot be executed for new watchonly wallet %s"), watchonly_wallet->GetName())};
}
// Copy orderPosNext
watchonly_batch.WriteOrderPosNext(wallet.nOrderPosNext);
// Write the best block locator to avoid rescanning on reload
CBlockLocator best_block_locator;
{
WalletBatch local_wallet_batch(wallet.GetDatabase());
if (!local_wallet_batch.ReadBestBlock(best_block_locator)) {
return util::Error{_("Error: Unable to read wallet's best block locator record")};
}
}
if (!watchonly_batch.WriteBestBlock(best_block_locator)) {
return util::Error{_("Error: Unable to write watchonly wallet best block locator record")};
}
// Copy the transactions
for (const auto& [txid, wtx] : wallet.mapWallet) {
if (!watchonly_wallet->LoadToWallet(txid, [&](CWalletTx& ins_wtx, bool new_tx) EXCLUSIVE_LOCKS_REQUIRED(watchonly_wallet->cs_wallet) {
if (!new_tx) return false;
ins_wtx.SetTx(wtx.tx);
ins_wtx.CopyFrom(wtx);
return true;
})) {
return util::Error{strprintf(_("Error: Could not add tx %s to watchonly wallet"), txid.GetHex())};
}
watchonly_batch.WriteTx(watchonly_wallet->mapWallet.at(txid));
}
// Copy address book
for (const auto& [dest, entry] : wallet.m_address_book) {
auto address{EncodeDestination(dest)};
if (entry.purpose) watchonly_batch.WritePurpose(address, PurposeToString(*entry.purpose));
if (entry.label) watchonly_batch.WriteName(address, *entry.label);
for (const auto& [id, request] : entry.receive_requests) {
watchonly_batch.WriteAddressReceiveRequest(dest, id, request);
}
if (entry.previously_spent) watchonly_batch.WriteAddressPreviouslySpent(dest, true);
}
if (!watchonly_batch.TxnCommit()) {
return util::Error{_("Error: cannot commit db transaction for watchonly wallet export")};
}
}
// Make a backup of this wallet at the specified destination directory
if (!watchonly_wallet->BackupWallet(fs::PathToString(destination))) {
return util::Error{_("Error: Unable to write the exported wallet")};
}
success = true;
}
return fs::PathToString(destination);
}
} // namespace wallet

View File

@@ -27,6 +27,10 @@ struct WalletDescInfo {
//! Export the descriptors from a wallet so that they can be imported elsewhere
util::Expected<std::vector<WalletDescInfo>, std::string> ExportDescriptors(const CWallet& wallet, bool export_private) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
//! Make a new watchonly wallet file containing the public descriptors from this wallet
//! The exported watchonly wallet file will be named and placed at the path specified in 'destination'
util::Result<std::string> ExportWatchOnlyWallet(const CWallet& wallet, const fs::path& destination, WalletContext& context) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
} // namespace wallet
#endif // BITCOIN_WALLET_EXPORT_H

View File

@@ -882,7 +882,7 @@ public:
*/
void postInitProcess();
bool BackupWallet(const std::string& strDest) const;
[[nodiscard]] bool BackupWallet(const std::string& strDest) const;
/* Returns true if HD is enabled */
bool IsHDEnabled() const;