aboutsummaryrefslogtreecommitdiff
path: root/src/test/fuzz/mini_miner.cpp
blob: 3a1663364fd031d2ca6d9189f3cda7aca69c487c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
#include <test/fuzz/FuzzedDataProvider.h>
#include <test/fuzz/fuzz.h>
#include <test/fuzz/util.h>
#include <test/fuzz/util/mempool.h>
#include <test/util/script.h>
#include <test/util/setup_common.h>
#include <test/util/txmempool.h>
#include <test/util/mining.h>

#include <node/miner.h>
#include <node/mini_miner.h>
#include <primitives/transaction.h>
#include <random.h>
#include <txmempool.h>
#include <util/check.h>
#include <util/translation.h>

#include <deque>
#include <vector>

namespace {

const TestingSetup* g_setup;
std::deque<COutPoint> g_available_coins;
void initialize_miner()
{
    static const auto testing_setup = MakeNoLogFileContext<const TestingSetup>();
    g_setup = testing_setup.get();
    for (uint32_t i = 0; i < uint32_t{100}; ++i) {
        g_available_coins.emplace_back(Txid::FromUint256(uint256::ZERO), i);
    }
}

// Test that the MiniMiner can run with various outpoints and feerates.
FUZZ_TARGET(mini_miner, .init = initialize_miner)
{
    FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
    bilingual_str error;
    CTxMemPool pool{CTxMemPool::Options{}, error};
    Assert(error.empty());
    std::vector<COutPoint> outpoints;
    std::deque<COutPoint> available_coins = g_available_coins;
    LOCK2(::cs_main, pool.cs);
    // Cluster size cannot exceed 500
    LIMITED_WHILE(!available_coins.empty(), 500)
    {
        CMutableTransaction mtx = CMutableTransaction();
        const size_t num_inputs = fuzzed_data_provider.ConsumeIntegralInRange<size_t>(1, available_coins.size());
        const size_t num_outputs = fuzzed_data_provider.ConsumeIntegralInRange<size_t>(1, 50);
        for (size_t n{0}; n < num_inputs; ++n) {
            auto prevout = available_coins.front();
            mtx.vin.emplace_back(prevout, CScript());
            available_coins.pop_front();
        }
        for (uint32_t n{0}; n < num_outputs; ++n) {
            mtx.vout.emplace_back(100, P2WSH_OP_TRUE);
        }
        CTransactionRef tx = MakeTransactionRef(mtx);
        TestMemPoolEntryHelper entry;
        const CAmount fee{ConsumeMoney(fuzzed_data_provider, /*max=*/MAX_MONEY/100000)};
        assert(MoneyRange(fee));
        pool.addUnchecked(entry.Fee(fee).FromTx(tx));

        // All outputs are available to spend
        for (uint32_t n{0}; n < num_outputs; ++n) {
            if (fuzzed_data_provider.ConsumeBool()) {
                available_coins.emplace_back(tx->GetHash(), n);
            }
        }

        if (fuzzed_data_provider.ConsumeBool() && !tx->vout.empty()) {
            // Add outpoint from this tx (may or not be spent by a later tx)
            outpoints.emplace_back(tx->GetHash(),
                                          (uint32_t)fuzzed_data_provider.ConsumeIntegralInRange<size_t>(0, tx->vout.size()));
        } else {
            // Add some random outpoint (will be interpreted as confirmed or not yet submitted
            // to mempool).
            auto outpoint = ConsumeDeserializable<COutPoint>(fuzzed_data_provider);
            if (outpoint.has_value() && std::find(outpoints.begin(), outpoints.end(), *outpoint) == outpoints.end()) {
                outpoints.push_back(*outpoint);
            }
        }

    }

    const CFeeRate target_feerate{CFeeRate{ConsumeMoney(fuzzed_data_provider, /*max=*/MAX_MONEY/1000)}};
    std::optional<CAmount> total_bumpfee;
    CAmount sum_fees = 0;
    {
        node::MiniMiner mini_miner{pool, outpoints};
        assert(mini_miner.IsReadyToCalculate());
        const auto bump_fees = mini_miner.CalculateBumpFees(target_feerate);
        for (const auto& outpoint : outpoints) {
            auto it = bump_fees.find(outpoint);
            assert(it != bump_fees.end());
            assert(it->second >= 0);
            sum_fees += it->second;
        }
        assert(!mini_miner.IsReadyToCalculate());
    }
    {
        node::MiniMiner mini_miner{pool, outpoints};
        assert(mini_miner.IsReadyToCalculate());
        total_bumpfee = mini_miner.CalculateTotalBumpFees(target_feerate);
        assert(total_bumpfee.has_value());
        assert(!mini_miner.IsReadyToCalculate());
    }
    // Overlapping ancestry across multiple outpoints can only reduce the total bump fee.
    assert (sum_fees >= *total_bumpfee);
}

// Test that MiniMiner and BlockAssembler build the same block given the same transactions and constraints.
FUZZ_TARGET(mini_miner_selection, .init = initialize_miner)
{
    FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
    bilingual_str error;
    CTxMemPool pool{CTxMemPool::Options{}, error};
    Assert(error.empty());
    // Make a copy to preserve determinism.
    std::deque<COutPoint> available_coins = g_available_coins;
    std::vector<CTransactionRef> transactions;

    LOCK2(::cs_main, pool.cs);
    LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 100)
    {
        CMutableTransaction mtx = CMutableTransaction();
        assert(!available_coins.empty());
        const size_t num_inputs = std::min(size_t{2}, available_coins.size());
        const size_t num_outputs = fuzzed_data_provider.ConsumeIntegralInRange<size_t>(2, 5);
        for (size_t n{0}; n < num_inputs; ++n) {
            auto prevout = available_coins.at(0);
            mtx.vin.emplace_back(prevout, CScript());
            available_coins.pop_front();
        }
        for (uint32_t n{0}; n < num_outputs; ++n) {
            mtx.vout.emplace_back(100, P2WSH_OP_TRUE);
        }
        CTransactionRef tx = MakeTransactionRef(mtx);

        // First 2 outputs are available to spend. The rest are added to outpoints to calculate bumpfees.
        // There is no overlap between spendable coins and outpoints passed to MiniMiner because the
        // MiniMiner interprets spent coins as to-be-replaced and excludes them.
        for (uint32_t n{0}; n < num_outputs - 1; ++n) {
            if (fuzzed_data_provider.ConsumeBool()) {
                available_coins.emplace_front(tx->GetHash(), n);
            } else {
                available_coins.emplace_back(tx->GetHash(), n);
            }
        }

        // Stop if pool reaches DEFAULT_BLOCK_MAX_WEIGHT because BlockAssembler will stop when the
        // block template reaches that, but the MiniMiner will keep going.
        if (pool.GetTotalTxSize() + GetVirtualTransactionSize(*tx) >= DEFAULT_BLOCK_MAX_WEIGHT) break;
        TestMemPoolEntryHelper entry;
        const CAmount fee{ConsumeMoney(fuzzed_data_provider, /*max=*/MAX_MONEY/100000)};
        assert(MoneyRange(fee));
        pool.addUnchecked(entry.Fee(fee).FromTx(tx));
        transactions.push_back(tx);
    }
    std::vector<COutPoint> outpoints;
    for (const auto& coin : g_available_coins) {
        if (!pool.GetConflictTx(coin)) outpoints.push_back(coin);
    }
    for (const auto& tx : transactions) {
        assert(pool.exists(GenTxid::Txid(tx->GetHash())));
        for (uint32_t n{0}; n < tx->vout.size(); ++n) {
            COutPoint coin{tx->GetHash(), n};
            if (!pool.GetConflictTx(coin)) outpoints.push_back(coin);
        }
    }
    const CFeeRate target_feerate{ConsumeMoney(fuzzed_data_provider, /*max=*/MAX_MONEY/100000)};

    node::BlockAssembler::Options miner_options;
    miner_options.blockMinFeeRate = target_feerate;
    miner_options.nBlockMaxWeight = DEFAULT_BLOCK_MAX_WEIGHT;
    miner_options.test_block_validity = false;

    node::BlockAssembler miner{g_setup->m_node.chainman->ActiveChainstate(), &pool, miner_options};
    node::MiniMiner mini_miner{pool, outpoints};
    assert(mini_miner.IsReadyToCalculate());

    CScript spk_placeholder = CScript() << OP_0;
    // Use BlockAssembler as oracle. BlockAssembler and MiniMiner should select the same
    // transactions, stopping once packages do not meet target_feerate.
    const auto blocktemplate{miner.CreateNewBlock(spk_placeholder)};
    mini_miner.BuildMockTemplate(target_feerate);
    assert(!mini_miner.IsReadyToCalculate());
    auto mock_template_txids = mini_miner.GetMockTemplateTxids();
    // MiniMiner doesn't add a coinbase tx.
    assert(mock_template_txids.count(blocktemplate->block.vtx[0]->GetHash()) == 0);
    mock_template_txids.emplace(blocktemplate->block.vtx[0]->GetHash());
    assert(mock_template_txids.size() <= blocktemplate->block.vtx.size());
    assert(mock_template_txids.size() >= blocktemplate->block.vtx.size());
    assert(mock_template_txids.size() == blocktemplate->block.vtx.size());
    for (const auto& tx : blocktemplate->block.vtx) {
        assert(mock_template_txids.count(tx->GetHash()));
    }
}
} // namespace