/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 |