Inside Bitcoin
Opening the selected file…
Inside Bitcoin
Opening the selected file…
The code behind the network, with room to understand it.
Release published 2026-10-28 · Snapshot fetched 2026-11-01. Latest stable release at fetch time; development branches and Git history are excluded. Only the selected code window and visible folders load here.
1// Copyright (c) 2021-present The Bitcoin Core developers2// Distributed under the MIT software license, see the accompanying3// file COPYING or http://www.opensource.org/licenses/mit-license.php.4#include <node/mini_miner.h>5#include <random.h>6#include <txmempool.h>7#include <util/time.h>9#include <test/util/setup_common.h>10#include <test/util/txmempool.h>12#include <boost/test/unit_test.hpp>13#include <optional>14#include <vector>16BOOST_FIXTURE_TEST_SUITE(miniminer_tests, TestingSetup)18const CAmount low_fee{CENT/2000}; // 500 ṩ19const CAmount med_fee{CENT/200}; // 5000 ṩ20const CAmount high_fee{CENT/10}; // 100_000 ṩ23static inline CTransactionRef make_tx(const std::vector<COutPoint>& inputs, size_t num_outputs)24{25CMutableTransaction tx = CMutableTransaction();26tx.vin.resize(inputs.size());27tx.vout.resize(num_outputs);28for (size_t i = 0; i < inputs.size(); ++i) {29tx.vin[i].prevout = inputs[i];30}31for (size_t i = 0; i < num_outputs; ++i) {32tx.vout[i].scriptPubKey = CScript() << OP_11 << OP_EQUAL;33// The actual input and output values of these transactions don't really34// matter, since all accounting will use the entries' cached fees.35tx.vout[i].nValue = COIN;36}37return MakeTransactionRef(tx);38}40static inline bool sanity_check(const std::vector<CTransactionRef>& transactions,41const std::map<COutPoint, CAmount>& bumpfees)42{43// No negative bumpfees.44for (const auto& [outpoint, fee] : bumpfees) {45if (fee < 0) return false;46if (fee == 0) continue;47auto outpoint_ = outpoint; // structured bindings can't be captured in C++17, so we need to use a variable48const bool found = std::any_of(transactions.cbegin(), transactions.cend(), [&](const auto& tx) {49return outpoint_.hash == tx->GetHash() && outpoint_.n < tx->vout.size();50});51if (!found) return false;52}53for (const auto& tx : transactions) {54// If tx has multiple outputs, they must all have the same bumpfee (if they exist).55if (tx->vout.size() > 1) {56std::set<CAmount> distinct_bumpfees;57for (size_t i{0}; i < tx->vout.size(); ++i) {58const auto bumpfee = bumpfees.find(COutPoint{tx->GetHash(), static_cast<uint32_t>(i)});59if (bumpfee != bumpfees.end()) distinct_bumpfees.insert(bumpfee->second);60}61if (distinct_bumpfees.size() > 1) return false;62}63}64return true;65}67template <typename Key, typename Value>68Value Find(const std::map<Key, Value>& map, const Key& key)69{70auto it = map.find(key);71BOOST_CHECK_MESSAGE(it != map.end(), strprintf("Cannot find %s", key.ToString()));72return it->second;73}75BOOST_FIXTURE_TEST_CASE(miniminer_negative, TestChain100Setup)76{77CTxMemPool& pool = *Assert(m_node.mempool);78LOCK2(::cs_main, pool.cs);79TestMemPoolEntryHelper entry;81// Create a transaction that will be prioritised to have a negative modified fee.82const CAmount positive_base_fee{1000};83const CAmount negative_fee_delta{-50000};84const CAmount negative_modified_fees{positive_base_fee + negative_fee_delta};85BOOST_CHECK(negative_modified_fees < 0);86const auto tx_mod_negative = make_tx({COutPoint{m_coinbase_txns[4]->GetHash(), 0}}, /*num_outputs=*/1);87TryAddToMempool(pool, entry.Fee(positive_base_fee).FromTx(tx_mod_negative));88pool.PrioritiseTransaction(tx_mod_negative->GetHash(), negative_fee_delta);89const COutPoint only_outpoint{tx_mod_negative->GetHash(), 0};91// When target feerate is 0, transactions with negative fees are not selected.92node::MiniMiner mini_miner_target0(pool, {only_outpoint});93BOOST_CHECK(mini_miner_target0.IsReadyToCalculate());94const CFeeRate feerate_zero(0);95mini_miner_target0.BuildMockTemplate(feerate_zero);96// Check the quit condition:97BOOST_CHECK(negative_modified_fees < feerate_zero.GetFee(Assert(pool.GetEntry(tx_mod_negative->GetHash()))->GetTxSize()));98BOOST_CHECK(mini_miner_target0.GetMockTemplateTxids().empty());100// With no target feerate, the template includes all transactions, even negative feerate ones.101node::MiniMiner mini_miner_no_target(pool, {only_outpoint});102BOOST_CHECK(mini_miner_no_target.IsReadyToCalculate());103mini_miner_no_target.BuildMockTemplate(std::nullopt);104const auto template_txids{mini_miner_no_target.GetMockTemplateTxids()};105BOOST_CHECK_EQUAL(template_txids.size(), 1);106BOOST_CHECK(template_txids.contains(tx_mod_negative->GetHash()));107}109BOOST_FIXTURE_TEST_CASE(miniminer_1p1c, TestChain100Setup)110{111CTxMemPool& pool = *Assert(m_node.mempool);112LOCK2(::cs_main, pool.cs);113TestMemPoolEntryHelper entry;115// Create a parent tx0 and child tx1 with normal fees:116const auto tx0 = make_tx({COutPoint{m_coinbase_txns[0]->GetHash(), 0}}, /*num_outputs=*/2);117TryAddToMempool(pool, entry.Fee(med_fee).FromTx(tx0));118const auto tx1 = make_tx({COutPoint{tx0->GetHash(), 0}}, /*num_outputs=*/1);119TryAddToMempool(pool, entry.Fee(med_fee).FromTx(tx1));121// Create a low-feerate parent tx2 and high-feerate child tx3 (cpfp)122const auto tx2 = make_tx({COutPoint{m_coinbase_txns[1]->GetHash(), 0}}, /*num_outputs=*/2);123TryAddToMempool(pool, entry.Fee(low_fee).FromTx(tx2));124const auto tx3 = make_tx({COutPoint{tx2->GetHash(), 0}}, /*num_outputs=*/1);125TryAddToMempool(pool, entry.Fee(high_fee).FromTx(tx3));127// Create a parent tx4 and child tx5 where both have low fees128const auto tx4 = make_tx({COutPoint{m_coinbase_txns[2]->GetHash(), 0}}, /*num_outputs=*/2);129TryAddToMempool(pool, entry.Fee(low_fee).FromTx(tx4));130const auto tx5 = make_tx({COutPoint{tx4->GetHash(), 0}}, /*num_outputs=*/1);131TryAddToMempool(pool, entry.Fee(low_fee).FromTx(tx5));132const CAmount tx5_delta{CENT/100};133// Make tx5's modified fee much higher than its base fee. This should cause it to pass134// the fee-related checks despite being low-feerate.135pool.PrioritiseTransaction(tx5->GetHash(), tx5_delta);136const CAmount tx5_mod_fee{low_fee + tx5_delta};138// Create a high-feerate parent tx6, low-feerate child tx7139const auto tx6 = make_tx({COutPoint{m_coinbase_txns[3]->GetHash(), 0}}, /*num_outputs=*/2);140TryAddToMempool(pool, entry.Fee(high_fee).FromTx(tx6));141const auto tx7 = make_tx({COutPoint{tx6->GetHash(), 0}}, /*num_outputs=*/1);142TryAddToMempool(pool, entry.Fee(low_fee).FromTx(tx7));144std::vector<COutPoint> all_unspent_outpoints({145COutPoint{tx0->GetHash(), 1},146COutPoint{tx1->GetHash(), 0},147COutPoint{tx2->GetHash(), 1},148COutPoint{tx3->GetHash(), 0},149COutPoint{tx4->GetHash(), 1},150COutPoint{tx5->GetHash(), 0},151COutPoint{tx6->GetHash(), 1},152COutPoint{tx7->GetHash(), 0}153});154for (const auto& outpoint : all_unspent_outpoints) BOOST_CHECK(!pool.isSpent(outpoint));156std::vector<COutPoint> all_spent_outpoints({157COutPoint{tx0->GetHash(), 0},158COutPoint{tx2->GetHash(), 0},159COutPoint{tx4->GetHash(), 0},160COutPoint{tx6->GetHash(), 0}161});162for (const auto& outpoint : all_spent_outpoints) BOOST_CHECK(pool.GetConflictTx(outpoint) != nullptr);164std::vector<COutPoint> all_parent_outputs({165COutPoint{tx0->GetHash(), 0},166COutPoint{tx0->GetHash(), 1},167COutPoint{tx2->GetHash(), 0},168COutPoint{tx2->GetHash(), 1},169COutPoint{tx4->GetHash(), 0},170COutPoint{tx4->GetHash(), 1},171COutPoint{tx6->GetHash(), 0},172COutPoint{tx6->GetHash(), 1}173});176std::vector<CTransactionRef> all_transactions{tx0, tx1, tx2, tx3, tx4, tx5, tx6, tx7};177struct TxDimensions {178int32_t vsize; CAmount mod_fee; CFeeRate feerate;179};180std::map<Txid, TxDimensions> tx_dims;181for (const auto& tx : all_transactions) {182const auto& entry{*Assert(pool.GetEntry(tx->GetHash()))};183tx_dims.emplace(tx->GetHash(), TxDimensions{entry.GetTxSize(), entry.GetModifiedFee(),184CFeeRate(entry.GetModifiedFee(), entry.GetTxSize())});185}187const std::vector<CFeeRate> various_normal_feerates({CFeeRate(0), CFeeRate(500), CFeeRate(999),188CFeeRate(1000), CFeeRate(2000), CFeeRate(2500),189CFeeRate(3333), CFeeRate(7800), CFeeRate(11199),190CFeeRate(23330), CFeeRate(50000), CFeeRate(5*CENT)});192// All nonexistent entries have a bumpfee of zero, regardless of feerate193std::vector<COutPoint> nonexistent_outpoints({ COutPoint{Txid::FromUint256(GetRandHash()), 0}, COutPoint{Txid::FromUint256(GetRandHash()), 3} });194for (const auto& outpoint : nonexistent_outpoints) BOOST_CHECK(!pool.isSpent(outpoint));195for (const auto& feerate : various_normal_feerates) {196node::MiniMiner mini_miner(pool, nonexistent_outpoints);197BOOST_CHECK(mini_miner.IsReadyToCalculate());198auto bump_fees = mini_miner.CalculateBumpFees(feerate);199BOOST_CHECK(!mini_miner.IsReadyToCalculate());200BOOST_CHECK(sanity_check(all_transactions, bump_fees));201BOOST_CHECK(bump_fees.size() == nonexistent_outpoints.size());202for (const auto& outpoint: nonexistent_outpoints) {203auto it = bump_fees.find(outpoint);204BOOST_CHECK(it != bump_fees.end());205BOOST_CHECK_EQUAL(it->second, 0);206}207}209// Gather bump fees for all available UTXOs.210for (const auto& target_feerate : various_normal_feerates) {211node::MiniMiner mini_miner(pool, all_unspent_outpoints);212BOOST_CHECK(mini_miner.IsReadyToCalculate());213auto bump_fees = mini_miner.CalculateBumpFees(target_feerate);214BOOST_CHECK(!mini_miner.IsReadyToCalculate());215BOOST_CHECK(sanity_check(all_transactions, bump_fees));216BOOST_CHECK_EQUAL(bump_fees.size(), all_unspent_outpoints.size());218// Check tx0 bumpfee: no other bumper.219const TxDimensions& tx0_dimensions = tx_dims.find(tx0->GetHash())->second;220CAmount bumpfee0 = Find(bump_fees, COutPoint{tx0->GetHash(), 1});221if (target_feerate <= tx0_dimensions.feerate) {222BOOST_CHECK_EQUAL(bumpfee0, 0);223} else {224// Difference is fee to bump tx0 from current to target feerate.225BOOST_CHECK_EQUAL(bumpfee0, target_feerate.GetFee(tx0_dimensions.vsize) - tx0_dimensions.mod_fee);226}228// Check tx2 bumpfee: assisted by tx3.229const TxDimensions& tx2_dimensions = tx_dims.find(tx2->GetHash())->second;230const TxDimensions& tx3_dimensions = tx_dims.find(tx3->GetHash())->second;231const CFeeRate tx2_feerate = CFeeRate(tx2_dimensions.mod_fee + tx3_dimensions.mod_fee, tx2_dimensions.vsize + tx3_dimensions.vsize);232CAmount bumpfee2 = Find(bump_fees, COutPoint{tx2->GetHash(), 1});233if (target_feerate <= tx2_feerate) {234// As long as target feerate is below tx3's ancestor feerate, there is no bump fee.235BOOST_CHECK_EQUAL(bumpfee2, 0);236} else {237// Difference is fee to bump tx2 from current to target feerate, without tx3.238BOOST_CHECK_EQUAL(bumpfee2, target_feerate.GetFee(tx2_dimensions.vsize) - tx2_dimensions.mod_fee);239}241// If tx5’s modified fees are sufficient for tx4 and tx5 to be picked242// into the block, our prospective new transaction would not need to243// bump tx4 when using tx4’s second output. If however even tx5’s244// modified fee (which essentially indicates "effective feerate") is245// not sufficient to bump tx4, using the second output of tx4 would246// require our transaction to bump tx4 from scratch since we evaluate247// transaction packages per ancestor sets and do not consider multiple248// children’s fees.249const TxDimensions& tx4_dimensions = tx_dims.find(tx4->GetHash())->second;250const TxDimensions& tx5_dimensions = tx_dims.find(tx5->GetHash())->second;
Independent explanations by MyBitcoinExplorer. Not affiliated with or endorsed by Bitcoin Core. Guides are educational and do not replace the source or its tests.
Bitcoin Core source © its contributors, distributed under the MIT license. Original notices and bundled third-party licenses are retained in the source tree.