Coverage Report

Created: 2026-09-14 20:36

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