Merge bitcoin/bitcoin#28762: MiniMiner changes for package linearization

d9cc99d04e813caed51c5f7b6ebdc4c39c8823c9 [test] MiniMiner::Linearize and manual construction (glozow)
dfd6a3788c35be121eba1ad84f20effadcb7e7dc [refactor] unify fee amounts in miniminer_tests (glozow)
f4b1b24a3bcd0199a6d2e828ad46033e1368336e [MiniMiner] track inclusion order and add Linearize() function (glozow)
004075963f12f17c3c399d81e3b48d6a8151aebd [test] add case for MiniMiner working with negative fee txns (glozow)
fe6332c0badf07e99044b00084fc6b07f735a051 [MiniMiner] make target_feerate optional (glozow)
5a83f55c96661a886dd6f5231920b2f730cf6773 [MiniMiner] allow manual construction with non-mempool txns (glozow)
e3b2e630b219ca15fe0b2640ca422712c86ac33d [refactor] change MiniMinerMempoolEntry ctor to take values, update includes (glozow)
4aa98b79b266cd526efa577762b0bcfccbdeda11 [lint] update expected boost includes (glozow)

Pull request description:

  This is part of #27463. It splits off the `MiniMiner`-specific changes from #26711 for ease of review, as suggested in https://github.com/bitcoin/bitcoin/pull/26711#issuecomment-1786392253.

  - Allow using `MiniMiner` on transactions that aren't in the mempool.
  - Make `target_feerate` param of `BuildMockTemplate` optional, meaning "don't stop building the template until all the transactions have been selected."
    - Add clarification for how this is different from `target_feerate=0` (https://github.com/bitcoin/bitcoin/pull/26711#discussion_r1377019133)
  - Track the order in which transactions are included in the template to get the "linearization order" of the transactions.
  - Tests

  Reviewers can take a look at #26711 to see how these functions are used to linearize the `AncestorPackage` there.

ACKs for top commit:
  TheCharlatan:
    ACK d9cc99d04e813caed51c5f7b6ebdc4c39c8823c9
  kevkevinpal:
    reACK [d9cc99d](d9cc99d04e)
  achow101:
    re-ACK d9cc99d04e813caed51c5f7b6ebdc4c39c8823c9

Tree-SHA512: 32b80064b6679536ac573d674825c5ca0cd6245e49c2fd5eaf260dc535335a57683c74ddd7ce1f249b5b12b2683de4362a7b0f1fc0814c3b3b9f14c682665583
This commit is contained in:
Andrew Chow 2023-11-03 10:45:34 -04:00
commit d9007f51a7
No known key found for this signature in database
GPG Key ID: 17565732E08E5E41
4 changed files with 345 additions and 29 deletions

View File

@ -4,9 +4,14 @@
#include <node/mini_miner.h>
#include <boost/multi_index/detail/hash_index_iterator.hpp>
#include <boost/operators.hpp>
#include <consensus/amount.h>
#include <policy/feerate.h>
#include <primitives/transaction.h>
#include <sync.h>
#include <txmempool.h>
#include <uint256.h>
#include <util/check.h>
#include <algorithm>
@ -72,7 +77,12 @@ MiniMiner::MiniMiner(const CTxMemPool& mempool, const std::vector<COutPoint>& ou
// Add every entry to m_entries_by_txid and m_entries, except the ones that will be replaced.
for (const auto& txiter : cluster) {
if (!m_to_be_replaced.count(txiter->GetTx().GetHash())) {
auto [mapiter, success] = m_entries_by_txid.emplace(txiter->GetTx().GetHash(), MiniMinerMempoolEntry(txiter));
auto [mapiter, success] = m_entries_by_txid.emplace(txiter->GetTx().GetHash(),
MiniMinerMempoolEntry{/*fee_self=*/txiter->GetModifiedFee(),
/*fee_ancestor=*/txiter->GetModFeesWithAncestors(),
/*vsize_self=*/txiter->GetTxSize(),
/*vsize_ancestor=*/txiter->GetSizeWithAncestors(),
/*tx_in=*/txiter->GetSharedTx()});
m_entries.push_back(mapiter);
} else {
auto outpoints_it = m_requested_outpoints_by_txid.find(txiter->GetTx().GetHash());
@ -122,6 +132,48 @@ MiniMiner::MiniMiner(const CTxMemPool& mempool, const std::vector<COutPoint>& ou
SanityCheck();
}
MiniMiner::MiniMiner(const std::vector<MiniMinerMempoolEntry>& manual_entries,
const std::map<Txid, std::set<Txid>>& descendant_caches)
{
for (const auto& entry : manual_entries) {
const auto& txid = entry.GetTx().GetHash();
// We need to know the descendant set of every transaction.
if (!Assume(descendant_caches.count(txid) > 0)) {
m_ready_to_calculate = false;
return;
}
// Just forward these args onto MiniMinerMempoolEntry
auto [mapiter, success] = m_entries_by_txid.emplace(txid, entry);
// Txids must be unique; this txid shouldn't already be an entry in m_entries_by_txid
if (Assume(success)) m_entries.push_back(mapiter);
}
// Descendant cache is already built, but we need to translate them to m_entries_by_txid iters.
for (const auto& [txid, desc_txids] : descendant_caches) {
// Descendant cache should include at least the tx itself.
if (!Assume(!desc_txids.empty())) {
m_ready_to_calculate = false;
return;
}
std::vector<MockEntryMap::iterator> cached_descendants;
for (const auto& desc_txid : desc_txids) {
auto desc_it{m_entries_by_txid.find(desc_txid)};
// Descendants should only include transactions with corresponding entries.
if (!Assume(desc_it != m_entries_by_txid.end())) {
m_ready_to_calculate = false;
return;
} else {
cached_descendants.emplace_back(desc_it);
}
}
m_descendant_set_by_txid.emplace(txid, cached_descendants);
}
Assume(m_to_be_replaced.empty());
Assume(m_requested_outpoints_by_txid.empty());
Assume(m_bump_fees.empty());
Assume(m_inclusion_order.empty());
SanityCheck();
}
// Compare by min(ancestor feerate, individual feerate), then iterator
//
// Under the ancestor-based mining approach, high-feerate children can pay for parents, but high-feerate
@ -201,8 +253,10 @@ void MiniMiner::SanityCheck() const
[&](const auto& txid){return m_entries_by_txid.find(txid) == m_entries_by_txid.end();}));
}
void MiniMiner::BuildMockTemplate(const CFeeRate& target_feerate)
void MiniMiner::BuildMockTemplate(std::optional<CFeeRate> target_feerate)
{
const auto num_txns{m_entries_by_txid.size()};
uint32_t sequence_num{0};
while (!m_entries_by_txid.empty()) {
// Sort again, since transaction removal may change some m_entries' ancestor feerates.
std::sort(m_entries.begin(), m_entries.end(), AncestorFeerateComparator());
@ -213,7 +267,8 @@ void MiniMiner::BuildMockTemplate(const CFeeRate& target_feerate)
const auto ancestor_package_size = (*best_iter)->second.GetSizeWithAncestors();
const auto ancestor_package_fee = (*best_iter)->second.GetModFeesWithAncestors();
// Stop here. Everything that didn't "make it into the block" has bumpfee.
if (ancestor_package_fee < target_feerate.GetFee(ancestor_package_size)) {
if (target_feerate.has_value() &&
ancestor_package_fee < target_feerate->GetFee(ancestor_package_size)) {
break;
}
@ -237,14 +292,32 @@ void MiniMiner::BuildMockTemplate(const CFeeRate& target_feerate)
to_process.erase(iter);
}
}
// Track the order in which transactions were selected.
for (const auto& ancestor : ancestors) {
m_inclusion_order.emplace(Txid::FromUint256(ancestor->first), sequence_num);
}
DeleteAncestorPackage(ancestors);
SanityCheck();
++sequence_num;
}
Assume(m_in_block.empty() || m_total_fees >= target_feerate.GetFee(m_total_vsize));
if (!target_feerate.has_value()) {
Assume(m_in_block.size() == num_txns);
} else {
Assume(m_in_block.empty() || m_total_fees >= target_feerate->GetFee(m_total_vsize));
}
Assume(m_in_block.empty() || sequence_num > 0);
Assume(m_in_block.size() == m_inclusion_order.size());
// Do not try to continue building the block template with a different feerate.
m_ready_to_calculate = false;
}
std::map<Txid, uint32_t> MiniMiner::Linearize()
{
BuildMockTemplate(std::nullopt);
return m_inclusion_order;
}
std::map<COutPoint, CAmount> MiniMiner::CalculateBumpFees(const CFeeRate& target_feerate)
{
if (!m_ready_to_calculate) return {};

View File

@ -5,33 +5,45 @@
#ifndef BITCOIN_NODE_MINI_MINER_H
#define BITCOIN_NODE_MINI_MINER_H
#include <txmempool.h>
#include <consensus/amount.h>
#include <primitives/transaction.h>
#include <uint256.h>
#include <map>
#include <memory>
#include <optional>
#include <set>
#include <stdint.h>
#include <vector>
class CFeeRate;
class CTxMemPool;
namespace node {
// Container for tracking updates to ancestor feerate as we include ancestors in the "block"
class MiniMinerMempoolEntry
{
const CAmount fee_individual;
const CTransactionRef tx;
const int64_t vsize_individual;
CAmount fee_with_ancestors;
int64_t vsize_with_ancestors;
const CAmount fee_individual;
CAmount fee_with_ancestors;
// This class must be constructed while holding mempool.cs. After construction, the object's
// methods can be called without holding that lock.
public:
explicit MiniMinerMempoolEntry(CTxMemPool::txiter entry) :
fee_individual{entry->GetModifiedFee()},
tx{entry->GetSharedTx()},
vsize_individual(entry->GetTxSize()),
fee_with_ancestors{entry->GetModFeesWithAncestors()},
vsize_with_ancestors(entry->GetSizeWithAncestors())
explicit MiniMinerMempoolEntry(CAmount fee_self,
CAmount fee_ancestor,
int64_t vsize_self,
int64_t vsize_ancestor,
const CTransactionRef& tx_in):
tx{tx_in},
vsize_individual{vsize_self},
vsize_with_ancestors{vsize_ancestor},
fee_individual{fee_self},
fee_with_ancestors{fee_ancestor}
{ }
CAmount GetModifiedFee() const { return fee_individual; }
@ -55,8 +67,14 @@ struct IteratorComparator
}
};
/** A minimal version of BlockAssembler. Allows us to run the mining algorithm on a subset of
* mempool transactions, ignoring consensus rules, to calculate mining scores. */
/** A minimal version of BlockAssembler, using the same ancestor set scoring algorithm. Allows us to
* run this algorithm on a limited set of transactions (e.g. subset of mempool or transactions that
* are not yet in mempool) instead of the entire mempool, ignoring consensus rules.
* Callers may use this to:
* - Calculate the "bump fee" needed to spend an unconfirmed UTXO at a given feerate
* - "Linearize" a list of transactions to see the order in which they would be selected for
* inclusion in a block
*/
class MiniMiner
{
// When true, a caller may use CalculateBumpFees(). Becomes false if we failed to retrieve
@ -72,7 +90,11 @@ class MiniMiner
// the same tx will have the same bumpfee. Excludes non-mempool transactions.
std::map<uint256, std::vector<COutPoint>> m_requested_outpoints_by_txid;
// What we're trying to calculate.
// Txid to a number representing the order in which this transaction was included (smaller
// number = included earlier). Transactions included in an ancestor set together have the same
// sequence number.
std::map<Txid, uint32_t> m_inclusion_order;
// What we're trying to calculate. Outpoint to the fee needed to bring the transaction to the target feerate.
std::map<COutPoint, CAmount> m_bump_fees;
// The constructed block template
@ -102,14 +124,32 @@ public:
/** Returns true if CalculateBumpFees may be called, false if not. */
bool IsReadyToCalculate() const { return m_ready_to_calculate; }
/** Build a block template until the target feerate is hit. */
void BuildMockTemplate(const CFeeRate& target_feerate);
/** Build a block template until the target feerate is hit. If target_feerate is not given,
* builds a block template until all transactions have been selected. */
void BuildMockTemplate(std::optional<CFeeRate> target_feerate);
/** Returns set of txids in the block template if one has been constructed. */
std::set<uint256> GetMockTemplateTxids() const { return m_in_block; }
/** Constructor that takes a list of outpoints that may or may not belong to transactions in the
* mempool. Copies out information about the relevant transactions in the mempool into
* MiniMinerMempoolEntrys.
*/
MiniMiner(const CTxMemPool& mempool, const std::vector<COutPoint>& outpoints);
/** Constructor in which the MiniMinerMempoolEntry entries have been constructed manually,
* presumably because these transactions are not in the mempool (yet). It is assumed that all
* entries are unique and their values are correct, otherwise results computed by MiniMiner may
* be incorrect. Callers should check IsReadyToCalculate() after construction.
* @param[in] descendant_caches A map from each transaction to the set of txids of this
* transaction's descendant set, including itself. Each tx in
* manual_entries must have a corresponding entry in this map, and
* all of the txids in a descendant set must correspond to a tx in
* manual_entries.
*/
MiniMiner(const std::vector<MiniMinerMempoolEntry>& manual_entries,
const std::map<Txid, std::set<Txid>>& descendant_caches);
/** Construct a new block template and, for each outpoint corresponding to a transaction that
* did not make it into the block, calculate the cost of bumping those transactions (and their
* ancestors) to the minimum feerate. Returns a map from outpoint to bump fee, or an empty map
@ -120,6 +160,12 @@ public:
* not make it into the block to the target feerate. Returns the total bump fee, or std::nullopt
* if it cannot be calculated. */
std::optional<CAmount> CalculateTotalBumpFees(const CFeeRate& target_feerate);
/** Construct a new block template with all of the transactions and calculate the order in which
* they are selected. Returns the sequence number (lower = selected earlier) with which each
* transaction was selected, indexed by txid, or an empty map if it cannot be calculated.
*/
std::map<Txid, uint32_t> Linearize();
};
} // namespace node

View File

@ -15,6 +15,11 @@
BOOST_FIXTURE_TEST_SUITE(miniminer_tests, TestingSetup)
const CAmount low_fee{CENT/2000}; // 500 ṩ
const CAmount med_fee{CENT/200}; // 5000 ṩ
const CAmount high_fee{CENT/10}; // 100_000 ṩ
static inline CTransactionRef make_tx(const std::vector<COutPoint>& inputs, size_t num_outputs)
{
CMutableTransaction tx = CMutableTransaction();
@ -67,21 +72,51 @@ Value Find(const std::map<Key, Value>& map, const Key& key)
return it->second;
}
BOOST_FIXTURE_TEST_CASE(miniminer_negative, TestChain100Setup)
{
CTxMemPool& pool = *Assert(m_node.mempool);
LOCK2(::cs_main, pool.cs);
TestMemPoolEntryHelper entry;
// Create a transaction that will be prioritised to have a negative modified fee.
const CAmount positive_base_fee{1000};
const CAmount negative_fee_delta{-50000};
const CAmount negative_modified_fees{positive_base_fee + negative_fee_delta};
BOOST_CHECK(negative_modified_fees < 0);
const auto tx_mod_negative = make_tx({COutPoint{m_coinbase_txns[4]->GetHash(), 0}}, /*num_outputs=*/1);
pool.addUnchecked(entry.Fee(positive_base_fee).FromTx(tx_mod_negative));
pool.PrioritiseTransaction(tx_mod_negative->GetHash(), negative_fee_delta);
const COutPoint only_outpoint{tx_mod_negative->GetHash(), 0};
// When target feerate is 0, transactions with negative fees are not selected.
node::MiniMiner mini_miner_target0(pool, {only_outpoint});
BOOST_CHECK(mini_miner_target0.IsReadyToCalculate());
const CFeeRate feerate_zero(0);
mini_miner_target0.BuildMockTemplate(feerate_zero);
// Check the quit condition:
BOOST_CHECK(negative_modified_fees < feerate_zero.GetFee(pool.GetIter(tx_mod_negative->GetHash()).value()->GetTxSize()));
BOOST_CHECK(mini_miner_target0.GetMockTemplateTxids().empty());
// With no target feerate, the template includes all transactions, even negative feerate ones.
node::MiniMiner mini_miner_no_target(pool, {only_outpoint});
BOOST_CHECK(mini_miner_no_target.IsReadyToCalculate());
mini_miner_no_target.BuildMockTemplate(std::nullopt);
const auto template_txids{mini_miner_no_target.GetMockTemplateTxids()};
BOOST_CHECK_EQUAL(template_txids.size(), 1);
BOOST_CHECK(template_txids.count(tx_mod_negative->GetHash().ToUint256()) > 0);
}
BOOST_FIXTURE_TEST_CASE(miniminer_1p1c, TestChain100Setup)
{
CTxMemPool& pool = *Assert(m_node.mempool);
LOCK2(::cs_main, pool.cs);
TestMemPoolEntryHelper entry;
const CAmount low_fee{CENT/2000};
const CAmount normal_fee{CENT/200};
const CAmount high_fee{CENT/10};
// Create a parent tx0 and child tx1 with normal fees:
const auto tx0 = make_tx({COutPoint{m_coinbase_txns[0]->GetHash(), 0}}, /*num_outputs=*/2);
pool.addUnchecked(entry.Fee(normal_fee).FromTx(tx0));
pool.addUnchecked(entry.Fee(med_fee).FromTx(tx0));
const auto tx1 = make_tx({COutPoint{tx0->GetHash(), 0}}, /*num_outputs=*/1);
pool.addUnchecked(entry.Fee(normal_fee).FromTx(tx1));
pool.addUnchecked(entry.Fee(med_fee).FromTx(tx1));
// Create a low-feerate parent tx2 and high-feerate child tx3 (cpfp)
const auto tx2 = make_tx({COutPoint{m_coinbase_txns[1]->GetHash(), 0}}, /*num_outputs=*/2);
@ -94,9 +129,11 @@ BOOST_FIXTURE_TEST_CASE(miniminer_1p1c, TestChain100Setup)
pool.addUnchecked(entry.Fee(low_fee).FromTx(tx4));
const auto tx5 = make_tx({COutPoint{tx4->GetHash(), 0}}, /*num_outputs=*/1);
pool.addUnchecked(entry.Fee(low_fee).FromTx(tx5));
const CAmount tx5_delta{CENT/100};
// Make tx5's modified fee much higher than its base fee. This should cause it to pass
// the fee-related checks despite being low-feerate.
pool.PrioritiseTransaction(tx5->GetHash(), CENT/100);
pool.PrioritiseTransaction(tx5->GetHash(), tx5_delta);
const CAmount tx5_mod_fee{low_fee + tx5_delta};
// Create a high-feerate parent tx6, low-feerate child tx7
const auto tx6 = make_tx({COutPoint{m_coinbase_txns[3]->GetHash(), 0}}, /*num_outputs=*/2);
@ -273,6 +310,64 @@ BOOST_FIXTURE_TEST_CASE(miniminer_1p1c, TestChain100Setup)
}
}
}
// Check m_inclusion_order for equivalent mempool- and manually-constructed MiniMiners.
// (We cannot check bump fees in manually-constructed MiniMiners because it doesn't know what
// outpoints are requested).
std::vector<node::MiniMinerMempoolEntry> miniminer_info;
{
const int32_t tx0_vsize{tx_dims.at(tx0->GetHash()).vsize};
const int32_t tx1_vsize{tx_dims.at(tx1->GetHash()).vsize};
const int32_t tx2_vsize{tx_dims.at(tx2->GetHash()).vsize};
const int32_t tx3_vsize{tx_dims.at(tx3->GetHash()).vsize};
const int32_t tx4_vsize{tx_dims.at(tx4->GetHash()).vsize};
const int32_t tx5_vsize{tx_dims.at(tx5->GetHash()).vsize};
const int32_t tx6_vsize{tx_dims.at(tx6->GetHash()).vsize};
const int32_t tx7_vsize{tx_dims.at(tx7->GetHash()).vsize};
miniminer_info.emplace_back(/*fee_self=*/med_fee,/*fee_ancestor=*/med_fee,/*vsize_self=*/tx0_vsize,/*vsize_ancestor=*/tx0_vsize, tx0);
miniminer_info.emplace_back( med_fee, 2*med_fee, tx1_vsize, tx0_vsize + tx1_vsize, tx1);
miniminer_info.emplace_back( low_fee, low_fee, tx2_vsize, tx2_vsize, tx2);
miniminer_info.emplace_back( high_fee, low_fee + high_fee, tx3_vsize, tx2_vsize + tx3_vsize, tx3);
miniminer_info.emplace_back( low_fee, low_fee, tx4_vsize, tx4_vsize, tx4);
miniminer_info.emplace_back( tx5_mod_fee, low_fee + tx5_mod_fee, tx5_vsize, tx4_vsize + tx5_vsize, tx5);
miniminer_info.emplace_back( high_fee, high_fee, tx6_vsize, tx6_vsize, tx6);
miniminer_info.emplace_back( low_fee, high_fee + low_fee, tx7_vsize, tx6_vsize + tx7_vsize, tx7);
}
std::map<Txid, std::set<Txid>> descendant_caches;
descendant_caches.emplace(tx0->GetHash(), std::set<Txid>{tx0->GetHash(), tx1->GetHash()});
descendant_caches.emplace(tx1->GetHash(), std::set<Txid>{tx1->GetHash()});
descendant_caches.emplace(tx2->GetHash(), std::set<Txid>{tx2->GetHash(), tx3->GetHash()});
descendant_caches.emplace(tx3->GetHash(), std::set<Txid>{tx3->GetHash()});
descendant_caches.emplace(tx4->GetHash(), std::set<Txid>{tx4->GetHash(), tx5->GetHash()});
descendant_caches.emplace(tx5->GetHash(), std::set<Txid>{tx5->GetHash()});
descendant_caches.emplace(tx6->GetHash(), std::set<Txid>{tx6->GetHash(), tx7->GetHash()});
descendant_caches.emplace(tx7->GetHash(), std::set<Txid>{tx7->GetHash()});
node::MiniMiner miniminer_manual(miniminer_info, descendant_caches);
// Use unspent outpoints to avoid entries being omitted.
node::MiniMiner miniminer_pool(pool, all_unspent_outpoints);
BOOST_CHECK(miniminer_manual.IsReadyToCalculate());
BOOST_CHECK(miniminer_pool.IsReadyToCalculate());
for (const auto& sequences : {miniminer_manual.Linearize(), miniminer_pool.Linearize()}) {
// tx6 is selected first: high feerate with no parents to bump
BOOST_CHECK_EQUAL(Find(sequences, tx6->GetHash()), 0);
// tx2 + tx3 CPFP are selected next
BOOST_CHECK_EQUAL(Find(sequences, tx2->GetHash()), 1);
BOOST_CHECK_EQUAL(Find(sequences, tx3->GetHash()), 1);
// tx4 + prioritised tx5 CPFP
BOOST_CHECK_EQUAL(Find(sequences, tx4->GetHash()), 2);
BOOST_CHECK_EQUAL(Find(sequences, tx5->GetHash()), 2);
BOOST_CHECK_EQUAL(Find(sequences, tx0->GetHash()), 3);
BOOST_CHECK_EQUAL(Find(sequences, tx1->GetHash()), 3);
// tx7 is selected last: low feerate with no children
BOOST_CHECK_EQUAL(Find(sequences, tx7->GetHash()), 4);
}
}
BOOST_FIXTURE_TEST_CASE(miniminer_overlap, TestChain100Setup)
@ -308,10 +403,6 @@ BOOST_FIXTURE_TEST_CASE(miniminer_overlap, TestChain100Setup)
LOCK2(::cs_main, pool.cs);
TestMemPoolEntryHelper entry;
const CAmount low_fee{CENT/2000}; // 500 ṩ
const CAmount med_fee{CENT/200}; // 5000 ṩ
const CAmount high_fee{CENT/10}; // 100_000 ṩ
// Create 3 parents of different feerates, and 1 child spending outputs from all 3 parents.
const auto tx0 = make_tx({COutPoint{m_coinbase_txns[0]->GetHash(), 0}}, /*num_outputs=*/2);
pool.addUnchecked(entry.Fee(low_fee).FromTx(tx0));
@ -450,6 +541,50 @@ BOOST_FIXTURE_TEST_CASE(miniminer_overlap, TestChain100Setup)
BOOST_CHECK(tx7_bumpfee != bump_fees.end());
BOOST_CHECK_EQUAL(tx7_bumpfee->second, 0);
}
// Check linearization order
std::vector<node::MiniMinerMempoolEntry> miniminer_info;
miniminer_info.emplace_back(/*fee_self=*/low_fee, /*fee_ancestor=*/low_fee,/*vsize_self=*/tx_vsizes[0], /*vsize_ancestor=*/tx_vsizes[0], tx0);
miniminer_info.emplace_back( med_fee, med_fee, tx_vsizes[1], tx_vsizes[1], tx1);
miniminer_info.emplace_back( high_fee, high_fee, tx_vsizes[2], tx_vsizes[2], tx2);
miniminer_info.emplace_back( high_fee, low_fee+med_fee+2*high_fee, tx_vsizes[3], tx_vsizes[0]+tx_vsizes[1]+tx_vsizes[2]+tx_vsizes[3], tx3);
miniminer_info.emplace_back( high_fee, high_fee, tx_vsizes[4], tx_vsizes[4], tx4);
miniminer_info.emplace_back( low_fee, low_fee + high_fee, tx_vsizes[5], tx_vsizes[4]+tx_vsizes[5], tx5);
miniminer_info.emplace_back( med_fee, high_fee+low_fee+med_fee, tx_vsizes[6], tx_vsizes[4]+tx_vsizes[5]+tx_vsizes[6], tx6);
miniminer_info.emplace_back( high_fee, high_fee+low_fee+high_fee, tx_vsizes[7], tx_vsizes[4]+tx_vsizes[5]+tx_vsizes[7], tx7);
std::map<Txid, std::set<Txid>> descendant_caches;
descendant_caches.emplace(tx0->GetHash(), std::set<Txid>{tx0->GetHash(), tx3->GetHash()});
descendant_caches.emplace(tx1->GetHash(), std::set<Txid>{tx1->GetHash(), tx3->GetHash()});
descendant_caches.emplace(tx2->GetHash(), std::set<Txid>{tx2->GetHash(), tx3->GetHash()});
descendant_caches.emplace(tx3->GetHash(), std::set<Txid>{tx3->GetHash()});
descendant_caches.emplace(tx4->GetHash(), std::set<Txid>{tx4->GetHash(), tx5->GetHash(), tx6->GetHash(), tx7->GetHash()});
descendant_caches.emplace(tx5->GetHash(), std::set<Txid>{tx5->GetHash(), tx6->GetHash(), tx7->GetHash()});
descendant_caches.emplace(tx6->GetHash(), std::set<Txid>{tx6->GetHash()});
descendant_caches.emplace(tx7->GetHash(), std::set<Txid>{tx7->GetHash()});
node::MiniMiner miniminer_manual(miniminer_info, descendant_caches);
// Use unspent outpoints to avoid entries being omitted.
node::MiniMiner miniminer_pool(pool, all_unspent_outpoints);
BOOST_CHECK(miniminer_manual.IsReadyToCalculate());
BOOST_CHECK(miniminer_pool.IsReadyToCalculate());
for (const auto& sequences : {miniminer_manual.Linearize(), miniminer_pool.Linearize()}) {
// tx2 and tx4 selected first: high feerate with nothing to bump
BOOST_CHECK_EQUAL(Find(sequences, tx4->GetHash()), 0);
BOOST_CHECK_EQUAL(Find(sequences, tx2->GetHash()), 1);
// tx5 + tx7 CPFP
BOOST_CHECK_EQUAL(Find(sequences, tx5->GetHash()), 2);
BOOST_CHECK_EQUAL(Find(sequences, tx7->GetHash()), 2);
// tx0 and tx1 CPFP'd by tx3
BOOST_CHECK_EQUAL(Find(sequences, tx0->GetHash()), 3);
BOOST_CHECK_EQUAL(Find(sequences, tx1->GetHash()), 3);
BOOST_CHECK_EQUAL(Find(sequences, tx3->GetHash()), 3);
// tx6 at medium feerate
BOOST_CHECK_EQUAL(Find(sequences, tx6->GetHash()), 4);
}
}
BOOST_FIXTURE_TEST_CASE(calculate_cluster, TestChain100Setup)
{
@ -507,4 +642,64 @@ BOOST_FIXTURE_TEST_CASE(calculate_cluster, TestChain100Setup)
}
}
BOOST_FIXTURE_TEST_CASE(manual_ctor, TestChain100Setup)
{
CTxMemPool& pool = *Assert(m_node.mempool);
LOCK2(cs_main, pool.cs);
{
// 3 pairs of fee-bumping grandparent + parent, plus 1 low-feerate child.
// 0 fee + high fee
auto grandparent_zero_fee = make_tx({{m_coinbase_txns.at(0)->GetHash(), 0}}, 1);
auto parent_high_feerate = make_tx({{grandparent_zero_fee->GetHash(), 0}}, 1);
// double low fee + med fee
auto grandparent_double_low_feerate = make_tx({{m_coinbase_txns.at(2)->GetHash(), 0}}, 1);
auto parent_med_feerate = make_tx({{grandparent_double_low_feerate->GetHash(), 0}}, 1);
// low fee + double low fee
auto grandparent_low_feerate = make_tx({{m_coinbase_txns.at(1)->GetHash(), 0}}, 1);
auto parent_double_low_feerate = make_tx({{grandparent_low_feerate->GetHash(), 0}}, 1);
// child is below the cpfp package feerates because it is larger than everything else
auto child = make_tx({{parent_high_feerate->GetHash(), 0}, {parent_double_low_feerate->GetHash(), 0}, {parent_med_feerate->GetHash(), 0}}, 1);
// We artificially record each transaction (except the child) with a uniform vsize of 100vB.
const int64_t tx_vsize{100};
const int64_t child_vsize{1000};
std::vector<node::MiniMinerMempoolEntry> miniminer_info;
miniminer_info.emplace_back(/*fee_self=*/0, /*fee_ancestor=*/0,/*vsize_self=*/tx_vsize,/*vsize_ancestor=*/tx_vsize, grandparent_zero_fee);
miniminer_info.emplace_back( high_fee, high_fee, tx_vsize, 2*tx_vsize, parent_high_feerate);
miniminer_info.emplace_back( 2*low_fee, 2*low_fee, tx_vsize, tx_vsize, grandparent_double_low_feerate);
miniminer_info.emplace_back( med_fee, 2*low_fee+med_fee, tx_vsize, 2*tx_vsize, parent_med_feerate);
miniminer_info.emplace_back( low_fee, low_fee, tx_vsize, tx_vsize, grandparent_low_feerate);
miniminer_info.emplace_back( 2*low_fee, 3*low_fee, tx_vsize, 2*tx_vsize, parent_double_low_feerate);
miniminer_info.emplace_back( low_fee, high_fee+med_fee+6*low_fee, child_vsize, 6*tx_vsize+child_vsize, child);
std::map<Txid, std::set<Txid>> descendant_caches;
descendant_caches.emplace(grandparent_zero_fee->GetHash(), std::set<Txid>{grandparent_zero_fee->GetHash(), parent_high_feerate->GetHash(), child->GetHash()});
descendant_caches.emplace(grandparent_low_feerate->GetHash(), std::set<Txid>{grandparent_low_feerate->GetHash(), parent_double_low_feerate->GetHash(), child->GetHash()});
descendant_caches.emplace(grandparent_double_low_feerate->GetHash(), std::set<Txid>{grandparent_double_low_feerate->GetHash(), parent_med_feerate->GetHash(), child->GetHash()});
descendant_caches.emplace(parent_high_feerate->GetHash(), std::set<Txid>{parent_high_feerate->GetHash(), child->GetHash()});
descendant_caches.emplace(parent_med_feerate->GetHash(), std::set<Txid>{parent_med_feerate->GetHash(), child->GetHash()});
descendant_caches.emplace(parent_double_low_feerate->GetHash(), std::set<Txid>{parent_double_low_feerate->GetHash(), child->GetHash()});
descendant_caches.emplace(child->GetHash(), std::set<Txid>{child->GetHash()});
node::MiniMiner miniminer_manual(miniminer_info, descendant_caches);
BOOST_CHECK(miniminer_manual.IsReadyToCalculate());
const auto sequences{miniminer_manual.Linearize()};
// CPFP zero + high
BOOST_CHECK_EQUAL(sequences.at(grandparent_zero_fee->GetHash()), 0);
BOOST_CHECK_EQUAL(sequences.at(parent_high_feerate->GetHash()), 0);
// CPFP double low + med
BOOST_CHECK_EQUAL(sequences.at(grandparent_double_low_feerate->GetHash()), 1);
BOOST_CHECK_EQUAL(sequences.at(parent_med_feerate->GetHash()), 1);
// CPFP low + med
BOOST_CHECK_EQUAL(sequences.at(grandparent_low_feerate->GetHash()), 2);
BOOST_CHECK_EQUAL(sequences.at(parent_double_low_feerate->GetHash()), 2);
// Child at the end
BOOST_CHECK_EQUAL(sequences.at(child->GetHash()), 3);
}
}
BOOST_AUTO_TEST_SUITE_END()

View File

@ -23,6 +23,7 @@ EXCLUDED_DIRS = ["contrib/devtools/bitcoin-tidy/",
]
EXPECTED_BOOST_INCLUDES = ["boost/date_time/posix_time/posix_time.hpp",
"boost/multi_index/detail/hash_index_iterator.hpp",
"boost/multi_index/hashed_index.hpp",
"boost/multi_index/identity.hpp",
"boost/multi_index/indexed_by.hpp",
@ -30,6 +31,7 @@ EXPECTED_BOOST_INCLUDES = ["boost/date_time/posix_time/posix_time.hpp",
"boost/multi_index/sequenced_index.hpp",
"boost/multi_index/tag.hpp",
"boost/multi_index_container.hpp",
"boost/operators.hpp",
"boost/process.hpp",
"boost/signals2/connection.hpp",
"boost/signals2/optional_last_value.hpp",