/tmp/bitcoin/src/wallet/test/wallet_tests.cpp
Line | Count | Source |
1 | | // Copyright (c) 2012-present The Bitcoin Core developers |
2 | | // Distributed under the MIT software license, see the accompanying |
3 | | // file COPYING or http://www.opensource.org/licenses/mit-license.php. |
4 | | |
5 | | #include <wallet/scan.h> |
6 | | #include <wallet/wallet.h> |
7 | | |
8 | | #include <array> |
9 | | #include <cstddef> |
10 | | #include <cstdint> |
11 | | #include <future> |
12 | | #include <limits> |
13 | | #include <memory> |
14 | | #include <optional> |
15 | | #include <stdexcept> |
16 | | #include <string> |
17 | | #include <utility> |
18 | | #include <vector> |
19 | | |
20 | | #include <addresstype.h> |
21 | | #include <blockfilter.h> |
22 | | #include <chain.h> |
23 | | #include <consensus/tx_verify.h> |
24 | | #include <consensus/validation.h> |
25 | | #include <index/blockfilterindex.h> |
26 | | #include <interfaces/chain.h> |
27 | | #include <key_io.h> |
28 | | #include <node/blockstorage.h> |
29 | | #include <node/types.h> |
30 | | #include <policy/feerate.h> |
31 | | #include <policy/policy.h> |
32 | | #include <rpc/server.h> |
33 | | #include <script/descriptor.h> |
34 | | #include <script/solver.h> |
35 | | #include <test/util/common.h> |
36 | | #include <test/util/logging.h> |
37 | | #include <test/util/random.h> |
38 | | #include <test/util/setup_common.h> |
39 | | #include <test/util/time.h> |
40 | | #include <util/byte_units.h> |
41 | | #include <util/check.h> |
42 | | #include <util/strencodings.h> |
43 | | #include <util/translation.h> |
44 | | #include <validation.h> |
45 | | #include <validationinterface.h> |
46 | | #include <wallet/coincontrol.h> |
47 | | #include <wallet/context.h> |
48 | | #include <wallet/imports.h> |
49 | | #include <wallet/receive.h> |
50 | | #include <wallet/spend.h> |
51 | | #include <wallet/test/util.h> |
52 | | #include <wallet/test/wallet_test_fixture.h> |
53 | | |
54 | | #include <boost/test/unit_test.hpp> |
55 | | |
56 | | using node::MAX_BLOCKFILE_SIZE; |
57 | | |
58 | | namespace wallet { |
59 | | |
60 | | // Ensure that fee levels defined in the wallet are at least as high |
61 | | // as the default levels for node policy. |
62 | | static_assert(DEFAULT_TRANSACTION_MINFEE >= DEFAULT_MIN_RELAY_TX_FEE, "wallet minimum fee is smaller than default relay fee"); |
63 | | static_assert(WALLET_INCREMENTAL_RELAY_FEE >= DEFAULT_INCREMENTAL_RELAY_FEE, "wallet incremental fee is smaller than default incremental relay fee"); |
64 | | |
65 | | BOOST_FIXTURE_TEST_SUITE(wallet_tests, WalletTestingSetup) |
66 | | |
67 | | static CMutableTransaction TestSimpleSpend(const CTransaction& from, uint32_t index, const CKey& key, const CScript& pubkey) |
68 | 5 | { |
69 | 5 | CMutableTransaction mtx; |
70 | 5 | mtx.vout.emplace_back(from.vout[index].nValue - DEFAULT_TRANSACTION_MAXFEE, pubkey); |
71 | 5 | mtx.vin.push_back({CTxIn{from.GetHash(), index}}); |
72 | 5 | FillableSigningProvider keystore; |
73 | 5 | keystore.AddKey(key); |
74 | 5 | std::map<COutPoint, Coin> coins; |
75 | 5 | coins[mtx.vin[0].prevout].out = from.vout[index]; |
76 | 5 | std::map<int, bilingual_str> input_errors; |
77 | 5 | BOOST_CHECK(SignTransaction(mtx, &keystore, coins, {.sighash_type = SIGHASH_ALL}, input_errors)); |
78 | 5 | return mtx; |
79 | 5 | } |
80 | | |
81 | | static bool BroadcastTestSimpleSpend(interfaces::Chain& chain, ChainstateManager& chainman, const CMutableTransaction& tx, std::string& error) |
82 | 2 | { |
83 | 2 | const auto tx_ref{MakeTransactionRef(tx)}; |
84 | 2 | const auto tx_sigops = WITH_LOCK(::cs_main, return GetTransactionSigOpCost( |
85 | 2 | *tx_ref, chainman.ActiveChainstate().CoinsTip(), STANDARD_SCRIPT_VERIFY_FLAGS)); |
86 | 2 | const auto tx_vsize{GetVirtualTransactionSize(*tx_ref, tx_sigops, nBytesPerSigOp)}; |
87 | 2 | const auto tx_feerate{CFeeRate{DEFAULT_TRANSACTION_MAXFEE, static_cast<int32_t>(tx_vsize)}}; |
88 | | // TestSimpleSpend pays a high fee; use a limit just above its feerate. |
89 | 2 | const auto tx_feerate_limit{CFeeRate{tx_feerate.GetFeePerK() + 1}}; |
90 | 2 | return chain.broadcastTransaction(tx_ref, DEFAULT_TRANSACTION_MAXFEE, tx_feerate_limit, node::TxBroadcast::MEMPOOL_NO_BROADCAST, error); |
91 | 2 | } |
92 | | |
93 | | static void AddKey(CWallet& wallet, const CKey& key) |
94 | 21 | { |
95 | 21 | LOCK(wallet.cs_wallet); |
96 | 21 | FlatSigningProvider provider; |
97 | 21 | std::string error; |
98 | 21 | auto descs = Parse("combo(" + EncodeSecret(key) + ")", provider, error, /* require_checksum=*/ false); |
99 | 21 | assert(descs.size() == 1); |
100 | 21 | auto& desc = descs.at(0); |
101 | 21 | WalletDescriptor w_desc(std::move(desc), 0, 0, 1, 1); |
102 | 21 | Assert(wallet.AddWalletDescriptor(w_desc, provider, "", false)); |
103 | 21 | } |
104 | | |
105 | | BOOST_AUTO_TEST_CASE(reject_invalid_descriptor_ranges) |
106 | 1 | { |
107 | 1 | const int height{*Assert(m_node.chain->getHeight())}; |
108 | 1 | { |
109 | 1 | LOCK(m_wallet.cs_wallet); |
110 | 1 | m_wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); |
111 | 1 | m_wallet.SetLastBlockProcessed(height, m_node.chain->getBlockHash(height)); |
112 | 1 | } |
113 | | |
114 | 1 | CExtKey ext_key; |
115 | 1 | ext_key.SetSeed(std::array<std::byte, 32>{}); |
116 | 1 | const std::string descriptor_without_checksum{"wpkh(" + EncodeExtKey(ext_key) + "/*)"}; |
117 | 1 | const std::string descriptor{descriptor_without_checksum + "#" + GetDescriptorChecksum(descriptor_without_checksum)}; |
118 | | |
119 | 1 | const std::array invalid_ranges{ |
120 | 1 | std::pair{std::pair<int64_t, int64_t>{2, 1}, "Range specified as [begin,end] must not have begin after end"}, |
121 | 1 | std::pair{std::pair<int64_t, int64_t>{-1, 10}, "Range should be greater or equal than 0"}, |
122 | 1 | std::pair{std::pair<int64_t, int64_t>{0, 1'000'000}, "Range is too large"}, |
123 | 1 | std::pair{std::pair<int64_t, int64_t>{0, std::numeric_limits<int64_t>::max()}, "End of range is too high"}, |
124 | 1 | std::pair{std::pair<int64_t, int64_t>{0, 1LL << 31}, "End of range is too high"}, |
125 | 1 | }; |
126 | | |
127 | 5 | for (const auto& [range, expected_error] : invalid_ranges) { |
128 | 5 | std::vector requests{ImportDescriptorRequest{ |
129 | 5 | .descriptor = descriptor, |
130 | 5 | .label = {}, |
131 | 5 | .timestamp = 0, |
132 | 5 | .active = false, |
133 | 5 | .internal = std::nullopt, |
134 | 5 | .range = range, |
135 | 5 | .next_index = std::nullopt, |
136 | 5 | }}; |
137 | 5 | const auto results{ProcessDescriptorsImport(m_wallet, requests)}; |
138 | 5 | BOOST_REQUIRE_EQUAL(results.size(), 1U); |
139 | 5 | BOOST_REQUIRE(results.front().error.has_value()); |
140 | 5 | BOOST_CHECK(results.front().error->wallet_error.code == WalletErrorCode::InvalidParameter); |
141 | 5 | BOOST_CHECK_EQUAL(results.front().error->wallet_error.message.original, expected_error); |
142 | 5 | BOOST_CHECK(!results.front().error->is_general_error); |
143 | 5 | } |
144 | 1 | } |
145 | | |
146 | | namespace { |
147 | | struct EncryptionFailureSetup : TestingSetup { |
148 | | WalletContext context; |
149 | | FaultInjectingDatabase* fail_db{nullptr}; |
150 | | std::shared_ptr<CWallet> wallet; |
151 | | FakeNodeClock clock; // Frozen time makes EncryptMasterKey use the default KDF iteration count |
152 | | |
153 | | EncryptionFailureSetup() |
154 | 7 | { |
155 | 7 | context.args = &m_args; |
156 | 7 | m_args.ForceSetArg("-keypool", "1"); // Failure injection does not depend on keypool depth |
157 | 7 | context.chain = m_node.chain.get(); |
158 | 7 | RecreateWallet(WALLET_FLAG_DESCRIPTORS); |
159 | 7 | } |
160 | | |
161 | | void RecreateWallet(uint64_t create_flags) |
162 | 12 | { |
163 | 12 | if (wallet) TestUnloadWallet(std::move(wallet)); |
164 | 12 | auto database{std::make_unique<FaultInjectingDatabase>()}; |
165 | 12 | fail_db = database.get(); |
166 | 12 | wallet = TestCreateWallet(std::move(database), context, create_flags); |
167 | 12 | } |
168 | | |
169 | 7 | ~EncryptionFailureSetup() { TestUnloadWallet(std::move(wallet)); } |
170 | | }; |
171 | | } // namespace |
172 | | |
173 | | BOOST_FIXTURE_TEST_CASE(encrypt_wallet_master_key_write_failure, EncryptionFailureSetup) |
174 | 1 | { |
175 | 1 | AddKey(*wallet, GenerateRandomKey()); |
176 | | |
177 | 1 | fail_db->FailNextWrite(DBKeys::MASTER_KEY); // The injected failure affects only the first attempt |
178 | 2 | for (bool success : {false, true}) { |
179 | 2 | BOOST_CHECK_EQUAL(wallet->EncryptWallet("passphrase"), success); |
180 | 2 | BOOST_CHECK_EQUAL(wallet->HasEncryptionKeys(), success); |
181 | 2 | BOOST_CHECK_EQUAL(wallet->HaveCryptedKeys(), success); |
182 | 2 | BOOST_CHECK_EQUAL(fail_db->HasRecordType(DBKeys::MASTER_KEY), success); |
183 | 2 | BOOST_CHECK_EQUAL(fail_db->HasRecordType(DBKeys::WALLETDESCRIPTORKEY), !success); |
184 | 2 | BOOST_CHECK_EQUAL(fail_db->HasRecordType(DBKeys::WALLETDESCRIPTORCKEY), success); |
185 | 2 | } |
186 | 1 | } |
187 | | |
188 | | BOOST_FIXTURE_TEST_CASE(encrypt_wallet_commit_failure, EncryptionFailureSetup) |
189 | 1 | { |
190 | 1 | AddKey(*wallet, GenerateRandomKey()); |
191 | | |
192 | 1 | fail_db->FailNextCommit(); // The injected failure affects only the first attempt |
193 | 1 | test_only_CheckFailuresAreExceptionsNotAborts mock_checks; // Keep abort regressions observable |
194 | 1 | BOOST_CHECK(!wallet->EncryptWallet("passphrase")); |
195 | 1 | BOOST_CHECK(!wallet->HasEncryptionKeys()); |
196 | 1 | BOOST_CHECK(!wallet->HaveCryptedKeys()); |
197 | 1 | BOOST_CHECK(!fail_db->HasRecordType(DBKeys::MASTER_KEY)); |
198 | 1 | BOOST_CHECK( fail_db->HasRecordType(DBKeys::WALLETDESCRIPTORKEY)); |
199 | 1 | BOOST_CHECK(!fail_db->HasRecordType(DBKeys::WALLETDESCRIPTORCKEY)); |
200 | 1 | BOOST_CHECK( wallet->EncryptWallet("passphrase")); |
201 | 1 | BOOST_CHECK( wallet->HasEncryptionKeys()); |
202 | 1 | BOOST_CHECK( wallet->HaveCryptedKeys()); |
203 | 1 | BOOST_CHECK( fail_db->HasRecordType(DBKeys::MASTER_KEY)); |
204 | 1 | BOOST_CHECK(!fail_db->HasRecordType(DBKeys::WALLETDESCRIPTORKEY)); |
205 | 1 | BOOST_CHECK( fail_db->HasRecordType(DBKeys::WALLETDESCRIPTORCKEY)); |
206 | 1 | } |
207 | | |
208 | | BOOST_FIXTURE_TEST_CASE(encrypt_wallet_descriptor_key_write_failure, EncryptionFailureSetup) |
209 | 1 | { |
210 | 1 | AddKey(*wallet, GenerateRandomKey()); |
211 | | |
212 | 1 | fail_db->FailNextWrite(DBKeys::WALLETDESCRIPTORCKEY, /*match_skip_count=*/1); // Only one write fails |
213 | 2 | for (bool success : {false, true}) { |
214 | 2 | BOOST_CHECK_EQUAL(wallet->EncryptWallet("passphrase"), success); |
215 | 2 | BOOST_CHECK_EQUAL(wallet->HasEncryptionKeys(), success); |
216 | 2 | BOOST_CHECK_EQUAL(wallet->HaveCryptedKeys(), success); |
217 | 2 | BOOST_CHECK_EQUAL(fail_db->HasRecordType(DBKeys::MASTER_KEY), success); |
218 | 2 | BOOST_CHECK_EQUAL(fail_db->HasRecordType(DBKeys::WALLETDESCRIPTORKEY), !success); |
219 | 2 | BOOST_CHECK_EQUAL(fail_db->HasRecordType(DBKeys::WALLETDESCRIPTORCKEY), success); |
220 | 2 | } |
221 | 1 | } |
222 | | |
223 | | BOOST_FIXTURE_TEST_CASE(encrypt_wallet_descriptor_key_erase_failure, EncryptionFailureSetup) |
224 | 1 | { |
225 | 1 | AddKey(*wallet, GenerateRandomKey()); |
226 | | |
227 | 1 | fail_db->FailNextErase(DBKeys::WALLETDESCRIPTORKEY); // Only one erase fails |
228 | 2 | for (bool success : {false, true}) { |
229 | 2 | BOOST_CHECK_EQUAL(wallet->EncryptWallet("passphrase"), success); |
230 | 2 | BOOST_CHECK_EQUAL(wallet->HasEncryptionKeys(), success); |
231 | 2 | BOOST_CHECK_EQUAL(wallet->HaveCryptedKeys(), success); |
232 | 2 | BOOST_CHECK_EQUAL(fail_db->HasRecordType(DBKeys::MASTER_KEY), success); |
233 | 2 | BOOST_CHECK_EQUAL(fail_db->HasRecordType(DBKeys::WALLETDESCRIPTORKEY), !success); |
234 | 2 | BOOST_CHECK_EQUAL(fail_db->HasRecordType(DBKeys::WALLETDESCRIPTORCKEY), success); |
235 | 2 | } |
236 | 1 | } |
237 | | |
238 | | BOOST_FIXTURE_TEST_CASE(change_passphrase_master_key_write_failure, EncryptionFailureSetup) |
239 | 1 | { |
240 | 1 | AddKey(*wallet, GenerateRandomKey()); |
241 | 1 | BOOST_REQUIRE(wallet->EncryptWallet("old_pass")); |
242 | 1 | BOOST_REQUIRE(wallet->IsLocked()); |
243 | 1 | const auto master_key_record{fail_db->GetRecordValue(DBKeys::MASTER_KEY)}; |
244 | 1 | BOOST_REQUIRE(master_key_record); |
245 | | |
246 | 1 | fail_db->FailNextWrite(DBKeys::MASTER_KEY); // The injected failure affects only the first attempt |
247 | 1 | const auto changed{wallet->ChangeWalletPassphrase("old_pass", "new_pass")}; |
248 | 1 | BOOST_REQUIRE(!changed); |
249 | 1 | BOOST_CHECK_EQUAL(changed.error().code, WalletErrorCode::GenericError); |
250 | 1 | BOOST_CHECK_EQUAL(changed.error().message.original, "Error: Writing the new encryption key to the wallet database failed"); |
251 | 1 | BOOST_CHECK( wallet->IsLocked()); |
252 | 1 | BOOST_CHECK( fail_db->GetRecordValue(DBKeys::MASTER_KEY) == master_key_record); |
253 | 1 | BOOST_CHECK( wallet->Unlock("old_pass")); |
254 | 1 | wallet->Lock(); |
255 | 1 | BOOST_CHECK(!wallet->Unlock("new_pass")); |
256 | 1 | BOOST_CHECK( wallet->ChangeWalletPassphrase("old_pass", "new_pass")); |
257 | 1 | BOOST_CHECK( wallet->IsLocked()); |
258 | 1 | BOOST_CHECK( fail_db->GetRecordValue(DBKeys::MASTER_KEY) != master_key_record); |
259 | 1 | BOOST_CHECK( wallet->Unlock("new_pass")); |
260 | 1 | wallet->Lock(); |
261 | 1 | const auto unlocked{wallet->Unlock("old_pass")}; |
262 | 1 | BOOST_REQUIRE(!unlocked); |
263 | 1 | BOOST_CHECK_EQUAL(unlocked.error().code, WalletErrorCode::PassphraseIncorrect); |
264 | 1 | BOOST_CHECK_EQUAL(unlocked.error().message.original, "Error: The wallet passphrase entered was incorrect."); |
265 | 1 | } |
266 | | |
267 | | BOOST_FIXTURE_TEST_CASE(add_encrypted_descriptor_key_without_plaintext_record, EncryptionFailureSetup) |
268 | 1 | { |
269 | 1 | RecreateWallet(WALLET_FLAG_DESCRIPTORS | WALLET_FLAG_BLANK_WALLET); |
270 | 1 | BOOST_REQUIRE(wallet->EncryptWallet("passphrase")); |
271 | 1 | BOOST_REQUIRE(wallet->Unlock("passphrase")); |
272 | | |
273 | 1 | AddKey(*wallet, GenerateRandomKey()); |
274 | 1 | BOOST_CHECK( wallet->HaveCryptedKeys()); |
275 | 1 | BOOST_CHECK( fail_db->HasRecordType(DBKeys::WALLETDESCRIPTORCKEY)); |
276 | 1 | BOOST_CHECK(!fail_db->HasRecordType(DBKeys::WALLETDESCRIPTORKEY)); |
277 | 1 | } |
278 | | |
279 | | BOOST_FIXTURE_TEST_CASE(add_descriptor_key_database_failure, EncryptionFailureSetup) |
280 | 1 | { |
281 | 1 | enum class Failure { PlaintextWrite, EncryptedWrite, Erase, Commit }; |
282 | 4 | for (auto failure : {Failure::PlaintextWrite, Failure::EncryptedWrite, Failure::Erase, Failure::Commit}) { |
283 | 4 | const bool encrypted{failure != Failure::PlaintextWrite}; |
284 | 4 | RecreateWallet(WALLET_FLAG_DESCRIPTORS | WALLET_FLAG_BLANK_WALLET); |
285 | 4 | CKey key{GenerateRandomKey()}; |
286 | | // Add a public descriptor first so the private-key update exercises an existing live manager |
287 | 4 | auto* spkm{CreateDescriptor(*wallet, strprintf("combo(%s)", HexStr(key.GetPubKey())), /*success=*/true)}; |
288 | 4 | WalletDescriptor descriptor{WITH_LOCK(spkm->cs_desc_man, return spkm->GetWalletDescriptor())}; |
289 | 4 | FlatSigningProvider provider; |
290 | 4 | provider.keys.emplace(key.GetPubKey().GetID(), key); |
291 | 8 | auto add_key{[&] { |
292 | 8 | LOCK(wallet->cs_wallet); |
293 | 8 | return wallet->AddWalletDescriptor(descriptor, provider, /*label=*/"", /*internal=*/false); |
294 | 8 | }}; |
295 | 12 | auto has_key{[&] { |
296 | 12 | LOCK(wallet->cs_wallet); |
297 | 12 | return wallet->GetKey(key.GetPubKey().GetID()).has_value(); |
298 | 12 | }}; |
299 | 4 | if (encrypted) { |
300 | 3 | BOOST_REQUIRE(wallet->EncryptWallet("passphrase")); |
301 | 3 | BOOST_REQUIRE(wallet->Unlock("passphrase")); |
302 | 3 | } |
303 | 4 | BOOST_CHECK(!has_key()); |
304 | | |
305 | 4 | const std::string record_type{encrypted ? DBKeys::WALLETDESCRIPTORCKEY : DBKeys::WALLETDESCRIPTORKEY}; |
306 | 4 | if (failure == Failure::Erase) { |
307 | 1 | fail_db->FailNextErase(DBKeys::WALLETDESCRIPTORKEY); |
308 | 3 | } else if (failure == Failure::Commit) { |
309 | 1 | fail_db->FailNextCommit(); |
310 | 2 | } else { |
311 | 2 | fail_db->FailNextWrite(record_type); |
312 | 2 | } |
313 | 4 | BOOST_CHECK_EXCEPTION((void)add_key(), std::runtime_error, HasReason{"UpdateWithSigningProvider: writing descriptor private key failed"}); |
314 | 4 | BOOST_CHECK(!has_key()); |
315 | 4 | BOOST_CHECK(!fail_db->HasRecordType(record_type)); |
316 | 4 | BOOST_CHECK( add_key()); |
317 | 4 | BOOST_CHECK( has_key()); |
318 | 4 | BOOST_CHECK( fail_db->HasRecordType(record_type)); |
319 | 4 | } |
320 | 1 | } |
321 | | |
322 | | BOOST_FIXTURE_TEST_CASE(update_non_range_descriptor, TestingSetup) |
323 | 1 | { |
324 | 1 | CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); |
325 | 1 | { |
326 | 1 | LOCK(wallet.cs_wallet); |
327 | 1 | wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); |
328 | 1 | auto key{GenerateRandomKey()}; |
329 | 1 | auto desc_str{"combo(" + EncodeSecret(key) + ")"}; |
330 | 1 | FlatSigningProvider provider; |
331 | 1 | std::string error; |
332 | 1 | auto descs{Parse(desc_str, provider, error, /* require_checksum=*/ false)}; |
333 | 1 | auto& desc{descs.at(0)}; |
334 | 1 | WalletDescriptor w_desc{std::move(desc), 0, 0, 0, 0}; |
335 | 1 | BOOST_CHECK(wallet.AddWalletDescriptor(w_desc, provider, "", false)); |
336 | | // Wallet should update the non-range descriptor successfully |
337 | 1 | BOOST_CHECK(wallet.AddWalletDescriptor(w_desc, provider, "", false)); |
338 | 1 | } |
339 | 1 | } |
340 | | |
341 | | BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup) |
342 | 1 | { |
343 | | // Cap last block file size, and mine new block in a new block file. |
344 | 1 | CBlockIndex* oldTip = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip()); |
345 | 1 | WITH_LOCK(::cs_main, m_node.chainman->m_blockman.GetBlockFileInfo(oldTip->GetBlockPos().nFile)->nSize = MAX_BLOCKFILE_SIZE); |
346 | 1 | CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); |
347 | 1 | CBlockIndex* newTip = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip()); |
348 | | |
349 | | // Verify Scan fails to read an unknown start block. |
350 | 1 | { |
351 | 1 | CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); |
352 | 1 | { |
353 | 1 | LOCK(wallet.cs_wallet); |
354 | 1 | LOCK(Assert(m_node.chainman)->GetMutex()); |
355 | 1 | wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); |
356 | 1 | wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash()); |
357 | 1 | } |
358 | 1 | AddKey(wallet, coinbaseKey); |
359 | 1 | WalletRescanReserver reserver(wallet); |
360 | 1 | reserver.reserve(); |
361 | 1 | ScanResult result = wallet.Scanner().Scan(/*start_block=*/{}, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false); |
362 | 1 | BOOST_CHECK_EQUAL(result.status, ScanResult::FAILURE); |
363 | 1 | BOOST_CHECK(result.last_failed_block.IsNull()); |
364 | 1 | BOOST_CHECK(result.last_scanned_block.IsNull()); |
365 | 1 | BOOST_CHECK(!result.last_scanned_height); |
366 | 1 | BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 0); |
367 | 1 | } |
368 | | |
369 | | // Verify Scan picks up transactions in both the old |
370 | | // and new block files. |
371 | 1 | { |
372 | 1 | CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); |
373 | 1 | { |
374 | 1 | LOCK(wallet.cs_wallet); |
375 | 1 | LOCK(Assert(m_node.chainman)->GetMutex()); |
376 | 1 | wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); |
377 | 1 | wallet.SetLastBlockProcessed(newTip->nHeight, newTip->GetBlockHash()); |
378 | 1 | } |
379 | 1 | AddKey(wallet, coinbaseKey); |
380 | 1 | WalletRescanReserver reserver(wallet); |
381 | 1 | std::chrono::steady_clock::time_point fake_time; |
382 | 7 | reserver.setNow([&] { fake_time += 60s; return fake_time; }); |
383 | 1 | reserver.reserve(); |
384 | | |
385 | 1 | { |
386 | 1 | CBlockLocator locator; |
387 | 1 | BOOST_CHECK(WalletBatch{wallet.GetDatabase()}.ReadBestBlock(locator)); |
388 | 1 | BOOST_REQUIRE(!locator.IsNull()); |
389 | 1 | BOOST_CHECK(locator.vHave.front() == newTip->GetBlockHash()); |
390 | 1 | } |
391 | | |
392 | 1 | ScanResult result = wallet.Scanner().Scan(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*save_progress=*/true); |
393 | 1 | BOOST_CHECK_EQUAL(result.status, ScanResult::SUCCESS); |
394 | 1 | BOOST_CHECK(result.last_failed_block.IsNull()); |
395 | 1 | BOOST_CHECK_EQUAL(result.last_scanned_block, newTip->GetBlockHash()); |
396 | 1 | BOOST_CHECK_EQUAL(*result.last_scanned_height, newTip->nHeight); |
397 | 1 | BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 100 * COIN); |
398 | | |
399 | 1 | { |
400 | 1 | CBlockLocator locator; |
401 | 1 | BOOST_CHECK(WalletBatch{wallet.GetDatabase()}.ReadBestBlock(locator)); |
402 | 1 | BOOST_REQUIRE(!locator.IsNull()); |
403 | 1 | BOOST_CHECK(locator.vHave.front() == newTip->GetBlockHash()); |
404 | 1 | } |
405 | 1 | } |
406 | | |
407 | | // Prune the older block file. |
408 | 1 | int file_number; |
409 | 1 | { |
410 | 1 | LOCK(cs_main); |
411 | 1 | file_number = oldTip->GetBlockPos().nFile; |
412 | 1 | Assert(m_node.chainman)->m_blockman.PruneOneBlockFile(file_number); |
413 | 1 | } |
414 | 1 | m_node.chainman->m_blockman.UnlinkPrunedFiles({file_number}); |
415 | | |
416 | | // Verify Scan only picks transactions in the new block |
417 | | // file. |
418 | 1 | { |
419 | 1 | CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); |
420 | 1 | { |
421 | 1 | LOCK(wallet.cs_wallet); |
422 | 1 | LOCK(Assert(m_node.chainman)->GetMutex()); |
423 | 1 | wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); |
424 | 1 | wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash()); |
425 | 1 | } |
426 | 1 | AddKey(wallet, coinbaseKey); |
427 | 1 | WalletRescanReserver reserver(wallet); |
428 | 1 | reserver.reserve(); |
429 | 1 | ScanResult result = wallet.Scanner().Scan(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*save_progress=*/false); |
430 | 1 | BOOST_CHECK_EQUAL(result.status, ScanResult::FAILURE); |
431 | 1 | BOOST_CHECK_EQUAL(result.last_failed_block, oldTip->GetBlockHash()); |
432 | 1 | BOOST_CHECK_EQUAL(result.last_scanned_block, newTip->GetBlockHash()); |
433 | 1 | BOOST_CHECK_EQUAL(*result.last_scanned_height, newTip->nHeight); |
434 | 1 | BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 50 * COIN); |
435 | 1 | } |
436 | | |
437 | | // Prune the remaining block file. |
438 | 1 | { |
439 | 1 | LOCK(cs_main); |
440 | 1 | file_number = newTip->GetBlockPos().nFile; |
441 | 1 | Assert(m_node.chainman)->m_blockman.PruneOneBlockFile(file_number); |
442 | 1 | } |
443 | 1 | m_node.chainman->m_blockman.UnlinkPrunedFiles({file_number}); |
444 | | |
445 | | // Verify Scan scans no blocks. |
446 | 1 | { |
447 | 1 | CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); |
448 | 1 | { |
449 | 1 | LOCK(wallet.cs_wallet); |
450 | 1 | LOCK(Assert(m_node.chainman)->GetMutex()); |
451 | 1 | wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); |
452 | 1 | wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash()); |
453 | 1 | } |
454 | 1 | AddKey(wallet, coinbaseKey); |
455 | 1 | WalletRescanReserver reserver(wallet); |
456 | 1 | reserver.reserve(); |
457 | 1 | ScanResult result = wallet.Scanner().Scan(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*save_progress=*/false); |
458 | 1 | BOOST_CHECK_EQUAL(result.status, ScanResult::FAILURE); |
459 | 1 | BOOST_CHECK_EQUAL(result.last_failed_block, newTip->GetBlockHash()); |
460 | 1 | BOOST_CHECK(result.last_scanned_block.IsNull()); |
461 | 1 | BOOST_CHECK(!result.last_scanned_height); |
462 | 1 | BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 0); |
463 | 1 | } |
464 | 1 | } |
465 | | |
466 | | BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_reorged_block, TestChain100Setup) |
467 | 1 | { |
468 | 1 | BOOST_REQUIRE(InitBlockFilterIndex([&]{ return interfaces::MakeChain(m_node); }, BlockFilterType::BASIC, 1_MiB, /*f_memory=*/true)); |
469 | 1 | BlockFilterIndex& filter_index{*Assert(GetBlockFilterIndex(BlockFilterType::BASIC))}; |
470 | 1 | BOOST_REQUIRE(filter_index.Init()); |
471 | 1 | filter_index.Sync(); |
472 | | |
473 | | // Reorg the tip out of the active chain: invalidate it, then mine a |
474 | | // longer replacement branch paying a script unrelated to the wallets |
475 | | // below. |
476 | 1 | CBlockIndex* stale_block = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip()); |
477 | 1 | const uint256 stale_hash{stale_block->GetBlockHash()}; |
478 | 1 | const int stale_height{stale_block->nHeight}; |
479 | 1 | BlockValidationState state; |
480 | 1 | BOOST_REQUIRE(m_node.chainman->ActiveChainstate().InvalidateBlock(state, stale_block)); |
481 | 1 | const CScript replacement_script{GetScriptForRawPubKey(GenerateRandomKey().GetPubKey())}; |
482 | 1 | CreateAndProcessBlock({}, replacement_script); |
483 | 1 | CreateAndProcessBlock({}, replacement_script); |
484 | 1 | BOOST_REQUIRE(filter_index.BlockUntilSyncedToCurrentChain()); |
485 | 1 | { |
486 | 1 | LOCK(Assert(m_node.chainman)->GetMutex()); |
487 | 1 | BOOST_REQUIRE(!m_node.chainman->ActiveChain().Contains(*stale_block)); |
488 | 1 | BOOST_REQUIRE_EQUAL(m_node.chainman->ActiveChain().Height(), stale_height + 1); |
489 | 1 | } |
490 | | |
491 | 1 | { |
492 | 1 | BlockFilter filter; |
493 | 1 | BOOST_REQUIRE(filter_index.LookupFilter(stale_block, filter)); |
494 | 1 | } |
495 | | |
496 | | // Test wallet whose scripts do not match the stale block's filter. |
497 | 1 | { |
498 | 1 | CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); |
499 | 1 | { |
500 | 1 | LOCK(wallet.cs_wallet); |
501 | 1 | LOCK(Assert(m_node.chainman)->GetMutex()); |
502 | 1 | wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); |
503 | 1 | wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash()); |
504 | 1 | } |
505 | 1 | WalletRescanReserver reserver(wallet); |
506 | 1 | reserver.reserve(); |
507 | 1 | ScanResult result = wallet.Scanner().Scan(stale_hash, stale_height, /*max_height=*/{}, reserver, /*save_progress=*/false); |
508 | 1 | BOOST_CHECK_EQUAL(result.status, ScanResult::SUCCESS); |
509 | 1 | BOOST_CHECK(result.last_failed_block.IsNull()); |
510 | 1 | BOOST_CHECK_EQUAL(result.last_scanned_block, stale_hash); |
511 | 1 | BOOST_CHECK_EQUAL(*result.last_scanned_height, stale_height); |
512 | 1 | } |
513 | | |
514 | | // Test wallet whose scripts do match the stale block's filter. |
515 | 1 | { |
516 | 1 | CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); |
517 | 1 | { |
518 | 1 | LOCK(wallet.cs_wallet); |
519 | 1 | LOCK(Assert(m_node.chainman)->GetMutex()); |
520 | 1 | wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); |
521 | 1 | wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash()); |
522 | 1 | } |
523 | 1 | AddKey(wallet, coinbaseKey); // the stale block's coinbase pays coinbaseKey |
524 | 1 | WalletRescanReserver reserver(wallet); |
525 | 1 | reserver.reserve(); |
526 | 1 | ScanResult result = wallet.Scanner().Scan(stale_hash, stale_height, /*max_height=*/{}, reserver, /*save_progress=*/false); |
527 | 1 | BOOST_CHECK_EQUAL(result.status, ScanResult::FAILURE); |
528 | 1 | BOOST_CHECK_EQUAL(result.last_failed_block, stale_hash); |
529 | 1 | BOOST_CHECK(result.last_scanned_block.IsNull()); |
530 | 1 | BOOST_CHECK(!result.last_scanned_height); |
531 | 1 | BOOST_CHECK(WITH_LOCK(wallet.cs_wallet, return wallet.mapWallet.empty())); |
532 | 1 | } |
533 | | |
534 | | // Prune the stale block's file — the block is now not active AND unreadable. |
535 | 1 | int file_number; |
536 | 1 | { |
537 | 1 | LOCK(cs_main); |
538 | 1 | file_number = stale_block->GetBlockPos().nFile; |
539 | 1 | Assert(m_node.chainman)->m_blockman.PruneOneBlockFile(file_number); |
540 | 1 | } |
541 | 1 | m_node.chainman->m_blockman.UnlinkPrunedFiles({file_number}); |
542 | | |
543 | 1 | { |
544 | 1 | CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); |
545 | 1 | { |
546 | 1 | LOCK(wallet.cs_wallet); |
547 | 1 | LOCK(Assert(m_node.chainman)->GetMutex()); |
548 | 1 | wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); |
549 | 1 | wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash()); |
550 | 1 | } |
551 | 1 | AddKey(wallet, coinbaseKey); |
552 | 1 | WalletRescanReserver reserver(wallet); |
553 | 1 | reserver.reserve(); |
554 | 1 | ScanResult result = wallet.Scanner().Scan(stale_hash, stale_height, /*max_height=*/{}, reserver, /*save_progress=*/false); |
555 | 1 | BOOST_CHECK_EQUAL(result.status, ScanResult::FAILURE); |
556 | 1 | BOOST_CHECK_EQUAL(result.last_failed_block, stale_hash); |
557 | 1 | BOOST_CHECK(result.last_scanned_block.IsNull()); |
558 | 1 | BOOST_CHECK(!result.last_scanned_height); |
559 | 1 | BOOST_CHECK(WITH_LOCK(wallet.cs_wallet, return wallet.mapWallet.empty())); |
560 | 1 | } |
561 | | |
562 | 1 | filter_index.Stop(); |
563 | 1 | BOOST_REQUIRE(DestroyBlockFilterIndex(BlockFilterType::BASIC)); |
564 | 1 | } |
565 | | |
566 | | BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_abort, TestChain100Setup) |
567 | 1 | { |
568 | 1 | CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); |
569 | 1 | uint256 genesis_hash; |
570 | 1 | { |
571 | 1 | LOCK(wallet.cs_wallet); |
572 | 1 | LOCK(Assert(m_node.chainman)->GetMutex()); |
573 | 1 | wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); |
574 | 1 | wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash()); |
575 | 1 | genesis_hash = m_node.chainman->ActiveChain().Genesis()->GetBlockHash(); |
576 | 1 | } |
577 | | |
578 | | // An abort requested while no rescan is held is stale and must |
579 | | // not cancel a later scan. |
580 | 1 | wallet.Scanner().Abort(); |
581 | 1 | WalletRescanReserver reserver(wallet); |
582 | 1 | BOOST_CHECK(reserver.reserve()); |
583 | 1 | BOOST_CHECK(!wallet.Scanner().IsAborting()); |
584 | | |
585 | | // An abort requested after the reservation but before the scan starts |
586 | | // (e.g. while importdescriptors is still deriving keys) must cancel the |
587 | | // scan. |
588 | 1 | wallet.Scanner().Abort(); |
589 | 1 | ScanResult result = wallet.Scanner().Scan(genesis_hash, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false); |
590 | 1 | BOOST_CHECK_EQUAL(result.status, ScanResult::USER_ABORT); |
591 | 1 | BOOST_CHECK(result.last_scanned_block.IsNull()); |
592 | 1 | BOOST_CHECK(!result.last_scanned_height); |
593 | 1 | BOOST_CHECK(result.last_failed_block.IsNull()); |
594 | 1 | } |
595 | | |
596 | | BOOST_FIXTURE_TEST_CASE(wallet_rescan_reserver, TestingSetup) |
597 | 1 | { |
598 | 1 | CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); |
599 | | |
600 | | // No scan in progress: accessors report idle state. |
601 | 1 | BOOST_CHECK(!wallet.Scanner().IsScanning()); |
602 | 1 | BOOST_CHECK(wallet.Scanner().ScanningDuration() == SteadyClock::duration{}); |
603 | 1 | BOOST_CHECK_EQUAL(wallet.Scanner().ScanningProgress(), 0.0); |
604 | | |
605 | 1 | { |
606 | 1 | WalletRescanReserver first_reserver(wallet); |
607 | 1 | BOOST_CHECK(first_reserver.reserve()); |
608 | 1 | BOOST_CHECK(first_reserver.isReserved()); |
609 | 1 | BOOST_CHECK(wallet.Scanner().IsScanning()); |
610 | 1 | BOOST_CHECK(!wallet.Scanner().IsScanningWithPassphrase()); |
611 | 1 | BOOST_CHECK_EQUAL(wallet.Scanner().ScanningProgress(), 0.0); |
612 | | |
613 | | // Only one reservation can be held at a time. |
614 | 1 | WalletRescanReserver second_reserver(wallet); |
615 | 1 | BOOST_CHECK(!second_reserver.reserve()); |
616 | 1 | BOOST_CHECK(!second_reserver.isReserved()); |
617 | 1 | } |
618 | | // Destroying the reserver (RAII) clears the scanning state. |
619 | 1 | BOOST_CHECK(!wallet.Scanner().IsScanning()); |
620 | | |
621 | 1 | { |
622 | 1 | WalletRescanReserver passphrase_reserver(wallet); |
623 | 1 | BOOST_CHECK(passphrase_reserver.reserve(/*with_passphrase=*/true)); |
624 | 1 | BOOST_CHECK(wallet.Scanner().IsScanningWithPassphrase()); |
625 | 1 | } |
626 | 1 | BOOST_CHECK(!wallet.Scanner().IsScanningWithPassphrase()); |
627 | 1 | } |
628 | | |
629 | | BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_bounded, TestChain100Setup) |
630 | 1 | { |
631 | 1 | uint256 genesis_hash, max_hash, tip_hash; |
632 | 1 | int max_height, tip_height; |
633 | 1 | { |
634 | 1 | LOCK(Assert(m_node.chainman)->GetMutex()); |
635 | 1 | genesis_hash = m_node.chainman->ActiveChain().Genesis()->GetBlockHash(); |
636 | 1 | tip_height = m_node.chainman->ActiveChain().Height(); |
637 | 1 | tip_hash = m_node.chainman->ActiveChain().Tip()->GetBlockHash(); |
638 | 1 | max_height = tip_height - 2; |
639 | 1 | max_hash = m_node.chainman->ActiveChain()[max_height]->GetBlockHash(); |
640 | 1 | } |
641 | | |
642 | | // A scan with max_height set stops exactly at max_height and does not |
643 | | // sync any blocks beyond it. |
644 | 1 | { |
645 | 1 | CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); |
646 | 1 | { |
647 | 1 | LOCK(wallet.cs_wallet); |
648 | 1 | wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); |
649 | 1 | wallet.SetLastBlockProcessed(tip_height, tip_hash); |
650 | 1 | } |
651 | 1 | AddKey(wallet, coinbaseKey); |
652 | 1 | WalletRescanReserver reserver(wallet); |
653 | 1 | reserver.reserve(); |
654 | 1 | ScanResult result = wallet.Scanner().Scan(genesis_hash, /*start_height=*/0, max_height, reserver, /*save_progress=*/false); |
655 | 1 | BOOST_CHECK_EQUAL(result.status, ScanResult::SUCCESS); |
656 | 1 | BOOST_CHECK(result.last_failed_block.IsNull()); |
657 | 1 | BOOST_CHECK_EQUAL(result.last_scanned_block, max_hash); |
658 | 1 | BOOST_CHECK_EQUAL(*result.last_scanned_height, max_height); |
659 | | // One coinbase per block from height 1 through max_height. |
660 | 1 | BOOST_CHECK_EQUAL(WITH_LOCK(wallet.cs_wallet, return wallet.mapWallet.size()), static_cast<size_t>(max_height)); |
661 | 1 | } |
662 | | |
663 | | // A single-block range (start == max_height == tip) scans exactly that |
664 | | // block. |
665 | 1 | { |
666 | 1 | CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); |
667 | 1 | { |
668 | 1 | LOCK(wallet.cs_wallet); |
669 | 1 | wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); |
670 | 1 | wallet.SetLastBlockProcessed(tip_height, tip_hash); |
671 | 1 | } |
672 | 1 | AddKey(wallet, coinbaseKey); |
673 | 1 | WalletRescanReserver reserver(wallet); |
674 | 1 | reserver.reserve(); |
675 | 1 | ScanResult result = wallet.Scanner().Scan(tip_hash, tip_height, tip_height, reserver, /*save_progress=*/false); |
676 | 1 | BOOST_CHECK_EQUAL(result.status, ScanResult::SUCCESS); |
677 | 1 | BOOST_CHECK(result.last_failed_block.IsNull()); |
678 | 1 | BOOST_CHECK_EQUAL(result.last_scanned_block, tip_hash); |
679 | 1 | BOOST_CHECK_EQUAL(*result.last_scanned_height, tip_height); |
680 | 1 | BOOST_CHECK_EQUAL(WITH_LOCK(wallet.cs_wallet, return wallet.mapWallet.size()), 1U); |
681 | 1 | } |
682 | 1 | } |
683 | | |
684 | | BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_tip_extension, TestChain100Setup) |
685 | 1 | { |
686 | 1 | CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); |
687 | 1 | uint256 genesis_hash; |
688 | 1 | int start_tip_height{0}; |
689 | 1 | { |
690 | 1 | LOCK(wallet.cs_wallet); |
691 | 1 | LOCK(Assert(m_node.chainman)->GetMutex()); |
692 | 1 | wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); |
693 | 1 | start_tip_height = m_node.chainman->ActiveChain().Height(); |
694 | 1 | wallet.SetLastBlockProcessed(start_tip_height, m_node.chainman->ActiveChain().Tip()->GetBlockHash()); |
695 | 1 | genesis_hash = m_node.chainman->ActiveChain().Genesis()->GetBlockHash(); |
696 | 1 | } |
697 | 1 | AddKey(wallet, coinbaseKey); |
698 | | |
699 | | // Connect a block while the scan is running (the handler fires on the |
700 | | // scanning thread as the scan starts) and advance the wallet's tip, as |
701 | | // the blockConnected notification would. The scan must pick up the new |
702 | | // tip instead of stopping at the height it started with. |
703 | 1 | uint256 new_tip_hash; |
704 | 1 | bool extended{false}; |
705 | 4 | auto handler = wallet.ShowProgress.connect([&](const std::string&, int) { |
706 | 4 | if (extended) return; |
707 | 1 | extended = true; |
708 | 1 | CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); |
709 | 1 | LOCK(wallet.cs_wallet); |
710 | 1 | LOCK(Assert(m_node.chainman)->GetMutex()); |
711 | 1 | const CBlockIndex* new_tip = m_node.chainman->ActiveChain().Tip(); |
712 | 1 | new_tip_hash = new_tip->GetBlockHash(); |
713 | 1 | wallet.SetLastBlockProcessed(new_tip->nHeight, new_tip_hash); |
714 | 1 | }); |
715 | | |
716 | 1 | WalletRescanReserver reserver(wallet); |
717 | 1 | reserver.reserve(); |
718 | 1 | ScanResult result = wallet.Scanner().Scan(genesis_hash, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false); |
719 | 1 | handler.disconnect(); |
720 | 1 | BOOST_CHECK_EQUAL(result.status, ScanResult::SUCCESS); |
721 | 1 | BOOST_CHECK_EQUAL(result.last_scanned_block, new_tip_hash); |
722 | 1 | BOOST_CHECK_EQUAL(*result.last_scanned_height, start_tip_height + 1); |
723 | 1 | } |
724 | | |
725 | | BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_no_progress_saved, TestChain100Setup) |
726 | 1 | { |
727 | 1 | CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); |
728 | 1 | uint256 genesis_hash, tip_hash; |
729 | 1 | int max_height; |
730 | 1 | { |
731 | 1 | LOCK(wallet.cs_wallet); |
732 | 1 | LOCK(Assert(m_node.chainman)->GetMutex()); |
733 | 1 | wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); |
734 | 1 | tip_hash = m_node.chainman->ActiveChain().Tip()->GetBlockHash(); |
735 | 1 | wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), tip_hash); |
736 | 1 | genesis_hash = m_node.chainman->ActiveChain().Genesis()->GetBlockHash(); |
737 | 1 | max_height = m_node.chainman->ActiveChain().Height() - 2; |
738 | 1 | } |
739 | 1 | AddKey(wallet, coinbaseKey); |
740 | | |
741 | 1 | WalletRescanReserver reserver(wallet); |
742 | | // Advance the clock on every call so that every scanned block would be |
743 | | // eligible for a progress write if save_progress were set. |
744 | 1 | std::chrono::steady_clock::time_point fake_time; |
745 | 201 | reserver.setNow([&] { fake_time += 60s; return fake_time; }); |
746 | 1 | reserver.reserve(); |
747 | | |
748 | 1 | ScanResult result = wallet.Scanner().Scan(genesis_hash, /*start_height=*/0, max_height, reserver, /*save_progress=*/false); |
749 | 1 | BOOST_CHECK_EQUAL(result.status, ScanResult::SUCCESS); |
750 | | |
751 | | // With save_progress=false the scan must not touch the wallet's best |
752 | | // block record: it still points at the tip written when the descriptor |
753 | | // was added, not at any block the scan visited. |
754 | 1 | CBlockLocator locator; |
755 | 1 | BOOST_CHECK(WalletBatch{wallet.GetDatabase()}.ReadBestBlock(locator)); |
756 | 1 | BOOST_CHECK(!locator.IsNull()); |
757 | 1 | BOOST_CHECK_EQUAL(locator.vHave.front(), tip_hash); |
758 | 1 | } |
759 | | |
760 | | BOOST_FIXTURE_TEST_CASE(rescan_from_time, TestChain100Setup) |
761 | 1 | { |
762 | | // Cap last block file size, and mine new block in a new block file. |
763 | 1 | CBlockIndex* old_tip = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip()); |
764 | 1 | WITH_LOCK(::cs_main, m_node.chainman->m_blockman.GetBlockFileInfo(old_tip->GetBlockPos().nFile)->nSize = MAX_BLOCKFILE_SIZE); |
765 | 1 | CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); |
766 | 1 | CBlockIndex* new_tip = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip()); |
767 | | |
768 | | // Prune the older block file. |
769 | 1 | int file_number; |
770 | 1 | { |
771 | 1 | LOCK(cs_main); |
772 | 1 | file_number = old_tip->GetBlockPos().nFile; |
773 | 1 | Assert(m_node.chainman)->m_blockman.PruneOneBlockFile(file_number); |
774 | 1 | } |
775 | 1 | m_node.chainman->m_blockman.UnlinkPrunedFiles({file_number}); |
776 | | |
777 | 1 | CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); |
778 | 1 | { |
779 | 1 | LOCK(wallet.cs_wallet); |
780 | 1 | LOCK(Assert(m_node.chainman)->GetMutex()); |
781 | 1 | wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); |
782 | 1 | wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash()); |
783 | 1 | } |
784 | 1 | AddKey(wallet, coinbaseKey); |
785 | 1 | WalletRescanReserver reserver(wallet); |
786 | 1 | reserver.reserve(); |
787 | | |
788 | | // Blocks before the prune point cannot be read: the returned timestamp |
789 | | // is moved past the last unreadable block, telling the caller from when |
790 | | // the rescan is actually complete. |
791 | 1 | const int64_t genesis_time{WITH_LOCK(::cs_main, return m_node.chainman->ActiveChain().Genesis()->GetBlockTime())}; |
792 | 1 | BOOST_CHECK_EQUAL(wallet.Scanner().ScanFromTime(genesis_time, reserver), |
793 | 1 | WITH_LOCK(::cs_main, return old_tip->GetBlockTimeMax()) + TIMESTAMP_WINDOW + 1); |
794 | | |
795 | 1 | bool scan_logged{false}; |
796 | 1 | DebugLogHelper scan_check{"Rescan started from block", [&](const std::string* s) { |
797 | 1 | if (s) scan_logged = true; |
798 | 1 | return false; |
799 | 1 | }}; |
800 | | // A timestamp past the tip requires no scanning and is returned unchanged. |
801 | 1 | const int64_t future_time{WITH_LOCK(::cs_main, return new_tip->GetBlockTimeMax()) + TIMESTAMP_WINDOW + 1}; |
802 | 1 | BOOST_CHECK(!scan_logged); |
803 | 1 | BOOST_CHECK_EQUAL(wallet.Scanner().ScanFromTime(future_time, reserver), future_time); |
804 | 1 | } |
805 | | |
806 | | BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_missing_filter, TestChain100Setup) |
807 | 1 | { |
808 | | // Enable the block filter index but do not sync it: no filters are |
809 | | // available, so the scan must inspect every block rather than treat |
810 | | // the missing filters as misses and skip blocks. |
811 | 1 | BOOST_REQUIRE(InitBlockFilterIndex([&]{ return interfaces::MakeChain(m_node); }, BlockFilterType::BASIC, 1_MiB, /*f_memory=*/true)); |
812 | 1 | BlockFilterIndex& filter_index{*Assert(GetBlockFilterIndex(BlockFilterType::BASIC))}; |
813 | 1 | BOOST_REQUIRE(filter_index.Init()); |
814 | | |
815 | 1 | { |
816 | 1 | CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); |
817 | 1 | uint256 genesis_hash, tip_hash; |
818 | 1 | int tip_height; |
819 | 1 | { |
820 | 1 | LOCK(wallet.cs_wallet); |
821 | 1 | LOCK(Assert(m_node.chainman)->GetMutex()); |
822 | 1 | wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); |
823 | 1 | genesis_hash = m_node.chainman->ActiveChain().Genesis()->GetBlockHash(); |
824 | 1 | tip_height = m_node.chainman->ActiveChain().Height(); |
825 | 1 | auto tip{m_node.chainman->ActiveChain().Tip()}; |
826 | 1 | tip_hash = tip->GetBlockHash(); |
827 | 1 | wallet.SetLastBlockProcessed(tip_height, tip_hash); |
828 | 1 | BlockFilter filter; |
829 | 1 | BOOST_REQUIRE(!filter_index.LookupFilter(tip, filter)); |
830 | 1 | } |
831 | 1 | AddKey(wallet, coinbaseKey); |
832 | 1 | WalletRescanReserver reserver(wallet); |
833 | 1 | reserver.reserve(); |
834 | 1 | bool fast_scan_logged{false}; |
835 | 2 | DebugLogHelper scan_check{"fast variant using block filters", [&](const std::string* s) { |
836 | 2 | if (s) fast_scan_logged = true; |
837 | 2 | return false; |
838 | 2 | }}; |
839 | 1 | ScanResult result = wallet.Scanner().Scan(genesis_hash, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false); |
840 | 1 | BOOST_REQUIRE(fast_scan_logged); |
841 | 1 | BOOST_CHECK_EQUAL(result.status, ScanResult::SUCCESS); |
842 | 1 | BOOST_CHECK(result.last_failed_block.IsNull()); |
843 | 1 | BOOST_CHECK_EQUAL(result.last_scanned_block, tip_hash); |
844 | 1 | BOOST_CHECK_EQUAL(*result.last_scanned_height, tip_height); |
845 | | // One coinbase per block from height 1 through the tip. |
846 | 1 | BOOST_CHECK_EQUAL(WITH_LOCK(wallet.cs_wallet, return wallet.mapWallet.size()), static_cast<size_t>(tip_height)); |
847 | 1 | } |
848 | | |
849 | 1 | filter_index.Stop(); |
850 | 1 | BOOST_REQUIRE(DestroyBlockFilterIndex(BlockFilterType::BASIC)); |
851 | 1 | } |
852 | | |
853 | | //! Test the rescan that loading a wallet performs when the wallet is behind |
854 | | //! the chain tip: it scans from the wallet's recorded best block - a |
855 | | //! mid-chain start - with cs_wallet held. |
856 | | BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_attach_chain, TestChain100Setup) |
857 | 1 | { |
858 | | // Do not wait for sqlite to flush data to disk to improve performance |
859 | 1 | m_args.ForceSetArg("-unsafesqlitesync", "1"); |
860 | | |
861 | | // Create a wallet owning the coinbases, and unload it at the current tip. |
862 | 1 | WalletContext context; |
863 | 1 | context.args = &m_args; |
864 | 1 | context.chain = m_node.chain.get(); |
865 | 1 | auto wallet = TestCreateWallet(context); |
866 | 1 | AddKey(*wallet, coinbaseKey); |
867 | 1 | TestUnloadWallet(std::move(wallet)); |
868 | | |
869 | | // Extend the chain while the wallet is not loaded. |
870 | 1 | constexpr int NEW_BLOCKS{5}; |
871 | 6 | for (int i = 0; i < NEW_BLOCKS; ++i) { |
872 | 5 | CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); |
873 | 5 | } |
874 | | |
875 | 1 | int tip_height; |
876 | 1 | uint256 tip_hash; |
877 | 1 | { |
878 | 1 | LOCK(Assert(m_node.chainman)->GetMutex()); |
879 | 1 | tip_height = m_node.chainman->ActiveChain().Height(); |
880 | 1 | tip_hash = m_node.chainman->ActiveChain().Tip()->GetBlockHash(); |
881 | 1 | } |
882 | | |
883 | | // Loading the wallet must rescan the extension from the recorded best |
884 | | // block and find its coinbases. |
885 | 1 | wallet = TestLoadWallet(context); |
886 | 1 | { |
887 | 1 | LOCK(wallet->cs_wallet); |
888 | 1 | BOOST_CHECK_EQUAL(wallet->GetLastBlockHeight(), tip_height); |
889 | 1 | BOOST_CHECK_EQUAL(wallet->GetLastBlockHash(), tip_hash); |
890 | | // The extension's coinbases plus the one of the recorded best block: |
891 | | // the load rescan starts mid-chain, at that block inclusive. |
892 | 1 | BOOST_CHECK_EQUAL(wallet->mapWallet.size(), static_cast<size_t>(NEW_BLOCKS + 1)); |
893 | 1 | } |
894 | 1 | TestUnloadWallet(std::move(wallet)); |
895 | 1 | } |
896 | | |
897 | | // This test verifies that wallet settings can be added and removed |
898 | | // concurrently, ensuring no race conditions occur during either process. |
899 | | BOOST_FIXTURE_TEST_CASE(write_wallet_settings_concurrently, TestingSetup) |
900 | 1 | { |
901 | 1 | auto chain = m_node.chain.get(); |
902 | 1 | const auto NUM_WALLETS{5}; |
903 | | |
904 | | // Since we're counting the number of wallets, ensure we start without any. |
905 | 1 | BOOST_REQUIRE(chain->getRwSetting("wallet").isNull()); |
906 | | |
907 | 2 | const auto& check_concurrent_wallet = [&](const auto& settings_function, int num_expected_wallets) { |
908 | 2 | std::vector<std::thread> threads; |
909 | 2 | threads.reserve(NUM_WALLETS); |
910 | 12 | for (auto i{0}; i < NUM_WALLETS; ++i) threads.emplace_back(settings_function, i); |
911 | 10 | for (auto& t : threads) t.join(); |
912 | | |
913 | 2 | auto wallets = chain->getRwSetting("wallet"); |
914 | 2 | BOOST_CHECK_EQUAL(wallets.getValues().size(), num_expected_wallets); |
915 | 2 | }; wallet_tests.cpp:_ZZN6wallet12wallet_tests34write_wallet_settings_concurrently11test_methodEvENK3$_1clIZNS1_11test_methodEvE3$_0EEDaRKT_i Line | Count | Source | 907 | 1 | const auto& check_concurrent_wallet = [&](const auto& settings_function, int num_expected_wallets) { | 908 | 1 | std::vector<std::thread> threads; | 909 | 1 | threads.reserve(NUM_WALLETS); | 910 | 6 | for (auto i{0}; i < NUM_WALLETS; ++i) threads.emplace_back(settings_function, i); | 911 | 5 | for (auto& t : threads) t.join(); | 912 | | | 913 | 1 | auto wallets = chain->getRwSetting("wallet"); | 914 | | BOOST_CHECK_EQUAL(wallets.getValues().size(), num_expected_wallets); | 915 | 1 | }; |
wallet_tests.cpp:_ZZN6wallet12wallet_tests34write_wallet_settings_concurrently11test_methodEvENK3$_1clIZNS1_11test_methodEvE3$_2EEDaRKT_i Line | Count | Source | 907 | 1 | const auto& check_concurrent_wallet = [&](const auto& settings_function, int num_expected_wallets) { | 908 | 1 | std::vector<std::thread> threads; | 909 | 1 | threads.reserve(NUM_WALLETS); | 910 | 6 | for (auto i{0}; i < NUM_WALLETS; ++i) threads.emplace_back(settings_function, i); | 911 | 5 | for (auto& t : threads) t.join(); | 912 | | | 913 | 1 | auto wallets = chain->getRwSetting("wallet"); | 914 | | BOOST_CHECK_EQUAL(wallets.getValues().size(), num_expected_wallets); | 915 | 1 | }; |
|
916 | | |
917 | | // Add NUM_WALLETS wallets concurrently, ensure we end up with NUM_WALLETS stored. |
918 | 5 | check_concurrent_wallet([&chain](int i) { |
919 | 5 | Assert(AddWalletSetting(*chain, strprintf("wallet_%d", i))); |
920 | 5 | }, |
921 | 1 | /*num_expected_wallets=*/NUM_WALLETS); |
922 | | |
923 | | // Remove NUM_WALLETS wallets concurrently, ensure we end up with 0 wallets. |
924 | 5 | check_concurrent_wallet([&chain](int i) { |
925 | 5 | Assert(RemoveWalletSetting(*chain, strprintf("wallet_%d", i))); |
926 | 5 | }, |
927 | 1 | /*num_expected_wallets=*/0); |
928 | 1 | } |
929 | | |
930 | | static int64_t AddTx(ChainstateManager& chainman, CWallet& wallet, uint32_t lockTime, std::chrono::seconds mock_time, int64_t blockTime) |
931 | 6 | { |
932 | 6 | CMutableTransaction tx; |
933 | 6 | TxState state = TxStateInactive{}; |
934 | 6 | tx.nLockTime = lockTime; |
935 | 6 | FakeNodeClock clock{mock_time}; |
936 | 6 | CBlockIndex* block = nullptr; |
937 | 6 | if (blockTime > 0) { |
938 | 5 | LOCK(cs_main); |
939 | 5 | auto inserted = chainman.BlockIndex().emplace(std::piecewise_construct, std::make_tuple(GetRandHash()), std::make_tuple()); |
940 | 5 | assert(inserted.second); |
941 | 5 | const uint256& hash = inserted.first->first; |
942 | 5 | block = &inserted.first->second; |
943 | 5 | block->nTime = blockTime; |
944 | 5 | block->phashBlock = &hash; |
945 | 5 | state = TxStateConfirmed{hash, block->nHeight, /*index=*/0}; |
946 | 5 | } |
947 | 6 | return wallet.AddToWallet(MakeTransactionRef(tx), state, [&](CWalletTx& wtx, bool /* new_tx */) { |
948 | | // Assign wtx.m_state to simplify test and avoid the need to simulate |
949 | | // reorg events. Without this, AddToWallet asserts false when the same |
950 | | // transaction is confirmed in different blocks. |
951 | 6 | wtx.m_state = state; |
952 | 6 | return true; |
953 | 6 | })->nTimeSmart; |
954 | 6 | } |
955 | | |
956 | | // Simple test to verify assignment of CWalletTx::nSmartTime value. Could be |
957 | | // expanded to cover more corner cases of smart time logic. |
958 | | BOOST_AUTO_TEST_CASE(ComputeTimeSmart) |
959 | 1 | { |
960 | | // New transaction should use clock time if lower than block time. |
961 | 1 | BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 1, 100s, 120), 100); |
962 | | |
963 | | // Test that updating existing transaction does not change smart time. |
964 | 1 | BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 1, 200s, 220), 100); |
965 | | |
966 | | // New transaction should use clock time if there's no block time. |
967 | 1 | BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 2, 300s, 0), 300); |
968 | | |
969 | | // New transaction should use block time if lower than clock time. |
970 | 1 | BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 3, 420s, 400), 400); |
971 | | |
972 | | // New transaction should use latest entry time if higher than |
973 | | // min(block time, clock time). |
974 | 1 | BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 4, 500s, 390), 400); |
975 | | |
976 | | // If there are future entries, new transaction should use time of the |
977 | | // newest entry that is no more than 300 seconds ahead of the clock time. |
978 | 1 | BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 5, 50s, 600), 300); |
979 | 1 | } |
980 | | |
981 | | void TestLoadWallet(const std::string& name, DatabaseFormat format, std::function<void(std::shared_ptr<CWallet>)> f) |
982 | 3 | { |
983 | 3 | node::NodeContext node; |
984 | 3 | auto chain{interfaces::MakeChain(node)}; |
985 | 3 | DatabaseOptions options; |
986 | 3 | options.require_format = format; |
987 | 3 | DatabaseStatus status; |
988 | 3 | bilingual_str error; |
989 | 3 | std::vector<bilingual_str> warnings; |
990 | 3 | auto database{MakeWalletDatabase(name, options, status, error)}; |
991 | 3 | auto wallet{std::make_shared<CWallet>(chain.get(), "", std::move(database))}; |
992 | 3 | BOOST_CHECK_EQUAL(wallet->PopulateWalletFromDB(error, warnings), DBErrors::LOAD_OK); |
993 | 3 | WITH_LOCK(wallet->cs_wallet, f(wallet)); |
994 | 3 | } |
995 | | |
996 | | BOOST_FIXTURE_TEST_CASE(LoadReceiveRequests, TestingSetup) |
997 | 1 | { |
998 | 1 | for (DatabaseFormat format : DATABASE_FORMATS) { |
999 | 1 | const std::string name{strprintf("receive-requests-%i", format)}; |
1000 | 1 | TestLoadWallet(name, format, [](std::shared_ptr<CWallet> wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet) { |
1001 | 1 | BOOST_CHECK(!wallet->IsAddressPreviouslySpent(PKHash())); |
1002 | 1 | WalletBatch batch{wallet->GetDatabase()}; |
1003 | 1 | BOOST_CHECK(batch.WriteAddressPreviouslySpent(PKHash(), true)); |
1004 | 1 | BOOST_CHECK(batch.WriteAddressPreviouslySpent(ScriptHash(), true)); |
1005 | 1 | BOOST_CHECK(wallet->SetAddressReceiveRequest(batch, PKHash(), "0", "val_rr00")); |
1006 | 1 | BOOST_CHECK(wallet->EraseAddressReceiveRequest(batch, PKHash(), "0")); |
1007 | 1 | BOOST_CHECK(wallet->SetAddressReceiveRequest(batch, PKHash(), "1", "val_rr10")); |
1008 | 1 | BOOST_CHECK(wallet->SetAddressReceiveRequest(batch, PKHash(), "1", "val_rr11")); |
1009 | 1 | BOOST_CHECK(wallet->SetAddressReceiveRequest(batch, ScriptHash(), "2", "val_rr20")); |
1010 | 1 | }); |
1011 | 1 | TestLoadWallet(name, format, [](std::shared_ptr<CWallet> wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet) { |
1012 | 1 | BOOST_CHECK(wallet->IsAddressPreviouslySpent(PKHash())); |
1013 | 1 | BOOST_CHECK(wallet->IsAddressPreviouslySpent(ScriptHash())); |
1014 | 1 | auto requests = wallet->GetAddressReceiveRequests(); |
1015 | 1 | auto erequests = {"val_rr11", "val_rr20"}; |
1016 | 1 | BOOST_CHECK_EQUAL_COLLECTIONS(requests.begin(), requests.end(), std::begin(erequests), std::end(erequests)); |
1017 | 1 | RunWithinTxn(wallet->GetDatabase(), /*process_desc=*/"test", [](WalletBatch& batch){ |
1018 | 1 | BOOST_CHECK(batch.WriteAddressPreviouslySpent(PKHash(), false)); |
1019 | 1 | BOOST_CHECK(batch.EraseAddressData(ScriptHash())); |
1020 | 1 | return true; |
1021 | 1 | }); |
1022 | 1 | }); |
1023 | 1 | TestLoadWallet(name, format, [](std::shared_ptr<CWallet> wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet) { |
1024 | 1 | BOOST_CHECK(!wallet->IsAddressPreviouslySpent(PKHash())); |
1025 | 1 | BOOST_CHECK(!wallet->IsAddressPreviouslySpent(ScriptHash())); |
1026 | 1 | auto requests = wallet->GetAddressReceiveRequests(); |
1027 | 1 | auto erequests = {"val_rr11"}; |
1028 | 1 | BOOST_CHECK_EQUAL_COLLECTIONS(requests.begin(), requests.end(), std::begin(erequests), std::end(erequests)); |
1029 | 1 | }); |
1030 | 1 | } |
1031 | 1 | } |
1032 | | |
1033 | | class ListCoinsTestingSetup : public TestChain100Setup |
1034 | | { |
1035 | | public: |
1036 | | ListCoinsTestingSetup() |
1037 | 2 | { |
1038 | 2 | CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); |
1039 | 2 | wallet = CreateSyncedWallet(*m_node.chain, WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain()), coinbaseKey); |
1040 | 2 | } |
1041 | | |
1042 | | ~ListCoinsTestingSetup() |
1043 | 2 | { |
1044 | 2 | wallet.reset(); |
1045 | 2 | } |
1046 | | |
1047 | | CWalletTx& AddTx(CRecipient recipient) |
1048 | 5 | { |
1049 | 5 | CTransactionRef tx; |
1050 | 5 | CCoinControl dummy; |
1051 | 5 | { |
1052 | 5 | auto res = CreateTransaction(*wallet, {recipient}, /*change_pos=*/std::nullopt, dummy); |
1053 | 5 | BOOST_CHECK(res); |
1054 | 5 | tx = res->tx; |
1055 | 5 | } |
1056 | 5 | wallet->CommitTransaction(tx); |
1057 | 5 | CMutableTransaction blocktx; |
1058 | 5 | { |
1059 | 5 | LOCK(wallet->cs_wallet); |
1060 | 5 | blocktx = CMutableTransaction(*wallet->mapWallet.at(tx->GetHash()).GetTx()); |
1061 | 5 | } |
1062 | 5 | CreateAndProcessBlock({CMutableTransaction(blocktx)}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); |
1063 | | |
1064 | 5 | LOCK(wallet->cs_wallet); |
1065 | 5 | LOCK(Assert(m_node.chainman)->GetMutex()); |
1066 | 5 | wallet->SetLastBlockProcessed(wallet->GetLastBlockHeight() + 1, m_node.chainman->ActiveChain().Tip()->GetBlockHash()); |
1067 | 5 | auto it = wallet->mapWallet.find(tx->GetHash()); |
1068 | 5 | BOOST_CHECK(it != wallet->mapWallet.end()); |
1069 | 5 | it->second.m_state = TxStateConfirmed{m_node.chainman->ActiveChain().Tip()->GetBlockHash(), m_node.chainman->ActiveChain().Height(), /*index=*/1}; |
1070 | 5 | return it->second; |
1071 | 5 | } |
1072 | | |
1073 | | std::unique_ptr<CWallet> wallet; |
1074 | | }; |
1075 | | |
1076 | | BOOST_FIXTURE_TEST_CASE(ListCoinsTest, ListCoinsTestingSetup) |
1077 | 1 | { |
1078 | 1 | std::string coinbaseAddress = coinbaseKey.GetPubKey().GetID().ToString(); |
1079 | | |
1080 | | // Confirm ListCoins initially returns 1 coin grouped under coinbaseKey |
1081 | | // address. |
1082 | 1 | std::map<CTxDestination, std::vector<COutput>> list; |
1083 | 1 | { |
1084 | 1 | LOCK(wallet->cs_wallet); |
1085 | 1 | list = ListCoins(*wallet); |
1086 | 1 | } |
1087 | 1 | BOOST_CHECK_EQUAL(list.size(), 1U); |
1088 | 1 | BOOST_CHECK_EQUAL(std::get<PKHash>(list.begin()->first).ToString(), coinbaseAddress); |
1089 | 1 | BOOST_CHECK_EQUAL(list.begin()->second.size(), 1U); |
1090 | | |
1091 | | // Check initial balance from one mature coinbase transaction. |
1092 | 1 | BOOST_CHECK_EQUAL(50 * COIN, WITH_LOCK(wallet->cs_wallet, return AvailableCoins(*wallet).GetTotalAmount())); |
1093 | | |
1094 | | // Add a transaction creating a change address, and confirm ListCoins still |
1095 | | // returns the coin associated with the change address underneath the |
1096 | | // coinbaseKey pubkey, even though the change address has a different |
1097 | | // pubkey. |
1098 | 1 | AddTx(CRecipient{PubKeyDestination{{}}, 1 * COIN, /*subtract_fee=*/false}); |
1099 | 1 | { |
1100 | 1 | LOCK(wallet->cs_wallet); |
1101 | 1 | list = ListCoins(*wallet); |
1102 | 1 | } |
1103 | 1 | BOOST_CHECK_EQUAL(list.size(), 1U); |
1104 | 1 | BOOST_CHECK_EQUAL(std::get<PKHash>(list.begin()->first).ToString(), coinbaseAddress); |
1105 | 1 | BOOST_CHECK_EQUAL(list.begin()->second.size(), 2U); |
1106 | | |
1107 | | // Lock both coins. Confirm number of available coins drops to 0. |
1108 | 1 | { |
1109 | 1 | LOCK(wallet->cs_wallet); |
1110 | 1 | BOOST_CHECK_EQUAL(AvailableCoins(*wallet).Size(), 2U); |
1111 | 1 | } |
1112 | 1 | for (const auto& group : list) { |
1113 | 2 | for (const auto& coin : group.second) { |
1114 | 2 | LOCK(wallet->cs_wallet); |
1115 | 2 | wallet->LockCoin(coin.outpoint, /*persist=*/false); |
1116 | 2 | } |
1117 | 1 | } |
1118 | 1 | { |
1119 | 1 | LOCK(wallet->cs_wallet); |
1120 | 1 | BOOST_CHECK_EQUAL(AvailableCoins(*wallet).Size(), 0U); |
1121 | 1 | } |
1122 | | // Confirm ListCoins still returns same result as before, despite coins |
1123 | | // being locked. |
1124 | 1 | { |
1125 | 1 | LOCK(wallet->cs_wallet); |
1126 | 1 | list = ListCoins(*wallet); |
1127 | 1 | } |
1128 | 1 | BOOST_CHECK_EQUAL(list.size(), 1U); |
1129 | 1 | BOOST_CHECK_EQUAL(std::get<PKHash>(list.begin()->first).ToString(), coinbaseAddress); |
1130 | 1 | BOOST_CHECK_EQUAL(list.begin()->second.size(), 2U); |
1131 | 1 | } |
1132 | | |
1133 | | void TestCoinsResult(ListCoinsTest& context, OutputType out_type, CAmount amount, |
1134 | | std::map<OutputType, size_t>& expected_coins_sizes) |
1135 | 4 | { |
1136 | 4 | LOCK(context.wallet->cs_wallet); |
1137 | 4 | util::Result<CTxDestination> dest = Assert(context.wallet->GetNewDestination(out_type, "")); |
1138 | 4 | CWalletTx& wtx = context.AddTx(CRecipient{*dest, amount, /*fSubtractFeeFromAmount=*/true}); |
1139 | 4 | CoinFilterParams filter; |
1140 | 4 | filter.skip_locked = false; |
1141 | 4 | CoinsResult available_coins = AvailableCoins(*context.wallet, nullptr, std::nullopt, filter); |
1142 | | // Lock outputs so they are not spent in follow-up transactions |
1143 | 12 | for (uint32_t i = 0; i < wtx.GetTx()->vout.size(); i++) context.wallet->LockCoin({wtx.GetHash(), i}, /*persist=*/false); |
1144 | 4 | for (const auto& [type, size] : expected_coins_sizes) BOOST_CHECK_EQUAL(size, available_coins.coins[type].size()); |
1145 | 4 | } |
1146 | | |
1147 | | BOOST_FIXTURE_TEST_CASE(BasicOutputTypesTest, ListCoinsTest) |
1148 | 1 | { |
1149 | 1 | std::map<OutputType, size_t> expected_coins_sizes; |
1150 | 4 | for (const auto& out_type : OUTPUT_TYPES) { expected_coins_sizes[out_type] = 0U; } |
1151 | | |
1152 | | // Verify our wallet has one usable coinbase UTXO before starting |
1153 | | // This UTXO is a P2PK, so it should show up in the Other bucket |
1154 | 1 | expected_coins_sizes[OutputType::UNKNOWN] = 1U; |
1155 | 1 | CoinsResult available_coins = WITH_LOCK(wallet->cs_wallet, return AvailableCoins(*wallet)); |
1156 | 1 | BOOST_CHECK_EQUAL(available_coins.Size(), expected_coins_sizes[OutputType::UNKNOWN]); |
1157 | 1 | BOOST_CHECK_EQUAL(available_coins.coins[OutputType::UNKNOWN].size(), expected_coins_sizes[OutputType::UNKNOWN]); |
1158 | | |
1159 | | // We will create a self transfer for each of the OutputTypes and |
1160 | | // verify it is put in the correct bucket after running GetAvailablecoins |
1161 | | // |
1162 | | // For each OutputType, We expect 2 UTXOs in our wallet following the self transfer: |
1163 | | // 1. One UTXO as the recipient |
1164 | | // 2. One UTXO from the change, due to payment address matching logic |
1165 | | |
1166 | 4 | for (const auto& out_type : OUTPUT_TYPES) { |
1167 | 4 | if (out_type == OutputType::UNKNOWN) continue; |
1168 | 4 | expected_coins_sizes[out_type] = 2U; |
1169 | 4 | TestCoinsResult(*this, out_type, 1 * COIN, expected_coins_sizes); |
1170 | 4 | } |
1171 | 1 | } |
1172 | | |
1173 | | BOOST_FIXTURE_TEST_CASE(wallet_disableprivkeys, TestChain100Setup) |
1174 | 1 | { |
1175 | 1 | const std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(m_node.chain.get(), "", CreateMockableWalletDatabase()); |
1176 | 1 | LOCK(wallet->cs_wallet); |
1177 | 1 | wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS); |
1178 | 1 | wallet->SetWalletFlag(WALLET_FLAG_DISABLE_PRIVATE_KEYS); |
1179 | 1 | BOOST_CHECK(!wallet->GetNewDestination(OutputType::BECH32, "")); |
1180 | 1 | } |
1181 | | |
1182 | | // Explicit calculation which is used to test the wallet constant |
1183 | | // We get the same virtual size due to rounding(weight/4) for both use_max_sig values |
1184 | | static size_t CalculateNestedKeyhashInputSize(bool use_max_sig) |
1185 | 2 | { |
1186 | | // Generate ephemeral valid pubkey |
1187 | 2 | CKey key = GenerateRandomKey(); |
1188 | 2 | CPubKey pubkey = key.GetPubKey(); |
1189 | | |
1190 | | // Generate pubkey hash |
1191 | 2 | uint160 key_hash(Hash160(pubkey)); |
1192 | | |
1193 | | // Create inner-script to enter into keystore. Key hash can't be 0... |
1194 | 2 | CScript inner_script = CScript() << OP_0 << std::vector<unsigned char>(key_hash.begin(), key_hash.end()); |
1195 | | |
1196 | | // Create outer P2SH script for the output |
1197 | 2 | uint160 script_id(Hash160(inner_script)); |
1198 | 2 | CScript script_pubkey = CScript() << OP_HASH160 << std::vector<unsigned char>(script_id.begin(), script_id.end()) << OP_EQUAL; |
1199 | | |
1200 | | // Add inner-script to key store and key to watchonly |
1201 | 2 | FillableSigningProvider keystore; |
1202 | 2 | keystore.AddCScript(inner_script); |
1203 | 2 | keystore.AddKeyPubKey(key, pubkey); |
1204 | | |
1205 | | // Fill in dummy signatures for fee calculation. |
1206 | 2 | SignatureData sig_data; |
1207 | | |
1208 | 2 | if (!ProduceSignature(keystore, use_max_sig ? DUMMY_MAXIMUM_SIGNATURE_CREATOR : DUMMY_SIGNATURE_CREATOR, script_pubkey, sig_data)) { |
1209 | | // We're hand-feeding it correct arguments; shouldn't happen |
1210 | 0 | assert(false); |
1211 | 0 | } |
1212 | | |
1213 | 2 | CTxIn tx_in; |
1214 | 2 | UpdateInput(tx_in, sig_data); |
1215 | 2 | return (size_t)GetVirtualTransactionInputSize(tx_in); |
1216 | 2 | } |
1217 | | |
1218 | | BOOST_FIXTURE_TEST_CASE(dummy_input_size_test, TestChain100Setup) |
1219 | 1 | { |
1220 | 1 | BOOST_CHECK_EQUAL(CalculateNestedKeyhashInputSize(false), DUMMY_NESTED_P2WPKH_INPUT_SIZE); |
1221 | 1 | BOOST_CHECK_EQUAL(CalculateNestedKeyhashInputSize(true), DUMMY_NESTED_P2WPKH_INPUT_SIZE); |
1222 | 1 | } |
1223 | | |
1224 | | bool malformed_descriptor(std::ios_base::failure e) |
1225 | 1 | { |
1226 | 1 | std::string s(e.what()); |
1227 | 1 | return s.find("Missing checksum") != std::string::npos; |
1228 | 1 | } |
1229 | | |
1230 | | BOOST_FIXTURE_TEST_CASE(wallet_descriptor_test, BasicTestingSetup) |
1231 | 1 | { |
1232 | 1 | std::vector<unsigned char> malformed_record; |
1233 | 1 | VectorWriter vw{malformed_record, 0}; |
1234 | 1 | vw << std::string("notadescriptor"); |
1235 | 1 | vw << uint64_t{0}; |
1236 | 1 | vw << int32_t{0}; |
1237 | 1 | vw << int32_t{0}; |
1238 | 1 | vw << int32_t{1}; |
1239 | | |
1240 | 1 | SpanReader vr{malformed_record}; |
1241 | 1 | std::optional<WalletDescriptor> w_desc; |
1242 | 1 | BOOST_CHECK_EXCEPTION(w_desc.emplace(WalletDescriptor::FromStream(deserialize, vr)), std::ios_base::failure, malformed_descriptor); |
1243 | 1 | } |
1244 | | |
1245 | | //! Test CWallet::CreateNew() and its behavior handling potential race |
1246 | | //! conditions if it's called the same time an incoming transaction shows up in |
1247 | | //! the mempool or a new block. |
1248 | | //! |
1249 | | //! It isn't possible to verify there aren't race condition in every case, so |
1250 | | //! this test just checks two specific cases and ensures that timing of |
1251 | | //! notifications in these cases doesn't prevent the wallet from detecting |
1252 | | //! transactions. |
1253 | | //! |
1254 | | //! In the first case, block and mempool transactions are created before the |
1255 | | //! wallet is loaded, but notifications about these transactions are delayed |
1256 | | //! until after it is loaded. The notifications are superfluous in this case, so |
1257 | | //! the test verifies the transactions are detected before they arrive. |
1258 | | //! |
1259 | | //! In the second case, block and mempool transactions are created after the |
1260 | | //! wallet rescan and notifications are immediately synced, to verify the wallet |
1261 | | //! must already have a handler in place for them, and there's no gap after |
1262 | | //! rescanning where new transactions in new blocks could be lost. |
1263 | | BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup) |
1264 | 1 | { |
1265 | 1 | m_args.ForceSetArg("-unsafesqlitesync", "1"); |
1266 | | // Create new wallet with known key and unload it. |
1267 | 1 | WalletContext context; |
1268 | 1 | context.args = &m_args; |
1269 | 1 | context.chain = m_node.chain.get(); |
1270 | 1 | auto wallet = TestCreateWallet(context); |
1271 | 1 | CKey key = GenerateRandomKey(); |
1272 | 1 | AddKey(*wallet, key); |
1273 | 1 | TestUnloadWallet(std::move(wallet)); |
1274 | | |
1275 | | |
1276 | | // Add log hook to detect AddToWallet events from rescans, blockConnected, |
1277 | | // and transactionAddedToMempool notifications |
1278 | 1 | int addtx_count = 0; |
1279 | 10 | DebugLogHelper addtx_counter("[default wallet] AddToWallet", [&](const std::string* s) { |
1280 | 10 | if (s) ++addtx_count; |
1281 | 10 | return false; |
1282 | 10 | }); |
1283 | | |
1284 | | |
1285 | 1 | bool rescan_completed = false; |
1286 | 2 | DebugLogHelper rescan_check("[default wallet] Rescan completed", [&](const std::string* s) { |
1287 | 2 | if (s) rescan_completed = true; |
1288 | 2 | return false; |
1289 | 2 | }); |
1290 | | |
1291 | | |
1292 | | // Block the queue to prevent the wallet receiving blockConnected and |
1293 | | // transactionAddedToMempool notifications, and create block and mempool |
1294 | | // transactions paying to the wallet |
1295 | 1 | std::promise<void> promise; |
1296 | 1 | m_node.validation_signals->CallFunctionInValidationInterfaceQueue([&promise] { |
1297 | 1 | promise.get_future().wait(); |
1298 | 1 | }); |
1299 | 1 | std::string error; |
1300 | 1 | m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]); |
1301 | 1 | auto block_tx = TestSimpleSpend(*m_coinbase_txns[0], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); |
1302 | 1 | m_coinbase_txns.push_back(CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]); |
1303 | 1 | auto mempool_tx = TestSimpleSpend(*m_coinbase_txns[1], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); |
1304 | 1 | BOOST_CHECK(BroadcastTestSimpleSpend(*m_node.chain, *Assert(m_node.chainman), mempool_tx, error)); |
1305 | | |
1306 | | // Reload wallet and make sure new transactions are detected despite events |
1307 | | // being blocked |
1308 | | // Loading will also ask for current mempool transactions |
1309 | 1 | wallet = TestLoadWallet(context); |
1310 | 1 | BOOST_CHECK(rescan_completed); |
1311 | | // AddToWallet events for block_tx and mempool_tx (x2) |
1312 | 1 | BOOST_CHECK_EQUAL(addtx_count, 3); |
1313 | 1 | { |
1314 | 1 | LOCK(wallet->cs_wallet); |
1315 | 1 | BOOST_CHECK(wallet->mapWallet.contains(block_tx.GetHash())); |
1316 | 1 | BOOST_CHECK(wallet->mapWallet.contains(mempool_tx.GetHash())); |
1317 | 1 | } |
1318 | | |
1319 | | |
1320 | | // Unblock notification queue and make sure stale blockConnected and |
1321 | | // transactionAddedToMempool events are processed |
1322 | 1 | promise.set_value(); |
1323 | 1 | m_node.validation_signals->SyncWithValidationInterfaceQueue(); |
1324 | | // AddToWallet events for block_tx and mempool_tx events are counted a |
1325 | | // second time as the notification queue is processed |
1326 | 1 | BOOST_CHECK_EQUAL(addtx_count, 5); |
1327 | | |
1328 | | |
1329 | 1 | TestUnloadWallet(std::move(wallet)); |
1330 | | |
1331 | | |
1332 | | // Load wallet again, this time creating new block and mempool transactions |
1333 | | // paying to the wallet as the wallet finishes loading and syncing the |
1334 | | // queue so the events have to be handled immediately. Releasing the wallet |
1335 | | // lock during the sync is a little artificial but is needed to avoid a |
1336 | | // deadlock during the sync and simulates a new block notification happening |
1337 | | // as soon as possible. |
1338 | 1 | addtx_count = 0; |
1339 | 1 | auto handler = HandleLoadWallet(context, [&](std::unique_ptr<interfaces::Wallet> wallet) { |
1340 | 1 | BOOST_CHECK(rescan_completed); |
1341 | 1 | m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]); |
1342 | 1 | block_tx = TestSimpleSpend(*m_coinbase_txns[2], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); |
1343 | 1 | m_coinbase_txns.push_back(CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]); |
1344 | 1 | mempool_tx = TestSimpleSpend(*m_coinbase_txns[3], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); |
1345 | 1 | BOOST_CHECK(BroadcastTestSimpleSpend(*m_node.chain, *Assert(m_node.chainman), mempool_tx, error)); |
1346 | 1 | m_node.validation_signals->SyncWithValidationInterfaceQueue(); |
1347 | 1 | }); |
1348 | 1 | wallet = TestLoadWallet(context); |
1349 | | // Since mempool transactions are requested at the end of loading, there will |
1350 | | // be 2 additional AddToWallet calls, one from the previous test, and a duplicate for mempool_tx |
1351 | 1 | BOOST_CHECK_EQUAL(addtx_count, 2 + 2); |
1352 | 1 | { |
1353 | 1 | LOCK(wallet->cs_wallet); |
1354 | 1 | BOOST_CHECK(wallet->mapWallet.contains(block_tx.GetHash())); |
1355 | 1 | BOOST_CHECK(wallet->mapWallet.contains(mempool_tx.GetHash())); |
1356 | 1 | } |
1357 | | |
1358 | | |
1359 | 1 | TestUnloadWallet(std::move(wallet)); |
1360 | 1 | } |
1361 | | |
1362 | | BOOST_FIXTURE_TEST_CASE(CreateWalletWithoutChain, BasicTestingSetup) |
1363 | 1 | { |
1364 | 1 | WalletContext context; |
1365 | 1 | context.args = &m_args; |
1366 | 1 | auto wallet = TestCreateWallet(context); |
1367 | 1 | BOOST_CHECK(wallet); |
1368 | 1 | WaitForDeleteWallet(std::move(wallet)); |
1369 | 1 | } |
1370 | | |
1371 | | BOOST_FIXTURE_TEST_CASE(RemoveTxs, TestChain100Setup) |
1372 | 1 | { |
1373 | 1 | m_args.ForceSetArg("-unsafesqlitesync", "1"); |
1374 | 1 | WalletContext context; |
1375 | 1 | context.args = &m_args; |
1376 | 1 | context.chain = m_node.chain.get(); |
1377 | 1 | auto wallet = TestCreateWallet(context); |
1378 | 1 | CKey key = GenerateRandomKey(); |
1379 | 1 | AddKey(*wallet, key); |
1380 | | |
1381 | 1 | m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]); |
1382 | 1 | auto block_tx = TestSimpleSpend(*m_coinbase_txns[0], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); |
1383 | 1 | CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); |
1384 | | |
1385 | 1 | m_node.validation_signals->SyncWithValidationInterfaceQueue(); |
1386 | | |
1387 | 1 | { |
1388 | 1 | auto block_hash = block_tx.GetHash(); |
1389 | 1 | auto prev_tx = m_coinbase_txns[0]; |
1390 | | |
1391 | 1 | LOCK(wallet->cs_wallet); |
1392 | 1 | BOOST_CHECK(wallet->HasWalletSpend(prev_tx)); |
1393 | 1 | BOOST_CHECK(wallet->mapWallet.contains(block_hash)); |
1394 | | |
1395 | 1 | std::vector<Txid> vHashIn{ block_hash }; |
1396 | 1 | BOOST_CHECK(wallet->RemoveTxs(vHashIn)); |
1397 | | |
1398 | 1 | BOOST_CHECK(!wallet->HasWalletSpend(prev_tx)); |
1399 | 1 | BOOST_CHECK(!wallet->mapWallet.contains(block_hash)); |
1400 | 1 | } |
1401 | | |
1402 | 1 | TestUnloadWallet(std::move(wallet)); |
1403 | 1 | } |
1404 | | |
1405 | | BOOST_AUTO_TEST_SUITE_END() |
1406 | | } // namespace wallet |