mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-04-09 20:59:38 +02:00
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:
commit
d9007f51a7
@ -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 {};
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user