Skip to content

Commit ac36413

Browse files
committed
Merge branch 'fee_histogram+pr15836_api' into rbf_opts-28+knots
2 parents cf0c04f + 14b872b commit ac36413

File tree

9 files changed

+397
-17
lines changed

9 files changed

+397
-17
lines changed

src/rest.cpp

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
#include <validation.h>
3434

3535
#include <any>
36+
#include <optional>
3637
#include <vector>
3738

3839
#include <univalue.h>
@@ -650,8 +651,8 @@ static bool rest_mempool(const std::any& context, HTTPRequest* req, const std::s
650651

651652
std::string param;
652653
const RESTResponseFormat rf = ParseDataFormat(param, str_uri_part);
653-
if (param != "contents" && param != "info") {
654-
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid URI format. Expected /rest/mempool/<info|contents>.json");
654+
if (param != "contents" && param != "info" && param != "info/with_fee_histogram") {
655+
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid URI format. Expected /rest/mempool/<info|info/with_fee_histogram|contents>.json");
655656
}
656657

657658
const CTxMemPool* mempool = GetMemPool(context, req);
@@ -685,8 +686,10 @@ static bool rest_mempool(const std::any& context, HTTPRequest* req, const std::s
685686
return RESTERR(req, HTTP_BAD_REQUEST, "Verbose results cannot contain mempool sequence values. (hint: set \"verbose=false\")");
686687
}
687688
str_json = MempoolToJSON(*mempool, verbose, mempool_sequence).write() + "\n";
689+
} else if (param == "info/with_fee_histogram") {
690+
str_json = MempoolInfoToJSON(*mempool, MempoolInfoToJSON_const_histogram_floors).write() + "\n";
688691
} else {
689-
str_json = MempoolInfoToJSON(*mempool).write() + "\n";
692+
str_json = MempoolInfoToJSON(*mempool, std::nullopt).write() + "\n";
690693
}
691694

692695
req->WriteHeader("Content-Type", "application/json");

src/rpc/client.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,8 @@ static const CRPCConvertParam vRPCConvertParams[] =
250250
{ "getblockstats", 1, "stats" },
251251
{ "pruneblockchain", 0, "height" },
252252
{ "keypoolrefill", 0, "newsize" },
253+
{ "getmempoolinfo", 0, "fee_histogram" },
254+
{ "getmempoolinfo", 0, "with_fee_histogram" },
253255
{ "getrawmempool", 0, "verbose" },
254256
{ "getrawmempool", 1, "mempool_sequence" },
255257
{ "estimatesmartfee", 0, "conf_target" },

src/rpc/mempool.cpp

Lines changed: 112 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include <policy/rbf.h>
1616
#include <policy/settings.h>
1717
#include <primitives/transaction.h>
18+
#include <rpc/mempool.h>
1819
#include <rpc/server.h>
1920
#include <rpc/server_util.h>
2021
#include <rpc/util.h>
@@ -25,6 +26,7 @@
2526
#include <util/strencodings.h>
2627
#include <util/time.h>
2728

29+
#include <optional>
2830
#include <utility>
2931

3032
using node::DumpMempool;
@@ -664,7 +666,7 @@ static RPCHelpMan gettxspendingprevout()
664666
};
665667
}
666668

667-
UniValue MempoolInfoToJSON(const CTxMemPool& pool)
669+
UniValue MempoolInfoToJSON(const CTxMemPool& pool, const std::optional<MempoolHistogramFeeRates>& histogram_floors)
668670
{
669671
// Make sure this call is atomic in the pool.
670672
LOCK(pool.cs);
@@ -680,14 +682,75 @@ UniValue MempoolInfoToJSON(const CTxMemPool& pool)
680682
ret.pushKV("incrementalrelayfee", ValueFromAmount(pool.m_opts.incremental_relay_feerate.GetFeePerK()));
681683
ret.pushKV("unbroadcastcount", uint64_t{pool.GetUnbroadcastTxs().size()});
682684
ret.pushKV("fullrbf", pool.m_opts.full_rbf);
685+
686+
if (histogram_floors) {
687+
const MempoolHistogramFeeRates& floors{histogram_floors.value()};
688+
689+
std::vector<uint64_t> sizes(floors.size(), 0);
690+
std::vector<uint64_t> count(floors.size(), 0);
691+
std::vector<CAmount> fees(floors.size(), 0);
692+
693+
for (const CTxMemPoolEntry& e : pool.mapTx) {
694+
const CAmount fee{e.GetFee()};
695+
const uint32_t size{uint32_t(e.GetTxSize())};
696+
697+
const CAmount afees{e.GetModFeesWithAncestors()}, dfees{e.GetModFeesWithDescendants()};
698+
const uint32_t asize{uint32_t(e.GetSizeWithAncestors())}, dsize{uint32_t(e.GetSizeWithDescendants())};
699+
700+
// Do not use CFeeRate here, since it rounds up, and this should be rounding down
701+
const CAmount fpb{fee / size}; // Fee rate per byte
702+
const CAmount afpb{afees / asize}; // Fee rate per byte including ancestors
703+
const CAmount dfpb{dfees / dsize}; // Fee rate per byte including descendants
704+
705+
// Fee rate per byte including ancestors & descendants
706+
// (fee/size are included in both, so subtracted to avoid double-counting)
707+
const CAmount tfpb{(afees + dfees - fee) / (asize + dsize - size)};
708+
709+
const CAmount fee_rate{std::max(std::min(dfpb, tfpb), std::min(fpb, afpb))};
710+
711+
// Distribute fee rates
712+
for (size_t i = floors.size(); i > 0;) {
713+
--i;
714+
if (fee_rate >= floors[i]) {
715+
sizes[i] += size;
716+
++count[i];
717+
fees[i] += fee;
718+
break;
719+
}
720+
}
721+
}
722+
723+
// Track total amount of available fees in fee rate groups
724+
CAmount total_fees = 0;
725+
UniValue info(UniValue::VOBJ);
726+
for (size_t i = 0; i < floors.size(); ++i) {
727+
UniValue info_sub(UniValue::VOBJ);
728+
info_sub.pushKV("sizes", sizes[i]);
729+
info_sub.pushKV("count", count.at(i));
730+
info_sub.pushKV("fees", fees.at(i));
731+
info_sub.pushKV("from_feerate", floors[i]);
732+
info_sub.pushKV("to_feerate", i == floors.size() - 1 ? std::numeric_limits<int64_t>::max() : floors[i + 1]);
733+
total_fees += fees.at(i);
734+
info.pushKV(ToString(floors[i]), info_sub);
735+
}
736+
info.pushKV("total_fees", total_fees);
737+
ret.pushKV("fee_histogram", info);
738+
}
739+
683740
return ret;
684741
}
685742

686743
static RPCHelpMan getmempoolinfo()
687744
{
688745
return RPCHelpMan{"getmempoolinfo",
689-
"Returns details on the active state of the TX memory pool.",
690-
{},
746+
"Returns details on the active state of the TX memory pool.\n",
747+
{
748+
{"fee_histogram|with_fee_histogram", {RPCArg::Type::ARR, RPCArg::Type::BOOL}, RPCArg::Optional::OMITTED, "Fee statistics grouped by fee rate ranges",
749+
{
750+
{"fee_rate", RPCArg::Type::NUM, RPCArg::Optional::NO, "Fee rate (in " + CURRENCY_ATOM + "/vB) to group the fees by"},
751+
},
752+
},
753+
},
691754
RPCResult{
692755
RPCResult::Type::OBJ, "", "",
693756
{
@@ -702,14 +765,57 @@ static RPCHelpMan getmempoolinfo()
702765
{RPCResult::Type::NUM, "incrementalrelayfee", "minimum fee rate increment for mempool limiting or replacement in " + CURRENCY_UNIT + "/kvB"},
703766
{RPCResult::Type::NUM, "unbroadcastcount", "Current number of transactions that haven't passed initial broadcast yet"},
704767
{RPCResult::Type::BOOL, "fullrbf", "True if the mempool accepts RBF without replaceability signaling inspection"},
768+
{RPCResult::Type::OBJ_DYN, "fee_histogram", /*optional=*/true, "",
769+
{
770+
{RPCResult::Type::OBJ, "<fee_rate_group>", "Fee rate group named by its lower bound (in " + CURRENCY_ATOM + "/vB), identical to the \"from_feerate\" field below",
771+
{
772+
{RPCResult::Type::NUM, "sizes", "Cumulative size of all transactions in the fee rate group (in vBytes)"},
773+
{RPCResult::Type::NUM, "count", "Number of transactions in the fee rate group"},
774+
{RPCResult::Type::NUM, "fees", "Cumulative fees of all transactions in the fee rate group (in " + CURRENCY_ATOM + ")"},
775+
{RPCResult::Type::NUM, "from_feerate", "Group contains transactions with fee rates equal or greater than this value (in " + CURRENCY_ATOM + "/vB)"},
776+
{RPCResult::Type::NUM, "to_feerate", /*optional=*/true, "Group contains transactions with fee rates equal or less than this value (in " + CURRENCY_ATOM + "/vB)"},
777+
}},
778+
{RPCResult::Type::ELISION, "", ""},
779+
{RPCResult::Type::NUM, "total_fees", "Total available fees in mempool (in " + CURRENCY_ATOM + ")"},
780+
}, /*skip_type_check=*/ true},
705781
}},
706782
RPCExamples{
707-
HelpExampleCli("getmempoolinfo", "")
708-
+ HelpExampleRpc("getmempoolinfo", "")
783+
HelpExampleCli("getmempoolinfo", "") +
784+
HelpExampleCli("getmempoolinfo", R"("[0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 17, 20, 25, 30, 40, 50, 60, 70, 80, 100, 120, 140, 170, 200]")") +
785+
HelpExampleRpc("getmempoolinfo", "") +
786+
HelpExampleRpc("getmempoolinfo", R"([0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 17, 20, 25, 30, 40, 50, 60, 70, 80, 100, 120, 140, 170, 200])")
709787
},
710788
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
711789
{
712-
return MempoolInfoToJSON(EnsureAnyMemPool(request.context));
790+
MempoolHistogramFeeRates histogram_floors;
791+
std::optional<MempoolHistogramFeeRates> histogram_floors_opt = std::nullopt;
792+
793+
if (request.params[0].isBool()) {
794+
if (request.params[0].isTrue()) {
795+
histogram_floors_opt = MempoolInfoToJSON_const_histogram_floors;
796+
}
797+
} else if (!request.params[0].isNull()) {
798+
const UniValue histogram_floors_univalue = request.params[0].get_array();
799+
800+
if (histogram_floors_univalue.empty()) {
801+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid number of parameters");
802+
}
803+
804+
for (size_t i = 0; i < histogram_floors_univalue.size(); ++i) {
805+
int64_t value = histogram_floors_univalue[i].getInt<int64_t>();
806+
807+
if (value < 0) {
808+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Non-negative values are expected");
809+
} else if (i > 0 && histogram_floors.back() >= value) {
810+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Strictly increasing values are expected");
811+
}
812+
813+
histogram_floors.push_back(value);
814+
}
815+
histogram_floors_opt = std::optional<MempoolHistogramFeeRates>(std::move(histogram_floors));
816+
}
817+
818+
return MempoolInfoToJSON(EnsureAnyMemPool(request.context), histogram_floors_opt);
713819
},
714820
};
715821
}

src/rpc/mempool.h

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,27 @@
55
#ifndef BITCOIN_RPC_MEMPOOL_H
66
#define BITCOIN_RPC_MEMPOOL_H
77

8+
#include <consensus/amount.h>
9+
10+
#include <optional>
11+
#include <vector>
12+
813
class CTxMemPool;
914
class UniValue;
1015

16+
typedef std::vector<CAmount> MempoolHistogramFeeRates;
17+
18+
/* TODO: define log scale formular for dynamically creating the
19+
* feelimits but with the property of not constantly changing
20+
* (and thus screw up client implementations) */
21+
static const MempoolHistogramFeeRates MempoolInfoToJSON_const_histogram_floors{
22+
1, 2, 3, 4, 5, 6, 7, 8, 10,
23+
12, 14, 17, 20, 25, 30, 40, 50, 60, 70, 80, 100,
24+
120, 140, 170, 200, 250, 300, 400, 500, 600, 700, 800, 1000,
25+
1200, 1400, 1700, 2000, 2500, 3000, 4000, 5000, 6000, 7000, 8000, 10000};
26+
1127
/** Mempool information to JSON */
12-
UniValue MempoolInfoToJSON(const CTxMemPool& pool);
28+
UniValue MempoolInfoToJSON(const CTxMemPool& pool, const std::optional<MempoolHistogramFeeRates>& histogram_floors);
1329

1430
/** Mempool to JSON */
1531
UniValue MempoolToJSON(const CTxMemPool& pool, bool verbose = false, bool include_mempool_sequence = false);

src/rpc/util.cpp

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -852,9 +852,15 @@ UniValue RPCHelpMan::GetArgMap() const
852852
for (int i{0}; i < int(m_args.size()); ++i) {
853853
const auto& arg = m_args.at(i);
854854
std::vector<std::string> arg_names = SplitString(arg.m_names, '|');
855+
RPCArg::Type argtype = arg.m_type;
856+
size_t arg_num = 0;
855857
for (const auto& arg_name : arg_names) {
856-
push_back_arg_info(m_name, i, arg_name, arg.m_type);
857-
if (arg.m_type == RPCArg::Type::OBJ_NAMED_PARAMS) {
858+
if (!arg.m_type_per_name.empty()) {
859+
argtype = arg.m_type_per_name.at(arg_num++);
860+
}
861+
862+
push_back_arg_info(m_name, i, arg_name, argtype);
863+
if (argtype == RPCArg::Type::OBJ_NAMED_PARAMS) {
858864
for (const auto& inner : arg.m_inner) {
859865
std::vector<std::string> inner_names = SplitString(inner.m_names, '|');
860866
for (const std::string& inner_name : inner_names) {
@@ -905,13 +911,15 @@ UniValue RPCArg::MatchesType(const UniValue& request) const
905911
{
906912
if (m_opts.skip_type_check) return true;
907913
if (IsOptional() && request.isNull()) return true;
908-
const auto exp_type{ExpectedType(m_type)};
909-
if (!exp_type) return true; // nothing to check
914+
for (auto type : m_type_per_name.empty() ? std::vector<RPCArg::Type>{m_type} : m_type_per_name) {
915+
const auto exp_type{ExpectedType(type)};
916+
if (!exp_type) return true; // nothing to check
910917

911-
if (*exp_type != request.getType()) {
912-
return strprintf("JSON value of type %s is not of expected type %s", uvTypeName(request.getType()), uvTypeName(*exp_type));
918+
if (*exp_type == request.getType()) {
919+
return true;
920+
}
913921
}
914-
return true;
922+
return strprintf("JSON value of type %s is not of expected type %s", uvTypeName(request.getType()), uvTypeName(*ExpectedType(m_type)));
915923
}
916924

917925
std::string RPCArg::GetFirstName() const

src/rpc/util.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include <univalue.h>
1919
#include <util/check.h>
2020

21+
#include <algorithm>
2122
#include <cstddef>
2223
#include <cstdint>
2324
#include <functional>
@@ -210,6 +211,7 @@ struct RPCArg {
210211

211212
const std::string m_names; //!< The name of the arg (can be empty for inner args, can contain multiple aliases separated by | for named request arguments)
212213
const Type m_type;
214+
const std::vector<Type> m_type_per_name;
213215
const std::vector<RPCArg> m_inner; //!< Only used for arrays or dicts
214216
const Fallback m_fallback;
215217
const std::string m_description;
@@ -230,6 +232,24 @@ struct RPCArg {
230232
CHECK_NONFATAL(type != Type::ARR && type != Type::OBJ && type != Type::OBJ_NAMED_PARAMS && type != Type::OBJ_USER_KEYS);
231233
}
232234

235+
RPCArg(
236+
std::string name,
237+
std::vector<Type> types,
238+
Fallback fallback,
239+
std::string description,
240+
std::vector<RPCArg> inner = {},
241+
RPCArgOptions opts = {})
242+
: m_names{std::move(name)},
243+
m_type{types.at(0)},
244+
m_type_per_name{std::move(types)},
245+
m_inner{std::move(inner)},
246+
m_fallback{std::move(fallback)},
247+
m_description{std::move(description)},
248+
m_opts{std::move(opts)}
249+
{
250+
CHECK_NONFATAL(m_type_per_name.size() == size_t(std::count(m_names.begin(), m_names.end(), '|')) + 1);
251+
}
252+
233253
RPCArg(
234254
std::string name,
235255
Type type,

test/functional/interface_rest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,9 @@ def run_test(self):
345345
for obj in [json_obj, mempool_info]:
346346
obj.pop("unbroadcastcount")
347347
assert_equal(json_obj, mempool_info)
348+
json_obj = self.test_rest_request("/mempool/info/with_fee_histogram")
349+
mempool_info = self.nodes[0].getmempoolinfo(with_fee_histogram=True)
350+
assert_equal(json_obj, mempool_info)
348351

349352
# Check that there are our submitted transactions in the TX memory pool
350353
json_obj = self.test_rest_request("/mempool/contents")

0 commit comments

Comments
 (0)