Skip to content

Commit 3966b0a

Browse files
committed
Merge bitcoin#28472: Remove MemPoolAccept::m_limits to avoid mutating it in package evaluation
ee589d4 Add regression test for m_limit mutation (Greg Sanders) 275579d Remove MemPoolAccept::m_limits, only have local copies for carveouts (Greg Sanders) Pull request description: Without remoing it, if we ever call `PreChecks()` multiple times for any reason during any one `MempoolAccept`, subsequent invocations may have incorrect limits, allowing longer/larger chains than should be allowed. Currently this is only an issue with `submitpackage`, so this is not exposed on mainnet. ACKs for top commit: achow101: ACK ee589d4 glozow: ACK ee589d4, nits can be ignored ariard: Code Review ACK ee589d4 Tree-SHA512: 14cf8edc73e014220def82563f5fb4192d1c2c111829712abf16340bfbfd9a85e2148d723af6fd4995d503dd67232b48dcf8b1711668d25b5aee5eab1bdb578c
2 parents e9a4793 + ee589d4 commit 3966b0a

File tree

4 files changed

+68
-18
lines changed

4 files changed

+68
-18
lines changed

src/txmempool.cpp

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,6 @@ util::Result<CTxMemPool::setEntries> CTxMemPool::CalculateAncestorsAndCheckLimit
197197
}
198198

199199
bool CTxMemPool::CheckPackageLimits(const Package& package,
200-
const Limits& limits,
201200
std::string &errString) const
202201
{
203202
CTxMemPoolEntry::Parents staged_ancestors;
@@ -208,8 +207,8 @@ bool CTxMemPool::CheckPackageLimits(const Package& package,
208207
std::optional<txiter> piter = GetIter(input.prevout.hash);
209208
if (piter) {
210209
staged_ancestors.insert(**piter);
211-
if (staged_ancestors.size() + package.size() > static_cast<uint64_t>(limits.ancestor_count)) {
212-
errString = strprintf("too many unconfirmed parents [limit: %u]", limits.ancestor_count);
210+
if (staged_ancestors.size() + package.size() > static_cast<uint64_t>(m_limits.ancestor_count)) {
211+
errString = strprintf("too many unconfirmed parents [limit: %u]", m_limits.ancestor_count);
213212
return false;
214213
}
215214
}
@@ -219,7 +218,7 @@ bool CTxMemPool::CheckPackageLimits(const Package& package,
219218
// considered together must be within limits even if they are not interdependent. This may be
220219
// stricter than the limits for each individual transaction.
221220
const auto ancestors{CalculateAncestorsAndCheckLimits(total_size, package.size(),
222-
staged_ancestors, limits)};
221+
staged_ancestors, m_limits)};
223222
// It's possible to overestimate the ancestor/descendant totals.
224223
if (!ancestors.has_value()) errString = "possibly " + util::ErrorString(ancestors).original;
225224
return ancestors.has_value();

src/txmempool.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -606,11 +606,9 @@ class CTxMemPool
606606
* @param[in] package Transaction package being evaluated for acceptance
607607
* to mempool. The transactions need not be direct
608608
* ancestors/descendants of each other.
609-
* @param[in] limits Maximum number and size of ancestors and descendants
610609
* @param[out] errString Populated with error reason if a limit is hit.
611610
*/
612611
bool CheckPackageLimits(const Package& package,
613-
const Limits& limits,
614612
std::string &errString) const EXCLUSIVE_LOCKS_REQUIRED(cs);
615613

616614
/** Populate setDescendants with all in-mempool descendants of hash.

src/validation.cpp

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -433,8 +433,7 @@ class MemPoolAccept
433433
m_pool(mempool),
434434
m_view(&m_dummy),
435435
m_viewmempool(&active_chainstate.CoinsTip(), m_pool),
436-
m_active_chainstate(active_chainstate),
437-
m_limits{m_pool.m_limits}
436+
m_active_chainstate(active_chainstate)
438437
{
439438
}
440439

@@ -684,8 +683,6 @@ class MemPoolAccept
684683

685684
Chainstate& m_active_chainstate;
686685

687-
CTxMemPool::Limits m_limits;
688-
689686
/** Whether the transaction(s) would replace any mempool transactions. If so, RBF rules apply. */
690687
bool m_rbf{false};
691688
};
@@ -874,6 +871,11 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
874871
if (!bypass_limits && !args.m_package_feerates && !CheckFeeRate(ws.m_vsize, ws.m_modified_fees, state)) return false;
875872

876873
ws.m_iters_conflicting = m_pool.GetIterSet(ws.m_conflicts);
874+
875+
// Note that these modifications are only applicable to single transaction scenarios;
876+
// carve-outs and package RBF are disabled for multi-transaction evaluations.
877+
CTxMemPool::Limits maybe_rbf_limits = m_pool.m_limits;
878+
877879
// Calculate in-mempool ancestors, up to a limit.
878880
if (ws.m_conflicts.size() == 1) {
879881
// In general, when we receive an RBF transaction with mempool conflicts, we want to know whether we
@@ -906,11 +908,11 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
906908
assert(ws.m_iters_conflicting.size() == 1);
907909
CTxMemPool::txiter conflict = *ws.m_iters_conflicting.begin();
908910

909-
m_limits.descendant_count += 1;
910-
m_limits.descendant_size_vbytes += conflict->GetSizeWithDescendants();
911+
maybe_rbf_limits.descendant_count += 1;
912+
maybe_rbf_limits.descendant_size_vbytes += conflict->GetSizeWithDescendants();
911913
}
912914

913-
auto ancestors{m_pool.CalculateMemPoolAncestors(*entry, m_limits)};
915+
auto ancestors{m_pool.CalculateMemPoolAncestors(*entry, maybe_rbf_limits)};
914916
if (!ancestors) {
915917
// If CalculateMemPoolAncestors fails second time, we want the original error string.
916918
// Contracting/payment channels CPFP carve-out:
@@ -926,9 +928,9 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
926928
// this, see https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2018-November/016518.html
927929
CTxMemPool::Limits cpfp_carve_out_limits{
928930
.ancestor_count = 2,
929-
.ancestor_size_vbytes = m_limits.ancestor_size_vbytes,
930-
.descendant_count = m_limits.descendant_count + 1,
931-
.descendant_size_vbytes = m_limits.descendant_size_vbytes + EXTRA_DESCENDANT_TX_SIZE_LIMIT,
931+
.ancestor_size_vbytes = maybe_rbf_limits.ancestor_size_vbytes,
932+
.descendant_count = maybe_rbf_limits.descendant_count + 1,
933+
.descendant_size_vbytes = maybe_rbf_limits.descendant_size_vbytes + EXTRA_DESCENDANT_TX_SIZE_LIMIT,
932934
};
933935
const auto error_message{util::ErrorString(ancestors).original};
934936
if (ws.m_vsize > EXTRA_DESCENDANT_TX_SIZE_LIMIT) {
@@ -1011,7 +1013,7 @@ bool MemPoolAccept::PackageMempoolChecks(const std::vector<CTransactionRef>& txn
10111013
{ return !m_pool.exists(GenTxid::Txid(tx->GetHash()));}));
10121014

10131015
std::string err_string;
1014-
if (!m_pool.CheckPackageLimits(txns, m_limits, err_string)) {
1016+
if (!m_pool.CheckPackageLimits(txns, err_string)) {
10151017
// This is a package-wide error, separate from an individual transaction error.
10161018
return package_state.Invalid(PackageValidationResult::PCKG_POLICY, "package-mempool-limits", err_string);
10171019
}
@@ -1166,7 +1168,7 @@ bool MemPoolAccept::SubmitPackage(const ATMPArgs& args, std::vector<Workspace>&
11661168
// Re-calculate mempool ancestors to call addUnchecked(). They may have changed since the
11671169
// last calculation done in PreChecks, since package ancestors have already been submitted.
11681170
{
1169-
auto ancestors{m_pool.CalculateMemPoolAncestors(*ws.m_entry, m_limits)};
1171+
auto ancestors{m_pool.CalculateMemPoolAncestors(*ws.m_entry, m_pool.m_limits)};
11701172
if(!ancestors) {
11711173
results.emplace(ws.m_ptx->GetWitnessHash(), MempoolAcceptResult::Failure(ws.m_state));
11721174
// Since PreChecks() and PackageMempoolChecks() both enforce limits, this should never fail.

test/functional/mempool_limit.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,56 @@ def fill_mempool(self):
7878
assert_equal(node.getmempoolinfo()['minrelaytxfee'], Decimal('0.00001000'))
7979
assert_greater_than(node.getmempoolinfo()['mempoolminfee'], Decimal('0.00001000'))
8080

81+
def test_rbf_carveout_disallowed(self):
82+
node = self.nodes[0]
83+
84+
self.log.info("Check that individually-evaluated transactions in a package don't increase package limits for other subpackage parts")
85+
86+
# We set chain limits to 2 ancestors, 1 descendant, then try to get a parents-and-child chain of 2 in mempool
87+
#
88+
# A: Solo transaction to be RBF'd (to bump descendant limit for package later)
89+
# B: First transaction in package, RBFs A by itself under individual evaluation, which would give it +1 descendant limit
90+
# C: Second transaction in package, spends B. If the +1 descendant limit persisted, would make it into mempool
91+
92+
self.restart_node(0, extra_args=self.extra_args[0] + ["-limitancestorcount=2", "-limitdescendantcount=1"])
93+
94+
# Generate a confirmed utxo we will double-spend
95+
rbf_utxo = self.wallet.send_self_transfer(
96+
from_node=node,
97+
confirmed_only=True
98+
)["new_utxo"]
99+
self.generate(node, 1)
100+
101+
# tx_A needs to be RBF'd, set minfee at set size
102+
A_weight = 1000
103+
mempoolmin_feerate = node.getmempoolinfo()["mempoolminfee"]
104+
tx_A = self.wallet.send_self_transfer(
105+
from_node=node,
106+
fee=(mempoolmin_feerate / 1000) * (A_weight // 4) + Decimal('0.000001'),
107+
target_weight=A_weight,
108+
utxo_to_spend=rbf_utxo,
109+
confirmed_only=True
110+
)
111+
112+
# RBF's tx_A, is not yet submitted
113+
tx_B = self.wallet.create_self_transfer(
114+
fee=tx_A["fee"] * 4,
115+
target_weight=A_weight,
116+
utxo_to_spend=rbf_utxo,
117+
confirmed_only=True
118+
)
119+
120+
# Spends tx_B's output, too big for cpfp carveout (because that would also increase the descendant limit by 1)
121+
non_cpfp_carveout_weight = 40001 # EXTRA_DESCENDANT_TX_SIZE_LIMIT + 1
122+
tx_C = self.wallet.create_self_transfer(
123+
target_weight=non_cpfp_carveout_weight,
124+
fee = (mempoolmin_feerate / 1000) * (non_cpfp_carveout_weight // 4) + Decimal('0.000001'),
125+
utxo_to_spend=tx_B["new_utxo"],
126+
confirmed_only=True
127+
)
128+
129+
assert_raises_rpc_error(-26, "too-long-mempool-chain", node.submitpackage, [tx_B["hex"], tx_C["hex"]])
130+
81131
def test_mid_package_eviction(self):
82132
node = self.nodes[0]
83133
self.log.info("Check a package where each parent passes the current mempoolminfee but would cause eviction before package submission terminates")
@@ -324,6 +374,7 @@ def run_test(self):
324374

325375
self.test_mid_package_replacement()
326376
self.test_mid_package_eviction()
377+
self.test_rbf_carveout_disallowed()
327378

328379

329380
if __name__ == '__main__':

0 commit comments

Comments
 (0)