cpfp carveout is excluded in packages

The behavior is not new, but this rule exits earlier than before.
Previously, a carve out could have been granted in PreChecks() but then
nullified in PackageMempoolChecks() when CheckPackageLimits() is called
with the default limits.
This commit is contained in:
glozow 2022-11-03 12:36:12 +00:00 committed by Greg Sanders
parent 69f7ab05ba
commit cbbfe719b2
3 changed files with 41 additions and 14 deletions

View file

@ -48,8 +48,13 @@ The following rules are enforced for all packages:
heavily connected, i.e. some transaction in the package is the ancestor or descendant of all heavily connected, i.e. some transaction in the package is the ancestor or descendant of all
the other transactions. the other transactions.
The following rules are only enforced for packages to be submitted to the mempool (not enforced for * [CPFP Carve Out](./mempool-limits.md#CPFP-Carve-Out) is disabled in packaged contexts. (#21800)
test accepts):
- *Rationale*: This carve out cannot be accurately applied when there are multiple transactions'
ancestors and descendants being considered at the same time.
The following rules are only enforced for packages to be submitted to the mempool (not
enforced for test accepts):
* Packages must be child-with-unconfirmed-parents packages. This also means packages must contain at * Packages must be child-with-unconfirmed-parents packages. This also means packages must contain at
least 2 transactions. (#22674) least 2 transactions. (#22674)

View file

@ -478,6 +478,9 @@ public:
*/ */
const std::optional<CFeeRate> m_client_maxfeerate; 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. */ /** Parameters for single transaction mempool validation. */
static ATMPArgs SingleAccept(const CChainParams& chainparams, int64_t accept_time, static ATMPArgs SingleAccept(const CChainParams& chainparams, int64_t accept_time,
bool bypass_limits, std::vector<COutPoint>& coins_to_uncache, bool bypass_limits, std::vector<COutPoint>& coins_to_uncache,
@ -492,6 +495,7 @@ public:
/* m_package_submission */ false, /* m_package_submission */ false,
/* m_package_feerates */ false, /* m_package_feerates */ false,
/* m_client_maxfeerate */ {}, // checked by caller /* m_client_maxfeerate */ {}, // checked by caller
/* m_allow_carveouts */ true,
}; };
} }
@ -508,6 +512,7 @@ public:
/* m_package_submission */ false, // not submitting to mempool /* m_package_submission */ false, // not submitting to mempool
/* m_package_feerates */ false, /* m_package_feerates */ false,
/* m_client_maxfeerate */ {}, // checked by caller /* m_client_maxfeerate */ {}, // checked by caller
/* m_allow_carveouts */ false,
}; };
} }
@ -524,6 +529,7 @@ public:
/* m_package_submission */ true, /* m_package_submission */ true,
/* m_package_feerates */ true, /* m_package_feerates */ true,
/* m_client_maxfeerate */ client_maxfeerate, /* m_client_maxfeerate */ client_maxfeerate,
/* m_allow_carveouts */ false,
}; };
} }
@ -539,6 +545,7 @@ public:
/* m_package_submission */ true, // do not LimitMempoolSize in Finalize() /* m_package_submission */ true, // do not LimitMempoolSize in Finalize()
/* m_package_feerates */ false, // only 1 transaction /* m_package_feerates */ false, // only 1 transaction
/* m_client_maxfeerate */ package_args.m_client_maxfeerate, /* m_client_maxfeerate */ package_args.m_client_maxfeerate,
/* m_allow_carveouts */ false,
}; };
} }
@ -554,7 +561,8 @@ public:
bool allow_sibling_eviction, bool allow_sibling_eviction,
bool package_submission, bool package_submission,
bool package_feerates, bool package_feerates,
std::optional<CFeeRate> client_maxfeerate) std::optional<CFeeRate> client_maxfeerate,
bool allow_carveouts)
: m_chainparams{chainparams}, : m_chainparams{chainparams},
m_accept_time{accept_time}, m_accept_time{accept_time},
m_bypass_limits{bypass_limits}, m_bypass_limits{bypass_limits},
@ -564,7 +572,8 @@ public:
m_allow_sibling_eviction{allow_sibling_eviction}, m_allow_sibling_eviction{allow_sibling_eviction},
m_package_submission{package_submission}, m_package_submission{package_submission},
m_package_feerates{package_feerates}, m_package_feerates{package_feerates},
m_client_maxfeerate{client_maxfeerate} m_client_maxfeerate{client_maxfeerate},
m_allow_carveouts{allow_carveouts}
{ {
} }
}; };
@ -917,7 +926,7 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
CTxMemPool::Limits maybe_rbf_limits = m_pool.m_opts.limits; CTxMemPool::Limits maybe_rbf_limits = m_pool.m_opts.limits;
// Calculate in-mempool ancestors, up to a limit. // 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 // 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 // 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 // way to do this short of calculating the ancestor and descendant sets with an overlay cache of
@ -956,6 +965,13 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
ws.m_ancestors = std::move(*ancestors); ws.m_ancestors = std::move(*ancestors);
} else { } else {
// If CalculateMemPoolAncestors fails second time, we want the original error string. // 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: // Contracting/payment channels CPFP carve-out:
// If the new transaction is relatively small (up to 40k weight) // If the new transaction is relatively small (up to 40k weight)
// and has at most one ancestor (ie ancestor limit of 2, including // and has at most one ancestor (ie ancestor limit of 2, including
@ -974,7 +990,6 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
.descendant_count = maybe_rbf_limits.descendant_count + 1, .descendant_count = maybe_rbf_limits.descendant_count + 1,
.descendant_size_vbytes = maybe_rbf_limits.descendant_size_vbytes + EXTRA_DESCENDANT_TX_SIZE_LIMIT, .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) { 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); return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "too-long-mempool-chain", error_message);
} }
@ -1431,9 +1446,7 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptMultipleTransactions(const std::
} }
// Apply package mempool ancestor/descendant limits. Skip if there is only one transaction, // 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 // because it's unnecessary.
// 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)) { if (txns.size() > 1 && !PackageMempoolChecks(txns, m_total_vsize, package_state)) {
return PackageMempoolAcceptResult(package_state, std::move(results)); return PackageMempoolAcceptResult(package_state, std::move(results));
} }

View file

@ -47,21 +47,30 @@ class MempoolPackagesTest(BitcoinTestFramework):
# Adding one more transaction on to the chain should fail. # Adding one more transaction on to the chain should fail.
assert_raises_rpc_error(-26, "too-long-mempool-chain, too many unconfirmed ancestors [limit: 25]", self.chain_tx, [utxo]) assert_raises_rpc_error(-26, "too-long-mempool-chain, too many unconfirmed ancestors [limit: 25]", self.chain_tx, [utxo])
# ...even if it chains on from some point in the middle of the chain. # ... or if it chains on from some point in the middle of the chain.
assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", self.chain_tx, [chain[2]]) assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", self.chain_tx, [chain[2]])
assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", self.chain_tx, [chain[1]]) assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", self.chain_tx, [chain[1]])
# ...even if it chains on to two parent transactions with one in the chain. # ...even if it chains on to two parent transactions with one in the chain.
assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", self.chain_tx, [chain[0], second_chain]) assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", self.chain_tx, [chain[0], second_chain])
# ...especially if its > 40k weight # ...especially if its > 40k weight
assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", self.chain_tx, [chain[0]], num_outputs=350) assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", self.chain_tx, [chain[0]], num_outputs=350)
# ...even if it's submitted with other transactions
replaceable_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=[chain[0]])
txns = [replaceable_tx["tx"], self.wallet.create_self_transfer_multi(utxos_to_spend=replaceable_tx["new_utxos"])["tx"]]
txns_hex = [tx.serialize().hex() for tx in txns]
assert_equal(self.nodes[0].testmempoolaccept(txns_hex)[0]["reject-reason"], "too-long-mempool-chain")
pkg_result = self.nodes[0].submitpackage(txns_hex)
assert "too-long-mempool-chain" in pkg_result["tx-results"][txns[0].getwtxid()]["error"]
assert_equal(pkg_result["tx-results"][txns[1].getwtxid()]["error"], "bad-txns-inputs-missingorspent")
# But not if it chains directly off the first transaction # But not if it chains directly off the first transaction
replacable_tx = self.wallet.send_self_transfer_multi(from_node=self.nodes[0], utxos_to_spend=[chain[0]])['tx'] self.nodes[0].sendrawtransaction(replaceable_tx["hex"])
# and the second chain should work just fine # and the second chain should work just fine
self.chain_tx([second_chain]) self.chain_tx([second_chain])
# Make sure we can RBF the chain which used our carve-out rule # Ensure an individual transaction with single direct conflict can RBF the chain which used our carve-out rule
replacable_tx.vout[0].nValue -= 1000000 replacement_tx = replaceable_tx["tx"]
self.nodes[0].sendrawtransaction(replacable_tx.serialize().hex()) replacement_tx.vout[0].nValue -= 1000000
self.nodes[0].sendrawtransaction(replacement_tx.serialize().hex())
# Finally, check that we added two transactions # Finally, check that we added two transactions
assert_equal(len(self.nodes[0].getrawmempool()), DEFAULT_ANCESTOR_LIMIT + 3) assert_equal(len(self.nodes[0].getrawmempool()), DEFAULT_ANCESTOR_LIMIT + 3)