Coverage Report

Created: 2026-06-12 16:53

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/root/bitcoin/src/test/fuzz/rbf.cpp
Line
Count
Source
1
// Copyright (c) 2020-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 <node/mempool_args.h>
6
#include <policy/rbf.h>
7
#include <primitives/transaction.h>
8
#include <sync.h>
9
#include <test/fuzz/FuzzedDataProvider.h>
10
#include <test/fuzz/fuzz.h>
11
#include <test/fuzz/util.h>
12
#include <test/fuzz/util/mempool.h>
13
#include <test/util/setup_common.h>
14
#include <test/util/time.h>
15
#include <test/util/txmempool.h>
16
#include <txmempool.h>
17
#include <util/check.h>
18
#include <util/translation.h>
19
20
#include <cstdint>
21
#include <optional>
22
#include <string>
23
#include <vector>
24
25
namespace {
26
const BasicTestingSetup* g_setup;
27
} // namespace
28
29
const int NUM_ITERS = 10000;
30
31
std::vector<COutPoint> g_outpoints;
32
33
void initialize_rbf()
34
0
{
35
0
    static const auto testing_setup = MakeNoLogFileContext<>();
36
0
    g_setup = testing_setup.get();
37
0
}
38
39
void initialize_package_rbf()
40
0
{
41
0
    static const auto testing_setup = MakeNoLogFileContext<>();
42
0
    g_setup = testing_setup.get();
43
44
    // Create a fixed set of unique "UTXOs" to source parents from
45
    // to avoid fuzzer giving circular references
46
0
    for (int i = 0; i < NUM_ITERS; ++i) {
  Branch (46:21): [True: 0, False: 0]
47
0
        g_outpoints.emplace_back();
48
0
        g_outpoints.back().n = i;
49
0
    }
50
51
0
}
52
53
FUZZ_TARGET(rbf, .init = initialize_rbf)
54
0
{
55
0
    SeedRandomStateForTest(SeedRand::ZEROS);
56
0
    FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
57
0
    FakeNodeClock clock{ConsumeTime(fuzzed_data_provider)};
58
0
    std::optional<CMutableTransaction> mtx = ConsumeDeserializable<CMutableTransaction>(fuzzed_data_provider, TX_WITH_WITNESS);
59
0
    if (!mtx) {
  Branch (59:9): [True: 0, False: 0]
60
0
        return;
61
0
    }
62
63
0
    bilingual_str error;
64
0
    CTxMemPool pool{MemPoolOptionsForTest(g_setup->m_node), error};
65
0
    Assert(error.empty());
66
67
0
    LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), NUM_ITERS)
68
0
    {
69
0
        const std::optional<CMutableTransaction> another_mtx = ConsumeDeserializable<CMutableTransaction>(fuzzed_data_provider, TX_WITH_WITNESS);
70
0
        if (!another_mtx) {
  Branch (70:13): [True: 0, False: 0]
71
0
            break;
72
0
        }
73
0
        const CTransaction another_tx{*another_mtx};
74
0
        if (fuzzed_data_provider.ConsumeBool() && !mtx->vin.empty()) {
  Branch (74:13): [True: 0, False: 0]
  Branch (74:51): [True: 0, False: 0]
75
0
            mtx->vin[0].prevout = COutPoint{another_tx.GetHash(), 0};
76
0
        }
77
0
        LOCK2(cs_main, pool.cs);
78
0
        if (!pool.GetIter(another_tx.GetHash())) {
  Branch (78:13): [True: 0, False: 0]
79
0
            TryAddToMempool(pool, ConsumeTxMemPoolEntry(fuzzed_data_provider, another_tx));
80
0
        }
81
0
    }
82
0
    const CTransaction tx{*mtx};
83
0
    if (fuzzed_data_provider.ConsumeBool()) {
  Branch (83:9): [True: 0, False: 0]
84
0
        LOCK2(cs_main, pool.cs);
85
0
        if (!pool.GetIter(tx.GetHash())) {
  Branch (85:13): [True: 0, False: 0]
86
0
            TryAddToMempool(pool, ConsumeTxMemPoolEntry(fuzzed_data_provider, tx));
87
0
        }
88
0
    }
89
0
    {
90
0
        LOCK(pool.cs);
91
0
        (void)IsRBFOptIn(tx, pool);
92
0
    }
93
0
}
94
95
FUZZ_TARGET(package_rbf, .init = initialize_package_rbf)
96
0
{
97
0
    SeedRandomStateForTest(SeedRand::ZEROS);
98
0
    FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
99
0
    FakeNodeClock clock{ConsumeTime(fuzzed_data_provider)};
100
101
    // "Real" virtual size is not important for this test since ConsumeTxMemPoolEntry generates its own virtual size values
102
    // so we construct small transactions for performance reasons. Child simply needs an input for later to perhaps connect to parent.
103
0
    CMutableTransaction child;
104
0
    child.vin.resize(1);
105
106
0
    bilingual_str error;
107
0
    CTxMemPool pool{MemPoolOptionsForTest(g_setup->m_node), error};
108
0
    Assert(error.empty());
109
110
    // Add a bunch of parent-child pairs to the mempool, and remember them.
111
0
    std::vector<CTransaction> mempool_txs;
112
0
    uint32_t iter{0};
113
114
    // Keep track of the total vsize of CTxMemPoolEntry's being added to the mempool to avoid overflow
115
    // Add replacement_vsize since this is added to new diagram during RBF check
116
0
    std::optional<CMutableTransaction> replacement_tx = ConsumeDeserializable<CMutableTransaction>(fuzzed_data_provider, TX_WITH_WITNESS);
117
0
    if (!replacement_tx) {
  Branch (117:9): [True: 0, False: 0]
118
0
        return;
119
0
    }
120
0
    replacement_tx->vin.resize(1);
121
0
    replacement_tx->vin[0].prevout = g_outpoints.at(iter++);
122
0
    CTransaction replacement_tx_final{*replacement_tx};
123
0
    auto replacement_entry = ConsumeTxMemPoolEntry(fuzzed_data_provider, replacement_tx_final);
124
0
    int32_t replacement_weight = replacement_entry.GetAdjustedWeight();
125
    // Ensure that we don't hit FeeFrac limits, as we store TxGraph entries in terms of FeePerWeight
126
0
    int64_t running_vsize_total{replacement_entry.GetTxSize()};
127
128
0
    LOCK2(cs_main, pool.cs);
129
130
0
    while (fuzzed_data_provider.ConsumeBool()) {
  Branch (130:12): [True: 0, False: 0]
131
0
        if (iter >= NUM_ITERS) break;
  Branch (131:13): [True: 0, False: 0]
132
133
        // Make sure txns only have one input, and that a unique input is given to avoid circular references
134
0
        CMutableTransaction parent;
135
0
        parent.vin.resize(1);
136
0
        parent.vin[0].prevout = g_outpoints.at(iter++);
137
0
        parent.vout.emplace_back(0, CScript());
138
139
0
        mempool_txs.emplace_back(parent);
140
0
        const auto parent_entry = ConsumeTxMemPoolEntry(fuzzed_data_provider, mempool_txs.back());
141
0
        running_vsize_total += parent_entry.GetTxSize();
142
0
        if (running_vsize_total * WITNESS_SCALE_FACTOR > std::numeric_limits<int32_t>::max()) {
  Branch (142:13): [True: 0, False: 0]
143
            // We aren't adding this final tx to mempool, so we don't want to conflict with it
144
0
            mempool_txs.pop_back();
145
0
            break;
146
0
        }
147
0
        assert(!pool.GetIter(parent_entry.GetTx().GetHash()));
  Branch (147:9): [True: 0, False: 0]
148
0
        TryAddToMempool(pool, parent_entry);
149
150
        // It's possible that adding this to the mempool failed due to cluster
151
        // size limits; if so bail out.
152
0
        if(!pool.GetIter(parent_entry.GetTx().GetHash())) {
  Branch (152:12): [True: 0, False: 0]
153
0
            mempool_txs.pop_back();
154
0
            continue;
155
0
        }
156
157
0
        child.vin[0].prevout = COutPoint{mempool_txs.back().GetHash(), 0};
158
0
        mempool_txs.emplace_back(child);
159
0
        const auto child_entry = ConsumeTxMemPoolEntry(fuzzed_data_provider, mempool_txs.back());
160
0
        running_vsize_total += child_entry.GetTxSize();
161
0
        if (running_vsize_total * WITNESS_SCALE_FACTOR > std::numeric_limits<int32_t>::max()) {
  Branch (161:13): [True: 0, False: 0]
162
            // We aren't adding this final tx to mempool, so we don't want to conflict with it
163
0
            mempool_txs.pop_back();
164
0
            break;
165
0
        }
166
0
        if (!pool.GetIter(child_entry.GetTx().GetHash())) {
  Branch (166:13): [True: 0, False: 0]
167
0
            TryAddToMempool(pool, child_entry);
168
            // Adding this transaction to the mempool may fail due to cluster
169
            // size limits; if so bail out.
170
0
            if(!pool.GetIter(child_entry.GetTx().GetHash())) {
  Branch (170:16): [True: 0, False: 0]
171
0
                mempool_txs.pop_back();
172
0
                continue;
173
0
            }
174
0
        }
175
176
0
        if (fuzzed_data_provider.ConsumeBool()) {
  Branch (176:13): [True: 0, False: 0]
177
0
            pool.PrioritiseTransaction(mempool_txs.back().GetHash(), fuzzed_data_provider.ConsumeIntegralInRange<int32_t>(-100000, 100000));
178
0
        }
179
0
    }
180
181
    // Pick some transactions at random to be the direct conflicts
182
0
    CTxMemPool::setEntries direct_conflicts;
183
0
    for (auto& tx : mempool_txs) {
  Branch (183:19): [True: 0, False: 0]
184
0
        if (fuzzed_data_provider.ConsumeBool() && pool.GetIter(tx.GetHash())) {
  Branch (184:13): [True: 0, False: 0]
  Branch (184:13): [True: 0, False: 0]
  Branch (184:51): [True: 0, False: 0]
185
0
            direct_conflicts.insert(*pool.GetIter(tx.GetHash()));
186
0
        }
187
0
    }
188
189
    // Calculate all conflicts:
190
0
    CTxMemPool::setEntries all_conflicts;
191
0
    for (auto& txiter : direct_conflicts) {
  Branch (191:23): [True: 0, False: 0]
192
0
        pool.CalculateDescendants(txiter, all_conflicts);
193
0
    }
194
195
0
    CAmount replacement_fees = ConsumeMoney(fuzzed_data_provider);
196
0
    auto changeset = pool.GetChangeSet();
197
0
    for (auto& txiter : all_conflicts) {
  Branch (197:23): [True: 0, False: 0]
198
0
        changeset->StageRemoval(txiter);
199
0
    }
200
0
    changeset->StageAddition(replacement_entry.GetSharedTx(), replacement_fees,
201
0
            replacement_entry.GetTime().count(), replacement_entry.GetHeight(),
202
0
            replacement_entry.GetSequence(), replacement_entry.GetSpendsCoinbase(),
203
0
            replacement_entry.GetSigOpCost(), replacement_entry.GetLockPoints());
204
    // Calculate the chunks for a replacement.
205
0
    auto calc_results{changeset->CalculateChunksForRBF()};
206
207
0
    if (calc_results.has_value()) {
  Branch (207:9): [True: 0, False: 0]
208
        // Sanity checks on the chunks.
209
210
        // Feerates are monotonically decreasing.
211
0
        FeeFrac first_sum;
212
0
        for (size_t i = 0; i < calc_results->first.size(); ++i) {
  Branch (212:28): [True: 0, False: 0]
213
0
            first_sum += calc_results->first[i];
214
0
            if (i) assert(ByRatio{calc_results->first[i - 1]} >= ByRatio{calc_results->first[i]});
  Branch (214:17): [True: 0, False: 0]
  Branch (214:20): [True: 0, False: 0]
215
0
        }
216
0
        FeeFrac second_sum;
217
0
        for (size_t i = 0; i < calc_results->second.size(); ++i) {
  Branch (217:28): [True: 0, False: 0]
218
0
            second_sum += calc_results->second[i];
219
0
            if (i) assert(ByRatio{calc_results->second[i - 1]} >= ByRatio{calc_results->second[i]});
  Branch (219:17): [True: 0, False: 0]
  Branch (219:20): [True: 0, False: 0]
220
0
        }
221
222
0
        FeeFrac replaced;
223
0
        for (auto txiter : all_conflicts) {
  Branch (223:26): [True: 0, False: 0]
224
0
            replaced.fee += txiter->GetModifiedFee();
225
0
            replaced.size += txiter->GetAdjustedWeight();
226
0
        }
227
        // The total fee & size of the new diagram minus replaced fee & size should be the total
228
        // fee & size of the old diagram minus replacement fee & size.
229
0
        assert((first_sum - replaced) == (second_sum - FeeFrac{replacement_fees, replacement_weight}));
  Branch (229:9): [True: 0, False: 0]
230
0
    }
231
232
    // If internals report error, wrapper should too
233
0
    auto err_tuple{ImprovesFeerateDiagram(*changeset)};
234
0
    if (!calc_results.has_value()) {
  Branch (234:9): [True: 0, False: 0]
235
0
         assert(err_tuple.value().first == DiagramCheckError::UNCALCULABLE);
  Branch (235:10): [True: 0, False: 0]
236
0
    } else {
237
        // Diagram check succeeded
238
0
        auto old_sum = std::accumulate(calc_results->first.begin(), calc_results->first.end(), FeeFrac{});
239
0
        auto new_sum = std::accumulate(calc_results->second.begin(), calc_results->second.end(), FeeFrac{});
240
0
        if (!err_tuple.has_value()) {
  Branch (240:13): [True: 0, False: 0]
241
            // New diagram's final fee should always match or exceed old diagram's
242
0
            assert(old_sum.fee <= new_sum.fee);
  Branch (242:13): [True: 0, False: 0]
243
0
        } else if (old_sum.fee > new_sum.fee) {
  Branch (243:20): [True: 0, False: 0]
244
            // Or it failed, and if old diagram had higher fees, it should be a failure
245
            assert(err_tuple.value().first == DiagramCheckError::FAILURE);
  Branch (245:13): [True: 0, False: 0]
246
0
        }
247
0
    }
248
0
}