Skip to content

Commit 2cd28e9

Browse files
ryanofskyajtowns
andcommitted
rpc: Add check for unintended option/parameter name clashes
Also add flag to allow RPC methods that intendionally accept options and parameters with the same name bypass the check. Check and flag were suggested by ajtowns bitcoin/bitcoin#26485 (comment) Co-authored-by: Anthony Towns <aj@erisian.com.au>
1 parent 95d7de0 commit 2cd28e9

File tree

4 files changed

+82
-6
lines changed

4 files changed

+82
-6
lines changed

src/rpc/util.cpp

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -486,12 +486,32 @@ RPCHelpMan::RPCHelpMan(std::string name, std::string description, std::vector<RP
486486
m_results{std::move(results)},
487487
m_examples{std::move(examples)}
488488
{
489-
std::set<std::string> named_args;
489+
// Map of parameter names and types just used to check whether the names are
490+
// unique. Parameter names always need to be unique, with the exception that
491+
// there can be pairs of POSITIONAL and NAMED parameters with the same name.
492+
enum ParamType { POSITIONAL = 1, NAMED = 2, NAMED_ONLY = 4 };
493+
std::map<std::string, int> param_names;
494+
490495
for (const auto& arg : m_args) {
491496
std::vector<std::string> names = SplitString(arg.m_names, '|');
492497
// Should have unique named arguments
493498
for (const std::string& name : names) {
494-
CHECK_NONFATAL(named_args.insert(name).second);
499+
auto& param_type = param_names[name];
500+
CHECK_NONFATAL(!(param_type & POSITIONAL));
501+
CHECK_NONFATAL(!(param_type & NAMED_ONLY));
502+
param_type |= POSITIONAL;
503+
}
504+
if (arg.m_type == RPCArg::Type::OBJ_NAMED_PARAMS) {
505+
for (const auto& inner : arg.m_inner) {
506+
std::vector<std::string> inner_names = SplitString(inner.m_names, '|');
507+
for (const std::string& inner_name : inner_names) {
508+
auto& param_type = param_names[inner_name];
509+
CHECK_NONFATAL(!(param_type & POSITIONAL) || inner.m_opts.also_positional);
510+
CHECK_NONFATAL(!(param_type & NAMED));
511+
CHECK_NONFATAL(!(param_type & NAMED_ONLY));
512+
param_type |= inner.m_opts.also_positional ? NAMED : NAMED_ONLY;
513+
}
514+
}
495515
}
496516
// Default value type should match argument type only when defined
497517
if (arg.m_fallback.index() == 2) {

src/rpc/util.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,15 @@ struct RPCArgOptions {
130130
std::string oneline_description{}; //!< Should be empty unless it is supposed to override the auto-generated summary line
131131
std::vector<std::string> type_str{}; //!< Should be empty unless it is supposed to override the auto-generated type strings. Vector length is either 0 or 2, m_opts.type_str.at(0) will override the type of the value in a key-value pair, m_opts.type_str.at(1) will override the type in the argument description.
132132
bool hidden{false}; //!< For testing only
133+
bool also_positional{false}; //!< If set allows a named-parameter field in an OBJ_NAMED_PARAM options object
134+
//!< to have the same name as a top-level parameter. By default the RPC
135+
//!< framework disallows this, because if an RPC request passes the value by
136+
//!< name, it is assigned to top-level parameter position, not to the options
137+
//!< position, defeating the purpose of using OBJ_NAMED_PARAMS instead OBJ for
138+
//!< that option. But sometimes it makes sense to allow less-commonly used
139+
//!< options to be passed by name only, and more commonly used options to be
140+
//!< passed by name or position, so the RPC framework allows this as long as
141+
//!< methods set the also_positional flag and read values from both positions.
133142
};
134143

135144
struct RPCArg {

src/test/rpc_tests.cpp

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,53 @@ BOOST_AUTO_TEST_CASE(rpc_getblockstats_calculate_percentiles_by_weight)
528528
}
529529
}
530530

531+
// Make sure errors are triggered appropriately if parameters have the same names.
532+
BOOST_AUTO_TEST_CASE(check_dup_param_names)
533+
{
534+
enum ParamType { POSITIONAL, NAMED, NAMED_ONLY };
535+
auto make_rpc = [](std::vector<std::tuple<std::string, ParamType>> param_names) {
536+
std::vector<RPCArg> params;
537+
std::vector<RPCArg> options;
538+
auto push_options = [&] { if (!options.empty()) params.emplace_back(RPCArg{strprintf("options%i", params.size()), RPCArg::Type::OBJ_NAMED_PARAMS, RPCArg::Optional::OMITTED, "", std::move(options)}); };
539+
for (auto& [param_name, param_type] : param_names) {
540+
if (param_type == POSITIONAL) {
541+
push_options();
542+
params.emplace_back(std::move(param_name), RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "description");
543+
} else {
544+
options.emplace_back(std::move(param_name), RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "description", RPCArgOptions{.also_positional = param_type == NAMED});
545+
}
546+
}
547+
push_options();
548+
return RPCHelpMan{"method_name", "description", params, RPCResults{}, RPCExamples{""}};
549+
};
550+
551+
// No errors if parameter names are unique.
552+
make_rpc({{"p1", POSITIONAL}, {"p2", POSITIONAL}});
553+
make_rpc({{"p1", POSITIONAL}, {"p2", NAMED}});
554+
make_rpc({{"p1", POSITIONAL}, {"p2", NAMED_ONLY}});
555+
make_rpc({{"p1", NAMED}, {"p2", POSITIONAL}});
556+
make_rpc({{"p1", NAMED}, {"p2", NAMED}});
557+
make_rpc({{"p1", NAMED}, {"p2", NAMED_ONLY}});
558+
make_rpc({{"p1", NAMED_ONLY}, {"p2", POSITIONAL}});
559+
make_rpc({{"p1", NAMED_ONLY}, {"p2", NAMED}});
560+
make_rpc({{"p1", NAMED_ONLY}, {"p2", NAMED_ONLY}});
561+
562+
// Error if parameters names are duplicates, unless one parameter is
563+
// positional and the other is named and .also_positional is true.
564+
BOOST_CHECK_THROW(make_rpc({{"p1", POSITIONAL}, {"p1", POSITIONAL}}), NonFatalCheckError);
565+
make_rpc({{"p1", POSITIONAL}, {"p1", NAMED}});
566+
BOOST_CHECK_THROW(make_rpc({{"p1", POSITIONAL}, {"p1", NAMED_ONLY}}), NonFatalCheckError);
567+
make_rpc({{"p1", NAMED}, {"p1", POSITIONAL}});
568+
BOOST_CHECK_THROW(make_rpc({{"p1", NAMED}, {"p1", NAMED}}), NonFatalCheckError);
569+
BOOST_CHECK_THROW(make_rpc({{"p1", NAMED}, {"p1", NAMED_ONLY}}), NonFatalCheckError);
570+
BOOST_CHECK_THROW(make_rpc({{"p1", NAMED_ONLY}, {"p1", POSITIONAL}}), NonFatalCheckError);
571+
BOOST_CHECK_THROW(make_rpc({{"p1", NAMED_ONLY}, {"p1", NAMED}}), NonFatalCheckError);
572+
BOOST_CHECK_THROW(make_rpc({{"p1", NAMED_ONLY}, {"p1", NAMED_ONLY}}), NonFatalCheckError);
573+
574+
// Make sure duplicate aliases are detected too.
575+
BOOST_CHECK_THROW(make_rpc({{"p1", POSITIONAL}, {"p2|p1", NAMED_ONLY}}), NonFatalCheckError);
576+
}
577+
531578
BOOST_AUTO_TEST_CASE(help_example)
532579
{
533580
// test different argument types

src/wallet/rpc/spend.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -453,9 +453,9 @@ RPCHelpMan settxfee()
453453
static std::vector<RPCArg> FundTxDoc(bool solving_data = true)
454454
{
455455
std::vector<RPCArg> args = {
456-
{"conf_target", RPCArg::Type::NUM, RPCArg::DefaultHint{"wallet -txconfirmtarget"}, "Confirmation target in blocks"},
456+
{"conf_target", RPCArg::Type::NUM, RPCArg::DefaultHint{"wallet -txconfirmtarget"}, "Confirmation target in blocks", RPCArgOptions{.also_positional = true}},
457457
{"estimate_mode", RPCArg::Type::STR, RPCArg::Default{"unset"}, "The fee estimate mode, must be one of (case insensitive):\n"
458-
"\"" + FeeModes("\"\n\"") + "\""},
458+
"\"" + FeeModes("\"\n\"") + "\"", RPCArgOptions{.also_positional = true}},
459459
{
460460
"replaceable", RPCArg::Type::BOOL, RPCArg::DefaultHint{"wallet default"}, "Marks this transaction as BIP125-replaceable.\n"
461461
"Allows this transaction to be replaced by a transaction with higher fees"
@@ -1200,7 +1200,7 @@ RPCHelpMan send()
12001200
{"change_address", RPCArg::Type::STR, RPCArg::DefaultHint{"automatic"}, "The bitcoin address to receive the change"},
12011201
{"change_position", RPCArg::Type::NUM, RPCArg::DefaultHint{"random"}, "The index of the change output"},
12021202
{"change_type", RPCArg::Type::STR, RPCArg::DefaultHint{"set by -changetype"}, "The output type to use. Only valid if change_address is not specified. Options are \"legacy\", \"p2sh-segwit\", \"bech32\" and \"bech32m\"."},
1203-
{"fee_rate", RPCArg::Type::AMOUNT, RPCArg::DefaultHint{"not set, fall back to wallet fee estimation"}, "Specify a fee rate in " + CURRENCY_ATOM + "/vB."},
1203+
{"fee_rate", RPCArg::Type::AMOUNT, RPCArg::DefaultHint{"not set, fall back to wallet fee estimation"}, "Specify a fee rate in " + CURRENCY_ATOM + "/vB.", RPCArgOptions{.also_positional = true}},
12041204
{"include_watching", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Also select inputs which are watch only.\n"
12051205
"Only solvable inputs can be used. Watch-only destinations are solvable if the public key and/or output script was imported,\n"
12061206
"e.g. with 'importpubkey' or 'importmulti' with the 'pubkeys' or 'desc' field."},
@@ -1306,7 +1306,7 @@ RPCHelpMan sendall()
13061306
Cat<std::vector<RPCArg>>(
13071307
{
13081308
{"add_to_wallet", RPCArg::Type::BOOL, RPCArg::Default{true}, "When false, returns the serialized transaction without broadcasting or adding it to the wallet"},
1309-
{"fee_rate", RPCArg::Type::AMOUNT, RPCArg::DefaultHint{"not set, fall back to wallet fee estimation"}, "Specify a fee rate in " + CURRENCY_ATOM + "/vB."},
1309+
{"fee_rate", RPCArg::Type::AMOUNT, RPCArg::DefaultHint{"not set, fall back to wallet fee estimation"}, "Specify a fee rate in " + CURRENCY_ATOM + "/vB.", RPCArgOptions{.also_positional = true}},
13101310
{"include_watching", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Also select inputs which are watch-only.\n"
13111311
"Only solvable inputs can be used. Watch-only destinations are solvable if the public key and/or output script was imported,\n"
13121312
"e.g. with 'importpubkey' or 'importmulti' with the 'pubkeys' or 'desc' field."},

0 commit comments

Comments
 (0)