Coverage Report

Created: 2026-09-21 19:49

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