/home/enochazariah/dev/bitcoin-invariants/src/wallet/export.cpp
Line | Count | Source |
1 | | // Copyright (c) 2026-present The Bitcoin Core developers |
2 | | // Distributed under the MIT software license, see the accompanying |
3 | | // file COPYING or https://www.opensource.org/licenses/mit-license.php. |
4 | | |
5 | | #include <wallet/export.h> |
6 | | |
7 | | #include <key_io.h> |
8 | | #include <util/fs.h> |
9 | | #include <util/expected.h> |
10 | | #include <wallet/scriptpubkeyman.h> |
11 | | #include <wallet/context.h> |
12 | | #include <wallet/sqlite.h> |
13 | | #include <wallet/wallet.h> |
14 | | |
15 | | #include <fstream> |
16 | | |
17 | | namespace wallet { |
18 | | util::Expected<std::vector<WalletDescInfo>, std::string> ExportDescriptors(const CWallet& wallet, bool export_private) |
19 | 0 | { |
20 | 0 | AssertLockHeld(wallet.cs_wallet); Line | Count | Source | 144 | 0 | #define AssertLockHeld(cs) AssertLockHeldInternal(#cs, __FILE__, __LINE__, &cs) |
|
21 | 0 | std::vector<WalletDescInfo> wallet_descriptors; |
22 | 0 | for (const auto& spk_man : wallet.GetAllScriptPubKeyMans()) { |
23 | 0 | const auto desc_spk_man = dynamic_cast<DescriptorScriptPubKeyMan*>(spk_man); |
24 | 0 | if (!desc_spk_man) { |
25 | 0 | return util::Unexpected{"Unexpected ScriptPubKey manager type."}; |
26 | 0 | } |
27 | 0 | LOCK(desc_spk_man->cs_desc_man); Line | Count | Source | 268 | 0 | #define LOCK(cs) UniqueLock BITCOIN_UNIQUE_NAME(criticalblock)(MaybeCheckNotHeld(cs), #cs, __FILE__, __LINE__) Line | Count | Source | 11 | 0 | #define BITCOIN_UNIQUE_NAME(name) PASTE2(name, __COUNTER__) Line | Count | Source | 9 | 0 | #define PASTE2(x, y) PASTE(x, y) Line | Count | Source | 8 | 0 | #define PASTE(x, y) x ## y |
|
|
|
|
28 | 0 | const auto& wallet_descriptor = desc_spk_man->GetWalletDescriptor(); |
29 | 0 | std::string descriptor; |
30 | 0 | if (!Assume(desc_spk_man->GetDescriptorString(descriptor, export_private))) {Line | Count | Source | 128 | 0 | #define Assume(val) inline_assertion_check<false>(val, std::source_location::current(), #val) |
|
31 | 0 | return util::Unexpected{"Can't get descriptor string."}; |
32 | 0 | } |
33 | 0 | const bool is_range = wallet_descriptor.descriptor->IsRange(); |
34 | 0 | wallet_descriptors.emplace_back( |
35 | 0 | descriptor, |
36 | 0 | wallet_descriptor.creation_time, |
37 | 0 | wallet.IsActiveScriptPubKeyMan(*desc_spk_man), |
38 | 0 | wallet.IsInternalScriptPubKeyMan(desc_spk_man), |
39 | 0 | is_range ? std::optional(std::make_pair(wallet_descriptor.range_start, wallet_descriptor.range_end)) : std::nullopt, |
40 | 0 | wallet_descriptor.next_index |
41 | 0 | ); |
42 | 0 | } |
43 | 0 | return wallet_descriptors; |
44 | 0 | } |
45 | | |
46 | | util::Result<std::string> ExportWatchOnlyWallet(const CWallet& wallet, const fs::path& destination, WalletContext& context) |
47 | 0 | { |
48 | 0 | AssertLockHeld(wallet.cs_wallet); Line | Count | Source | 144 | 0 | #define AssertLockHeld(cs) AssertLockHeldInternal(#cs, __FILE__, __LINE__, &cs) |
|
49 | |
|
50 | 0 | if (destination.empty()) { |
51 | 0 | return util::Error{_("Error: Export destination cannot be empty")}; |
52 | 0 | } |
53 | 0 | if (fs::exists(destination)) { |
54 | 0 | return util::Error{strprintf(_("Error: Export destination '%s' already exists"), fs::PathToString(destination))};Line | Count | Source | 1172 | 0 | #define strprintf tfm::format |
|
55 | 0 | } |
56 | 0 | if (!std::ofstream{fs::PathToString(destination)}) { |
57 | 0 | return util::Error{strprintf(_("Error: Could not create file '%s'"), fs::PathToString(destination))};Line | Count | Source | 1172 | 0 | #define strprintf tfm::format |
|
58 | 0 | } |
59 | 0 | bool success = false; |
60 | 0 | auto cleanup_destination = interfaces::MakeCleanupHandler([&success, &destination] { |
61 | 0 | if (!success) fs::remove(destination); |
62 | 0 | }); |
63 | | |
64 | | // Get the descriptors from this wallet |
65 | 0 | util::Expected<std::vector<WalletDescInfo>, std::string> exported = ExportDescriptors(wallet, /*export_private=*/false); |
66 | 0 | if (!exported) { |
67 | 0 | return util::Error{Untranslated(exported.error())}; |
68 | 0 | } |
69 | 0 | if (exported->empty()) { |
70 | 0 | return util::Error{_("Error: Wallet has no descriptors to export")}; |
71 | 0 | } |
72 | | |
73 | | // Make the wallet with the same flags as this wallet, but without private keys |
74 | 0 | const uint64_t create_flags = wallet.GetWalletFlags() | WALLET_FLAG_DISABLE_PRIVATE_KEYS; |
75 | | |
76 | | // Create the temporary watchonly wallet in memory to avoid leaving files on disk |
77 | 0 | std::vector<bilingual_str> warnings; |
78 | 0 | bilingual_str error; |
79 | 0 | WalletContext empty_context; |
80 | 0 | empty_context.args = context.args; |
81 | 0 | std::shared_ptr<CWallet> watchonly_wallet = CWallet::CreateNew(empty_context, /*name=*/wallet.GetName() + "_watchonly_temp", MakeInMemoryWalletDatabase(), create_flags, /*born_encrypted=*/false, error, warnings); |
82 | 0 | if (!watchonly_wallet) { |
83 | 0 | return util::Error{strprintf(_("Error: Failed to create new watchonly wallet. %s"), error)};Line | Count | Source | 1172 | 0 | #define strprintf tfm::format |
|
84 | 0 | } |
85 | | |
86 | 0 | { |
87 | 0 | LOCK(watchonly_wallet->cs_wallet); Line | Count | Source | 268 | 0 | #define LOCK(cs) UniqueLock BITCOIN_UNIQUE_NAME(criticalblock)(MaybeCheckNotHeld(cs), #cs, __FILE__, __LINE__) Line | Count | Source | 11 | 0 | #define BITCOIN_UNIQUE_NAME(name) PASTE2(name, __COUNTER__) Line | Count | Source | 9 | 0 | #define PASTE2(x, y) PASTE(x, y) Line | Count | Source | 8 | 0 | #define PASTE(x, y) x ## y |
|
|
|
|
88 | | |
89 | | // Parse the descriptors and add them to the new wallet |
90 | 0 | for (const WalletDescInfo& desc_info : *Assert(exported)) {Line | Count | Source | 116 | 0 | #define Assert(val) inline_assertion_check<true>(val, std::source_location::current(), #val) |
|
91 | | // Parse the descriptor |
92 | 0 | FlatSigningProvider dummy_keys; |
93 | 0 | std::string dummy_err; |
94 | 0 | std::vector<std::unique_ptr<Descriptor>> descs = Parse(desc_info.descriptor, dummy_keys, dummy_err, /*require_checksum=*/true); |
95 | 0 | CHECK_NONFATAL(descs.size() == 1); // All of our descriptors should be valid, and not multipath Line | Count | Source | 113 | 0 | inline_check_non_fatal(condition, std::source_location::current(), #condition) |
|
96 | 0 | CHECK_NONFATAL(dummy_keys.keys.size() == 0); // No private keys should be present in our exported descriptors Line | Count | Source | 113 | 0 | inline_check_non_fatal(condition, std::source_location::current(), #condition) |
|
97 | | |
98 | | // Get the range if there is one |
99 | 0 | int32_t range_start = 0; |
100 | 0 | int32_t range_end = 0; |
101 | 0 | if (desc_info.range) { |
102 | 0 | range_start = desc_info.range->first; |
103 | 0 | range_end = desc_info.range->second; |
104 | 0 | } |
105 | |
|
106 | 0 | WalletDescriptor w_desc(std::move(descs.at(0)), desc_info.creation_time, range_start, range_end, desc_info.next_index); |
107 | | |
108 | | // For descriptors that cannot self expand (i.e. needs private keys or cache), retrieve the cache |
109 | 0 | uint256 desc_id = w_desc.id; |
110 | 0 | if (!w_desc.descriptor->CanSelfExpand()) { |
111 | 0 | DescriptorScriptPubKeyMan* desc_spkm = dynamic_cast<DescriptorScriptPubKeyMan*>(wallet.GetScriptPubKeyMan(desc_id)); |
112 | 0 | w_desc.cache = WITH_LOCK(desc_spkm->cs_desc_man, return desc_spkm->GetWalletDescriptor().cache); Line | Count | Source | 299 | 0 | #define WITH_LOCK(cs, code) (MaybeCheckNotHeld(cs), [&]() -> decltype(auto) { LOCK(cs); code; }()) |
|
113 | 0 | } |
114 | | |
115 | | // Add to the watchonly wallet |
116 | 0 | if (auto spkm_res = watchonly_wallet->AddWalletDescriptor(w_desc, dummy_keys, /*label=*/"", /*internal=*/false); !spkm_res) { |
117 | 0 | return util::Error{util::ErrorString(spkm_res)}; |
118 | 0 | } |
119 | | |
120 | | // Set active spkms as active |
121 | 0 | if (desc_info.active) { |
122 | | // Determine whether this descriptor is internal |
123 | | // This is only set for active spkms |
124 | 0 | bool internal = false; |
125 | 0 | if (desc_info.internal) { |
126 | 0 | internal = *desc_info.internal; |
127 | 0 | } |
128 | 0 | watchonly_wallet->AddActiveScriptPubKeyMan(desc_id, *Assert(w_desc.descriptor->GetOutputType()), internal); Line | Count | Source | 116 | 0 | #define Assert(val) inline_assertion_check<true>(val, std::source_location::current(), #val) |
|
129 | 0 | } |
130 | 0 | } |
131 | | |
132 | | // Copy locked coins that are persisted |
133 | 0 | for (const auto& [coin, persisted] : wallet.m_locked_coins) { |
134 | 0 | if (!persisted) continue; |
135 | 0 | watchonly_wallet->LockCoin(coin, persisted); |
136 | 0 | } |
137 | |
|
138 | 0 | { |
139 | | // Make a WalletBatch for the watchonly wallet so that everything else can be written atomically |
140 | 0 | WalletBatch watchonly_batch(watchonly_wallet->GetDatabase()); |
141 | 0 | if (!watchonly_batch.TxnBegin()) { |
142 | 0 | return util::Error{strprintf(_("Error: database transaction cannot be executed for new watchonly wallet %s"), watchonly_wallet->GetName())};Line | Count | Source | 1172 | 0 | #define strprintf tfm::format |
|
143 | 0 | } |
144 | | |
145 | | // Copy orderPosNext |
146 | 0 | watchonly_batch.WriteOrderPosNext(wallet.nOrderPosNext); |
147 | | |
148 | | // Write the best block locator to avoid rescanning on reload |
149 | 0 | CBlockLocator best_block_locator; |
150 | 0 | { |
151 | 0 | WalletBatch local_wallet_batch(wallet.GetDatabase()); |
152 | 0 | if (!local_wallet_batch.ReadBestBlock(best_block_locator)) { |
153 | 0 | return util::Error{_("Error: Unable to read wallet's best block locator record")}; |
154 | 0 | } |
155 | 0 | } |
156 | 0 | if (!watchonly_batch.WriteBestBlock(best_block_locator)) { |
157 | 0 | return util::Error{_("Error: Unable to write watchonly wallet best block locator record")}; |
158 | 0 | } |
159 | | |
160 | | // Copy the transactions |
161 | 0 | for (const auto& [txid, wtx] : wallet.mapWallet) { |
162 | 0 | if (!watchonly_wallet->LoadToWallet(txid, [&](CWalletTx& ins_wtx, bool new_tx) EXCLUSIVE_LOCKS_REQUIRED(watchonly_wallet->cs_wallet) { |
163 | 0 | if (!new_tx) return false; |
164 | 0 | ins_wtx.SetTx(wtx.tx); |
165 | 0 | ins_wtx.CopyFrom(wtx); |
166 | 0 | return true; |
167 | 0 | })) { |
168 | 0 | return util::Error{strprintf(_("Error: Could not add tx %s to watchonly wallet"), txid.GetHex())};Line | Count | Source | 1172 | 0 | #define strprintf tfm::format |
|
169 | 0 | } |
170 | 0 | watchonly_batch.WriteTx(watchonly_wallet->mapWallet.at(txid)); |
171 | 0 | } |
172 | | |
173 | | // Copy address book |
174 | 0 | for (const auto& [dest, entry] : wallet.m_address_book) { |
175 | 0 | auto address{EncodeDestination(dest)}; |
176 | 0 | if (entry.purpose) watchonly_batch.WritePurpose(address, PurposeToString(*entry.purpose)); |
177 | 0 | if (entry.label) watchonly_batch.WriteName(address, *entry.label); |
178 | 0 | for (const auto& [id, request] : entry.receive_requests) { |
179 | 0 | watchonly_batch.WriteAddressReceiveRequest(dest, id, request); |
180 | 0 | } |
181 | 0 | if (entry.previously_spent) watchonly_batch.WriteAddressPreviouslySpent(dest, true); |
182 | 0 | } |
183 | |
|
184 | 0 | if (!watchonly_batch.TxnCommit()) { |
185 | 0 | return util::Error{_("Error: cannot commit db transaction for watchonly wallet export")}; |
186 | 0 | } |
187 | 0 | } |
188 | | |
189 | | // Make a backup of this wallet at the specified destination directory |
190 | 0 | if (!watchonly_wallet->BackupWallet(fs::PathToString(destination))) { |
191 | 0 | return util::Error{_("Error: Unable to write the exported wallet")}; |
192 | 0 | } |
193 | 0 | success = true; |
194 | 0 | } |
195 | | |
196 | 0 | return fs::PathToString(destination); |
197 | 0 | } |
198 | | } // namespace wallet |