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

d9cc99d04e [test] MiniMiner::Linearize and manual construction (glozow)
dfd6a3788c [refactor] unify fee amounts in miniminer_tests (glozow)
f4b1b24a3b [MiniMiner] track inclusion order and add Linearize() function (glozow)
004075963f [test] add case for MiniMiner working with negative fee txns (glozow)
fe6332c0ba [MiniMiner] make target_feerate optional (glozow)
5a83f55c96 [MiniMiner] allow manual construction with non-mempool txns (glozow)
e3b2e630b2 [refactor] change MiniMinerMempoolEntry ctor to take values, update includes (glozow)
4aa98b79b2 [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 d9cc99d04e
  kevkevinpal:
    reACK [d9cc99d](d9cc99d04e)
  achow101:
    re-ACK d9cc99d04e

Tree-SHA512: 32b80064b6679536ac573d674825c5ca0cd6245e49c2fd5eaf260dc535335a57683c74ddd7ce1f249b5b12b2683de4362a7b0f1fc0814c3b3b9f14c682665583
This commit is contained in:
Andrew Chow
2023-11-03 10:45:34 -04:00
4 changed files with 345 additions and 29 deletions

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()