Skip to content

Commit

Permalink
[policy] package rbf
Browse files Browse the repository at this point in the history
Support package RBF where the conflicting package would result
in a mempool cluster of size two, and each of its direct
conflicts are also part of an up-to-size-2 mempool cluster.

This restricted topology allows for exact calculation
of miner scores for each side of the equation, reducing
the surface area for new pins, or incentive-incompatible
replacements.

This allows wallets to create simple CPFP packages
that can fee bump other simple CPFP packages. This,
leveraged with other restrictions such as V3 transactions,
can create pin-resistant applications.

Future package RBF relaxations can be considered when appropriate.

Co-authored-by: glozow <gloriajzhao@gmail.com>
Co-authored-by: Greg Sanders <gsanders87@gmail.com>
  • Loading branch information
3 people committed May 8, 2024
1 parent 97b0e86 commit b86487c
Showing 1 changed file with 93 additions and 20 deletions.
113 changes: 93 additions & 20 deletions src/validation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,7 @@ class MemPoolAccept
/* m_bypass_limits */ false,
/* m_coins_to_uncache */ coins_to_uncache,
/* m_test_accept */ false,
/* m_allow_replacement */ false,
/* m_allow_replacement */ true,
/* m_package_submission */ true,
/* m_package_feerates */ true,
/* m_client_maxfeerate */ client_maxfeerate,
Expand Down Expand Up @@ -652,12 +652,13 @@ class MemPoolAccept
// only tests that are fast should be done here (to avoid CPU DoS).
bool PreChecks(ATMPArgs& args, Workspace& ws) EXCLUSIVE_LOCKS_REQUIRED(cs_main, m_pool.cs);

// Run checks for mempool replace-by-fee.
// Run checks for mempool replace-by-fee, only used in AcceptSingleTransaction.
bool ReplacementChecks(Workspace& ws) EXCLUSIVE_LOCKS_REQUIRED(cs_main, m_pool.cs);

// Enforce package mempool ancestor/descendant limits (distinct from individual
// ancestor/descendant limits done in PreChecks).
// ancestor/descendant limits done in PreChecks) and run Package RBF checks.
bool PackageMempoolChecks(const std::vector<CTransactionRef>& txns,
std::vector<Workspace>& workspaces,
int64_t total_vsize,
PackageValidationState& package_state) EXCLUSIVE_LOCKS_REQUIRED(cs_main, m_pool.cs);

Expand Down Expand Up @@ -1073,10 +1074,9 @@ bool MemPoolAccept::ReplacementChecks(Workspace& ws)
// descendant transaction of a direct conflict to pay a higher feerate than the transaction that
// might replace them, under these rules.
if (const auto err_string{PaysMoreThanConflicts(ws.m_iters_conflicting, newFeeRate, 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.
// This must be changed if package RBF is enabled.
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY,
// This fee-related failure is TX_RECONSIDERABLE because validating in a package may change
// the result.
return state.Invalid(TxValidationResult::TX_RECONSIDERABLE,
strprintf("insufficient fee%s", ws.m_sibling_eviction ? " (including sibling eviction)" : ""), *err_string);
}

Expand All @@ -1101,16 +1101,15 @@ bool MemPoolAccept::ReplacementChecks(Workspace& ws)
}
if (const auto err_string{PaysForRBF(m_conflicting_fees, ws.m_modified_fees, ws.m_vsize,
m_pool.m_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.
// This must be changed if package RBF is enabled.
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY,
// Result may change in a package context
return state.Invalid(TxValidationResult::TX_RECONSIDERABLE,
strprintf("insufficient fee%s", ws.m_sibling_eviction ? " (including sibling eviction)" : ""), *err_string);
}
return true;
}

bool MemPoolAccept::PackageMempoolChecks(const std::vector<CTransactionRef>& txns,
std::vector<Workspace>& workspaces,
const int64_t total_vsize,
PackageValidationState& package_state)
{
Expand All @@ -1126,7 +1125,71 @@ bool MemPoolAccept::PackageMempoolChecks(const std::vector<CTransactionRef>& txn
// This is a package-wide error, separate from an individual transaction error.
return package_state.Invalid(PackageValidationResult::PCKG_POLICY, "package-mempool-limits", util::ErrorString(result).original);
}
return true;

// No conflicts means we're finished. Further checks are all RBF-only.
if (!m_rbf) return true;

// We're in package RBF context; replacement proposal must be size 2
if (workspaces.size() != 2 || !Assume(IsChildWithParents(txns))) {
return package_state.Invalid(PackageValidationResult::PCKG_POLICY, "package RBF failed: replacing cluster not size two");
}

// If the package has in-mempool ancestors, we won't consider a package RBF
// since it would result in a cluster larger than 2
for (const auto& ws : workspaces) {
if (!ws.m_ancestors.empty()) {
return package_state.Invalid(PackageValidationResult::PCKG_POLICY, "package RBF failed: replacing cluster with ancestors not size two");
}
}

// Aggregate all conflicts into one set.
CTxMemPool::setEntries direct_conflict_iters;
for (Workspace& ws : workspaces) {
// Aggregate all conflicts into one set.
direct_conflict_iters.merge(ws.m_iters_conflicting);
}

for (const auto& ws : workspaces) {
// The aggregated set of conflicts cannot exceed 100.
if (const auto err_string{GetEntriesForConflicts(*ws.m_ptx, m_pool, direct_conflict_iters,
m_all_conflicts)}) {
return package_state.Invalid(PackageValidationResult::PCKG_POLICY,
"package RBF failed: too many potential replacements", *err_string);
}
}

const auto& parent_ws = workspaces[0];
const auto& child_ws = workspaces[1];

// Ensure this two transaction package is a "chunk" on its own; we don't want the child
// to be only paying anti-DoS fees
if (CFeeRate(parent_ws.m_modified_fees, parent_ws.m_vsize) >=
CFeeRate(parent_ws.m_modified_fees + child_ws.m_modified_fees, parent_ws.m_vsize + child_ws.m_vsize)) {
return package_state.Invalid(PackageValidationResult::PCKG_POLICY,
"package RBF failed: parent paying for child anti-DoS", "");
}

// Check if it's economically rational to mine this package rather than the ones it replaces.
if (const auto err_tup{ImprovesFeerateDiagram(m_pool, direct_conflict_iters, m_all_conflicts, m_total_modified_fees, m_total_vsize)}) {
return package_state.Invalid(PackageValidationResult::PCKG_POLICY,
"package RBF failed: " + err_tup.value().second, "");
}
m_conflicting_fees = 0;
m_conflicting_size = 0;
for (CTxMemPool::txiter it : m_all_conflicts) {
m_conflicting_fees += it->GetModifiedFee();
m_conflicting_size += it->GetTxSize();
}

// Use the child as the transaction for attributing errors to.
const Txid child_hash = child_ws.m_ptx->GetHash();
if (const auto err_string{PaysForRBF(/*original_fees=*/m_conflicting_fees, /*replacement_fees=*/m_total_modified_fees, m_total_vsize,
m_pool.m_incremental_relay_feerate, child_hash)}) {
return package_state.Invalid(PackageValidationResult::PCKG_POLICY,
"package RBF failed: insufficient anti-DoS fees", *err_string);
}

return true;
}

bool MemPoolAccept::PolicyScriptChecks(const ATMPArgs& args, Workspace& ws)
Expand Down Expand Up @@ -1200,6 +1263,7 @@ bool MemPoolAccept::Finalize(const ATMPArgs& args, Workspace& ws)
const bool bypass_limits = args.m_bypass_limits;
std::unique_ptr<CTxMemPoolEntry>& entry = ws.m_entry;

if (!m_all_conflicts.empty()) Assume(args.m_allow_replacement);
// Remove conflicting transactions from the mempool
for (CTxMemPool::txiter it : m_all_conflicts)
{
Expand Down Expand Up @@ -1344,7 +1408,13 @@ MempoolAcceptResult MemPoolAccept::AcceptSingleTransaction(const CTransactionRef
return MempoolAcceptResult::Failure(ws.m_state);
}

if (m_rbf && !ReplacementChecks(ws)) return MempoolAcceptResult::Failure(ws.m_state);
if (m_rbf && !ReplacementChecks(ws)) {
if (ws.m_state.GetResult() == TxValidationResult::TX_RECONSIDERABLE) {
// Failed for incentives-based fee reasons. Provide the effective feerate and which tx was included.
return MempoolAcceptResult::FeeFailure(ws.m_state, CFeeRate(ws.m_modified_fees, ws.m_vsize), single_wtxid);
}
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.
Expand Down Expand Up @@ -1417,11 +1487,14 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptMultipleTransactions(const std::
}

// Make the coins created by this transaction available for subsequent transactions in the
// package to spend. Since we already checked conflicts in the package and we don't allow
// 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_rbf);
// package to spend. If there are no conflicts, no transaction can spend a coin
// needed by another transaction in the package. We also need to make sure that no package
// tx replaces (or replaces the ancestor of) the parent of another package tx. As long as we
// check these two things, we don't need to track the coins spent.
// If there are conflicts, PackageMempoolChecks() ensures later that any package RBF attempt
// has *no* in-mempool ancestors, so we don't have to worry about subsequent transactions in
// same package spending the same in-mempool outpoints. This needs to be revisited for general
// package RBF.
m_viewmempool.PackageAddTransaction(ws.m_ptx);
}

Expand Down Expand Up @@ -1460,8 +1533,8 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptMultipleTransactions(const std::
MempoolAcceptResult::FeeFailure(placeholder_state, CFeeRate(m_total_modified_fees, m_total_vsize), all_package_wtxids)}});
}

// Apply package mempool ancestor/descendant limits.
if (!PackageMempoolChecks(txns, m_total_vsize, package_state)) {
// Apply package mempool ancestor/descendant limits and RBF checks.
if (!PackageMempoolChecks(txns, workspaces, m_total_vsize, package_state)) {
return PackageMempoolAcceptResult(package_state, std::move(results));
}

Expand Down

0 comments on commit b86487c

Please sign in to comment.