Coverage Report

Created: 2025-04-14 16:24

/Users/mcomp/contrib/bitcoin/src/test/fuzz/mini_miner.cpp
Line
Count
Source (jump to first uncovered line)
1
// Copyright (c) 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 <test/fuzz/FuzzedDataProvider.h>
6
#include <test/fuzz/fuzz.h>
7
#include <test/fuzz/util.h>
8
#include <test/fuzz/util/mempool.h>
9
#include <test/util/script.h>
10
#include <test/util/setup_common.h>
11
#include <test/util/txmempool.h>
12
#include <test/util/mining.h>
13
14
#include <node/miner.h>
15
#include <node/mini_miner.h>
16
#include <node/types.h>
17
#include <primitives/transaction.h>
18
#include <random.h>
19
#include <txmempool.h>
20
#include <util/check.h>
21
#include <util/time.h>
22
#include <util/translation.h>
23
24
#include <deque>
25
#include <vector>
26
27
namespace {
28
29
const TestingSetup* g_setup;
30
std::deque<COutPoint> g_available_coins;
31
void initialize_miner()
32
0
{
33
0
    static const auto testing_setup = MakeNoLogFileContext<const TestingSetup>();
34
0
    g_setup = testing_setup.get();
35
0
    for (uint32_t i = 0; i < uint32_t{100}; ++i) {
36
0
        g_available_coins.emplace_back(Txid::FromUint256(uint256::ZERO), i);
37
0
    }
38
0
}
39
40
// Test that the MiniMiner can run with various outpoints and feerates.
41
FUZZ_TARGET(mini_miner, .init = initialize_miner)
42
0
{
43
0
    SeedRandomStateForTest(SeedRand::ZEROS);
44
0
    FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
45
0
    SetMockTime(ConsumeTime(fuzzed_data_provider));
46
0
    bilingual_str error;
47
0
    CTxMemPool pool{CTxMemPool::Options{}, error};
48
0
    Assert(error.empty());
49
0
    std::vector<COutPoint> outpoints;
50
0
    std::deque<COutPoint> available_coins = g_available_coins;
51
0
    LOCK2(::cs_main, pool.cs);
52
    // Cluster size cannot exceed 500
53
0
    LIMITED_WHILE(!available_coins.empty(), 500)
54
0
    {
55
0
        CMutableTransaction mtx = CMutableTransaction();
56
0
        const size_t num_inputs = fuzzed_data_provider.ConsumeIntegralInRange<size_t>(1, available_coins.size());
57
0
        const size_t num_outputs = fuzzed_data_provider.ConsumeIntegralInRange<size_t>(1, 50);
58
0
        for (size_t n{0}; n < num_inputs; ++n) {
59
0
            auto prevout = available_coins.front();
60
0
            mtx.vin.emplace_back(prevout, CScript());
61
0
            available_coins.pop_front();
62
0
        }
63
0
        for (uint32_t n{0}; n < num_outputs; ++n) {
64
0
            mtx.vout.emplace_back(100, P2WSH_OP_TRUE);
65
0
        }
66
0
        CTransactionRef tx = MakeTransactionRef(mtx);
67
0
        TestMemPoolEntryHelper entry;
68
0
        const CAmount fee{ConsumeMoney(fuzzed_data_provider, /*max=*/MAX_MONEY/100000)};
69
0
        assert(MoneyRange(fee));
70
0
        AddToMempool(pool, entry.Fee(fee).FromTx(tx));
71
72
        // All outputs are available to spend
73
0
        for (uint32_t n{0}; n < num_outputs; ++n) {
74
0
            if (fuzzed_data_provider.ConsumeBool()) {
75
0
                available_coins.emplace_back(tx->GetHash(), n);
76
0
            }
77
0
        }
78
79
0
        if (fuzzed_data_provider.ConsumeBool() && !tx->vout.empty()) {
80
            // Add outpoint from this tx (may or not be spent by a later tx)
81
0
            outpoints.emplace_back(tx->GetHash(),
82
0
                                          (uint32_t)fuzzed_data_provider.ConsumeIntegralInRange<size_t>(0, tx->vout.size()));
83
0
        } else {
84
            // Add some random outpoint (will be interpreted as confirmed or not yet submitted
85
            // to mempool).
86
0
            auto outpoint = ConsumeDeserializable<COutPoint>(fuzzed_data_provider);
87
0
            if (outpoint.has_value() && std::find(outpoints.begin(), outpoints.end(), *outpoint) == outpoints.end()) {
88
0
                outpoints.push_back(*outpoint);
89
0
            }
90
0
        }
91
92
0
    }
93
94
0
    const CFeeRate target_feerate{CFeeRate{ConsumeMoney(fuzzed_data_provider, /*max=*/MAX_MONEY/1000)}};
95
0
    std::optional<CAmount> total_bumpfee;
96
0
    CAmount sum_fees = 0;
97
0
    {
98
0
        node::MiniMiner mini_miner{pool, outpoints};
99
0
        assert(mini_miner.IsReadyToCalculate());
100
0
        const auto bump_fees = mini_miner.CalculateBumpFees(target_feerate);
101
0
        for (const auto& outpoint : outpoints) {
102
0
            auto it = bump_fees.find(outpoint);
103
0
            assert(it != bump_fees.end());
104
0
            assert(it->second >= 0);
105
0
            sum_fees += it->second;
106
0
        }
107
0
        assert(!mini_miner.IsReadyToCalculate());
108
0
    }
109
0
    {
110
0
        node::MiniMiner mini_miner{pool, outpoints};
111
0
        assert(mini_miner.IsReadyToCalculate());
112
0
        total_bumpfee = mini_miner.CalculateTotalBumpFees(target_feerate);
113
0
        assert(total_bumpfee.has_value());
114
0
        assert(!mini_miner.IsReadyToCalculate());
115
0
    }
116
    // Overlapping ancestry across multiple outpoints can only reduce the total bump fee.
117
0
    assert (sum_fees >= *total_bumpfee);
118
0
}
119
120
// Test that MiniMiner and BlockAssembler build the same block given the same transactions and constraints.
121
FUZZ_TARGET(mini_miner_selection, .init = initialize_miner)
122
2
{
123
2
    SeedRandomStateForTest(SeedRand::ZEROS);
124
2
    FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
125
2
    SetMockTime(ConsumeTime(fuzzed_data_provider));
126
2
    bilingual_str error;
127
2
    CTxMemPool pool{CTxMemPool::Options{}, error};
128
2
    Assert(error.empty());
129
    // Make a copy to preserve determinism.
130
2
    std::deque<COutPoint> available_coins = g_available_coins;
131
2
    std::vector<CTransactionRef> transactions;
132
    // The maximum block template size we expect to produce
133
2
    const auto block_adjusted_max_weight = MAX_BLOCK_WEIGHT - MINIMUM_BLOCK_RESERVED_WEIGHT;
134
    // When this is set to true we try to fill up the rest of the block with
135
    // a lot of small transactions so we can actually get close to actual
136
    // maximum block size
137
2
    std::optional<bool> min_size_tx{std::nullopt};
138
    // This decides if we target a larger, potentially full block or a smaller
139
    // block that will complete the test much faster
140
2
    bool use_limited_loop = fuzzed_data_provider.ConsumeBool();
141
440
    auto should_continue = [&]() -> bool {
142
440
        if (use_limited_loop) {
143
0
            return fuzzed_data_provider.ConsumeBool();
144
0
        }
145
440
        return true;
146
440
    };
147
148
2
    LOCK2(::cs_main, pool.cs);
149
    // Limited to 500 because of ClusterMempool DoS protection
150
2
    LIMITED_WHILE(should_continue(), 500)
151
440
    {
152
440
        CMutableTransaction mtx = CMutableTransaction();
153
440
        assert(!available_coins.empty());
154
440
        size_t num_inputs = std::min(size_t{2}, available_coins.size());
155
440
        size_t num_outputs = fuzzed_data_provider.ConsumeIntegralInRange<size_t>(50, 500);
156
440
        if (min_size_tx.has_value() && min_size_tx.value()) {
157
0
            num_inputs = fuzzed_data_provider.ConsumeIntegralInRange<size_t>(1, 5);
158
0
            num_outputs = fuzzed_data_provider.ConsumeIntegralInRange<size_t>(1, 5);
159
0
        }
160
1.32k
        for (size_t n{0}; n < num_inputs; ++n) {
161
880
            auto prevout = available_coins.at(0);
162
880
            mtx.vin.emplace_back(prevout, CScript());
163
880
            available_coins.pop_front();
164
880
        }
165
22.7k
        for (uint32_t n{0}; n < num_outputs; ++n) {
166
22.3k
            mtx.vout.emplace_back(100, P2WSH_OP_TRUE);
167
22.3k
        }
168
440
        CTransactionRef tx = MakeTransactionRef(mtx);
169
170
        // First 2 outputs are available to spend. The rest are added to outpoints to calculate bumpfees.
171
        // There is no overlap between spendable coins and outpoints passed to MiniMiner because the
172
        // MiniMiner interprets spent coins as to-be-replaced and excludes them.
173
22.3k
        for (uint32_t n{0}; n < num_outputs - 1; ++n) {
174
21.8k
            if (fuzzed_data_provider.ConsumeBool()) {
175
302
                available_coins.emplace_front(tx->GetHash(), n);
176
21.5k
            } else {
177
21.5k
                available_coins.emplace_back(tx->GetHash(), n);
178
21.5k
            }
179
21.8k
        }
180
181
        // Stop if pool reaches block_adjusted_max_weight because BlockAssembler will stop when the
182
        // block template reaches that, but the MiniMiner will keep going.
183
440
        if ((pool.GetTotalTxSize() + GetVirtualTransactionSize(*tx)) * 4 >= block_adjusted_max_weight) {
184
            // Either stop here or fill up the rest of the block with very small
185
            // transactions and break when the block is close to the possible max
186
1
            if (!min_size_tx.has_value()) {
187
1
                min_size_tx = fuzzed_data_provider.ConsumeBool();
188
1
                if (!min_size_tx.value()) break;
189
1
            }
190
0
            break;
191
1
        }
192
439
        TestMemPoolEntryHelper entry;
193
439
        const CAmount fee{ConsumeMoney(fuzzed_data_provider, /*max=*/MAX_MONEY/100000)};
194
439
        assert(MoneyRange(fee));
195
439
        AddToMempool(pool, entry.Fee(fee).FromTx(tx));
196
439
        transactions.push_back(tx);
197
439
    }
198
2
    std::vector<COutPoint> outpoints;
199
100
    for (const auto& coin : g_available_coins) {
200
100
        if (!pool.GetConflictTx(coin)) outpoints.push_back(coin);
201
100
    }
202
439
    for (const auto& tx : transactions) {
203
439
        assert(pool.exists(GenTxid::Txid(tx->GetHash())));
204
22.7k
        for (uint32_t n{0}; n < tx->vout.size(); ++n) {
205
22.2k
            COutPoint coin{tx->GetHash(), n};
206
22.2k
            if (!pool.GetConflictTx(coin)) outpoints.push_back(coin);
207
22.2k
        }
208
439
    }
209
2
    const CFeeRate target_feerate{ConsumeMoney(fuzzed_data_provider, /*max=*/MAX_MONEY/100000)};
210
211
2
    node::BlockAssembler::Options miner_options;
212
2
    miner_options.blockMinFeeRate = target_feerate;
213
2
    miner_options.nBlockMaxWeight = MAX_BLOCK_WEIGHT;
214
    // Only setting reserved weight when necessary based on the template size
215
2
    const auto reserved_weight = MAX_BLOCK_WEIGHT - pool.GetTotalTxSize() * 4;
216
2
    if (reserved_weight < DEFAULT_BLOCK_RESERVED_WEIGHT) {
217
0
        miner_options.block_reserved_weight = reserved_weight - 1;
218
0
    }
219
2
    miner_options.test_block_validity = false;
220
2
    miner_options.coinbase_output_script = CScript() << OP_0;
221
222
2
    node::BlockAssembler miner{g_setup->m_node.chainman->ActiveChainstate(), &pool, miner_options};
223
2
    node::MiniMiner mini_miner{pool, outpoints};
224
2
    assert(mini_miner.IsReadyToCalculate());
225
226
    // Use BlockAssembler as oracle. BlockAssembler and MiniMiner should select the same
227
    // transactions, stopping once packages do not meet target_feerate.
228
2
    const auto blocktemplate{miner.CreateNewBlock()};
229
2
    mini_miner.BuildMockTemplate(target_feerate);
230
2
    assert(!mini_miner.IsReadyToCalculate());
231
2
    auto mock_template_txids = mini_miner.GetMockTemplateTxids();
232
    // MiniMiner doesn't add a coinbase tx.
233
2
    assert(mock_template_txids.count(blocktemplate->block.vtx[0]->GetHash()) == 0);
234
2
    auto [iter, new_entry] = mock_template_txids.emplace(blocktemplate->block.vtx[0]->GetHash());
235
2
    assert(new_entry);
236
237
2
    assert(mock_template_txids.size() == blocktemplate->block.vtx.size());
238
1
    for (const auto& tx : blocktemplate->block.vtx) {
239
0
        assert(mock_template_txids.count(tx->GetHash()));
240
0
    }
241
1
}
242
} // namespace