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