Coverage Report

Created: 2026-09-15 16:03

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