diff --git a/src/rpc/mempool.cpp b/src/rpc/mempool.cpp index f07f06f88f4..2f8a48702d7 100644 --- a/src/rpc/mempool.cpp +++ b/src/rpc/mempool.cpp @@ -258,6 +258,23 @@ static RPCHelpMan testmempoolaccept() }; } +static std::vector ClusterDescription() +{ + return { + RPCResult{RPCResult::Type::NUM, "weight", "total sigops-adjusted weight (as defined in BIP 141 and modified by '-bytespersigop'"}, + RPCResult{RPCResult::Type::NUM, "txcount", "number of transactions"}, + RPCResult{RPCResult::Type::ARR, "txs", "transactions in this cluster in mining order", + {RPCResult{RPCResult::Type::OBJ, "txentry", "", + { + RPCResult{RPCResult::Type::STR_HEX, "txid", "the transaction id"}, + RPCResult{RPCResult::Type::NUM, "chunkfee", "fee of the chunk containing this tx"}, + RPCResult{RPCResult::Type::NUM, "chunkweight", "sigops-adjusted weight of the chunk containing this transaction"} + } + }} + } + }; +} + static std::vector MempoolEntryDescription() { return { @@ -269,6 +286,7 @@ static std::vector MempoolEntryDescription() RPCResult{RPCResult::Type::NUM, "descendantsize", "virtual transaction size of in-mempool descendants (including this one)"}, RPCResult{RPCResult::Type::NUM, "ancestorcount", "number of in-mempool ancestor transactions (including this one)"}, RPCResult{RPCResult::Type::NUM, "ancestorsize", "virtual transaction size of in-mempool ancestors (including this one)"}, + RPCResult{RPCResult::Type::NUM, "chunkweight", "sigops-adjusted weight (as defined in BIP 141 and modified by '-bytespersigop') of this transaction's chunk"}, RPCResult{RPCResult::Type::STR_HEX, "wtxid", "hash of serialized transaction, including witness data"}, RPCResult{RPCResult::Type::OBJ, "fees", "", { @@ -276,6 +294,7 @@ static std::vector MempoolEntryDescription() RPCResult{RPCResult::Type::STR_AMOUNT, "modified", "transaction fee with fee deltas used for mining priority, denominated in " + CURRENCY_UNIT}, RPCResult{RPCResult::Type::STR_AMOUNT, "ancestor", "transaction fees of in-mempool ancestors (including this one) with fee deltas used for mining priority, denominated in " + CURRENCY_UNIT}, RPCResult{RPCResult::Type::STR_AMOUNT, "descendant", "transaction fees of in-mempool descendants (including this one) with fee deltas used for mining priority, denominated in " + CURRENCY_UNIT}, + RPCResult{RPCResult::Type::STR_AMOUNT, "chunk", "transaction fees of chunk, denominated in " + CURRENCY_UNIT}, }}, RPCResult{RPCResult::Type::ARR, "depends", "unconfirmed transactions used as inputs for this transaction", {RPCResult{RPCResult::Type::STR_HEX, "transactionid", "parent transaction id"}}}, @@ -286,6 +305,27 @@ static std::vector MempoolEntryDescription() }; } +static void clusterToJSON(const CTxMemPool& pool, UniValue& info, std::vector cluster) EXCLUSIVE_LOCKS_REQUIRED(pool.cs) +{ + AssertLockHeld(pool.cs); + int total_weight{0}; + for (const auto& tx : cluster) { + total_weight += tx->GetAdjustedWeight(); + } + info.pushKV("weight", total_weight); + info.pushKV("txcount", (int)cluster.size()); + UniValue txs(UniValue::VARR); + for (const auto& tx : cluster) { + UniValue txentry(UniValue::VOBJ); + auto feerate = pool.GetMainChunkFeerate(*tx); + txentry.pushKV("txid", tx->GetTx().GetHash().ToString()); + txentry.pushKV("chunkfee", ValueFromAmount((int)feerate.fee)); + txentry.pushKV("chunkweight", feerate.size); + txs.push_back(txentry); + } + info.pushKV("txs", txs); +} + static void entryToJSON(const CTxMemPool& pool, UniValue& info, const CTxMemPoolEntry& e) EXCLUSIVE_LOCKS_REQUIRED(pool.cs) { AssertLockHeld(pool.cs); @@ -302,12 +342,15 @@ static void entryToJSON(const CTxMemPool& pool, UniValue& info, const CTxMemPool info.pushKV("ancestorcount", ancestor_count); info.pushKV("ancestorsize", ancestor_size); info.pushKV("wtxid", e.GetTx().GetWitnessHash().ToString()); + auto feerate = pool.GetMainChunkFeerate(e); + info.pushKV("chunkweight", feerate.size); UniValue fees(UniValue::VOBJ); fees.pushKV("base", ValueFromAmount(e.GetFee())); fees.pushKV("modified", ValueFromAmount(e.GetModifiedFee())); fees.pushKV("ancestor", ValueFromAmount(ancestor_fees)); fees.pushKV("descendant", ValueFromAmount(descendant_fees)); + fees.pushKV("chunk", ValueFromAmount((int)feerate.fee)); info.pushKV("fees", std::move(fees)); const CTransaction& tx = e.GetTx(); @@ -384,6 +427,49 @@ UniValue MempoolToJSON(const CTxMemPool& pool, bool verbose, bool include_mempoo } } +static RPCHelpMan getmempoolfeeratediagram() +{ + return RPCHelpMan{"getmempoolfeeratediagram", + "Returns the feerate diagram for the whole mempool.", + {}, + { + RPCResult{"mempool chunks", + RPCResult::Type::ARR, "", "", + { + { + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::NUM, "weight", "cumulative sigops-adjusted weight"}, + {RPCResult::Type::NUM, "fee", "cumulative fee"} + } + } + } + } + }, + RPCExamples{ + HelpExampleCli("getmempoolfeeratediagram", "") + + HelpExampleRpc("getmempoolfeeratediagram", "") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue + { + const CTxMemPool& mempool = EnsureAnyMemPool(request.context); + LOCK(mempool.cs); + + UniValue result(UniValue::VARR); + + auto diagram = mempool.GetFeerateDiagram(); + + for (auto f : diagram) { + UniValue o(UniValue::VOBJ); + o.pushKV("weight", f.size); + o.pushKV("fee", ValueFromAmount(f.fee)); + result.push_back(o); + } + return result; + } + }; +} + static RPCHelpMan getrawmempool() { return RPCHelpMan{ @@ -561,6 +647,35 @@ static RPCHelpMan getmempooldescendants() }; } +static RPCHelpMan getmempoolcluster() +{ + return RPCHelpMan{"getmempoolcluster", + "Returns mempool data for given cluster\n", + { + {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The txid of a transaction in the cluster"}, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", ClusterDescription()}, + RPCExamples{ + HelpExampleCli("getmempoolcluster", "txid") + + HelpExampleRpc("getmempoolcluster", "txid") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + uint256 hash = ParseHashV(request.params[0], "parameter 1"); + + const CTxMemPool& mempool = EnsureAnyMemPool(request.context); + LOCK(mempool.cs); + + auto cluster = mempool.GetCluster(Txid::FromUint256(hash)); + + UniValue info(UniValue::VOBJ); + clusterToJSON(mempool, info, cluster); + return info; +}, + }; +} + static RPCHelpMan getmempoolentry() { return RPCHelpMan{ @@ -694,6 +809,8 @@ UniValue MempoolInfoToJSON(const CTxMemPool& pool) ret.pushKV("fullrbf", true); ret.pushKV("permitbaremultisig", pool.m_opts.permit_bare_multisig); 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); return ret; } @@ -718,6 +835,8 @@ static RPCHelpMan getmempoolinfo() {RPCResult::Type::BOOL, "fullrbf", "True if the mempool accepts RBF without replaceability signaling inspection (DEPRECATED)"}, {RPCResult::Type::BOOL, "permitbaremultisig", "True if the mempool accepts transactions with bare multisig outputs"}, {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)"}, }}, RPCExamples{ HelpExampleCli("getmempoolinfo", "") @@ -1145,8 +1264,10 @@ void RegisterMempoolRPCCommands(CRPCTable& t) {"blockchain", &getmempoolancestors}, {"blockchain", &getmempooldescendants}, {"blockchain", &getmempoolentry}, + {"blockchain", &getmempoolcluster}, {"blockchain", &gettxspendingprevout}, {"blockchain", &getmempoolinfo}, + {"hidden", &getmempoolfeeratediagram}, {"blockchain", &getrawmempool}, {"blockchain", &importmempool}, {"blockchain", &savemempool}, diff --git a/src/test/fuzz/rpc.cpp b/src/test/fuzz/rpc.cpp index 580a6338a84..9fd1021fdb8 100644 --- a/src/test/fuzz/rpc.cpp +++ b/src/test/fuzz/rpc.cpp @@ -136,6 +136,8 @@ const std::vector RPC_COMMANDS_SAFE_FOR_FUZZING{ "getmempoolancestors", "getmempooldescendants", "getmempoolentry", + "getmempoolfeeratediagram", + "getmempoolcluster", "getmempoolinfo", "getmininginfo", "getnettotals", diff --git a/src/txmempool.cpp b/src/txmempool.cpp index d7303cc6749..597d4864094 100644 --- a/src/txmempool.cpp +++ b/src/txmempool.cpp @@ -1010,3 +1010,25 @@ bool CTxMemPool::ChangeSet::CheckMemPoolPolicyLimits() return !m_pool->m_txgraph->IsOversized(TxGraph::Level::TOP); } + +std::vector CTxMemPool::GetFeerateDiagram() const +{ + FeePerWeight zero{}; + std::vector ret; + + ret.emplace_back(zero); + + StartBlockBuilding(); + + std::vector dummy; + + FeePerWeight last_selection = GetBlockBuilderChunk(dummy); + while (last_selection != FeePerWeight{}) { + last_selection += ret.back(); + ret.emplace_back(last_selection); + IncludeBuilderChunk(); + last_selection = GetBlockBuilderChunk(dummy); + } + StopBlockBuilding(); + return ret; +} diff --git a/src/txmempool.h b/src/txmempool.h index 1891c8d48a7..e2020b65260 100644 --- a/src/txmempool.h +++ b/src/txmempool.h @@ -406,6 +406,23 @@ public: */ void UpdateTransactionsFromBlock(const std::vector& vHashesToUpdate) EXCLUSIVE_LOCKS_REQUIRED(cs, cs_main) LOCKS_EXCLUDED(m_epoch); + std::vector GetFeerateDiagram() const EXCLUSIVE_LOCKS_REQUIRED(cs); + FeePerWeight GetMainChunkFeerate(const CTxMemPoolEntry& tx) const EXCLUSIVE_LOCKS_REQUIRED(cs) { + return m_txgraph->GetMainChunkFeerate(tx); + } + std::vector GetCluster(Txid txid) const EXCLUSIVE_LOCKS_REQUIRED(cs) { + auto tx = GetIter(txid); + if (!tx) return {}; + auto cluster = m_txgraph->GetCluster(**tx, TxGraph::Level::MAIN); + std::vector ret; + ret.reserve(cluster.size()); + for (const auto& tx : cluster) { + ret.emplace_back(static_cast(tx)); + } + return ret; + } + + size_t GetUniqueClusterCount(const setEntries& iters_conflicting) const EXCLUSIVE_LOCKS_REQUIRED(cs) { std::vector entries; entries.reserve(iters_conflicting.size()); diff --git a/test/functional/mempool_persist.py b/test/functional/mempool_persist.py index a9560dd6e3b..ceb69cf1ccb 100755 --- a/test/functional/mempool_persist.py +++ b/test/functional/mempool_persist.py @@ -128,7 +128,8 @@ class MempoolPersistTest(BitcoinTestFramework): assert_equal(fees['base'] + Decimal('0.00001000'), fees['modified']) self.log.debug('Verify all fields are loaded correctly') - assert_equal(last_entry, self.nodes[0].getmempoolentry(txid=last_txid)) + new_entry = self.nodes[0].getmempoolentry(txid=last_txid) + assert_equal({**last_entry, "clusterid": None}, {**new_entry, "clusterid": None}) self.nodes[0].sendrawtransaction(tx_prioritised_not_submitted['hex']) entry_prioritised_before_restart = self.nodes[0].getmempoolentry(txid=tx_prioritised_not_submitted['txid']) assert_equal(entry_prioritised_before_restart['fees']['base'] + Decimal('0.00009999'), entry_prioritised_before_restart['fees']['modified']) diff --git a/test/functional/mining_prioritisetransaction.py b/test/functional/mining_prioritisetransaction.py index a8646d49181..c3b56e8d3a4 100755 --- a/test/functional/mining_prioritisetransaction.py +++ b/test/functional/mining_prioritisetransaction.py @@ -101,14 +101,21 @@ class PrioritiseTransactionTest(BitcoinTestFramework): self.nodes[0].prioritisetransaction(txid=txid_c, fee_delta=int(fee_delta_c_1 * COIN)) self.nodes[0].prioritisetransaction(txid=txid_c, fee_delta=int(fee_delta_c_2 * COIN)) raw_before[txid_a]["fees"]["descendant"] += fee_delta_b + fee_delta_c_1 + fee_delta_c_2 + # We expect tx_a to have a chunk fee that includes tx_b and tx_c. + raw_before[txid_a]["fees"]["chunk"] += fee_delta_b + fee_delta_c_1 + fee_delta_c_2 raw_before[txid_b]["fees"]["modified"] += fee_delta_b raw_before[txid_b]["fees"]["ancestor"] += fee_delta_b raw_before[txid_b]["fees"]["descendant"] += fee_delta_b + # We also expect tx_b and tx_c to have their chunk fees modified too, + # since they chunk together. + raw_before[txid_b]["fees"]["chunk"] += fee_delta_b + fee_delta_c_1 + fee_delta_c_2 raw_before[txid_c]["fees"]["modified"] += fee_delta_c_1 + fee_delta_c_2 raw_before[txid_c]["fees"]["ancestor"] += fee_delta_c_1 + fee_delta_c_2 raw_before[txid_c]["fees"]["descendant"] += fee_delta_c_1 + fee_delta_c_2 + raw_before[txid_c]["fees"]["chunk"] += fee_delta_b + fee_delta_c_1 + fee_delta_c_2 raw_before[txid_d]["fees"]["ancestor"] += fee_delta_b + fee_delta_c_1 + fee_delta_c_2 raw_after = self.nodes[0].getrawmempool(verbose=True) + # Don't bother comparing cluster ids, which are not meant to be stable. assert_equal(raw_before[txid_a], raw_after[txid_a]) assert_equal(raw_before, raw_after) assert_equal(self.nodes[0].getprioritisedtransactions(), {txid_b: {"fee_delta" : fee_delta_b*COIN, "in_mempool" : True, "modified_fee": int(fee_delta_b*COIN + COIN * tx_o_b["fee"])}, txid_c: {"fee_delta" : (fee_delta_c_1 + fee_delta_c_2)*COIN, "in_mempool" : True, "modified_fee": int((fee_delta_c_1 + fee_delta_c_2 ) * COIN + COIN * tx_o_c["fee"])}})