Merge bitcoin/bitcoin#30072: refactor prep for package rbf

2fd34ba504 Add sanity checks for various ATMPArgs booleans (Greg Sanders)
20d8936d8b [refactor] make some members MemPoolAccept-wide (glozow)
cbbfe719b2 cpfp carveout is excluded in packages (glozow)
69f7ab05ba Add m_allow_sibling_eviction as separate ATMPArgs flag (Greg Sanders)
57ee3029dd Add description for m_test_accept (Greg Sanders)

Pull request description:

  First few commits of https://github.com/bitcoin/bitcoin/pull/28984 to set the stage for the package RBF logic.

  These refactors are preparation for evaluating an RBF in a multi-proposed-transaction context instead of only a single proposed transaction. Also, carveouts and sibling evictions only should work in single RBF cases so add logic to preclude multi-tx cases in the future.

  No behavior changes aside from bailing earlier from failed carve-outs.

ACKs for top commit:
  glozow:
    reACK 2fd34ba504 via range-diff
  sr-gi:
    utACK [2fd34ba](2fd34ba504)
  theStack:
    re-ACK 2fd34ba504

Tree-SHA512: 5071c5b8d9b8d2c9faa278c8c4df31de288cb407a68e4d55544c588caff6c86160cce7825453549c6ed69e29d9ccb5ee2d4a518b18f563bfb12f2ced073fe42a
This commit is contained in:
glozow
2024-05-24 10:08:18 +01:00
3 changed files with 128 additions and 56 deletions

View File

@@ -455,11 +455,14 @@ public:
* the mempool.
*/
std::vector<COutPoint>& m_coins_to_uncache;
/** When true, the transaction or package will not be submitted to the mempool. */
const bool m_test_accept;
/** Whether we allow transactions to replace mempool transactions by BIP125 rules. If false,
/** Whether we allow transactions to replace mempool transactions. If false,
* any transaction spending the same inputs as a transaction in the mempool is considered
* a conflict. */
const bool m_allow_replacement;
/** When true, allow sibling eviction. This only occurs in single transaction package settings. */
const bool m_allow_sibling_eviction;
/** When true, the mempool will not be trimmed when any transactions are submitted in
* Finalize(). Instead, limits should be enforced at the end to ensure the package is not
* partially submitted.
@@ -475,6 +478,9 @@ public:
*/
const std::optional<CFeeRate> m_client_maxfeerate;
/** Whether CPFP carveout and RBF carveout are granted. */
const bool m_allow_carveouts;
/** Parameters for single transaction mempool validation. */
static ATMPArgs SingleAccept(const CChainParams& chainparams, int64_t accept_time,
bool bypass_limits, std::vector<COutPoint>& coins_to_uncache,
@@ -485,9 +491,11 @@ public:
/* m_coins_to_uncache */ coins_to_uncache,
/* m_test_accept */ test_accept,
/* m_allow_replacement */ true,
/* m_allow_sibling_eviction */ true,
/* m_package_submission */ false,
/* m_package_feerates */ false,
/* m_client_maxfeerate */ {}, // checked by caller
/* m_allow_carveouts */ true,
};
}
@@ -500,9 +508,11 @@ public:
/* m_coins_to_uncache */ coins_to_uncache,
/* m_test_accept */ true,
/* m_allow_replacement */ false,
/* m_allow_sibling_eviction */ false,
/* m_package_submission */ false, // not submitting to mempool
/* m_package_feerates */ false,
/* m_client_maxfeerate */ {}, // checked by caller
/* m_allow_carveouts */ false,
};
}
@@ -515,9 +525,11 @@ public:
/* m_coins_to_uncache */ coins_to_uncache,
/* m_test_accept */ false,
/* m_allow_replacement */ false,
/* m_allow_sibling_eviction */ false,
/* m_package_submission */ true,
/* m_package_feerates */ true,
/* m_client_maxfeerate */ client_maxfeerate,
/* m_allow_carveouts */ false,
};
}
@@ -529,9 +541,11 @@ public:
/* m_coins_to_uncache */ package_args.m_coins_to_uncache,
/* m_test_accept */ package_args.m_test_accept,
/* m_allow_replacement */ true,
/* m_allow_sibling_eviction */ true,
/* m_package_submission */ true, // do not LimitMempoolSize in Finalize()
/* m_package_feerates */ false, // only 1 transaction
/* m_client_maxfeerate */ package_args.m_client_maxfeerate,
/* m_allow_carveouts */ false,
};
}
@@ -544,19 +558,31 @@ public:
std::vector<COutPoint>& coins_to_uncache,
bool test_accept,
bool allow_replacement,
bool allow_sibling_eviction,
bool package_submission,
bool package_feerates,
std::optional<CFeeRate> client_maxfeerate)
std::optional<CFeeRate> client_maxfeerate,
bool allow_carveouts)
: m_chainparams{chainparams},
m_accept_time{accept_time},
m_bypass_limits{bypass_limits},
m_coins_to_uncache{coins_to_uncache},
m_test_accept{test_accept},
m_allow_replacement{allow_replacement},
m_allow_sibling_eviction{allow_sibling_eviction},
m_package_submission{package_submission},
m_package_feerates{package_feerates},
m_client_maxfeerate{client_maxfeerate}
m_client_maxfeerate{client_maxfeerate},
m_allow_carveouts{allow_carveouts}
{
// If we are using package feerates, we must be doing package submission.
// It also means carveouts and sibling eviction are not permitted.
if (m_package_feerates) {
Assume(m_package_submission);
Assume(!m_allow_carveouts);
Assume(!m_allow_sibling_eviction);
}
if (m_allow_sibling_eviction) Assume(m_allow_replacement);
}
};
@@ -603,18 +629,11 @@ private:
/** Iterators to mempool entries that this transaction directly conflicts with or may
* replace via sibling eviction. */
CTxMemPool::setEntries m_iters_conflicting;
/** Iterators to all mempool entries that would be replaced by this transaction, including
* m_conflicts and their descendants. */
CTxMemPool::setEntries m_all_conflicting;
/** All mempool ancestors of this transaction. */
CTxMemPool::setEntries m_ancestors;
/** Mempool entry constructed for this transaction. Constructed in PreChecks() but not
* inserted into the mempool until Finalize(). */
std::unique_ptr<CTxMemPoolEntry> m_entry;
/** Pointers to the transactions that have been removed from the mempool and replaced by
* this transaction (everything in m_all_conflicting), used to return to the MemPoolAccept caller. Only populated if
* validation is successful and the original transactions are removed. */
std::list<CTransactionRef> m_replaced_transactions;
/** Whether RBF-related data structures (m_conflicts, m_iters_conflicting, m_all_conflicting,
* m_replaced_transactions) include a sibling in addition to txns with conflicting inputs. */
bool m_sibling_eviction{false};
@@ -626,10 +645,6 @@ private:
CAmount m_base_fees;
/** Base fees + any fee delta set by the user with prioritisetransaction. */
CAmount m_modified_fees;
/** Total modified fees of all transactions being replaced. */
CAmount m_conflicting_fees{0};
/** Total virtual size of all transactions being replaced. */
size_t m_conflicting_size{0};
/** If we're doing package validation (i.e. m_package_feerates=true), the "effective"
* package feerate of this transaction is the total fees divided by the total size of
@@ -707,9 +722,39 @@ private:
Chainstate& m_active_chainstate;
/** Whether the transaction(s) would replace any mempool transactions and/or evict any siblings.
* If so, RBF rules apply. */
bool m_rbf{false};
// Fields below are per *sub*package state and must be reset prior to subsequent
// AcceptSingleTransaction and AcceptMultipleTransactions invocations
struct SubPackageState {
/** Aggregated modified fees of all transactions, used to calculate package feerate. */
CAmount m_total_modified_fees{0};
/** Aggregated virtual size of all transactions, used to calculate package feerate. */
int64_t m_total_vsize{0};
// RBF-related members
/** Whether the transaction(s) would replace any mempool transactions and/or evict any siblings.
* If so, RBF rules apply. */
bool m_rbf{false};
/** All directly conflicting mempool transactions and their descendants. */
CTxMemPool::setEntries m_all_conflicts;
/** Mempool transactions that were replaced. */
std::list<CTransactionRef> m_replaced_transactions;
/** Total modified fees of mempool transactions being replaced. */
CAmount m_conflicting_fees{0};
/** Total size (in virtual bytes) of mempool transactions being replaced. */
size_t m_conflicting_size{0};
};
struct SubPackageState m_subpackage;
/** Re-set sub-package state to not leak between evaluations */
void ClearSubPackageState() EXCLUSIVE_LOCKS_REQUIRED(cs_main, m_pool.cs)
{
m_subpackage = SubPackageState{};
// And clean coins while at it
CleanupTemporaryCoins();
}
};
bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
@@ -908,7 +953,7 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
CTxMemPool::Limits maybe_rbf_limits = m_pool.m_opts.limits;
// Calculate in-mempool ancestors, up to a limit.
if (ws.m_conflicts.size() == 1) {
if (ws.m_conflicts.size() == 1 && args.m_allow_carveouts) {
// In general, when we receive an RBF transaction with mempool conflicts, we want to know whether we
// would meet the chain limits after the conflicts have been removed. However, there isn't a practical
// way to do this short of calculating the ancestor and descendant sets with an overlay cache of
@@ -947,6 +992,13 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
ws.m_ancestors = std::move(*ancestors);
} else {
// If CalculateMemPoolAncestors fails second time, we want the original error string.
const auto error_message{util::ErrorString(ancestors).original};
// Carve-out is not allowed in this context; fail
if (!args.m_allow_carveouts) {
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "too-long-mempool-chain", error_message);
}
// Contracting/payment channels CPFP carve-out:
// If the new transaction is relatively small (up to 40k weight)
// and has at most one ancestor (ie ancestor limit of 2, including
@@ -965,7 +1017,6 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
.descendant_count = maybe_rbf_limits.descendant_count + 1,
.descendant_size_vbytes = maybe_rbf_limits.descendant_size_vbytes + EXTRA_DESCENDANT_TX_SIZE_LIMIT,
};
const auto error_message{util::ErrorString(ancestors).original};
if (ws.m_vsize > EXTRA_DESCENDANT_TX_SIZE_LIMIT || ws.m_ptx->nVersion == 3) {
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "too-long-mempool-chain", error_message);
}
@@ -980,8 +1031,11 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
// check using the full ancestor set here because it's more convenient to use what we have
// already calculated.
if (const auto err{SingleV3Checks(ws.m_ptx, ws.m_ancestors, ws.m_conflicts, ws.m_vsize)}) {
// Disabled within package validation.
if (err->second != nullptr && args.m_allow_replacement) {
// Single transaction contexts only.
if (args.m_allow_sibling_eviction && err->second != nullptr) {
// We should only be considering where replacement is considered valid as well.
Assume(args.m_allow_replacement);
// Potential sibling eviction. Add the sibling to our list of mempool conflicts to be
// included in RBF checks.
ws.m_conflicts.insert(err->second->GetHash());
@@ -1009,7 +1063,8 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-txns-spends-conflicting-tx", *err_string);
}
m_rbf = !ws.m_conflicts.empty();
// We want to detect conflicts in any tx in a package to trigger package RBF logic
m_subpackage.m_rbf |= !ws.m_conflicts.empty();
return true;
}
@@ -1041,24 +1096,25 @@ bool MemPoolAccept::ReplacementChecks(Workspace& ws)
}
// Calculate all conflicting entries and enforce Rule #5.
if (const auto err_string{GetEntriesForConflicts(tx, m_pool, ws.m_iters_conflicting, ws.m_all_conflicting)}) {
if (const auto err_string{GetEntriesForConflicts(tx, m_pool, ws.m_iters_conflicting, m_subpackage.m_all_conflicts)}) {
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY,
strprintf("too many potential replacements%s", ws.m_sibling_eviction ? " (including sibling eviction)" : ""), *err_string);
}
// Enforce Rule #2.
if (const auto err_string{HasNoNewUnconfirmed(tx, m_pool, ws.m_iters_conflicting)}) {
if (const auto err_string{HasNoNewUnconfirmed(tx, m_pool, m_subpackage.m_all_conflicts)}) {
// Sibling eviction is only done for v3 transactions, which cannot have multiple ancestors.
Assume(!ws.m_sibling_eviction);
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY,
strprintf("replacement-adds-unconfirmed%s", ws.m_sibling_eviction ? " (including sibling eviction)" : ""), *err_string);
}
// Check if it's economically rational to mine this transaction rather than the ones it
// replaces and pays for its own relay fees. Enforce Rules #3 and #4.
for (CTxMemPool::txiter it : ws.m_all_conflicting) {
ws.m_conflicting_fees += it->GetModifiedFee();
ws.m_conflicting_size += it->GetTxSize();
for (CTxMemPool::txiter it : m_subpackage.m_all_conflicts) {
m_subpackage.m_conflicting_fees += it->GetModifiedFee();
m_subpackage.m_conflicting_size += it->GetTxSize();
}
if (const auto err_string{PaysForRBF(ws.m_conflicting_fees, ws.m_modified_fees, ws.m_vsize,
if (const auto err_string{PaysForRBF(m_subpackage.m_conflicting_fees, ws.m_modified_fees, ws.m_vsize,
m_pool.m_opts.incremental_relay_feerate, hash)}) {
// Even though this is a fee-related failure, this result is TX_MEMPOOL_POLICY, not
// TX_RECONSIDERABLE, because it cannot be bypassed using package validation.
@@ -1157,19 +1213,18 @@ bool MemPoolAccept::Finalize(const ATMPArgs& args, Workspace& ws)
const uint256& hash = ws.m_hash;
TxValidationState& state = ws.m_state;
const bool bypass_limits = args.m_bypass_limits;
std::unique_ptr<CTxMemPoolEntry>& entry = ws.m_entry;
// Remove conflicting transactions from the mempool
for (CTxMemPool::txiter it : ws.m_all_conflicting)
for (CTxMemPool::txiter it : m_subpackage.m_all_conflicts)
{
LogPrint(BCLog::MEMPOOL, "replacing tx %s (wtxid=%s) with %s (wtxid=%s) for %s additional fees, %d delta bytes\n",
it->GetTx().GetHash().ToString(),
it->GetTx().GetWitnessHash().ToString(),
hash.ToString(),
tx.GetWitnessHash().ToString(),
FormatMoney(ws.m_modified_fees - ws.m_conflicting_fees),
(int)entry->GetTxSize() - (int)ws.m_conflicting_size);
FormatMoney(ws.m_modified_fees - m_subpackage.m_conflicting_fees),
(int)entry->GetTxSize() - (int)m_subpackage.m_conflicting_size);
TRACE7(mempool, replaced,
it->GetTx().GetHash().data(),
it->GetTxSize(),
@@ -1179,9 +1234,12 @@ bool MemPoolAccept::Finalize(const ATMPArgs& args, Workspace& ws)
entry->GetTxSize(),
entry->GetFee()
);
ws.m_replaced_transactions.push_back(it->GetSharedTx());
m_subpackage.m_replaced_transactions.push_back(it->GetSharedTx());
}
m_pool.RemoveStaged(ws.m_all_conflicting, false, MemPoolRemovalReason::REPLACED);
m_pool.RemoveStaged(m_subpackage.m_all_conflicts, false, MemPoolRemovalReason::REPLACED);
// Don't attempt to process the same conflicts repeatedly during subpackage evaluation:
// they no longer exist on subsequent calls to Finalize() post-RemoveStaged
m_subpackage.m_all_conflicts.clear();
// Store transaction in memory
m_pool.addUnchecked(*entry, ws.m_ancestors);
@@ -1267,7 +1325,7 @@ bool MemPoolAccept::SubmitPackage(const ATMPArgs& args, std::vector<Workspace>&
const auto effective_feerate_wtxids = args.m_package_feerates ? all_package_wtxids :
std::vector<Wtxid>{ws.m_ptx->GetWitnessHash()};
results.emplace(ws.m_ptx->GetWitnessHash(),
MempoolAcceptResult::Success(std::move(ws.m_replaced_transactions), ws.m_vsize,
MempoolAcceptResult::Success(std::move(m_subpackage.m_replaced_transactions), ws.m_vsize,
ws.m_base_fees, effective_feerate, effective_feerate_wtxids));
if (!m_pool.m_opts.signals) continue;
const CTransaction& tx = *ws.m_ptx;
@@ -1303,7 +1361,7 @@ MempoolAcceptResult MemPoolAccept::AcceptSingleTransaction(const CTransactionRef
return MempoolAcceptResult::Failure(ws.m_state);
}
if (m_rbf && !ReplacementChecks(ws)) return MempoolAcceptResult::Failure(ws.m_state);
if (m_subpackage.m_rbf && !ReplacementChecks(ws)) return MempoolAcceptResult::Failure(ws.m_state);
// Perform the inexpensive checks first and avoid hashing and signature verification unless
// those checks pass, to mitigate CPU exhaustion denial-of-service attacks.
@@ -1314,7 +1372,7 @@ MempoolAcceptResult MemPoolAccept::AcceptSingleTransaction(const CTransactionRef
const CFeeRate effective_feerate{ws.m_modified_fees, static_cast<uint32_t>(ws.m_vsize)};
// Tx was accepted, but not added
if (args.m_test_accept) {
return MempoolAcceptResult::Success(std::move(ws.m_replaced_transactions), ws.m_vsize,
return MempoolAcceptResult::Success(std::move(m_subpackage.m_replaced_transactions), ws.m_vsize,
ws.m_base_fees, effective_feerate, single_wtxid);
}
@@ -1335,7 +1393,7 @@ MempoolAcceptResult MemPoolAccept::AcceptSingleTransaction(const CTransactionRef
m_pool.m_opts.signals->TransactionAddedToMempool(tx_info, m_pool.GetAndIncrementSequence());
}
return MempoolAcceptResult::Success(std::move(ws.m_replaced_transactions), ws.m_vsize, ws.m_base_fees,
return MempoolAcceptResult::Success(std::move(m_subpackage.m_replaced_transactions), ws.m_vsize, ws.m_base_fees,
effective_feerate, single_wtxid);
}
@@ -1380,6 +1438,7 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptMultipleTransactions(const std::
// replacements, we don't need to track the coins spent. Note that this logic will need to be
// updated if package replace-by-fee is allowed in the future.
assert(!args.m_allow_replacement);
assert(!m_subpackage.m_rbf);
m_viewmempool.PackageAddTransaction(ws.m_ptx);
}
@@ -1401,28 +1460,26 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptMultipleTransactions(const std::
// a child that is below mempool minimum feerate. To avoid these behaviors, callers of
// AcceptMultipleTransactions need to restrict txns topology (e.g. to ancestor sets) and check
// the feerates of individuals and subsets.
const auto m_total_vsize = std::accumulate(workspaces.cbegin(), workspaces.cend(), int64_t{0},
m_subpackage.m_total_vsize = std::accumulate(workspaces.cbegin(), workspaces.cend(), int64_t{0},
[](int64_t sum, auto& ws) { return sum + ws.m_vsize; });
const auto m_total_modified_fees = std::accumulate(workspaces.cbegin(), workspaces.cend(), CAmount{0},
m_subpackage.m_total_modified_fees = std::accumulate(workspaces.cbegin(), workspaces.cend(), CAmount{0},
[](CAmount sum, auto& ws) { return sum + ws.m_modified_fees; });
const CFeeRate package_feerate(m_total_modified_fees, m_total_vsize);
const CFeeRate package_feerate(m_subpackage.m_total_modified_fees, m_subpackage.m_total_vsize);
std::vector<Wtxid> all_package_wtxids;
all_package_wtxids.reserve(workspaces.size());
std::transform(workspaces.cbegin(), workspaces.cend(), std::back_inserter(all_package_wtxids),
[](const auto& ws) { return ws.m_ptx->GetWitnessHash(); });
TxValidationState placeholder_state;
if (args.m_package_feerates &&
!CheckFeeRate(m_total_vsize, m_total_modified_fees, placeholder_state)) {
!CheckFeeRate(m_subpackage.m_total_vsize, m_subpackage.m_total_modified_fees, placeholder_state)) {
package_state.Invalid(PackageValidationResult::PCKG_TX, "transaction failed");
return PackageMempoolAcceptResult(package_state, {{workspaces.back().m_ptx->GetWitnessHash(),
MempoolAcceptResult::FeeFailure(placeholder_state, CFeeRate(m_total_modified_fees, m_total_vsize), all_package_wtxids)}});
MempoolAcceptResult::FeeFailure(placeholder_state, CFeeRate(m_subpackage.m_total_modified_fees, m_subpackage.m_total_vsize), all_package_wtxids)}});
}
// Apply package mempool ancestor/descendant limits. Skip if there is only one transaction,
// because it's unnecessary. Also, CPFP carve out can increase the limit for individual
// transactions, but this exemption is not extended to packages in CheckPackageLimits().
std::string err_string;
if (txns.size() > 1 && !PackageMempoolChecks(txns, m_total_vsize, package_state)) {
// because it's unnecessary.
if (txns.size() > 1 && !PackageMempoolChecks(txns, m_subpackage.m_total_vsize, package_state)) {
return PackageMempoolAcceptResult(package_state, std::move(results));
}
@@ -1440,7 +1497,7 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptMultipleTransactions(const std::
const auto effective_feerate_wtxids = args.m_package_feerates ? all_package_wtxids :
std::vector<Wtxid>{ws.m_ptx->GetWitnessHash()};
results.emplace(ws.m_ptx->GetWitnessHash(),
MempoolAcceptResult::Success(std::move(ws.m_replaced_transactions),
MempoolAcceptResult::Success(std::move(m_subpackage.m_replaced_transactions),
ws.m_vsize, ws.m_base_fees, effective_feerate,
effective_feerate_wtxids));
}
@@ -1505,7 +1562,8 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptSubPackage(const std::vector<CTr
// Clean up m_view and m_viewmempool so that other subpackage evaluations don't have access to
// coins they shouldn't. Keep some coins in order to minimize re-fetching coins from the UTXO set.
CleanupTemporaryCoins();
// Clean up package feerate and rbf calculations
ClearSubPackageState();
return result;
}