From a3fb3dd55c2326452a5085add220bd3682052352 Mon Sep 17 00:00:00 2001 From: Greg Sanders Date: Wed, 18 Feb 2026 09:49:53 -0500 Subject: [PATCH 1/2] mempool: log if we detect a non-optimal mempool We expect this to be rare in practice, and to not be the usual state of the mempool. If we we detect non-optimal ordering after a DoWork() invocation, allow this to be observed in MEMPOOL logs. --- src/txmempool.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/txmempool.cpp b/src/txmempool.cpp index b8be3e080e7..27b4c886093 100644 --- a/src/txmempool.cpp +++ b/src/txmempool.cpp @@ -221,7 +221,9 @@ void CTxMemPool::Apply(ChangeSet* changeset) addNewTransaction(it); } - m_txgraph->DoWork(POST_CHANGE_WORK); + if (!m_txgraph->DoWork(POST_CHANGE_WORK)) { + LogDebug(BCLog::MEMPOOL, "Mempool in non-optimal ordering after addition(s)."); + } } void CTxMemPool::addNewTransaction(CTxMemPool::txiter newit) @@ -378,7 +380,9 @@ void CTxMemPool::removeForReorg(CChain& chain, std::function check for (indexed_transaction_set::const_iterator it = mapTx.begin(); it != mapTx.end(); it++) { assert(TestLockPointValidity(chain, it->GetLockPoints())); } - m_txgraph->DoWork(POST_CHANGE_WORK); + if (!m_txgraph->DoWork(POST_CHANGE_WORK)) { + LogDebug(BCLog::MEMPOOL, "Mempool in non-optimal ordering after reorg."); + } } void CTxMemPool::removeConflicts(const CTransaction &tx) @@ -421,7 +425,9 @@ void CTxMemPool::removeForBlock(const std::vector& vtx, unsigne } lastRollingFeeUpdate = GetTime(); blockSinceLastRollingFeeBump = true; - m_txgraph->DoWork(POST_CHANGE_WORK); + if (!m_txgraph->DoWork(POST_CHANGE_WORK)) { + LogDebug(BCLog::MEMPOOL, "Mempool in non-optimal ordering after block."); + } } void CTxMemPool::check(const CCoinsViewCache& active_coins_tip, int64_t spendheight) const From a9e59f7d955f995078b3e0bf3b527c03c74fef8d Mon Sep 17 00:00:00 2001 From: Greg Sanders Date: Wed, 18 Feb 2026 10:09:16 -0500 Subject: [PATCH 2/2] rpc: add optimal result to getmempoolinfo Expose this value to allow rpc based tooling to track this value for network health diagnostics. --- src/rpc/mempool.cpp | 2 ++ test/functional/mempool_cluster.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/src/rpc/mempool.cpp b/src/rpc/mempool.cpp index 66ce1c61582..d9e135b63a9 100644 --- a/src/rpc/mempool.cpp +++ b/src/rpc/mempool.cpp @@ -874,6 +874,7 @@ UniValue MempoolInfoToJSON(const CTxMemPool& pool) ret.pushKV("maxdatacarriersize", pool.m_opts.max_datacarrier_bytes.value_or(0)); ret.pushKV("limitclustercount", pool.m_opts.limits.cluster_count); ret.pushKV("limitclustersize", pool.m_opts.limits.cluster_size_vbytes); + ret.pushKV("optimal", pool.m_txgraph->DoWork(0)); // 0 work is a quick check for known optimality return ret; } @@ -900,6 +901,7 @@ static RPCHelpMan getmempoolinfo() {RPCResult::Type::NUM, "maxdatacarriersize", "Maximum number of bytes that can be used by OP_RETURN outputs in the mempool"}, {RPCResult::Type::NUM, "limitclustercount", "Maximum number of transactions that can be in a cluster (configured by -limitclustercount)"}, {RPCResult::Type::NUM, "limitclustersize", "Maximum size of a cluster in virtual bytes (configured by -limitclustersize)"}, + {RPCResult::Type::BOOL, "optimal", "If the mempool is in a known-optimal transaction ordering"}, }}, RPCExamples{ HelpExampleCli("getmempoolinfo", "") diff --git a/test/functional/mempool_cluster.py b/test/functional/mempool_cluster.py index 921f74419d4..6fe38771cfb 100755 --- a/test/functional/mempool_cluster.py +++ b/test/functional/mempool_cluster.py @@ -306,6 +306,9 @@ class MempoolClusterTest(BitcoinTestFramework): assert_equal(node.getrawmempool(), []) + # Key should exist and be trivially optimal + assert node.getmempoolinfo()["optimal"] + # Not in-mempool not_mempool_tx = self.wallet.create_self_transfer() assert_raises_rpc_error(-5, "Transaction not in mempool", node.getmempoolcluster, not_mempool_tx["txid"]) @@ -367,6 +370,9 @@ class MempoolClusterTest(BitcoinTestFramework): chunkfee = first_chunk_tx["fee"] + second_chunk_tx["fee"] + third_chunk_tx["fee"] assert_equal(first_chunk_info, {'clusterweight': first_chunkweight + second_chunkweight + third_chunkweight, 'txcount': 3, 'chunks': [{'chunkfee': first_chunk_tx["fee"], 'chunkweight': first_chunkweight, 'txs': [first_chunk_tx["txid"]]}, {'chunkfee': second_chunk_tx["fee"], 'chunkweight': second_chunkweight, 'txs': [second_chunk_tx["txid"]]}, {'chunkfee': third_chunk_tx["fee"], 'chunkweight': third_chunkweight, 'txs': [third_chunk_tx["txid"]]}]}) + # We expect known optimality directly after txn submission + assert node.getmempoolinfo()["optimal"] + # If we prioritise the last transaction it can join the second transaction's chunk. node.prioritisetransaction(third_chunk_tx["txid"], 0, int(third_chunk_tx["fee"]*COIN) + 1) first_chunk_info = node.getmempoolcluster(first_chunk_tx["txid"])