Skip to content

Commit 3724e9b

Browse files
committed
Merge bitcoin/bitcoin#32973: validation: docs and cleanups for MemPoolAccept coins views
b6d4688 [doc] reword comments in test_mid_package_replacement (glozow) f3a613a [cleanup] delete brittle test_mid_package_eviction (glozow) c3cd7fc [doc] remove references to now-nonexistent Finalize() function (glozow) d8140f5 don't make a copy of m_non_base_coins (glozow) 98ba2b1 [doc] MemPoolAccept coins views (glozow) ba02c30 [doc] always CleanupTemporaryCoins after a mempool trim (glozow) Pull request description: Deletes `test_mid_package_eviction` that is brittle and already covered in other places. It was introduced in #28251 addressing 2 issues: (1) calling `LimitMempoolSize()` in the middle of package validation and (2) not updating coins view cache when the mempool contents change, leading to "disappearing coins." (1) If you let `AcceptSingleTransaction` call `LimitMempoolSize` in the middle of package validation, you should get a failure in `test_mid_package_eviction_success` (the package is rejected): ``` diff --git a/src/validation.cpp b/src/validation.cpp index f2f6098..4bd6f059849 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1485,7 +1485,7 @@ MempoolAcceptResult MemPoolAccept::AcceptSingleTransaction(const CTransactionRef FinalizeSubpackage(args); // Limit the mempool, if appropriate. - if (!args.m_package_submission && !args.m_bypass_limits) { + if (!args.m_bypass_limits) { LimitMempoolSize(m_pool, m_active_chainstate.CoinsTip()); // If mempool contents change, then the m_view cache is dirty. Given this isn't a package // submission, we won't be using the cache anymore, but clear it anyway for clarity. ``` Mempool modifications have a pretty narrow interface since #31122 and `TrimToSize()` cannot be called while there is an outstanding mempool changeset. So I think there is a low likelihood of accidentally reintroducing this problem and not immediately hitting e.g. a fuzzer crash on this line https://github.com/bitcoin/bitcoin/blob/b53fab1467fde73c40402e2022b25edfff1e4668/src/txmempool.cpp#L1143 (2) If you remove the `CleanupTemporaryCoins()` call from `ClearSubPackageState()` you should get a failure from `test_mid_package_replacement`: ``` diff --git a/src/validation.cpp b/src/validation.cpp index f2f6098..01b904b69ef 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -779,7 +779,7 @@ private: m_subpackage = SubPackageState{}; // And clean coins while at it - CleanupTemporaryCoins(); + // CleanupTemporaryCoins(); } }; ``` I also added/cleaned up the documentation about coins views to hopefully make it extremely clear when people should `CleanupTemporaryCoins`. ACKs for top commit: instagibbs: reACK bitcoin/bitcoin@b6d4688 sdaftuar: utACK b6d4688 marcofleon: ACK b6d4688 Tree-SHA512: 79c68e263013b1153520f5453e6b579b8fe7e1d6a9952b1ac2c3c3c017034e6d21d7000a140bba4cc9d2ce50ea3a84cc6f91fd5febc52d7b3fa4f797955d987d
2 parents 3219847 + b6d4688 commit 3724e9b

File tree

3 files changed

+33
-105
lines changed

3 files changed

+33
-105
lines changed

src/txmempool.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -940,7 +940,7 @@ class CCoinsViewMemPool : public CCoinsViewBacked
940940
* m_temp_added and cannot be flushed to the back end. Only used for package validation. */
941941
void PackageAddTransaction(const CTransactionRef& tx);
942942
/** Get all coins in m_non_base_coins. */
943-
std::unordered_set<COutPoint, SaltedOutpointHasher> GetNonBaseCoins() const { return m_non_base_coins; }
943+
const std::unordered_set<COutPoint, SaltedOutpointHasher>& GetNonBaseCoins() const { return m_non_base_coins; }
944944
/** Clear m_temp_added and m_non_base_coins. */
945945
void Reset();
946946
};

src/validation.cpp

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -469,10 +469,8 @@ class MemPoolAccept
469469
const bool m_allow_replacement;
470470
/** When true, allow sibling eviction. This only occurs in single transaction package settings. */
471471
const bool m_allow_sibling_eviction;
472-
/** When true, the mempool will not be trimmed when any transactions are submitted in
473-
* Finalize(). Instead, limits should be enforced at the end to ensure the package is not
474-
* partially submitted.
475-
*/
472+
/** Used to skip the LimitMempoolSize() call within AcceptSingleTransaction(). This should be used when multiple
473+
* AcceptSubPackage calls are expected and the mempool will be trimmed at the end of AcceptPackage(). */
476474
const bool m_package_submission;
477475
/** When true, use package feerates instead of individual transaction feerates for fee-based
478476
* policies such as mempool min fee and min relay fee.
@@ -548,7 +546,7 @@ class MemPoolAccept
548546
/* m_test_accept */ package_args.m_test_accept,
549547
/* m_allow_replacement */ true,
550548
/* m_allow_sibling_eviction */ true,
551-
/* m_package_submission */ true, // do not LimitMempoolSize in Finalize()
549+
/* m_package_submission */ true, // trim at the end of AcceptPackage()
552550
/* m_package_feerates */ false, // only 1 transaction
553551
/* m_client_maxfeerate */ package_args.m_client_maxfeerate,
554552
/* m_allow_carveouts */ false,
@@ -725,8 +723,27 @@ class MemPoolAccept
725723

726724
private:
727725
CTxMemPool& m_pool;
726+
727+
/** Holds a cached view of available coins from the UTXO set, mempool, and artificial temporary coins (to enable package validation).
728+
* The view doesn't track whether a coin previously existed but has now been spent. We detect conflicts in other ways:
729+
* - conflicts within a transaction are checked in CheckTransaction (bad-txns-inputs-duplicate)
730+
* - conflicts within a package are checked in IsWellFormedPackage (conflict-in-package)
731+
* - conflicts with an existing mempool transaction are found in CTxMemPool::GetConflictTx and replacements are allowed
732+
* The temporary coins should persist between individual transaction checks so that package validation is possible,
733+
* but must be cleaned up when we finish validating a subpackage, whether accepted or rejected. The cache must also
734+
* be cleared when mempool contents change (when a changeset is applied or when the mempool trims itself) because it
735+
* can return cached coins that no longer exist in the backend. Use CleanupTemporaryCoins() anytime you are finished
736+
* with a SubPackageState or call LimitMempoolSize().
737+
*/
728738
CCoinsViewCache m_view;
739+
740+
// These are the two possible backends for m_view.
741+
/** When m_view is connected to m_viewmempool as its backend, it can pull coins from the mempool and from the UTXO
742+
* set. This is also where temporary coins are stored. */
729743
CCoinsViewMemPool m_viewmempool;
744+
/** When m_view is connected to m_dummy, it can no longer look up coins from the mempool or UTXO set (meaning no disk
745+
* operations happen), but can still return coins it accessed previously. Useful for keeping track of which coins
746+
* were pulled from disk. */
730747
CCoinsView m_dummy;
731748

732749
Chainstate& m_active_chainstate;
@@ -1470,6 +1487,10 @@ MempoolAcceptResult MemPoolAccept::AcceptSingleTransaction(const CTransactionRef
14701487
// Limit the mempool, if appropriate.
14711488
if (!args.m_package_submission && !args.m_bypass_limits) {
14721489
LimitMempoolSize(m_pool, m_active_chainstate.CoinsTip());
1490+
// If mempool contents change, then the m_view cache is dirty. Given this isn't a package
1491+
// submission, we won't be using the cache anymore, but clear it anyway for clarity.
1492+
CleanupTemporaryCoins();
1493+
14731494
if (!m_pool.exists(ws.m_hash)) {
14741495
// The tx no longer meets our (new) mempool minimum feerate but could be reconsidered in a package.
14751496
ws.m_state.Invalid(TxValidationResult::TX_RECONSIDERABLE, "mempool full");
@@ -1647,7 +1668,8 @@ void MemPoolAccept::CleanupTemporaryCoins()
16471668
// (3) Confirmed coins don't need to be removed. The chainstate has not changed (we are
16481669
// holding cs_main and no blocks have been processed) so the confirmed tx cannot disappear like
16491670
// a mempool tx can. The coin may now be spent after we submitted a tx to mempool, but
1650-
// we have already checked that the package does not have 2 transactions spending the same coin.
1671+
// we have already checked that the package does not have 2 transactions spending the same coin
1672+
// and we check whether a mempool transaction spends conflicting coins (CTxMemPool::GetConflictTx).
16511673
// Keeping them in m_view is an optimization to not re-fetch confirmed coins if we later look up
16521674
// inputs for this transaction again.
16531675
for (const auto& outpoint : m_viewmempool.GetNonBaseCoins()) {
@@ -1831,6 +1853,7 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptPackage(const Package& package,
18311853

18321854
// Make sure we haven't exceeded max mempool size.
18331855
// Package transactions that were submitted to mempool or already in mempool may be evicted.
1856+
// If mempool contents change, then the m_view cache is dirty. It has already been cleared above.
18341857
LimitMempoolSize(m_pool, m_active_chainstate.CoinsTip());
18351858

18361859
for (const auto& tx : package) {

test/functional/mempool_limit.py

Lines changed: 3 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -177,98 +177,6 @@ def test_mid_package_eviction_success(self):
177177
evicted_feerate_btc_per_kvb = entry["fees"]["modified"] / (Decimal(entry["vsize"]) / 1000)
178178
assert_greater_than(evicted_feerate_btc_per_kvb, max_parent_feerate)
179179

180-
def test_mid_package_eviction(self):
181-
node = self.nodes[0]
182-
self.log.info("Check a package where each parent passes the current mempoolminfee but would cause eviction before package submission terminates")
183-
184-
self.restart_node(0, extra_args=self.extra_args[0])
185-
186-
# Restarting the node resets mempool minimum feerate
187-
assert_equal(node.getmempoolinfo()['minrelaytxfee'], Decimal('0.00001000'))
188-
assert_equal(node.getmempoolinfo()['mempoolminfee'], Decimal('0.00001000'))
189-
190-
fill_mempool(self, node)
191-
current_info = node.getmempoolinfo()
192-
mempoolmin_feerate = current_info["mempoolminfee"]
193-
194-
package_hex = []
195-
# UTXOs to be spent by the ultimate child transaction
196-
parent_utxos = []
197-
198-
evicted_vsize = 2000
199-
# Mempool transaction which is evicted due to being at the "bottom" of the mempool when the
200-
# mempool overflows and evicts by descendant score. It's important that the eviction doesn't
201-
# happen in the middle of package evaluation, as it can invalidate the coins cache.
202-
mempool_evicted_tx = self.wallet.send_self_transfer(
203-
from_node=node,
204-
fee_rate=mempoolmin_feerate,
205-
target_vsize=evicted_vsize,
206-
confirmed_only=True
207-
)
208-
# Already in mempool when package is submitted.
209-
assert mempool_evicted_tx["txid"] in node.getrawmempool()
210-
211-
# This parent spends the above mempool transaction that exists when its inputs are first
212-
# looked up, but disappears later. It is rejected for being too low fee (but eligible for
213-
# reconsideration), and its inputs are cached. When the mempool transaction is evicted, its
214-
# coin is no longer available, but the cache could still contains the tx.
215-
cpfp_parent = self.wallet.create_self_transfer(
216-
utxo_to_spend=mempool_evicted_tx["new_utxo"],
217-
fee_rate=mempoolmin_feerate - Decimal('0.00001'),
218-
confirmed_only=True)
219-
package_hex.append(cpfp_parent["hex"])
220-
parent_utxos.append(cpfp_parent["new_utxo"])
221-
assert_equal(node.testmempoolaccept([cpfp_parent["hex"]])[0]["reject-reason"], "mempool min fee not met")
222-
223-
self.wallet.rescan_utxos()
224-
225-
# Series of parents that don't need CPFP and are submitted individually. Each one is large and
226-
# high feerate, which means they should trigger eviction but not be evicted.
227-
parent_vsize = 25000
228-
num_big_parents = 3
229-
# Need to be large enough to trigger eviction
230-
# (note that the mempool usage of a tx is about three times its vsize)
231-
assert_greater_than(parent_vsize * num_big_parents * 3, current_info["maxmempool"] - current_info["bytes"])
232-
parent_feerate = 100 * mempoolmin_feerate
233-
234-
big_parent_txids = []
235-
for i in range(num_big_parents):
236-
parent = self.wallet.create_self_transfer(fee_rate=parent_feerate, target_vsize=parent_vsize, confirmed_only=True)
237-
parent_utxos.append(parent["new_utxo"])
238-
package_hex.append(parent["hex"])
239-
big_parent_txids.append(parent["txid"])
240-
# There is room for each of these transactions independently
241-
assert node.testmempoolaccept([parent["hex"]])[0]["allowed"]
242-
243-
# Create a child spending everything, bumping cpfp_parent just above mempool minimum
244-
# feerate. It's important not to bump too much as otherwise mempool_evicted_tx would not be
245-
# evicted, making this test much less meaningful.
246-
approx_child_vsize = self.wallet.create_self_transfer_multi(utxos_to_spend=parent_utxos)["tx"].get_vsize()
247-
cpfp_fee = (mempoolmin_feerate / 1000) * (cpfp_parent["tx"].get_vsize() + approx_child_vsize) - cpfp_parent["fee"]
248-
# Specific number of satoshis to fit within a small window. The parent_cpfp + child package needs to be
249-
# - When there is mid-package eviction, high enough feerate to meet the new mempoolminfee
250-
# - When there is no mid-package eviction, low enough feerate to be evicted immediately after submission.
251-
magic_satoshis = 1200
252-
cpfp_satoshis = int(cpfp_fee * COIN) + magic_satoshis
253-
254-
child = self.wallet.create_self_transfer_multi(utxos_to_spend=parent_utxos, fee_per_output=cpfp_satoshis)
255-
package_hex.append(child["hex"])
256-
257-
# Package should be submitted, temporarily exceeding maxmempool, and then evicted.
258-
with node.assert_debug_log(expected_msgs=["rolling minimum fee bumped"]):
259-
assert_equal(node.submitpackage(package_hex)["package_msg"], "transaction failed")
260-
261-
# Maximum size must never be exceeded.
262-
assert_greater_than(node.getmempoolinfo()["maxmempool"], node.getmempoolinfo()["bytes"])
263-
264-
# Evicted transaction and its descendants must not be in mempool.
265-
resulting_mempool_txids = node.getrawmempool()
266-
assert mempool_evicted_tx["txid"] not in resulting_mempool_txids
267-
assert cpfp_parent["txid"] not in resulting_mempool_txids
268-
assert child["txid"] not in resulting_mempool_txids
269-
for txid in big_parent_txids:
270-
assert txid in resulting_mempool_txids
271-
272180
def test_mid_package_replacement(self):
273181
node = self.nodes[0]
274182
self.log.info("Check a package where an early tx depends on a later-replaced mempool tx")
@@ -283,9 +191,7 @@ def test_mid_package_replacement(self):
283191
current_info = node.getmempoolinfo()
284192
mempoolmin_feerate = current_info["mempoolminfee"]
285193

286-
# Mempool transaction which is evicted due to being at the "bottom" of the mempool when the
287-
# mempool overflows and evicts by descendant score. It's important that the eviction doesn't
288-
# happen in the middle of package evaluation, as it can invalidate the coins cache.
194+
# Mempool transaction is replaced by a package transaction.
289195
double_spent_utxo = self.wallet.get_utxo(confirmed_only=True)
290196
replaced_tx = self.wallet.send_self_transfer(
291197
from_node=node,
@@ -297,8 +203,8 @@ def test_mid_package_replacement(self):
297203
assert replaced_tx["txid"] in node.getrawmempool()
298204

299205
# This parent spends the above mempool transaction that exists when its inputs are first
300-
# looked up, but disappears later. It is rejected for being too low fee (but eligible for
301-
# reconsideration), and its inputs are cached. When the mempool transaction is evicted, its
206+
# looked up, but will disappear if the replacement occurs. It is rejected for being too low fee (but eligible for
207+
# reconsideration), and its inputs are cached. When the mempool transaction is replaced, its
302208
# coin is no longer available, but the cache could still contain the tx.
303209
cpfp_parent = self.wallet.create_self_transfer(
304210
utxo_to_spend=replaced_tx["new_utxo"],
@@ -433,7 +339,6 @@ def run_test(self):
433339

434340
self.test_mid_package_eviction_success()
435341
self.test_mid_package_replacement()
436-
self.test_mid_package_eviction()
437342
self.test_rbf_carveout_disallowed()
438343

439344

0 commit comments

Comments
 (0)