Coverage Report

Created: 2025-10-29 16:48

/root/bitcoin/src/test/fuzz/tx_pool.cpp
Line
Count
Source (jump to first uncovered line)
1
// Copyright (c) 2021-2022 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 <consensus/validation.h>
6
#include <node/context.h>
7
#include <node/mempool_args.h>
8
#include <node/miner.h>
9
#include <policy/truc_policy.h>
10
#include <test/fuzz/FuzzedDataProvider.h>
11
#include <test/fuzz/fuzz.h>
12
#include <test/fuzz/util.h>
13
#include <test/fuzz/util/mempool.h>
14
#include <test/util/mining.h>
15
#include <test/util/script.h>
16
#include <test/util/setup_common.h>
17
#include <test/util/txmempool.h>
18
#include <util/check.h>
19
#include <util/rbf.h>
20
#include <util/translation.h>
21
#include <validation.h>
22
#include <validationinterface.h>
23
24
using node::BlockAssembler;
25
using node::NodeContext;
26
using util::ToString;
27
28
namespace {
29
30
const TestingSetup* g_setup;
31
std::vector<COutPoint> g_outpoints_coinbase_init_mature;
32
std::vector<COutPoint> g_outpoints_coinbase_init_immature;
33
34
struct MockedTxPool : public CTxMemPool {
35
    void RollingFeeUpdate() EXCLUSIVE_LOCKS_REQUIRED(!cs)
36
0
    {
37
0
        LOCK(cs);
38
0
        lastRollingFeeUpdate = GetTime();
39
0
        blockSinceLastRollingFeeBump = true;
40
0
    }
41
};
42
43
void initialize_tx_pool()
44
0
{
45
0
    static const auto testing_setup = MakeNoLogFileContext<const TestingSetup>();
46
0
    g_setup = testing_setup.get();
47
0
    SetMockTime(WITH_LOCK(g_setup->m_node.chainman->GetMutex(), return g_setup->m_node.chainman->ActiveTip()->Time()));
48
49
0
    BlockAssembler::Options options;
50
0
    options.coinbase_output_script = P2WSH_OP_TRUE;
51
52
0
    for (int i = 0; i < 2 * COINBASE_MATURITY; ++i) {
53
0
        COutPoint prevout{MineBlock(g_setup->m_node, options)};
54
        // Remember the txids to avoid expensive disk access later on
55
0
        auto& outpoints = i < COINBASE_MATURITY ?
56
0
                              g_outpoints_coinbase_init_mature :
57
0
                              g_outpoints_coinbase_init_immature;
58
0
        outpoints.push_back(prevout);
59
0
    }
60
0
    g_setup->m_node.validation_signals->SyncWithValidationInterfaceQueue();
61
0
}
62
63
struct TransactionsDelta final : public CValidationInterface {
64
    std::set<CTransactionRef>& m_removed;
65
    std::set<CTransactionRef>& m_added;
66
67
    explicit TransactionsDelta(std::set<CTransactionRef>& r, std::set<CTransactionRef>& a)
68
0
        : m_removed{r}, m_added{a} {}
69
70
    void TransactionAddedToMempool(const NewMempoolTransactionInfo& tx, uint64_t /* mempool_sequence */) override
71
0
    {
72
0
        Assert(m_added.insert(tx.info.m_tx).second);
73
0
    }
74
75
    void TransactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRemovalReason reason, uint64_t /* mempool_sequence */) override
76
0
    {
77
0
        Assert(m_removed.insert(tx).second);
78
0
    }
79
};
80
81
void SetMempoolConstraints(ArgsManager& args, FuzzedDataProvider& fuzzed_data_provider)
82
0
{
83
0
    args.ForceSetArg("-limitancestorcount",
84
0
                     ToString(fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 50)));
85
0
    args.ForceSetArg("-limitancestorsize",
86
0
                     ToString(fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 202)));
87
0
    args.ForceSetArg("-limitdescendantcount",
88
0
                     ToString(fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 50)));
89
0
    args.ForceSetArg("-limitdescendantsize",
90
0
                     ToString(fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 202)));
91
0
    args.ForceSetArg("-maxmempool",
92
0
                     ToString(fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 200)));
93
0
    args.ForceSetArg("-mempoolexpiry",
94
0
                     ToString(fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 999)));
95
0
}
96
97
void Finish(FuzzedDataProvider& fuzzed_data_provider, MockedTxPool& tx_pool, Chainstate& chainstate)
98
0
{
99
0
    WITH_LOCK(::cs_main, tx_pool.check(chainstate.CoinsTip(), chainstate.m_chain.Height() + 1));
100
0
    {
101
0
        BlockAssembler::Options options;
102
0
        options.nBlockMaxWeight = fuzzed_data_provider.ConsumeIntegralInRange(0U, MAX_BLOCK_WEIGHT);
103
0
        options.blockMinFeeRate = CFeeRate{ConsumeMoney(fuzzed_data_provider, /*max=*/COIN)};
104
0
        auto assembler = BlockAssembler{chainstate, &tx_pool, options};
105
0
        auto block_template = assembler.CreateNewBlock();
106
0
        Assert(block_template->block.vtx.size() >= 1);
107
0
    }
108
0
    const auto info_all = tx_pool.infoAll();
109
0
    if (!info_all.empty()) {
110
0
        const auto& tx_to_remove = *PickValue(fuzzed_data_provider, info_all).tx;
111
0
        WITH_LOCK(tx_pool.cs, tx_pool.removeRecursive(tx_to_remove, MemPoolRemovalReason::BLOCK /* dummy */));
112
0
        assert(tx_pool.size() < info_all.size());
113
0
        WITH_LOCK(::cs_main, tx_pool.check(chainstate.CoinsTip(), chainstate.m_chain.Height() + 1));
114
0
    }
115
0
    g_setup->m_node.validation_signals->SyncWithValidationInterfaceQueue();
116
0
}
117
118
void MockTime(FuzzedDataProvider& fuzzed_data_provider, const Chainstate& chainstate)
119
0
{
120
0
    const auto time = ConsumeTime(fuzzed_data_provider,
121
0
                                  chainstate.m_chain.Tip()->GetMedianTimePast() + 1,
122
0
                                  std::numeric_limits<decltype(chainstate.m_chain.Tip()->nTime)>::max());
123
0
    SetMockTime(time);
124
0
}
125
126
std::unique_ptr<CTxMemPool> MakeMempool(FuzzedDataProvider& fuzzed_data_provider, const NodeContext& node)
127
0
{
128
    // Take the default options for tests...
129
0
    CTxMemPool::Options mempool_opts{MemPoolOptionsForTest(node)};
130
131
    // ...override specific options for this specific fuzz suite
132
0
    mempool_opts.check_ratio = 1;
133
0
    mempool_opts.require_standard = fuzzed_data_provider.ConsumeBool();
134
135
    // ...and construct a CTxMemPool from it
136
0
    bilingual_str error;
137
0
    auto mempool{std::make_unique<CTxMemPool>(std::move(mempool_opts), error)};
138
    // ... ignore the error since it might be beneficial to fuzz even when the
139
    // mempool size is unreasonably small
140
0
    Assert(error.empty() || error.original.starts_with("-maxmempool must be at least "));
141
0
    return mempool;
142
0
}
143
144
void CheckATMPInvariants(const MempoolAcceptResult& res, bool txid_in_mempool, bool wtxid_in_mempool)
145
0
{
146
147
0
    switch (res.m_result_type) {
148
0
    case MempoolAcceptResult::ResultType::VALID:
149
0
    {
150
0
        Assert(txid_in_mempool);
151
0
        Assert(wtxid_in_mempool);
152
0
        Assert(res.m_state.IsValid());
153
0
        Assert(!res.m_state.IsInvalid());
154
0
        Assert(res.m_vsize);
155
0
        Assert(res.m_base_fees);
156
0
        Assert(res.m_effective_feerate);
157
0
        Assert(res.m_wtxids_fee_calculations);
158
0
        Assert(!res.m_other_wtxid);
159
0
        break;
160
0
    }
161
0
    case MempoolAcceptResult::ResultType::INVALID:
162
0
    {
163
        // It may be already in the mempool since in ATMP cases we don't set MEMPOOL_ENTRY or DIFFERENT_WITNESS
164
0
        Assert(!res.m_state.IsValid());
165
0
        Assert(res.m_state.IsInvalid());
166
167
0
        const bool is_reconsiderable{res.m_state.GetResult() == TxValidationResult::TX_RECONSIDERABLE};
168
0
        Assert(!res.m_vsize);
169
0
        Assert(!res.m_base_fees);
170
        // Fee information is provided if the failure is TX_RECONSIDERABLE.
171
        // In other cases, validation may be unable or unwilling to calculate the fees.
172
0
        Assert(res.m_effective_feerate.has_value() == is_reconsiderable);
173
0
        Assert(res.m_wtxids_fee_calculations.has_value() == is_reconsiderable);
174
0
        Assert(!res.m_other_wtxid);
175
0
        break;
176
0
    }
177
0
    case MempoolAcceptResult::ResultType::MEMPOOL_ENTRY:
178
0
    {
179
        // ATMP never sets this; only set in package settings
180
0
        Assert(false);
181
0
        break;
182
0
    }
183
0
    case MempoolAcceptResult::ResultType::DIFFERENT_WITNESS:
184
0
    {
185
        // ATMP never sets this; only set in package settings
186
0
        Assert(false);
187
0
        break;
188
0
    }
189
0
    }
190
0
}
191
192
FUZZ_TARGET(tx_pool_standard, .init = initialize_tx_pool)
193
0
{
194
0
    SeedRandomStateForTest(SeedRand::ZEROS);
195
0
    FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
196
0
    const auto& node = g_setup->m_node;
197
0
    auto& chainstate{static_cast<DummyChainState&>(node.chainman->ActiveChainstate())};
198
199
0
    MockTime(fuzzed_data_provider, chainstate);
200
201
    // All RBF-spendable outpoints
202
0
    std::set<COutPoint> outpoints_rbf;
203
    // All outpoints counting toward the total supply (subset of outpoints_rbf)
204
0
    std::set<COutPoint> outpoints_supply;
205
0
    for (const auto& outpoint : g_outpoints_coinbase_init_mature) {
206
0
        Assert(outpoints_supply.insert(outpoint).second);
207
0
    }
208
0
    outpoints_rbf = outpoints_supply;
209
210
    // The sum of the values of all spendable outpoints
211
0
    constexpr CAmount SUPPLY_TOTAL{COINBASE_MATURITY * 50 * COIN};
212
213
0
    SetMempoolConstraints(*node.args, fuzzed_data_provider);
214
0
    auto tx_pool_{MakeMempool(fuzzed_data_provider, node)};
215
0
    MockedTxPool& tx_pool = *static_cast<MockedTxPool*>(tx_pool_.get());
216
217
0
    chainstate.SetMempool(&tx_pool);
218
219
    // Helper to query an amount
220
0
    const CCoinsViewMemPool amount_view{WITH_LOCK(::cs_main, return &chainstate.CoinsTip()), tx_pool};
221
0
    const auto GetAmount = [&](const COutPoint& outpoint) {
222
0
        auto coin{amount_view.GetCoin(outpoint).value()};
223
0
        return coin.out.nValue;
224
0
    };
225
226
0
    LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 100)
227
0
    {
228
0
        {
229
            // Total supply is the mempool fee + all outpoints
230
0
            CAmount supply_now{WITH_LOCK(tx_pool.cs, return tx_pool.GetTotalFee())};
231
0
            for (const auto& op : outpoints_supply) {
232
0
                supply_now += GetAmount(op);
233
0
            }
234
0
            Assert(supply_now == SUPPLY_TOTAL);
235
0
        }
236
0
        Assert(!outpoints_supply.empty());
237
238
        // Create transaction to add to the mempool
239
0
        const CTransactionRef tx = [&] {
240
0
            CMutableTransaction tx_mut;
241
0
            tx_mut.version = fuzzed_data_provider.ConsumeBool() ? TRUC_VERSION : CTransaction::CURRENT_VERSION;
242
0
            tx_mut.nLockTime = fuzzed_data_provider.ConsumeBool() ? 0 : fuzzed_data_provider.ConsumeIntegral<uint32_t>();
243
0
            const auto num_in = fuzzed_data_provider.ConsumeIntegralInRange<int>(1, outpoints_rbf.size());
244
0
            const auto num_out = fuzzed_data_provider.ConsumeIntegralInRange<int>(1, outpoints_rbf.size() * 2);
245
246
0
            CAmount amount_in{0};
247
0
            for (int i = 0; i < num_in; ++i) {
248
                // Pop random outpoint
249
0
                auto pop = outpoints_rbf.begin();
250
0
                std::advance(pop, fuzzed_data_provider.ConsumeIntegralInRange<size_t>(0, outpoints_rbf.size() - 1));
251
0
                const auto outpoint = *pop;
252
0
                outpoints_rbf.erase(pop);
253
0
                amount_in += GetAmount(outpoint);
254
255
                // Create input
256
0
                const auto sequence = ConsumeSequence(fuzzed_data_provider);
257
0
                const auto script_sig = CScript{};
258
0
                const auto script_wit_stack = std::vector<std::vector<uint8_t>>{WITNESS_STACK_ELEM_OP_TRUE};
259
0
                CTxIn in;
260
0
                in.prevout = outpoint;
261
0
                in.nSequence = sequence;
262
0
                in.scriptSig = script_sig;
263
0
                in.scriptWitness.stack = script_wit_stack;
264
265
0
                tx_mut.vin.push_back(in);
266
0
            }
267
0
            const auto amount_fee = fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(-1000, amount_in);
268
0
            const auto amount_out = (amount_in - amount_fee) / num_out;
269
0
            for (int i = 0; i < num_out; ++i) {
270
0
                tx_mut.vout.emplace_back(amount_out, P2WSH_OP_TRUE);
271
0
            }
272
0
            auto tx = MakeTransactionRef(tx_mut);
273
            // Restore previously removed outpoints
274
0
            for (const auto& in : tx->vin) {
275
0
                Assert(outpoints_rbf.insert(in.prevout).second);
276
0
            }
277
0
            return tx;
278
0
        }();
279
280
0
        if (fuzzed_data_provider.ConsumeBool()) {
281
0
            MockTime(fuzzed_data_provider, chainstate);
282
0
        }
283
0
        if (fuzzed_data_provider.ConsumeBool()) {
284
0
            tx_pool.RollingFeeUpdate();
285
0
        }
286
0
        if (fuzzed_data_provider.ConsumeBool()) {
287
0
            const auto& txid = fuzzed_data_provider.ConsumeBool() ?
288
0
                                   tx->GetHash() :
289
0
                                   PickValue(fuzzed_data_provider, outpoints_rbf).hash;
290
0
            const auto delta = fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(-50 * COIN, +50 * COIN);
291
0
            tx_pool.PrioritiseTransaction(txid, delta);
292
0
        }
293
294
        // Remember all removed and added transactions
295
0
        std::set<CTransactionRef> removed;
296
0
        std::set<CTransactionRef> added;
297
0
        auto txr = std::make_shared<TransactionsDelta>(removed, added);
298
0
        node.validation_signals->RegisterSharedValidationInterface(txr);
299
300
        // Make sure ProcessNewPackage on one transaction works.
301
        // The result is not guaranteed to be the same as what is returned by ATMP.
302
0
        const auto result_package = WITH_LOCK(::cs_main,
303
0
                                    return ProcessNewPackage(chainstate, tx_pool, {tx}, true, /*client_maxfeerate=*/{}));
304
        // If something went wrong due to a package-specific policy, it might not return a
305
        // validation result for the transaction.
306
0
        if (result_package.m_state.GetResult() != PackageValidationResult::PCKG_POLICY) {
307
0
            auto it = result_package.m_tx_results.find(tx->GetWitnessHash());
308
0
            Assert(it != result_package.m_tx_results.end());
309
0
            Assert(it->second.m_result_type == MempoolAcceptResult::ResultType::VALID ||
310
0
                   it->second.m_result_type == MempoolAcceptResult::ResultType::INVALID);
311
0
        }
312
313
0
        const auto res = WITH_LOCK(::cs_main, return AcceptToMemoryPool(chainstate, tx, GetTime(), /*bypass_limits=*/false, /*test_accept=*/false));
314
0
        const bool accepted = res.m_result_type == MempoolAcceptResult::ResultType::VALID;
315
0
        node.validation_signals->SyncWithValidationInterfaceQueue();
316
0
        node.validation_signals->UnregisterSharedValidationInterface(txr);
317
318
0
        bool txid_in_mempool = tx_pool.exists(tx->GetHash());
319
0
        bool wtxid_in_mempool = tx_pool.exists(tx->GetWitnessHash());
320
0
        CheckATMPInvariants(res, txid_in_mempool, wtxid_in_mempool);
321
322
0
        Assert(accepted != added.empty());
323
0
        if (accepted) {
324
0
            Assert(added.size() == 1); // For now, no package acceptance
325
0
            Assert(tx == *added.begin());
326
0
            CheckMempoolTRUCInvariants(tx_pool);
327
0
        } else {
328
            // Do not consider rejected transaction removed
329
0
            removed.erase(tx);
330
0
        }
331
332
        // Helper to insert spent and created outpoints of a tx into collections
333
0
        using Sets = std::vector<std::reference_wrapper<std::set<COutPoint>>>;
334
0
        const auto insert_tx = [](Sets created_by_tx, Sets consumed_by_tx, const auto& tx) {
335
0
            for (size_t i{0}; i < tx.vout.size(); ++i) {
336
0
                for (auto& set : created_by_tx) {
337
0
                    Assert(set.get().emplace(tx.GetHash(), i).second);
338
0
                }
339
0
            }
340
0
            for (const auto& in : tx.vin) {
341
0
                for (auto& set : consumed_by_tx) {
342
0
                    Assert(set.get().insert(in.prevout).second);
343
0
                }
344
0
            }
345
0
        };
346
        // Add created outpoints, remove spent outpoints
347
0
        {
348
            // Outpoints that no longer exist at all
349
0
            std::set<COutPoint> consumed_erased;
350
            // Outpoints that no longer count toward the total supply
351
0
            std::set<COutPoint> consumed_supply;
352
0
            for (const auto& removed_tx : removed) {
353
0
                insert_tx(/*created_by_tx=*/{consumed_erased}, /*consumed_by_tx=*/{outpoints_supply}, /*tx=*/*removed_tx);
354
0
            }
355
0
            for (const auto& added_tx : added) {
356
0
                insert_tx(/*created_by_tx=*/{outpoints_supply, outpoints_rbf}, /*consumed_by_tx=*/{consumed_supply}, /*tx=*/*added_tx);
357
0
            }
358
0
            for (const auto& p : consumed_erased) {
359
0
                Assert(outpoints_supply.erase(p) == 1);
360
0
                Assert(outpoints_rbf.erase(p) == 1);
361
0
            }
362
0
            for (const auto& p : consumed_supply) {
363
0
                Assert(outpoints_supply.erase(p) == 1);
364
0
            }
365
0
        }
366
0
    }
367
0
    Finish(fuzzed_data_provider, tx_pool, chainstate);
368
0
}
369
370
FUZZ_TARGET(tx_pool, .init = initialize_tx_pool)
371
0
{
372
0
    SeedRandomStateForTest(SeedRand::ZEROS);
373
0
    FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
374
0
    const auto& node = g_setup->m_node;
375
0
    auto& chainstate{static_cast<DummyChainState&>(node.chainman->ActiveChainstate())};
376
377
0
    MockTime(fuzzed_data_provider, chainstate);
378
379
0
    std::vector<Txid> txids;
380
0
    txids.reserve(g_outpoints_coinbase_init_mature.size());
381
0
    for (const auto& outpoint : g_outpoints_coinbase_init_mature) {
382
0
        txids.push_back(outpoint.hash);
383
0
    }
384
0
    for (int i{0}; i <= 3; ++i) {
385
        // Add some immature and non-existent outpoints
386
0
        txids.push_back(g_outpoints_coinbase_init_immature.at(i).hash);
387
0
        txids.push_back(Txid::FromUint256(ConsumeUInt256(fuzzed_data_provider)));
388
0
    }
389
390
0
    SetMempoolConstraints(*node.args, fuzzed_data_provider);
391
0
    auto tx_pool_{MakeMempool(fuzzed_data_provider, node)};
392
0
    MockedTxPool& tx_pool = *static_cast<MockedTxPool*>(tx_pool_.get());
393
394
0
    chainstate.SetMempool(&tx_pool);
395
396
    // If we ever bypass limits, do not do TRUC invariants checks
397
0
    bool ever_bypassed_limits{false};
398
399
0
    LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 300)
400
0
    {
401
0
        const auto mut_tx = ConsumeTransaction(fuzzed_data_provider, txids);
402
403
0
        if (fuzzed_data_provider.ConsumeBool()) {
404
0
            MockTime(fuzzed_data_provider, chainstate);
405
0
        }
406
0
        if (fuzzed_data_provider.ConsumeBool()) {
407
0
            tx_pool.RollingFeeUpdate();
408
0
        }
409
0
        if (fuzzed_data_provider.ConsumeBool()) {
410
0
            const auto txid = fuzzed_data_provider.ConsumeBool() ?
411
0
                                   mut_tx.GetHash() :
412
0
                                   PickValue(fuzzed_data_provider, txids);
413
0
            const auto delta = fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(-50 * COIN, +50 * COIN);
414
0
            tx_pool.PrioritiseTransaction(txid, delta);
415
0
        }
416
417
0
        const bool bypass_limits{fuzzed_data_provider.ConsumeBool()};
418
0
        ever_bypassed_limits |= bypass_limits;
419
420
0
        const auto tx = MakeTransactionRef(mut_tx);
421
0
        const auto res = WITH_LOCK(::cs_main, return AcceptToMemoryPool(chainstate, tx, GetTime(), bypass_limits, /*test_accept=*/false));
422
0
        const bool accepted = res.m_result_type == MempoolAcceptResult::ResultType::VALID;
423
0
        if (accepted) {
424
0
            txids.push_back(tx->GetHash());
425
0
            if (!ever_bypassed_limits) {
426
0
                CheckMempoolTRUCInvariants(tx_pool);
427
0
            }
428
0
        }
429
0
    }
430
0
    Finish(fuzzed_data_provider, tx_pool, chainstate);
431
0
}
432
} // namespace