/root/bitcoin/src/test/fuzz/mini_miner.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | #include <test/fuzz/FuzzedDataProvider.h> |
2 | | #include <test/fuzz/fuzz.h> |
3 | | #include <test/fuzz/util.h> |
4 | | #include <test/fuzz/util/mempool.h> |
5 | | #include <test/util/script.h> |
6 | | #include <test/util/setup_common.h> |
7 | | #include <test/util/txmempool.h> |
8 | | #include <test/util/mining.h> |
9 | | |
10 | | #include <node/miner.h> |
11 | | #include <node/mini_miner.h> |
12 | | #include <primitives/transaction.h> |
13 | | #include <random.h> |
14 | | #include <txmempool.h> |
15 | | #include <util/check.h> |
16 | | #include <util/translation.h> |
17 | | |
18 | | #include <deque> |
19 | | #include <vector> |
20 | | |
21 | | namespace { |
22 | | |
23 | | const TestingSetup* g_setup; |
24 | | std::deque<COutPoint> g_available_coins; |
25 | | void initialize_miner() |
26 | 0 | { |
27 | 0 | static const auto testing_setup = MakeNoLogFileContext<const TestingSetup>(); |
28 | 0 | g_setup = testing_setup.get(); |
29 | 0 | for (uint32_t i = 0; i < uint32_t{100}; ++i) { |
30 | 0 | g_available_coins.emplace_back(Txid::FromUint256(uint256::ZERO), i); |
31 | 0 | } |
32 | 0 | } |
33 | | |
34 | | // Test that the MiniMiner can run with various outpoints and feerates. |
35 | | FUZZ_TARGET(mini_miner, .init = initialize_miner) |
36 | 0 | { |
37 | 0 | FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()}; |
38 | 0 | bilingual_str error; |
39 | 0 | CTxMemPool pool{CTxMemPool::Options{}, error}; |
40 | 0 | Assert(error.empty()); |
41 | 0 | std::vector<COutPoint> outpoints; |
42 | 0 | std::deque<COutPoint> available_coins = g_available_coins; |
43 | 0 | LOCK2(::cs_main, pool.cs); |
44 | | // Cluster size cannot exceed 500 |
45 | 0 | LIMITED_WHILE(!available_coins.empty(), 500) |
46 | 0 | { |
47 | 0 | CMutableTransaction mtx = CMutableTransaction(); |
48 | 0 | const size_t num_inputs = fuzzed_data_provider.ConsumeIntegralInRange<size_t>(1, available_coins.size()); |
49 | 0 | const size_t num_outputs = fuzzed_data_provider.ConsumeIntegralInRange<size_t>(1, 50); |
50 | 0 | for (size_t n{0}; n < num_inputs; ++n) { |
51 | 0 | auto prevout = available_coins.front(); |
52 | 0 | mtx.vin.emplace_back(prevout, CScript()); |
53 | 0 | available_coins.pop_front(); |
54 | 0 | } |
55 | 0 | for (uint32_t n{0}; n < num_outputs; ++n) { |
56 | 0 | mtx.vout.emplace_back(100, P2WSH_OP_TRUE); |
57 | 0 | } |
58 | 0 | CTransactionRef tx = MakeTransactionRef(mtx); |
59 | 0 | TestMemPoolEntryHelper entry; |
60 | 0 | const CAmount fee{ConsumeMoney(fuzzed_data_provider, /*max=*/MAX_MONEY/100000)}; |
61 | 0 | assert(MoneyRange(fee)); |
62 | 0 | pool.addUnchecked(entry.Fee(fee).FromTx(tx)); |
63 | | |
64 | | // All outputs are available to spend |
65 | 0 | for (uint32_t n{0}; n < num_outputs; ++n) { |
66 | 0 | if (fuzzed_data_provider.ConsumeBool()) { |
67 | 0 | available_coins.emplace_back(tx->GetHash(), n); |
68 | 0 | } |
69 | 0 | } |
70 | |
|
71 | 0 | if (fuzzed_data_provider.ConsumeBool() && !tx->vout.empty()) { |
72 | | // Add outpoint from this tx (may or not be spent by a later tx) |
73 | 0 | outpoints.emplace_back(tx->GetHash(), |
74 | 0 | (uint32_t)fuzzed_data_provider.ConsumeIntegralInRange<size_t>(0, tx->vout.size())); |
75 | 0 | } else { |
76 | | // Add some random outpoint (will be interpreted as confirmed or not yet submitted |
77 | | // to mempool). |
78 | 0 | auto outpoint = ConsumeDeserializable<COutPoint>(fuzzed_data_provider); |
79 | 0 | if (outpoint.has_value() && std::find(outpoints.begin(), outpoints.end(), *outpoint) == outpoints.end()) { |
80 | 0 | outpoints.push_back(*outpoint); |
81 | 0 | } |
82 | 0 | } |
83 | |
|
84 | 0 | } |
85 | | |
86 | 0 | const CFeeRate target_feerate{CFeeRate{ConsumeMoney(fuzzed_data_provider, /*max=*/MAX_MONEY/1000)}}; |
87 | 0 | std::optional<CAmount> total_bumpfee; |
88 | 0 | CAmount sum_fees = 0; |
89 | 0 | { |
90 | 0 | node::MiniMiner mini_miner{pool, outpoints}; |
91 | 0 | assert(mini_miner.IsReadyToCalculate()); |
92 | 0 | const auto bump_fees = mini_miner.CalculateBumpFees(target_feerate); |
93 | 0 | for (const auto& outpoint : outpoints) { |
94 | 0 | auto it = bump_fees.find(outpoint); |
95 | 0 | assert(it != bump_fees.end()); |
96 | 0 | assert(it->second >= 0); |
97 | 0 | sum_fees += it->second; |
98 | 0 | } |
99 | 0 | assert(!mini_miner.IsReadyToCalculate()); |
100 | 0 | } |
101 | 0 | { |
102 | 0 | node::MiniMiner mini_miner{pool, outpoints}; |
103 | 0 | assert(mini_miner.IsReadyToCalculate()); |
104 | 0 | total_bumpfee = mini_miner.CalculateTotalBumpFees(target_feerate); |
105 | 0 | assert(total_bumpfee.has_value()); |
106 | 0 | assert(!mini_miner.IsReadyToCalculate()); |
107 | 0 | } |
108 | | // Overlapping ancestry across multiple outpoints can only reduce the total bump fee. |
109 | 0 | assert (sum_fees >= *total_bumpfee); |
110 | 0 | } |
111 | | |
112 | | // Test that MiniMiner and BlockAssembler build the same block given the same transactions and constraints. |
113 | | FUZZ_TARGET(mini_miner_selection, .init = initialize_miner) |
114 | 0 | { |
115 | 0 | FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()}; |
116 | 0 | bilingual_str error; |
117 | 0 | CTxMemPool pool{CTxMemPool::Options{}, error}; |
118 | 0 | Assert(error.empty()); |
119 | | // Make a copy to preserve determinism. |
120 | 0 | std::deque<COutPoint> available_coins = g_available_coins; |
121 | 0 | std::vector<CTransactionRef> transactions; |
122 | |
|
123 | 0 | LOCK2(::cs_main, pool.cs); |
124 | 0 | LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 100) |
125 | 0 | { |
126 | 0 | CMutableTransaction mtx = CMutableTransaction(); |
127 | 0 | assert(!available_coins.empty()); |
128 | 0 | const size_t num_inputs = std::min(size_t{2}, available_coins.size()); |
129 | 0 | const size_t num_outputs = fuzzed_data_provider.ConsumeIntegralInRange<size_t>(2, 5); |
130 | 0 | for (size_t n{0}; n < num_inputs; ++n) { |
131 | 0 | auto prevout = available_coins.at(0); |
132 | 0 | mtx.vin.emplace_back(prevout, CScript()); |
133 | 0 | available_coins.pop_front(); |
134 | 0 | } |
135 | 0 | for (uint32_t n{0}; n < num_outputs; ++n) { |
136 | 0 | mtx.vout.emplace_back(100, P2WSH_OP_TRUE); |
137 | 0 | } |
138 | 0 | CTransactionRef tx = MakeTransactionRef(mtx); |
139 | | |
140 | | // First 2 outputs are available to spend. The rest are added to outpoints to calculate bumpfees. |
141 | | // There is no overlap between spendable coins and outpoints passed to MiniMiner because the |
142 | | // MiniMiner interprets spent coins as to-be-replaced and excludes them. |
143 | 0 | for (uint32_t n{0}; n < num_outputs - 1; ++n) { |
144 | 0 | if (fuzzed_data_provider.ConsumeBool()) { |
145 | 0 | available_coins.emplace_front(tx->GetHash(), n); |
146 | 0 | } else { |
147 | 0 | available_coins.emplace_back(tx->GetHash(), n); |
148 | 0 | } |
149 | 0 | } |
150 | | |
151 | | // Stop if pool reaches DEFAULT_BLOCK_MAX_WEIGHT because BlockAssembler will stop when the |
152 | | // block template reaches that, but the MiniMiner will keep going. |
153 | 0 | if (pool.GetTotalTxSize() + GetVirtualTransactionSize(*tx) >= DEFAULT_BLOCK_MAX_WEIGHT) break; |
154 | 0 | TestMemPoolEntryHelper entry; |
155 | 0 | const CAmount fee{ConsumeMoney(fuzzed_data_provider, /*max=*/MAX_MONEY/100000)}; |
156 | 0 | assert(MoneyRange(fee)); |
157 | 0 | pool.addUnchecked(entry.Fee(fee).FromTx(tx)); |
158 | 0 | transactions.push_back(tx); |
159 | 0 | } |
160 | 0 | std::vector<COutPoint> outpoints; |
161 | 0 | for (const auto& coin : g_available_coins) { |
162 | 0 | if (!pool.GetConflictTx(coin)) outpoints.push_back(coin); |
163 | 0 | } |
164 | 0 | for (const auto& tx : transactions) { |
165 | 0 | assert(pool.exists(GenTxid::Txid(tx->GetHash()))); |
166 | 0 | for (uint32_t n{0}; n < tx->vout.size(); ++n) { |
167 | 0 | COutPoint coin{tx->GetHash(), n}; |
168 | 0 | if (!pool.GetConflictTx(coin)) outpoints.push_back(coin); |
169 | 0 | } |
170 | 0 | } |
171 | 0 | const CFeeRate target_feerate{ConsumeMoney(fuzzed_data_provider, /*max=*/MAX_MONEY/100000)}; |
172 | |
|
173 | 0 | node::BlockAssembler::Options miner_options; |
174 | 0 | miner_options.blockMinFeeRate = target_feerate; |
175 | 0 | miner_options.nBlockMaxWeight = DEFAULT_BLOCK_MAX_WEIGHT; |
176 | 0 | miner_options.test_block_validity = false; |
177 | |
|
178 | 0 | node::BlockAssembler miner{g_setup->m_node.chainman->ActiveChainstate(), &pool, miner_options}; |
179 | 0 | node::MiniMiner mini_miner{pool, outpoints}; |
180 | 0 | assert(mini_miner.IsReadyToCalculate()); |
181 | | |
182 | 0 | CScript spk_placeholder = CScript() << OP_0; |
183 | | // Use BlockAssembler as oracle. BlockAssembler and MiniMiner should select the same |
184 | | // transactions, stopping once packages do not meet target_feerate. |
185 | 0 | const auto blocktemplate{miner.CreateNewBlock(spk_placeholder)}; |
186 | 0 | mini_miner.BuildMockTemplate(target_feerate); |
187 | 0 | assert(!mini_miner.IsReadyToCalculate()); |
188 | 0 | auto mock_template_txids = mini_miner.GetMockTemplateTxids(); |
189 | | // MiniMiner doesn't add a coinbase tx. |
190 | 0 | assert(mock_template_txids.count(blocktemplate->block.vtx[0]->GetHash()) == 0); |
191 | 0 | auto [iter, new_entry] = mock_template_txids.emplace(blocktemplate->block.vtx[0]->GetHash()); |
192 | 0 | assert(new_entry); |
193 | | |
194 | 0 | assert(mock_template_txids.size() == blocktemplate->block.vtx.size()); |
195 | 0 | for (const auto& tx : blocktemplate->block.vtx) { |
196 | 0 | assert(mock_template_txids.count(tx->GetHash())); |
197 | 0 | } |
198 | 0 | } |
199 | | } // namespace |